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}