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.CollectionUtils;
036import com.nimbusds.oauth2.sdk.util.X509CertificateUtils;
037import net.jcip.annotations.ThreadSafe;
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>OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound
051 *         Access Tokens (RFC 8705), section 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         * @deprecated Replaced by pkiCertBindingVerifier
068         */
069        @Deprecated
070        private final ClientX509CertificateBindingVerifier<T> certBindingVerifier;
071
072
073        /**
074         * Optional client X.509 certificate binding verifier for
075         * {@code tls_client_auth}.
076         */
077        private final PKIClientX509CertificateBindingVerifier<T> pkiCertBindingVerifier;
078
079
080        /**
081         * The JWT assertion claims set verifier.
082         */
083        private final JWTAuthenticationClaimsSetVerifier claimsSetVerifier;
084
085
086        /**
087         * JWS verifier factory for private_key_jwt authentication.
088         */
089        private final JWSVerifierFactory jwsVerifierFactory = new DefaultJWSVerifierFactory();
090
091
092        /**
093         * Creates a new client authentication verifier.
094         *
095         * @param clientCredentialsSelector The client credentials selector.
096         *                                  Must not be {@code null}.
097         * @param certBindingVerifier       Optional client X.509 certificate
098         *                                  binding verifier for
099         *                                  {@code tls_client_auth},
100         *                                  {@code null} if not supported.
101         * @param expectedAudience          The permitted audience (aud) claim
102         *                                  values in JWT authentication
103         *                                  assertions. Must not be empty or
104         *                                  {@code null}. Should typically
105         *                                  contain the token endpoint URI and
106         *                                  for OpenID provider it may also
107         *                                  include the issuer URI.
108         * @deprecated Use the constructor with {@link PKIClientX509CertificateBindingVerifier}
109         */
110        @Deprecated
111        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
112                                            final ClientX509CertificateBindingVerifier<T> certBindingVerifier,
113                                            final Set<Audience> expectedAudience) {
114
115                claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience);
116
117                if (clientCredentialsSelector == null) {
118                        throw new IllegalArgumentException("The client credentials selector must not be null");
119                }
120                
121                this.certBindingVerifier = certBindingVerifier;
122                this.pkiCertBindingVerifier = null;
123
124                this.clientCredentialsSelector = clientCredentialsSelector;
125        }
126
127        
128        /**
129         * Creates a new client authentication verifier without support for
130         * {@code tls_client_auth}.
131         *
132         * @param clientCredentialsSelector The client credentials selector.
133         *                                  Must not be {@code null}.
134         * @param expectedAudience          The permitted audience (aud) claim
135         *                                  values in JWT authentication
136         *                                  assertions. Must not be empty or
137         *                                  {@code null}. Should typically
138         *                                  contain the token endpoint URI and
139         *                                  for OpenID provider it may also
140         *                                  include the issuer URI.
141         */
142        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
143                                            final Set<Audience> expectedAudience) {
144
145                claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience);
146
147                if (clientCredentialsSelector == null) {
148                        throw new IllegalArgumentException("The client credentials selector must not be null");
149                }
150                
151                this.certBindingVerifier = null;
152                this.pkiCertBindingVerifier = null;
153
154                this.clientCredentialsSelector = clientCredentialsSelector;
155        }
156        
157
158        /**
159         * Creates a new client authentication verifier.
160         *
161         * @param clientCredentialsSelector The client credentials selector.
162         *                                  Must not be {@code null}.
163         * @param pkiCertBindingVerifier    Optional client X.509 certificate
164         *                                  binding verifier for
165         *                                  {@code tls_client_auth},
166         *                                  {@code null} if not supported.
167         * @param expectedAudience          The permitted audience (aud) claim
168         *                                  values in JWT authentication
169         *                                  assertions. Must not be empty or
170         *                                  {@code null}. Should typically
171         *                                  contain the token endpoint URI and
172         *                                  for OpenID provider it may also
173         *                                  include the issuer URI.
174         */
175        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
176                                            final PKIClientX509CertificateBindingVerifier<T> pkiCertBindingVerifier,
177                                            final Set<Audience> expectedAudience) {
178
179                claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience);
180
181                if (clientCredentialsSelector == null) {
182                        throw new IllegalArgumentException("The client credentials selector must not be null");
183                }
184                
185                this.certBindingVerifier = null;
186                this.pkiCertBindingVerifier = pkiCertBindingVerifier;
187
188                this.clientCredentialsSelector = clientCredentialsSelector;
189        }
190
191
192        /**
193         * Returns the client credentials selector.
194         *
195         * @return The client credentials selector.
196         */
197        public ClientCredentialsSelector<T> getClientCredentialsSelector() {
198
199                return clientCredentialsSelector;
200        }
201        
202        
203        /**
204         * Returns the client X.509 certificate binding verifier for use in
205         * {@code tls_client_auth}.
206         *
207         * @return The client X.509 certificate binding verifier, {@code null}
208         *         if not specified.
209         * @deprecated See {@link PKIClientX509CertificateBindingVerifier}
210         */
211        @Deprecated
212        public ClientX509CertificateBindingVerifier<T> getClientX509CertificateBindingVerifier() {
213                
214                return certBindingVerifier;
215        }
216        
217        
218        /**
219         * Returns the client X.509 certificate binding verifier for use in
220         * {@code tls_client_auth}.
221         *
222         * @return The client X.509 certificate binding verifier, {@code null}
223         *         if not specified.
224         */
225        public PKIClientX509CertificateBindingVerifier<T> getPKIClientX509CertificateBindingVerifier() {
226                
227                return pkiCertBindingVerifier;
228        }
229        
230        
231        /**
232         * Returns the permitted audience values in JWT authentication
233         * assertions.
234         *
235         * @return The permitted audience (aud) claim values.
236         */
237        public Set<Audience> getExpectedAudience() {
238
239                return claimsSetVerifier.getExpectedAudience();
240        }
241
242
243        /**
244         * Verifies a client authentication request.
245         *
246         * @param clientAuth The client authentication. Must not be
247         *                   {@code null}.
248         * @param hints      Optional hints to the verifier, empty set of
249         *                   {@code null} if none.
250         * @param context    Additional context to be passed to the client
251         *                   credentials selector. May be {@code null}.
252         *
253         * @throws InvalidClientException If the client authentication is
254         *                                invalid, typically due to bad
255         *                                credentials.
256         * @throws JOSEException          If authentication failed due to an
257         *                                internal JOSE / JWT processing
258         *                                exception.
259         */
260        public void verify(final ClientAuthentication clientAuth, final Set<Hint> hints, final Context<T> context)
261                throws InvalidClientException, JOSEException {
262
263                if (clientAuth instanceof PlainClientSecret) {
264
265                        List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets(
266                                clientAuth.getClientID(),
267                                clientAuth.getMethod(),
268                                context);
269
270                        if (CollectionUtils.isEmpty(secretCandidates)) {
271                                throw InvalidClientException.NO_REGISTERED_SECRET;
272                        }
273
274                        PlainClientSecret plainAuth = (PlainClientSecret)clientAuth;
275
276                        for (Secret candidate: secretCandidates) {
277                                // constant time, SHA-256 based
278                                if (plainAuth.getClientSecret().equalsSHA256Based(candidate)) {
279                                        return; // success
280                                }
281                        }
282
283                        throw InvalidClientException.BAD_SECRET;
284
285                } else if (clientAuth instanceof ClientSecretJWT) {
286
287                        ClientSecretJWT jwtAuth = (ClientSecretJWT) clientAuth;
288
289                        // Check claims first before requesting secret from backend
290                        try {
291                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
292                        } catch (BadJWTException e) {
293                                throw new InvalidClientException("Bad / expired JWT claims: " + e.getMessage());
294                        }
295
296                        List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets(
297                                clientAuth.getClientID(),
298                                clientAuth.getMethod(),
299                                context);
300
301                        if (CollectionUtils.isEmpty(secretCandidates)) {
302                                throw InvalidClientException.NO_REGISTERED_SECRET;
303                        }
304
305                        SignedJWT assertion = jwtAuth.getClientAssertion();
306
307                        for (Secret candidate : secretCandidates) {
308
309                                boolean valid = assertion.verify(new MACVerifier(candidate.getValueBytes()));
310
311                                if (valid) {
312                                        return; // success
313                                }
314                        }
315
316                        throw InvalidClientException.BAD_JWT_HMAC;
317
318                } else if (clientAuth instanceof PrivateKeyJWT) {
319                        
320                        PrivateKeyJWT jwtAuth = (PrivateKeyJWT) clientAuth;
321                        
322                        // Check claims first before requesting / retrieving public keys
323                        try {
324                                claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet());
325                        } catch (BadJWTException e) {
326                                throw new InvalidClientException("Bad / expired JWT claims: " + e.getMessage());
327                        }
328                        
329                        List<? extends PublicKey> keyCandidates = clientCredentialsSelector.selectPublicKeys(
330                                jwtAuth.getClientID(),
331                                jwtAuth.getMethod(),
332                                jwtAuth.getClientAssertion().getHeader(),
333                                false,        // don't force refresh if we have a remote JWK set;
334                                // selector may however do so if it encounters an unknown key ID
335                                context);
336                        
337                        if (CollectionUtils.isEmpty(keyCandidates)) {
338                                throw InvalidClientException.NO_MATCHING_JWK;
339                        }
340                        
341                        SignedJWT assertion = jwtAuth.getClientAssertion();
342                        
343                        for (PublicKey candidate : keyCandidates) {
344                                
345                                if (candidate == null) {
346                                        continue; // skip
347                                }
348                                
349                                JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
350                                        jwtAuth.getClientAssertion().getHeader(),
351                                        candidate);
352                                
353                                boolean valid = assertion.verify(jwsVerifier);
354                                
355                                if (valid) {
356                                        return; // success
357                                }
358                        }
359                        
360                        // Second pass
361                        if (hints != null && hints.contains(Hint.CLIENT_HAS_REMOTE_JWK_SET)) {
362                                // Client possibly registered JWK set URL with keys that have no IDs
363                                // force JWK set reload from URL and retry
364                                keyCandidates = clientCredentialsSelector.selectPublicKeys(
365                                        jwtAuth.getClientID(),
366                                        jwtAuth.getMethod(),
367                                        jwtAuth.getClientAssertion().getHeader(),
368                                        true, // force reload of remote JWK set
369                                        context);
370                                
371                                if (CollectionUtils.isEmpty(keyCandidates)) {
372                                        throw InvalidClientException.NO_MATCHING_JWK;
373                                }
374                                
375                                assertion = jwtAuth.getClientAssertion();
376                                
377                                for (PublicKey candidate : keyCandidates) {
378                                        
379                                        if (candidate == null) {
380                                                continue; // skip
381                                        }
382                                        
383                                        JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
384                                                jwtAuth.getClientAssertion().getHeader(),
385                                                candidate);
386                                        
387                                        boolean valid = assertion.verify(jwsVerifier);
388                                        
389                                        if (valid) {
390                                                return; // success
391                                        }
392                                }
393                        }
394                        
395                        throw InvalidClientException.BAD_JWT_SIGNATURE;
396                        
397                } else if (clientAuth instanceof SelfSignedTLSClientAuthentication) {
398                        
399                        SelfSignedTLSClientAuthentication tlsClientAuth = (SelfSignedTLSClientAuthentication) clientAuth;
400                        
401                        X509Certificate clientCert = tlsClientAuth.getClientX509Certificate();
402                        
403                        if (clientCert == null) {
404                                // Sanity check
405                                throw new InvalidClientException("Missing client X.509 certificate");
406                        }
407                        
408                        // Self-signed certs bound to registered public key in client jwks / jwks_uri
409                        List<? extends PublicKey> keyCandidates = clientCredentialsSelector.selectPublicKeys(
410                                tlsClientAuth.getClientID(),
411                                tlsClientAuth.getMethod(),
412                                null,
413                                false, // don't force refresh if we have a remote JWK set;
414                                // selector may however do so if it encounters an unknown key ID
415                                context);
416                        
417                        if (CollectionUtils.isEmpty(keyCandidates)) {
418                                throw InvalidClientException.NO_MATCHING_JWK;
419                        }
420                        
421                        for (PublicKey candidate : keyCandidates) {
422                                
423                                if (candidate == null) {
424                                        continue; // skip
425                                }
426                                
427                                boolean valid = X509CertificateUtils.publicKeyMatches(clientCert, candidate);
428                                
429                                if (valid) {
430                                        return; // success
431                                }
432                        }
433                        
434                        // Second pass
435                        if (hints != null && hints.contains(Hint.CLIENT_HAS_REMOTE_JWK_SET)) {
436                                // Client possibly registered JWK set URL with keys that have no IDs
437                                // force JWK set reload from URL and retry
438                                keyCandidates = clientCredentialsSelector.selectPublicKeys(
439                                        tlsClientAuth.getClientID(),
440                                        tlsClientAuth.getMethod(),
441                                        null,
442                                        true, // force reload of remote JWK set
443                                        context);
444                                
445                                if (CollectionUtils.isEmpty(keyCandidates)) {
446                                        throw InvalidClientException.NO_MATCHING_JWK;
447                                }
448                                
449                                for (PublicKey candidate : keyCandidates) {
450                                        
451                                        if (candidate == null) {
452                                                continue; // skip
453                                        }
454                                        
455                                        boolean valid = X509CertificateUtils.publicKeyMatches(clientCert, candidate);
456                                        
457                                        if (valid) {
458                                                return; // success
459                                        }
460                                }
461                        }
462                        
463                        throw InvalidClientException.BAD_SELF_SIGNED_CLIENT_CERTIFICATE;
464                        
465                } else if (clientAuth instanceof PKITLSClientAuthentication) {
466                        
467                        PKITLSClientAuthentication tlsClientAuth = (PKITLSClientAuthentication) clientAuth;
468                        if (pkiCertBindingVerifier != null) {
469                                pkiCertBindingVerifier.verifyCertificateBinding(
470                                                clientAuth.getClientID(),
471                                                tlsClientAuth.getClientX509Certificate(),
472                                                context);
473                                
474                        } else if (certBindingVerifier != null) {
475                                certBindingVerifier.verifyCertificateBinding(
476                                                clientAuth.getClientID(),
477                                                tlsClientAuth.getClientX509CertificateSubjectDN(),
478                                                context);
479                        } else {
480                                throw new InvalidClientException("Mutual TLS client Authentication (tls_client_auth) not supported");
481                        }
482                } else {
483                        throw new RuntimeException("Unexpected client authentication: " + clientAuth.getMethod());
484                }
485        }
486}