001package com.nimbusds.oauth2.sdk.auth.verifier; 002 003 004import java.security.PublicKey; 005import java.util.List; 006import java.util.Set; 007 008import net.jcip.annotations.ThreadSafe; 009 010import com.nimbusds.jose.JOSEException; 011import com.nimbusds.jose.JWSVerifier; 012import com.nimbusds.jose.crypto.MACVerifier; 013import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; 014import com.nimbusds.jose.proc.JWSVerifierFactory; 015import com.nimbusds.jwt.SignedJWT; 016import com.nimbusds.jwt.proc.BadJWTException; 017 018import com.nimbusds.oauth2.sdk.auth.*; 019import com.nimbusds.oauth2.sdk.id.Audience; 020 021 022/** 023 * Client authentication verifier. 024 * 025 * <p>Related specifications: 026 * 027 * <ul> 028 * <li>OAuth 2.0 (RFC 6749), sections 2.3.1 and 3.2.1. 029 * <li>OpenID Connect Core 1.0, section 9. 030 * <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and 031 * Authorization Grants (RFC 7523). 032 * </ul> 033 */ 034@ThreadSafe 035public class ClientAuthenticationVerifier<T> { 036 037 038 /** 039 * The client credentials selector. 040 */ 041 private final ClientCredentialsSelector<T> clientCredentialsSelector; 042 043 044 /** 045 * The JWT assertion claims set verifier. 046 */ 047 private final JWTAuthenticationClaimsSetVerifier claimsSetVerifier; 048 049 050 /** 051 * JWS verifier factory for private_key_jwt authentication. 052 */ 053 private final JWSVerifierFactory jwsVerifierFactory = new DefaultJWSVerifierFactory(); 054 055 056 /** 057 * Creates a new client authentication verifier. 058 * 059 * @param clientCredentialsSelector The client credentials selector. 060 * Must not be {@code null}. 061 * @param expectedAudience The permitted audience (aud) claim 062 * values in JWT authentication 063 * assertions. Must not be empty or 064 * {@code null}. Should typically 065 * contain the token endpoint URI and 066 * for OpenID provider it may also 067 * include the issuer URI. 068 */ 069 public ClientAuthenticationVerifier(final ClientCredentialsSelector<T> clientCredentialsSelector, 070 final Set<Audience> expectedAudience) { 071 072 claimsSetVerifier = new JWTAuthenticationClaimsSetVerifier(expectedAudience); 073 074 if (clientCredentialsSelector == null) { 075 throw new IllegalArgumentException("The client credentials selector must not be null"); 076 } 077 078 this.clientCredentialsSelector = clientCredentialsSelector; 079 } 080 081 082 /** 083 * Returns the client credentials selector. 084 * 085 * @return The client credentials selector. 086 */ 087 public ClientCredentialsSelector<T> getClientCredentialsSelector() { 088 089 return clientCredentialsSelector; 090 } 091 092 093 /** 094 * Returns the permitted audience values in JWT authentication 095 * assertions. 096 * 097 * @return The permitted audience (aud) claim values. 098 */ 099 public Set<Audience> getExpectedAudience() { 100 101 return claimsSetVerifier.getExpectedAudience(); 102 } 103 104 105 /** 106 * Verifies a client authentication request. 107 * 108 * @param clientAuth The client authentication. Must not be 109 * {@code null}. 110 * @param context Additional context to be passed to the client 111 * credentials selector. May be {@code null}. 112 * 113 * @return {@code true} if the client was successfully authenticated, 114 * {@code false} if the authentication failed due to an unknown 115 * client, invalid credential or unsupported authentication 116 * method. 117 * 118 * @throws JOSEException 119 */ 120 public boolean verify(final ClientAuthentication clientAuth, final Context<T> context) 121 throws JOSEException { 122 123 if (clientAuth instanceof PlainClientSecret) { 124 125 List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets( 126 clientAuth.getClientID(), 127 clientAuth.getMethod(), 128 context); 129 130 if (secretCandidates == null) { 131 return false; // invalid client 132 } 133 134 PlainClientSecret plainAuth = (PlainClientSecret)clientAuth; 135 136 for (Secret candidate: secretCandidates) { 137 if (plainAuth.getClientSecret().equals(candidate)) { 138 return true; // success 139 } 140 } 141 142 return false; // invalid client 143 144 } else if (clientAuth instanceof ClientSecretJWT) { 145 146 ClientSecretJWT jwtAuth = (ClientSecretJWT) clientAuth; 147 148 // Check claims first before calling backend 149 try { 150 claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet()); 151 } catch (BadJWTException e) { 152 return false; // invalid client 153 } 154 155 List<Secret> secretCandidates = clientCredentialsSelector.selectClientSecrets( 156 clientAuth.getClientID(), 157 clientAuth.getMethod(), 158 context); 159 160 if (secretCandidates == null) { 161 return false; // invalid client 162 } 163 164 SignedJWT assertion = jwtAuth.getClientAssertion(); 165 166 for (Secret candidate : secretCandidates) { 167 168 boolean valid = assertion.verify(new MACVerifier(candidate.getValueBytes())); 169 170 if (valid) { 171 return true; // success 172 } 173 } 174 175 return false; // invalid client 176 177 } else if (clientAuth instanceof PrivateKeyJWT) { 178 179 PrivateKeyJWT jwtAuth = (PrivateKeyJWT)clientAuth; 180 181 // Check claims first before calling backend 182 try { 183 claimsSetVerifier.verify(jwtAuth.getJWTAuthenticationClaimsSet().toJWTClaimsSet()); 184 } catch (BadJWTException e) { 185 return false; // invalid client 186 } 187 188 List<? extends PublicKey> keyCandidates = clientCredentialsSelector.selectPublicKeys( 189 jwtAuth.getClientID(), 190 jwtAuth.getMethod(), 191 jwtAuth.getClientAssertion().getHeader(), 192 context); 193 194 if (keyCandidates == null) { 195 return false; // invalid client 196 } 197 198 SignedJWT assertion = jwtAuth.getClientAssertion(); 199 200 for (PublicKey candidate: keyCandidates) { 201 202 if (candidate == null) { 203 continue; // skip 204 } 205 206 JWSVerifier jwsVerifier = jwsVerifierFactory.createJWSVerifier( 207 jwtAuth.getClientAssertion().getHeader(), 208 candidate); 209 210 boolean valid = assertion.verify(jwsVerifier); 211 212 if (valid) { 213 return true; // success 214 } 215 } 216 217 return false; // invalid client 218 219 } else { 220 throw new RuntimeException("Unexpected client authentication: " + clientAuth.getMethod()); 221 } 222 } 223}