001/*
002 * oauth2-oidc-sdk
003 *
004 * Copyright 2012-2016, Connect2id Ltd and contributors.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.oauth2.sdk.auth.verifier;
019
020
021import java.security.PublicKey;
022import java.security.cert.X509Certificate;
023import java.util.List;
024import java.util.Set;
025
026import com.nimbusds.jose.JOSEException;
027import com.nimbusds.jose.JWSVerifier;
028import com.nimbusds.jose.crypto.MACVerifier;
029import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory;
030import com.nimbusds.jose.proc.JWSVerifierFactory;
031import com.nimbusds.jwt.SignedJWT;
032import com.nimbusds.jwt.proc.BadJWTException;
033import com.nimbusds.oauth2.sdk.auth.*;
034import com.nimbusds.oauth2.sdk.id.Audience;
035import com.nimbusds.oauth2.sdk.util.X509CertificateUtils;
036import net.jcip.annotations.ThreadSafe;
037import org.apache.commons.collections4.CollectionUtils;
038
039
040/**
041 * Client authentication verifier.
042 *
043 * <p>Related specifications:
044 *
045 * <ul>
046 *     <li>OAuth 2.0 (RFC 6749), sections 2.3.1 and 3.2.1.
047 *     <li>OpenID Connect Core 1.0, section 9.
048 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
049 *         Authorization Grants (RFC 7523).
050 *     <li>Mutual TLS Profile for OAuth 2.0 (draft-ietf-oauth-mtls-04), section
051 *         2.
052 * </ul>
053 */
054@ThreadSafe
055public class ClientAuthenticationVerifier<T> {
056
057
058        /**
059         * The client credentials selector.
060         */
061        private final ClientCredentialsSelector<T> clientCredentialsSelector;
062        
063        
064        /**
065         * Optional client X.509 certificate binding verifier for
066         * {@code tls_client_auth}.
067         */
068        private final ClientX509CertificateBindingVerifier<T> certBindingVerifier;
069
070
071        /**
072         * The JWT assertion claims set verifier.
073         */
074        private final JWTAuthenticationClaimsSetVerifier claimsSetVerifier;
075
076
077        /**
078         * JWS verifier factory for private_key_jwt authentication.
079         */
080        private final JWSVerifierFactory jwsVerifierFactory = new DefaultJWSVerifierFactory();
081
082
083        /**
084         * Creates a new client authentication verifier.
085         *
086         * @param clientCredentialsSelector The client credentials selector.
087         *                                  Must not be {@code null}.
088         * @param certBindingVerifier       Optional client X.509 certificate
089         *                                  binding verifier for
090         *                                  {@code tls_client_auth},
091         *                                  {@code null} if not supported.
092         * @param expectedAudience          The permitted audience (aud) claim
093         *                                  values in JWT authentication
094         *                                  assertions. Must not be empty or
095         *                                  {@code null}. Should typically
096         *                                  contain the token endpoint URI and
097         *                                  for OpenID provider it may also
098         *                                  include the issuer URI.
099         */
100        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
101                                            final ClientX509CertificateBindingVerifier<T> certBindingVerifier,
102                                            final Set<Audience> expectedAudience) {
103
104                claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience);
105
106                if (clientCredentialsSelector == null) {
107                        throw new IllegalArgumentException("The client credentials selector must not be null");
108                }
109                
110                this.certBindingVerifier = certBindingVerifier;
111
112                this.clientCredentialsSelector = clientCredentialsSelector;
113        }
114
115
116        /**
117         * Returns the client credentials selector.
118         *
119         * @return The client credentials selector.
120         */
121        public ClientCredentialsSelector<T> getClientCredentialsSelector() {
122
123                return clientCredentialsSelector;
124        }
125        
126        
127        /**
128         * Returns the client X.509 certificate binding verifier for use in
129         * {@code tls_client_auth}.
130         *
131         * @return The client X.509 certificate binding verifier, {@code null}
132         *         if not specified.
133         */
134        public ClientX509CertificateBindingVerifier<T> getClientX509CertificateBindingVerifier() {
135                
136                return certBindingVerifier;
137        }
138        
139        
140        /**
141         * Returns the permitted audience values in JWT authentication
142         * assertions.
143         *
144         * @return The permitted audience (aud) claim values.
145         */
146        public Set<Audience> getExpectedAudience() {
147
148                return claimsSetVerifier.getExpectedAudience();
149        }
150
151
152        /**
153         * Verifies a client authentication request.
154         *
155         * @param clientAuth The client authentication. Must not be
156         *                   {@code null}.
157         * @param hints      Optional hints to the verifier, empty set of
158         *                   {@code null} if none.
159         * @param context    Additional context to be passed to the client
160         *                   credentials selector. May be {@code null}.
161         *
162         * @throws InvalidClientException If the client authentication is
163         *                                invalid, typically due to bad
164         *                                credentials.
165         * @throws JOSEException          If authentication failed due to an
166         *                                internal JOSE / JWT processing
167         *                                exception.
168         */
169        public void verify(final ClientAuthentication clientAuth, final Set<Hint> hints, final Context<T> context)
170                throws InvalidClientException, JOSEException {
171
172                if (clientAuth instanceof PlainClientSecret) {
173
174                        List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets(
175                                clientAuth.getClientID(),
176                                clientAuth.getMethod(),
177                                context);
178
179                        if (CollectionUtils.isEmpty(secretCandidates)) {
180                                throw InvalidClientException.NO_REGISTERED_SECRET;
181                        }
182
183                        PlainClientSecret plainAuth = (PlainClientSecret)clientAuth;
184
185                        for (Secret candidate: secretCandidates) {
186                                // constant time, SHA-256 based
187                                if (plainAuth.getClientSecret().equalsSHA256Based(candidate)) {
188                                        return; // success
189                                }
190                        }
191
192                        throw InvalidClientException.BAD_SECRET;
193
194                } else if (clientAuth instanceof ClientSecretJWT) {
195
196                        ClientSecretJWT jwtAuth = (ClientSecretJWT) clientAuth;
197
198                        // Check claims first before requesting secret from backend
199                        try {
200                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
201                        } catch (BadJWTException e) {
202                                throw new InvalidClientException("Bad / expired JWT claims: " + e.getMessage());
203                        }
204
205                        List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets(
206                                clientAuth.getClientID(),
207                                clientAuth.getMethod(),
208                                context);
209
210                        if (CollectionUtils.isEmpty(secretCandidates)) {
211                                throw InvalidClientException.NO_REGISTERED_SECRET;
212                        }
213
214                        SignedJWT assertion = jwtAuth.getClientAssertion();
215
216                        for (Secret candidate : secretCandidates) {
217
218                                boolean valid = assertion.verify(new MACVerifier(candidate.getValueBytes()));
219
220                                if (valid) {
221                                        return; // success
222                                }
223                        }
224
225                        throw InvalidClientException.BAD_JWT_HMAC;
226
227                } else if (clientAuth instanceof PrivateKeyJWT) {
228                        
229                        PrivateKeyJWT jwtAuth = (PrivateKeyJWT) clientAuth;
230                        
231                        // Check claims first before requesting / retrieving public keys
232                        try {
233                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
234                        } catch (BadJWTException e) {
235                                throw new InvalidClientException("Bad / expired JWT claims: " + e.getMessage());
236                        }
237                        
238                        List<? extends PublicKey> keyCandidates = clientCredentialsSelector.selectPublicKeys(
239                                jwtAuth.getClientID(),
240                                jwtAuth.getMethod(),
241                                jwtAuth.getClientAssertion().getHeader(),
242                                false,        // don't force refresh if we have a remote JWK set;
243                                // selector may however do so if it encounters an unknown key ID
244                                context);
245                        
246                        if (CollectionUtils.isEmpty(keyCandidates)) {
247                                throw InvalidClientException.NO_MATCHING_JWK;
248                        }
249                        
250                        SignedJWT assertion = jwtAuth.getClientAssertion();
251                        
252                        for (PublicKey candidate : keyCandidates) {
253                                
254                                if (candidate == null) {
255                                        continue; // skip
256                                }
257                                
258                                JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
259                                        jwtAuth.getClientAssertion().getHeader(),
260                                        candidate);
261                                
262                                boolean valid = assertion.verify(jwsVerifier);
263                                
264                                if (valid) {
265                                        return; // success
266                                }
267                        }
268                        
269                        // Second pass
270                        if (hints != null && hints.contains(Hint.CLIENT_HAS_REMOTE_JWK_SET)) {
271                                // Client possibly registered JWK set URL with keys that have no IDs
272                                // force JWK set reload from URL and retry
273                                keyCandidates = clientCredentialsSelector.selectPublicKeys(
274                                        jwtAuth.getClientID(),
275                                        jwtAuth.getMethod(),
276                                        jwtAuth.getClientAssertion().getHeader(),
277                                        true, // force reload of remote JWK set
278                                        context);
279                                
280                                if (CollectionUtils.isEmpty(keyCandidates)) {
281                                        throw InvalidClientException.NO_MATCHING_JWK;
282                                }
283                                
284                                assertion = jwtAuth.getClientAssertion();
285                                
286                                for (PublicKey candidate : keyCandidates) {
287                                        
288                                        if (candidate == null) {
289                                                continue; // skip
290                                        }
291                                        
292                                        JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
293                                                jwtAuth.getClientAssertion().getHeader(),
294                                                candidate);
295                                        
296                                        boolean valid = assertion.verify(jwsVerifier);
297                                        
298                                        if (valid) {
299                                                return; // success
300                                        }
301                                }
302                        }
303                        
304                        throw InvalidClientException.BAD_JWT_SIGNATURE;
305                        
306                } else if (clientAuth instanceof SelfSignedTLSClientAuthentication) {
307                        
308                        SelfSignedTLSClientAuthentication tlsClientAuth = (SelfSignedTLSClientAuthentication) clientAuth;
309                        
310                        X509Certificate clientCert = tlsClientAuth.getClientX509Certificate();
311                        
312                        if (clientCert == null) {
313                                // Sanity check
314                                throw new InvalidClientException("Missing client X.509 certificate");
315                        }
316                        
317                        // Self-signed certs bound to registered public key in client jwks / jwks_uri
318                        List<? extends PublicKey> keyCandidates = clientCredentialsSelector.selectPublicKeys(
319                                tlsClientAuth.getClientID(),
320                                tlsClientAuth.getMethod(),
321                                null,
322                                false, // don't force refresh if we have a remote JWK set;
323                                // selector may however do so if it encounters an unknown key ID
324                                context);
325                        
326                        if (CollectionUtils.isEmpty(keyCandidates)) {
327                                throw InvalidClientException.NO_MATCHING_JWK;
328                        }
329                        
330                        for (PublicKey candidate : keyCandidates) {
331                                
332                                if (candidate == null) {
333                                        continue; // skip
334                                }
335                                
336                                boolean valid = X509CertificateUtils.hasValidSignature(clientCert, candidate);
337                                
338                                if (valid) {
339                                        return; // success
340                                }
341                        }
342                        
343                        // Second pass
344                        if (hints != null && hints.contains(Hint.CLIENT_HAS_REMOTE_JWK_SET)) {
345                                // Client possibly registered JWK set URL with keys that have no IDs
346                                // force JWK set reload from URL and retry
347                                keyCandidates = clientCredentialsSelector.selectPublicKeys(
348                                        tlsClientAuth.getClientID(),
349                                        tlsClientAuth.getMethod(),
350                                        null,
351                                        true, // force reload of remote JWK set
352                                        context);
353                                
354                                if (CollectionUtils.isEmpty(keyCandidates)) {
355                                        throw InvalidClientException.NO_MATCHING_JWK;
356                                }
357                                
358                                for (PublicKey candidate : keyCandidates) {
359                                        
360                                        if (candidate == null) {
361                                                continue; // skip
362                                        }
363                                        
364                                        boolean valid = X509CertificateUtils.hasValidSignature(clientCert, candidate);
365                                        
366                                        if (valid) {
367                                                return; // success
368                                        }
369                                }
370                        }
371                        
372                        throw InvalidClientException.BAD_SELF_SIGNED_CLIENT_CERTIFICATE;
373                        
374                } else if (clientAuth instanceof TLSClientAuthentication) {
375                        
376                        if (certBindingVerifier == null) {
377                                throw new InvalidClientException("Mutual TLS client Authentication (tls_client_auth) not supported");
378                        }
379                        
380                        TLSClientAuthentication tlsClientAuth = (TLSClientAuthentication) clientAuth;
381                        
382                        certBindingVerifier.verifyCertificateBinding(
383                                clientAuth.getClientID(),
384                                tlsClientAuth.getClientX509CertificateSubjectDN(),
385                                tlsClientAuth.getClientX509CertificateRootDN(),
386                                context);
387
388                } else {
389                        throw new RuntimeException("Unexpected client authentication: " + clientAuth.getMethod());
390                }
391        }
392}