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