Skip to content

Commit bed3371

Browse files
committed
Support symmetric key for JwtDecoder
Fixes gh-5465
1 parent fc6b66f commit bed3371

File tree

15 files changed

+1024
-161
lines changed

15 files changed

+1024
-161
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java

+92-11
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,30 @@
1515
*/
1616
package org.springframework.security.oauth2.client.oidc.authentication;
1717

18-
import java.util.Map;
19-
import java.util.concurrent.ConcurrentHashMap;
20-
import java.util.function.Function;
21-
2218
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2319
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2420
import org.springframework.security.oauth2.core.OAuth2Error;
2521
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
2622
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
23+
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
24+
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
25+
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
2726
import org.springframework.security.oauth2.jwt.Jwt;
2827
import org.springframework.security.oauth2.jwt.JwtDecoder;
2928
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
3029
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
3130
import org.springframework.util.Assert;
3231
import org.springframework.util.StringUtils;
3332

33+
import javax.crypto.spec.SecretKeySpec;
34+
import java.nio.charset.StandardCharsets;
35+
import java.util.HashMap;
36+
import java.util.Map;
37+
import java.util.concurrent.ConcurrentHashMap;
38+
import java.util.function.Function;
39+
3440
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
41+
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withSecretKey;
3542

3643
/**
3744
* A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder}
@@ -47,14 +54,45 @@
4754
*/
4855
public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<ClientRegistration> {
4956
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
57+
private static Map<JwsAlgorithm, String> jcaAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
58+
{
59+
put(MacAlgorithm.HS256, "HmacSHA256");
60+
put(MacAlgorithm.HS384, "HmacSHA384");
61+
put(MacAlgorithm.HS512, "HmacSHA512");
62+
}
63+
};
5064
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
5165
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
66+
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
5267

5368
@Override
5469
public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
5570
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
5671
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
57-
if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
72+
NimbusJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
73+
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
74+
jwtDecoder.setJwtValidator(jwtValidator);
75+
return jwtDecoder;
76+
});
77+
}
78+
79+
private NimbusJwtDecoder buildDecoder(ClientRegistration clientRegistration) {
80+
JwsAlgorithm jwsAlgorithm = this.jwsAlgorithmResolver.apply(clientRegistration);
81+
if (jwsAlgorithm != null && SignatureAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
82+
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
83+
//
84+
// 6. If the ID Token is received via direct communication between the Client
85+
// and the Token Endpoint (which it is in this flow),
86+
// the TLS server validation MAY be used to validate the issuer in place of checking the token signature.
87+
// The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
88+
// using the algorithm specified in the JWT alg Header Parameter.
89+
// The Client MUST use the keys provided by the Issuer.
90+
//
91+
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
92+
// in the id_token_signed_response_alg parameter during Registration.
93+
94+
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
95+
if (!StringUtils.hasText(jwkSetUri)) {
5896
OAuth2Error oauth2Error = new OAuth2Error(
5997
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
6098
"Failed to find a Signature Verifier for Client Registration: '" +
@@ -64,12 +102,42 @@ public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
64102
);
65103
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
66104
}
67-
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
68-
NimbusJwtDecoder jwtDecoder = withJwkSetUri(jwkSetUri).build();
69-
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
70-
jwtDecoder.setJwtValidator(jwtValidator);
71-
return jwtDecoder;
72-
});
105+
return withJwkSetUri(jwkSetUri).jwsAlgorithm(jwsAlgorithm).build();
106+
} else if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
107+
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
108+
//
109+
// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, HS384, or HS512,
110+
// the octets of the UTF-8 representation of the client_secret
111+
// corresponding to the client_id contained in the aud (audience) Claim
112+
// are used as the key to validate the signature.
113+
// For MAC based algorithms, the behavior is unspecified if the aud is multi-valued or
114+
// if an azp value is present that is different than the aud value.
115+
116+
String clientSecret = clientRegistration.getClientSecret();
117+
if (!StringUtils.hasText(clientSecret)) {
118+
OAuth2Error oauth2Error = new OAuth2Error(
119+
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
120+
"Failed to find a Signature Verifier for Client Registration: '" +
121+
clientRegistration.getRegistrationId() +
122+
"'. Check to ensure you have configured the client secret.",
123+
null
124+
);
125+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
126+
}
127+
SecretKeySpec secretKeySpec = new SecretKeySpec(
128+
clientSecret.getBytes(StandardCharsets.UTF_8), jcaAlgorithmMappings.get(jwsAlgorithm));
129+
return withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
130+
}
131+
132+
OAuth2Error oauth2Error = new OAuth2Error(
133+
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
134+
"Failed to find a Signature Verifier for Client Registration: '" +
135+
clientRegistration.getRegistrationId() +
136+
"'. Check to ensure you have configured a valid JWS Algorithm: '" +
137+
jwsAlgorithm + "'",
138+
null
139+
);
140+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
73141
}
74142

75143
/**
@@ -82,4 +150,17 @@ public final void setJwtValidatorFactory(Function<ClientRegistration, OAuth2Toke
82150
Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
83151
this.jwtValidatorFactory = jwtValidatorFactory;
84152
}
153+
154+
/**
155+
* Sets the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
156+
* used for the signature or MAC on the {@link OidcIdToken ID Token}.
157+
* The default resolves to {@link SignatureAlgorithm#RS256 RS256} for all {@link ClientRegistration clients}.
158+
*
159+
* @param jwsAlgorithmResolver the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
160+
* for a specific {@link ClientRegistration client}
161+
*/
162+
public final void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver) {
163+
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
164+
this.jwsAlgorithmResolver = jwsAlgorithmResolver;
165+
}
85166
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java

