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 com.nimbusds.jose.JOSEException;
022import com.nimbusds.jose.JWSVerifier;
023import com.nimbusds.jose.crypto.MACVerifier;
024import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory;
025import com.nimbusds.jose.proc.JWSVerifierFactory;
026import com.nimbusds.jwt.SignedJWT;
027import com.nimbusds.jwt.proc.BadJWTException;
028import com.nimbusds.oauth2.sdk.auth.*;
029import com.nimbusds.oauth2.sdk.id.Audience;
030import com.nimbusds.oauth2.sdk.id.ClientID;
031import com.nimbusds.oauth2.sdk.id.JWTID;
032import com.nimbusds.oauth2.sdk.util.CollectionUtils;
033import com.nimbusds.oauth2.sdk.util.ListUtils;
034import com.nimbusds.oauth2.sdk.util.X509CertificateUtils;
035import net.jcip.annotations.ThreadSafe;
036
037import java.security.PublicKey;
038import java.security.cert.X509Certificate;
039import java.util.Date;
040import java.util.LinkedList;
041import java.util.List;
042import java.util.Set;
043
044
045/**
046 * Client authentication verifier.
047 *
048 * <p>Related specifications:
049 *
050 * <ul>
051 *     <li>OAuth 2.0 (RFC 6749), sections 2.3.1 and 3.2.1.
052 *     <li>OpenID Connect Core 1.0, section 9.
053 *     <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and
054 *         Authorization Grants (RFC 7523).
055 *     <li>OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound
056 *         Access Tokens (RFC 8705), section 2.
057 * </ul>
058 */
059@ThreadSafe
060public class ClientAuthenticationVerifier<T> {
061
062
063        /**
064         * The client credentials selector.
065         */
066        private final ClientCredentialsSelector<T> clientCredentialsSelector;
067        
068        
069        /**
070         * Optional client X.509 certificate binding verifier for
071         * {@code tls_client_auth}.
072         * @deprecated Replaced by pkiCertBindingVerifier
073         */
074        @Deprecated
075        private final ClientX509CertificateBindingVerifier<T> certBindingVerifier;
076
077
078        /**
079         * Optional client X.509 certificate binding verifier for
080         * {@code tls_client_auth}.
081         */
082        private final PKIClientX509CertificateBindingVerifier<T> pkiCertBindingVerifier;
083
084
085        /**
086         * The JWT assertion claims set verifier.
087         */
088        private final JWTAuthenticationClaimsSetVerifier claimsSetVerifier;
089
090
091        /**
092         * Optional expended JWT ID (jti) checker.
093         */
094        private final ExpendedJTIChecker<T> expendedJTIChecker;
095
096
097        /**
098         * JWS verifier factory for private_key_jwt authentication.
099         */
100        private final JWSVerifierFactory jwsVerifierFactory = new DefaultJWSVerifierFactory();
101
102
103        /**
104         * Creates a new client authentication verifier.
105         *
106         * @param clientCredentialsSelector The client credentials selector.
107         *                                  Must not be {@code null}.
108         * @param certBindingVerifier       Optional client X.509 certificate
109         *                                  binding verifier for
110         *                                  {@code tls_client_auth},
111         *                                  {@code null} if not supported.
112         * @param expectedAudience          The permitted audience (aud) claim
113         *                                  values in JWT authentication
114         *                                  assertions. Must not be empty or
115         *                                  {@code null}. Should typically
116         *                                  contain the token endpoint URI and
117         *                                  for OpenID provider it may also
118         *                                  include the issuer URI.
119         *
120         * @deprecated Use the constructor with {@link PKIClientX509CertificateBindingVerifier}
121         */
122        @Deprecated
123        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
124                                            final ClientX509CertificateBindingVerifier<T> certBindingVerifier,
125                                            final Set<Audience> expectedAudience) {
126
127                claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience);
128
129                if (clientCredentialsSelector == null) {
130                        throw new IllegalArgumentException("The client credentials selector must not be null");
131                }
132                
133                this.certBindingVerifier = certBindingVerifier;
134                this.pkiCertBindingVerifier = null;
135
136                this.clientCredentialsSelector = clientCredentialsSelector;
137
138                this.expendedJTIChecker = null;
139        }
140
141        
142        /**
143         * Creates a new client authentication verifier without support for
144         * {@code tls_client_auth}.
145         *
146         * @param clientCredentialsSelector The client credentials selector.
147         *                                  Must not be {@code null}.
148         * @param expectedAudience          The permitted audience (aud) claim
149         *                                  values in JWT authentication
150         *                                  assertions. Must not be empty or
151         *                                  {@code null}. Should typically
152         *                                  contain the token endpoint URI and
153         *                                  for OpenID provider it may also
154         *                                  include the issuer URI.
155         */
156        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
157                                            final Set<Audience> expectedAudience) {
158
159                this(clientCredentialsSelector, expectedAudience, null);
160        }
161
162
163        /**
164         * Creates a new client authentication verifier without support for
165         * {@code tls_client_auth}.
166         *
167         * @param clientCredentialsSelector The client credentials selector.
168         *                                  Must not be {@code null}.
169         * @param expectedAudience          The permitted audience (aud) claim
170         *                                  values in JWT authentication
171         *                                  assertions. Must not be empty or
172         *                                  {@code null}. Should typically
173         *                                  contain the token endpoint URI and
174         *                                  for OpenID provider it may also
175         *                                  include the issuer URI.
176         * @param expendedJTIChecker        Optional expended JWT ID (jti)
177         *                                  claim checker to prevent JWT
178         *                                  replay, {@code null} if none.
179         */
180        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
181                                            final Set<Audience> expectedAudience,
182                                            final ExpendedJTIChecker<T> expendedJTIChecker) {
183
184                claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience);
185
186                if (clientCredentialsSelector == null) {
187                        throw new IllegalArgumentException("The client credentials selector must not be null");
188                }
189
190                this.certBindingVerifier = null;
191                this.pkiCertBindingVerifier = null;
192
193                this.clientCredentialsSelector = clientCredentialsSelector;
194
195                this.expendedJTIChecker = expendedJTIChecker;
196        }
197        
198
199        /**
200         * Creates a new client authentication verifier.
201         *
202         * @param clientCredentialsSelector The client credentials selector.
203         *                                  Must not be {@code null}.
204         * @param pkiCertBindingVerifier    Optional client X.509 certificate
205         *                                  binding verifier for
206         *                                  {@code tls_client_auth},
207         *                                  {@code null} if not supported.
208         * @param expectedAudience          The permitted audience (aud) claim
209         *                                  values in JWT authentication
210         *                                  assertions. Must not be empty or
211         *                                  {@code null}. Should typically
212         *                                  contain the token endpoint URI and
213         *                                  for OpenID provider it may also
214         *                                  include the issuer URI.
215         */
216        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
217                                            final PKIClientX509CertificateBindingVerifier<T> pkiCertBindingVerifier,
218                                            final Set<Audience> expectedAudience) {
219
220                this(clientCredentialsSelector, pkiCertBindingVerifier, expectedAudience, null, -1L);
221        }
222
223
224        /**
225         * Creates a new client authentication verifier.
226         *
227         * @param clientCredentialsSelector The client credentials selector.
228         *                                  Must not be {@code null}.
229         * @param pkiCertBindingVerifier    Optional client X.509 certificate
230         *                                  binding verifier for
231         *                                  {@code tls_client_auth},
232         *                                  {@code null} if not supported.
233         * @param expectedAudience          The permitted audience (aud) claim
234         *                                  values in JWT authentication
235         *                                  assertions. Must not be empty or
236         *                                  {@code null}. Should typically
237         *                                  contain the token endpoint URI and
238         *                                  for OpenID provider it may also
239         *                                  include the issuer URI.
240         * @param expendedJTIChecker        Optional expended JWT ID (jti)
241         *                                  claim checker to prevent JWT
242         *                                  replay, {@code null} if none.
243         * @param expMaxAhead               The maximum number of seconds the
244         *                                  expiration time (exp) claim can be
245         *                                  ahead of the current time, if zero
246         *                                  or negative this check is disabled.
247         */
248        public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector,
249                                            final PKIClientX509CertificateBindingVerifier<T> pkiCertBindingVerifier,
250                                            final Set<Audience> expectedAudience,
251                                            final ExpendedJTIChecker<T> expendedJTIChecker,
252                                            final long expMaxAhead) {
253
254                claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience, expMaxAhead);
255
256                if (clientCredentialsSelector == null) {
257                        throw new IllegalArgumentException("The client credentials selector must not be null");
258                }
259
260                this.certBindingVerifier = null;
261                this.pkiCertBindingVerifier = pkiCertBindingVerifier;
262                this.clientCredentialsSelector = clientCredentialsSelector;
263                this.expendedJTIChecker = expendedJTIChecker;
264        }
265
266
267        /**
268         * Returns the client credentials selector.
269         *
270         * @return The client credentials selector.
271         */
272        public ClientCredentialsSelector<T> getClientCredentialsSelector() {
273
274                return clientCredentialsSelector;
275        }
276        
277        
278        /**
279         * Returns the client X.509 certificate binding verifier for use in
280         * {@code tls_client_auth}.
281         *
282         * @return The client X.509 certificate binding verifier, {@code null}
283         *         if not specified.
284         * @deprecated See {@link PKIClientX509CertificateBindingVerifier}
285         */
286        @Deprecated
287        public ClientX509CertificateBindingVerifier<T> getClientX509CertificateBindingVerifier() {
288                
289                return certBindingVerifier;
290        }
291        
292        
293        /**
294         * Returns the client X.509 certificate binding verifier for use in
295         * {@code tls_client_auth}.
296         *
297         * @return The client X.509 certificate binding verifier, {@code null}
298         *         if not specified.
299         */
300        public PKIClientX509CertificateBindingVerifier<T> getPKIClientX509CertificateBindingVerifier() {
301                
302                return pkiCertBindingVerifier;
303        }
304        
305        
306        /**
307         * Returns the permitted audience values in JWT authentication
308         * assertions.
309         *
310         * @return The permitted audience (aud) claim values.
311         */
312        public Set<Audience> getExpectedAudience() {
313
314                return claimsSetVerifier.getExpectedAudience();
315        }
316
317
318        /**
319         * Returns the optional expended JWT ID (jti) claim checker to prevent
320         * JWT replay.
321         *
322         * @return The expended JWT ID (jti) claim checker, {@code null} if
323         *         none.
324         */
325        public ExpendedJTIChecker<T> getExpendedJTIChecker() {
326
327                return expendedJTIChecker;
328        }
329
330
331        private static List<Secret> removeNullOrErased(final List<Secret> secrets) {
332                List<Secret> allSet = ListUtils.removeNullItems(secrets);
333                if (allSet == null) {
334                        return null;
335                }
336                List<Secret> out = new LinkedList<>();
337                for (Secret secret: secrets) {
338                        if (secret.getValue() != null && secret.getValueBytes() != null) {
339                                out.add(secret);
340                        }
341                }
342                return out;
343        }
344
345
346        private void preventJWTReplay(final JWTID jti,
347                                      final ClientID clientID,
348                                      final ClientAuthenticationMethod method,
349                                      final Context<T> context)
350                throws InvalidClientException {
351
352                if (jti == null || getExpendedJTIChecker() == null) {
353                        return;
354                }
355
356                if (getExpendedJTIChecker().isExpended(jti, clientID, method, context)) {
357                        throw new InvalidClientException("Detected JWT ID replay");
358                }
359        }
360
361
362        private void markExpended(final JWTID jti,
363                                  final Date exp,
364                                  final ClientID clientID,
365                                  final ClientAuthenticationMethod method,
366                                  final Context<T> context) {
367
368                if (jti == null || getExpendedJTIChecker() == null) {
369                        return;
370                }
371
372                getExpendedJTIChecker().markExpended(jti, exp, clientID, method, context);
373        }
374
375
376        /**
377         * Verifies a client authentication request.
378         *
379         * @param clientAuth The client authentication. Must not be
380         *                   {@code null}.
381         * @param hints      Optional hints to the verifier, empty set of
382         *                   {@code null} if none.
383         * @param context    Additional context to be passed to the client
384         *                   credentials selector. May be {@code null}.
385         *
386         * @throws InvalidClientException If the client authentication is
387         *                                invalid, typically due to bad
388         *                                credentials.
389         * @throws JOSEException          If authentication failed due to an
390         *                                internal JOSE / JWT processing
391         *                                exception.
392         */
393        public void verify(final ClientAuthentication clientAuth, final Set<Hint> hints, final Context<T> context)
394                throws InvalidClientException, JOSEException {
395
396                if (clientAuth instanceof PlainClientSecret) {
397
398                        List<Secret> secretCandidates = ListUtils.removeNullItems(
399                                clientCredentialsSelector.selectClientSecrets(
400                                        clientAuth.getClientID(),
401                                        clientAuth.getMethod(),
402                                        context
403                                )
404                        );
405
406                        if (CollectionUtils.isEmpty(secretCandidates)) {
407                                throw InvalidClientException.NO_REGISTERED_SECRET;
408                        }
409
410                        PlainClientSecret plainAuth = (PlainClientSecret)clientAuth;
411
412                        for (Secret candidate: secretCandidates) {
413                                
414                                // Constant time, SHA-256 based, unless overridden
415                                if (candidate.equals(plainAuth.getClientSecret())) {
416                                        return; // success
417                                }
418                        }
419
420                        throw InvalidClientException.BAD_SECRET;
421
422                } else if (clientAuth instanceof ClientSecretJWT) {
423
424                        ClientSecretJWT jwtAuth = (ClientSecretJWT) clientAuth;
425
426                        // Check claims first before requesting secret from backend
427                        JWTAuthenticationClaimsSet jwtAuthClaims = jwtAuth.getJWTAuthenticationClaimsSet();
428
429                        preventJWTReplay(jwtAuthClaims.getJWTID(), clientAuth.getClientID(), ClientAuthenticationMethod.CLIENT_SECRET_JWT, context);
430
431                        try {
432                                claimsSetVerifier.verify(jwtAuthClaims.toJWTClaimsSet(), null);
433                        } catch (BadJWTException e) {
434                                throw new InvalidClientException("Bad / expired JWT claims: " + e.getMessage());
435                        }
436
437                        List<Secret> secretCandidates = removeNullOrErased(
438                                clientCredentialsSelector.selectClientSecrets(
439                                        clientAuth.getClientID(),
440                                        clientAuth.getMethod(),
441                                        context
442                                )
443                        );
444
445                        if (CollectionUtils.isEmpty(secretCandidates)) {
446                                throw InvalidClientException.NO_REGISTERED_SECRET;
447                        }
448
449                        SignedJWT assertion = jwtAuth.getClientAssertion();
450
451                        for (Secret candidate : secretCandidates) {
452
453                                boolean valid = assertion.verify(new MACVerifier(candidate.getValueBytes()));
454
455                                if (valid) {
456                                        markExpended(jwtAuthClaims.getJWTID(), jwtAuthClaims.getExpirationTime(), clientAuth.getClientID(), ClientAuthenticationMethod.CLIENT_SECRET_JWT, context);
457                                        return; // success
458                                }
459                        }
460
461                        throw InvalidClientException.BAD_JWT_HMAC;
462
463                } else if (clientAuth instanceof PrivateKeyJWT) {
464                        
465                        PrivateKeyJWT jwtAuth = (PrivateKeyJWT) clientAuth;
466                        
467                        // Check claims first before requesting / retrieving public keys
468                        JWTAuthenticationClaimsSet jwtAuthClaims = jwtAuth.getJWTAuthenticationClaimsSet();
469
470                        preventJWTReplay(jwtAuthClaims.getJWTID(), clientAuth.getClientID(), ClientAuthenticationMethod.PRIVATE_KEY_JWT, context);
471
472                        try {
473                                claimsSetVerifier.verify(jwtAuthClaims.toJWTClaimsSet(), null);
474                        } catch (BadJWTException e) {
475                                throw new InvalidClientException("Bad / expired JWT claims: " + e.getMessage());
476                        }
477                        
478                        List<? extends PublicKey> keyCandidates = ListUtils.removeNullItems(
479                                clientCredentialsSelector.selectPublicKeys(
480                                        jwtAuth.getClientID(),
481                                        jwtAuth.getMethod(),
482                                        jwtAuth.getClientAssertion().getHeader(),
483                                        false,        // don't force refresh if we have a remote JWK set;
484                                        // selector may however do so if it encounters an unknown key ID
485                                        context
486                                )
487                        );
488                        
489                        if (CollectionUtils.isEmpty(keyCandidates)) {
490                                throw InvalidClientException.NO_MATCHING_JWK;
491                        }
492                        
493                        SignedJWT assertion = jwtAuth.getClientAssertion();
494                        
495                        for (PublicKey candidate : keyCandidates) {
496                                
497                                JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
498                                        jwtAuth.getClientAssertion().getHeader(),
499                                        candidate);
500                                
501                                boolean valid = assertion.verify(jwsVerifier);
502                                
503                                if (valid) {
504                                        markExpended(jwtAuthClaims.getJWTID(), jwtAuthClaims.getExpirationTime(), clientAuth.getClientID(), ClientAuthenticationMethod.PRIVATE_KEY_JWT, context);
505                                        return; // success
506                                }
507                        }
508                        
509                        // Second pass
510                        if (hints != null && hints.contains(Hint.CLIENT_HAS_REMOTE_JWK_SET)) {
511                                // Client possibly registered JWK set URL with keys that have no IDs
512                                // force JWK set reload from URL and retry
513                                keyCandidates = ListUtils.removeNullItems(
514                                        clientCredentialsSelector.selectPublicKeys(
515                                                jwtAuth.getClientID(),
516                                                jwtAuth.getMethod(),
517                                                jwtAuth.getClientAssertion().getHeader(),
518                                                true, // force reload of remote JWK set
519                                                context
520                                        )
521                                );
522                                
523                                if (CollectionUtils.isEmpty(keyCandidates)) {
524                                        throw InvalidClientException.NO_MATCHING_JWK;
525                                }
526                                
527                                assertion = jwtAuth.getClientAssertion();
528                                
529                                for (PublicKey candidate : keyCandidates) {
530                                        
531                                        JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier(
532                                                jwtAuth.getClientAssertion().getHeader(),
533                                                candidate);
534                                        
535                                        boolean valid = assertion.verify(jwsVerifier);
536                                        
537                                        if (valid) {
538                                                markExpended(jwtAuthClaims.getJWTID(), jwtAuthClaims.getExpirationTime(), clientAuth.getClientID(), ClientAuthenticationMethod.PRIVATE_KEY_JWT, context);
539                                                return; // success
540                                        }
541                                }
542                        }
543                        
544                        throw InvalidClientException.BAD_JWT_SIGNATURE;
545                        
546                } else if (clientAuth instanceof SelfSignedTLSClientAuthentication) {
547                        
548                        SelfSignedTLSClientAuthentication tlsClientAuth = (SelfSignedTLSClientAuthentication) clientAuth;
549                        
550                        X509Certificate clientCert = tlsClientAuth.getClientX509Certificate();
551                        
552                        if (clientCert == null) {
553                                // Sanity check
554                                throw new InvalidClientException("Missing client X.509 certificate");
555                        }
556                        
557                        // Self-signed certs bound to registered public key in client jwks / jwks_uri
558                        List<? extends PublicKey> keyCandidates = ListUtils.removeNullItems(
559                                clientCredentialsSelector.selectPublicKeys(
560                                        tlsClientAuth.getClientID(),
561                                        tlsClientAuth.getMethod(),
562                                        null,
563                                        false, // don't force refresh if we have a remote JWK set;
564                                        // selector may however do so if it encounters an unknown key ID
565                                        context
566                                )
567                        );
568                        
569                        if (CollectionUtils.isEmpty(keyCandidates)) {
570                                throw InvalidClientException.NO_MATCHING_JWK;
571                        }
572                        
573                        for (PublicKey candidate : keyCandidates) {
574                                
575                                boolean valid = X509CertificateUtils.publicKeyMatches(clientCert, candidate);
576                                
577                                if (valid) {
578                                        return; // success
579                                }
580                        }
581                        
582                        // Second pass
583                        if (hints != null && hints.contains(Hint.CLIENT_HAS_REMOTE_JWK_SET)) {
584                                // Client possibly registered JWK set URL with keys that have no IDs
585                                // force JWK set reload from URL and retry
586                                keyCandidates = ListUtils.removeNullItems(
587                                        clientCredentialsSelector.selectPublicKeys(
588                                                tlsClientAuth.getClientID(),
589                                                tlsClientAuth.getMethod(),
590                                                null,
591                                                true, // force reload of remote JWK set
592                                                context
593                                        )
594                                );
595                                
596                                if (CollectionUtils.isEmpty(keyCandidates)) {
597                                        throw InvalidClientException.NO_MATCHING_JWK;
598                                }
599                                
600                                for (PublicKey candidate : keyCandidates) {
601                                        
602                                        if (candidate == null) {
603                                                continue; // skip
604                                        }
605                                        
606                                        boolean valid = X509CertificateUtils.publicKeyMatches(clientCert, candidate);
607                                        
608                                        if (valid) {
609                                                return; // success
610                                        }
611                                }
612                        }
613                        
614                        throw InvalidClientException.BAD_SELF_SIGNED_CLIENT_CERTIFICATE;
615                        
616                } else if (clientAuth instanceof PKITLSClientAuthentication) {
617                        
618                        PKITLSClientAuthentication tlsClientAuth = (PKITLSClientAuthentication) clientAuth;
619                        if (pkiCertBindingVerifier != null) {
620                                pkiCertBindingVerifier.verifyCertificateBinding(
621                                                clientAuth.getClientID(),
622                                                tlsClientAuth.getClientX509Certificate(),
623                                                context);
624                                
625                        } else if (certBindingVerifier != null) {
626                                certBindingVerifier.verifyCertificateBinding(
627                                                clientAuth.getClientID(),
628                                                tlsClientAuth.getClientX509CertificateSubjectDN(),
629                                                context);
630                        } else {
631                                throw new InvalidClientException("Mutual TLS client Authentication (tls_client_auth) not supported");
632                        }
633                } else {
634                        throw new RuntimeException("Unexpected client authentication: " + clientAuth.getMethod());
635                }
636        }
637}