001package com.nimbusds.oauth2.sdk.auth.verifier; 002 003 004import java.security.PublicKey; 005import java.util.List; 006import java.util.Set; 007 008import com.nimbusds.jose.JOSEException; 009import com.nimbusds.jose.JWSVerifier; 010import com.nimbusds.jose.crypto.MACVerifier; 011import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; 012import com.nimbusds.jose.proc.JWSVerifierFactory; 013import com.nimbusds.jwt.SignedJWT; 014import com.nimbusds.jwt.proc.BadJWTException; 015import com.nimbusds.oauth2.sdk.auth.*; 016import com.nimbusds.oauth2.sdk.id.Audience; 017import net.jcip.annotations.ThreadSafe; 018import org.apache.commons.collections4.CollectionUtils; 019 020 021/** 022 * Client authentication verifier. 023 * 024 * <p>Related specifications: 025 * 026 * <ul> 027 * <li>OAuth 2.0 (RFC 6749), sections 2.3.1 and 3.2.1. 028 * <li>OpenID Connect Core 1.0, section 9. 029 * <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and 030 * Authorization Grants (RFC 7523). 031 * </ul> 032 */ 033@ThreadSafe 034public class ClientAuthenticationVerifier<T> { 035 036 037 /** 038 * The client credentials selector. 039 */ 040 private final ClientCredentialsSelector<T> clientCredentialsSelector; 041 042 043 /** 044 * The JWT assertion claims set verifier. 045 */ 046 private final JWTAuthenticationClaimsSetVerifier claimsSetVerifier; 047 048 049 /** 050 * JWS verifier factory for private_key_jwt authentication. 051 */ 052 private final JWSVerifierFactory jwsVerifierFactory = new DefaultJWSVerifierFactory(); 053 054 055 /** 056 * Creates a new client authentication verifier. 057 * 058 * @param clientCredentialsSelector The client credentials selector. 059 * Must not be {@code null}. 060 * @param expectedAudience The permitted audience (aud) claim 061 * values in JWT authentication 062 * assertions. Must not be empty or 063 * {@code null}. Should typically 064 * contain the token endpoint URI and 065 * for OpenID provider it may also 066 * include the issuer URI. 067 */ 068 public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector, 069 final Set<Audience> expectedAudience) { 070 071 claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience); 072 073 if (clientCredentialsSelector == null) { 074 throw new IllegalArgumentException("The client credentials selector must not be null"); 075 } 076 077 this.clientCredentialsSelector = clientCredentialsSelector; 078 } 079 080 081 /** 082 * Returns the client credentials selector. 083 * 084 * @return The client credentials selector. 085 */ 086 public ClientCredentialsSelector<T> getClientCredentialsSelector() { 087 088 return clientCredentialsSelector; 089 } 090 091 092 /** 093 * Returns the permitted audience values in JWT authentication 094 * assertions. 095 * 096 * @return The permitted audience (aud) claim values. 097 */ 098 public Set<Audience> getExpectedAudience() { 099 100 return claimsSetVerifier.getExpectedAudience(); 101 } 102 103 104 /** 105 * Verifies a client authentication request. 106 * 107 * @param clientAuth The client authentication. Must not be 108 * {@code null}. 109 * @param hints Optional hints to the verifier, empty set of 110 * {@code null} if none. 111 * @param context Additional context to be passed to the client 112 * credentials selector. May be {@code null}. 113 * 114 * @throws InvalidClientException If the client authentication is 115 * invalid, typically due to bad 116 * credentials. 117 * @throws JOSEException If authentication failed due to an 118 * internal JOSE / JWT processing 119 * exception. 120 */ 121 public void verify(final ClientAuthentication clientAuth, final Set<Hint> hints, final Context<T> context) 122 throws InvalidClientException, JOSEException { 123 124 if (clientAuth instanceof PlainClientSecret) { 125 126 List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets( 127 clientAuth.getClientID(), 128 clientAuth.getMethod(), 129 context); 130 131 if (CollectionUtils.isEmpty(secretCandidates)) { 132 throw InvalidClientException.NO_REGISTERED_SECRET; 133 } 134 135 PlainClientSecret plainAuth = (PlainClientSecret)clientAuth; 136 137 for (Secret candidate: secretCandidates) { 138 if (plainAuth.getClientSecret().equals(candidate)) { 139 return; // success 140 } 141 } 142 143 throw InvalidClientException.BAD_SECRET; 144 145 } else if (clientAuth instanceof ClientSecretJWT) { 146 147 ClientSecretJWT jwtAuth = (ClientSecretJWT) clientAuth; 148 149 // Check claims first before requesting secret from backend 150 try { 151 claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet()); 152 } catch (BadJWTException e) { 153 throw InvalidClientException.BAD_JWT_CLAIMS; 154 } 155 156 List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets( 157 clientAuth.getClientID(), 158 clientAuth.getMethod(), 159 context); 160 161 if (CollectionUtils.isEmpty(secretCandidates)) { 162 throw InvalidClientException.NO_REGISTERED_SECRET; 163 } 164 165 SignedJWT assertion = jwtAuth.getClientAssertion(); 166 167 for (Secret candidate : secretCandidates) { 168 169 boolean valid = assertion.verify(new MACVerifier(candidate.getValueBytes())); 170 171 if (valid) { 172 return; // success 173 } 174 } 175 176 throw InvalidClientException.BAD_JWT_HMAC; 177 178 } else if (clientAuth instanceof PrivateKeyJWT) { 179 180 PrivateKeyJWT jwtAuth = (PrivateKeyJWT)clientAuth; 181 182 // Check claims first before requesting / retrieving public keys 183 try { 184 claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet()); 185 } catch (BadJWTException e) { 186 throw InvalidClientException.BAD_JWT_CLAIMS; 187 } 188 189 List<? extends PublicKey> keyCandidates = clientCredentialsSelector.selectPublicKeys( 190 jwtAuth.getClientID(), 191 jwtAuth.getMethod(), 192 jwtAuth.getClientAssertion().getHeader(), 193 false, // don't force refresh if we have a remote JWK set; 194 // selector may however do so if it encounters an unknown key ID 195 context); 196 197 if (CollectionUtils.isEmpty(keyCandidates)) { 198 throw InvalidClientException.NO_MATCHING_JWK; 199 } 200 201 SignedJWT assertion = jwtAuth.getClientAssertion(); 202 203 for (PublicKey candidate: keyCandidates) { 204 205 if (candidate == null) { 206 continue; // skip 207 } 208 209 JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier( 210 jwtAuth.getClientAssertion().getHeader(), 211 candidate); 212 213 boolean valid = assertion.verify(jwsVerifier); 214 215 if (valid) { 216 return; // success 217 } 218 } 219 220 // Second pass 221 if (hints != null && hints.contains(Hint.CLIENT_HAS_REMOTE_JWK_SET)) { 222 // Client possibly registered JWK set URL with keys that have no IDs 223 // force JWK set reload from URL and retry 224 keyCandidates = clientCredentialsSelector.selectPublicKeys( 225 jwtAuth.getClientID(), 226 jwtAuth.getMethod(), 227 jwtAuth.getClientAssertion().getHeader(), 228 true, // force reload of remote JWK set 229 context); 230 231 if (CollectionUtils.isEmpty(keyCandidates)) { 232 throw InvalidClientException.NO_MATCHING_JWK; 233 } 234 235 assertion = jwtAuth.getClientAssertion(); 236 237 for (PublicKey candidate: keyCandidates) { 238 239 if (candidate == null) { 240 continue; // skip 241 } 242 243 JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier( 244 jwtAuth.getClientAssertion().getHeader(), 245 candidate); 246 247 boolean valid = assertion.verify(jwsVerifier); 248 249 if (valid) { 250 return; // success 251 } 252 } 253 } 254 255 throw InvalidClientException.BAD_JWT_SIGNATURE; 256 257 } else { 258 throw new RuntimeException("Unexpected client authentication: " + clientAuth.getMethod()); 259 } 260 } 261}