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