+90-7
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,26 @@
2020
import org.springframework.security.oauth2.core.OAuth2Error;
2121
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
2222
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
23+
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
24+
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
25+
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
2326
import org.springframework.security.oauth2.jwt.Jwt;
2427
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
2528
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
2629
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
2730
import org.springframework.util.Assert;
2831
import org.springframework.util.StringUtils;
2932

33+
import javax.crypto.spec.SecretKeySpec;
34+
import java.nio.charset.StandardCharsets;
35+
import java.util.HashMap;
3036
import java.util.Map;
3137
import java.util.concurrent.ConcurrentHashMap;
3238
import java.util.function.Function;
3339

40+
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSetUri;
41+
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withSecretKey;
42+
3443
/**
3544
* A {@link ReactiveJwtDecoderFactory factory} that provides a {@link ReactiveJwtDecoder}
3645
* used for {@link OidcIdToken} signature verification.
@@ -45,14 +54,45 @@
4554
*/
4655
public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecoderFactory<ClientRegistration> {
4756
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
57+
private static Map<JwsAlgorithm, String> jcaAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
58+
{
59+
put(MacAlgorithm.HS256, "HmacSHA256");
60+
put(MacAlgorithm.HS384, "HmacSHA384");
61+
put(MacAlgorithm.HS512, "HmacSHA512");
62+
}
63+
};
4864
private final Map<String, ReactiveJwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
4965
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
66+
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
5067

5168
@Override
5269
public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
5370
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
5471
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
55-
if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
72+
NimbusReactiveJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
73+
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
74+
jwtDecoder.setJwtValidator(jwtValidator);
75+
return jwtDecoder;
76+
});
77+
}
78+
79+
private NimbusReactiveJwtDecoder buildDecoder(ClientRegistration clientRegistration) {
80+
JwsAlgorithm jwsAlgorithm = this.jwsAlgorithmResolver.apply(clientRegistration);
81+
if (jwsAlgorithm != null && SignatureAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
82+
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
83+
//
84+
// 6. If the ID Token is received via direct communication between the Client
85+
// and the Token Endpoint (which it is in this flow),
86+
// the TLS server validation MAY be used to validate the issuer in place of checking the token signature.
87+
// The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
88+
// using the algorithm specified in the JWT alg Header Parameter.
89+
// The Client MUST use the keys provided by the Issuer.
90+
//
91+
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
92+
// in the id_token_signed_response_alg parameter during Registration.
93+
94+
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
95+
if (!StringUtils.hasText(jwkSetUri)) {
5696
OAuth2Error oauth2Error = new OAuth2Error(
5797
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
5898
"Failed to find a Signature Verifier for Client Registration: '" +
@@ -62,12 +102,42 @@ public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
62102
);
63103
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
64104
}
65-
NimbusReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(
66-
clientRegistration.getProviderDetails().getJwkSetUri());
67-
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
68-
jwtDecoder.setJwtValidator(jwtValidator);
69-
return jwtDecoder;
70-
});
105+
return withJwkSetUri(jwkSetUri).jwsAlgorithm(jwsAlgorithm).build();
106+
} else if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
107+
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
108+
//
109+
// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, HS384, or HS512,
110+
// the octets of the UTF-8 representation of the client_secret
111+
// corresponding to the client_id contained in the aud (audience) Claim
112+
// are used as the key to validate the signature.
113+
// For MAC based algorithms, the behavior is unspecified if the aud is multi-valued or
114+
// if an azp value is present that is different than the aud value.
115+
116+
String clientSecret = clientRegistration.getClientSecret();
117+
if (!StringUtils.hasText(clientSecret)) {
118+
OAuth2Error oauth2Error = new OAuth2Error(
119+
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
120+
"Failed to find a Signature Verifier for Client Registration: '" +
121+
clientRegistration.getRegistrationId() +
122+
"'. Check to ensure you have configured the client secret.",
123+
null
124+
);
125+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
126+
}
127+
SecretKeySpec secretKeySpec = new SecretKeySpec(
128+
clientSecret.getBytes(StandardCharsets.UTF_8), jcaAlgorithmMappings.get(jwsAlgorithm));
129+
return withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
130+
}
131+
132+
OAuth2Error oauth2Error = new OAuth2Error(
133+
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
134+
"Failed to find a Signature Verifier for Client Registration: '" +
135+
clientRegistration.getRegistrationId() +
136+
"'. Check to ensure you have configured a valid JWS Algorithm: '" +
137+
jwsAlgorithm + "'",
138+
null
139+
);
140+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
71141
}
72142

73143
/**
@@ -80,4 +150,17 @@ public final void setJwtValidatorFactory(Function<ClientRegistration, OAuth2Toke
80150
Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
81151
this.jwtValidatorFactory = jwtValidatorFactory;
82152
}
153+
154+
/**
155+
* Sets the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
156+
* used for the signature or MAC on the {@link OidcIdToken ID Token}.
157+
* The default resolves to {@link SignatureAlgorithm#RS256 RS256} for all {@link ClientRegistration clients}.
158+
*
159+
* @param jwsAlgorithmResolver the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
160+
* for a specific {@link ClientRegistration client}
161+
*/
162+
public final void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver) {
163+
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
164+
this.jwsAlgorithmResolver = jwsAlgorithmResolver;
165+
}
83166
}

0 commit comments

Comments
 (0)