Support symmetric key for JwtDecoder

Fixes gh-5465
This commit is contained in:
Joe Grandja 2019-04-11 05:05:16 -04:00
parent fc6b66fdb3
commit bed3371b80
15 changed files with 1024 additions and 161 deletions

View File

@ -15,15 +15,14 @@
*/
package org.springframework.security.oauth2.client.oidc.authentication;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
@ -31,7 +30,15 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withSecretKey;
/**
* A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder}
@ -47,14 +54,45 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSe
*/
public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<ClientRegistration> {
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
private static Map<JwsAlgorithm, String> jcaAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
{
put(MacAlgorithm.HS256, "HmacSHA256");
put(MacAlgorithm.HS384, "HmacSHA384");
put(MacAlgorithm.HS512, "HmacSHA512");
}
};
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
@Override
public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
NimbusJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
jwtDecoder.setJwtValidator(jwtValidator);
return jwtDecoder;
});
}
private NimbusJwtDecoder buildDecoder(ClientRegistration clientRegistration) {
JwsAlgorithm jwsAlgorithm = this.jwsAlgorithmResolver.apply(clientRegistration);
if (jwsAlgorithm != null && SignatureAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//
// 6. If the ID Token is received via direct communication between the Client
// and the Token Endpoint (which it is in this flow),
// the TLS server validation MAY be used to validate the issuer in place of checking the token signature.
// The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
// using the algorithm specified in the JWT alg Header Parameter.
// The Client MUST use the keys provided by the Issuer.
//
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
// in the id_token_signed_response_alg parameter during Registration.
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
if (!StringUtils.hasText(jwkSetUri)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
@ -64,12 +102,42 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
NimbusJwtDecoder jwtDecoder = withJwkSetUri(jwkSetUri).build();
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
jwtDecoder.setJwtValidator(jwtValidator);
return jwtDecoder;
});
return withJwkSetUri(jwkSetUri).jwsAlgorithm(jwsAlgorithm).build();
} else if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//
// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, HS384, or HS512,
// the octets of the UTF-8 representation of the client_secret
// corresponding to the client_id contained in the aud (audience) Claim
// are used as the key to validate the signature.
// For MAC based algorithms, the behavior is unspecified if the aud is multi-valued or
// if an azp value is present that is different than the aud value.
String clientSecret = clientRegistration.getClientSecret();
if (!StringUtils.hasText(clientSecret)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured the client secret.",
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
SecretKeySpec secretKeySpec = new SecretKeySpec(
clientSecret.getBytes(StandardCharsets.UTF_8), jcaAlgorithmMappings.get(jwsAlgorithm));
return withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
}
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured a valid JWS Algorithm: '" +
jwsAlgorithm + "'",
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
/**
@ -82,4 +150,17 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
this.jwtValidatorFactory = jwtValidatorFactory;
}
/**
* Sets the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
* used for the signature or MAC on the {@link OidcIdToken ID Token}.
* The default resolves to {@link SignatureAlgorithm#RS256 RS256} for all {@link ClientRegistration clients}.
*
* @param jwsAlgorithmResolver the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
* for a specific {@link ClientRegistration client}
*/
public final void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver) {
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
this.jwsAlgorithmResolver = jwsAlgorithmResolver;
}
}

View File

@ -20,6 +20,9 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
@ -27,10 +30,16 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSetUri;
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withSecretKey;
/**
* A {@link ReactiveJwtDecoderFactory factory} that provides a {@link ReactiveJwtDecoder}
* used for {@link OidcIdToken} signature verification.
@ -45,14 +54,45 @@ import java.util.function.Function;
*/
public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecoderFactory<ClientRegistration> {
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
private static Map<JwsAlgorithm, String> jcaAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
{
put(MacAlgorithm.HS256, "HmacSHA256");
put(MacAlgorithm.HS384, "HmacSHA384");
put(MacAlgorithm.HS512, "HmacSHA512");
}
};
private final Map<String, ReactiveJwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
@Override
public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
NimbusReactiveJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
jwtDecoder.setJwtValidator(jwtValidator);
return jwtDecoder;
});
}
private NimbusReactiveJwtDecoder buildDecoder(ClientRegistration clientRegistration) {
JwsAlgorithm jwsAlgorithm = this.jwsAlgorithmResolver.apply(clientRegistration);
if (jwsAlgorithm != null && SignatureAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//
// 6. If the ID Token is received via direct communication between the Client
// and the Token Endpoint (which it is in this flow),
// the TLS server validation MAY be used to validate the issuer in place of checking the token signature.
// The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
// using the algorithm specified in the JWT alg Header Parameter.
// The Client MUST use the keys provided by the Issuer.
//
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
// in the id_token_signed_response_alg parameter during Registration.
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
if (!StringUtils.hasText(jwkSetUri)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
@ -62,12 +102,42 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
NimbusReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(
clientRegistration.getProviderDetails().getJwkSetUri());
OAuth2TokenValidator<Jwt> jwtValidator = this.jwtValidatorFactory.apply(clientRegistration);
jwtDecoder.setJwtValidator(jwtValidator);
return jwtDecoder;
});
return withJwkSetUri(jwkSetUri).jwsAlgorithm(jwsAlgorithm).build();
} else if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//
// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, HS384, or HS512,
// the octets of the UTF-8 representation of the client_secret
// corresponding to the client_id contained in the aud (audience) Claim
// are used as the key to validate the signature.
// For MAC based algorithms, the behavior is unspecified if the aud is multi-valued or
// if an azp value is present that is different than the aud value.
String clientSecret = clientRegistration.getClientSecret();
if (!StringUtils.hasText(clientSecret)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured the client secret.",
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
SecretKeySpec secretKeySpec = new SecretKeySpec(
clientSecret.getBytes(StandardCharsets.UTF_8), jcaAlgorithmMappings.get(jwsAlgorithm));
return withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
}
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured a valid JWS Algorithm: '" +
jwsAlgorithm + "'",
null
);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
/**
@ -80,4 +150,17 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
this.jwtValidatorFactory = jwtValidatorFactory;
}
/**
* Sets the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
* used for the signature or MAC on the {@link OidcIdToken ID Token}.
* The default resolves to {@link SignatureAlgorithm#RS256 RS256} for all {@link ClientRegistration clients}.
*
* @param jwsAlgorithmResolver the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
* for a specific {@link ClientRegistration client}
*/
public final void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver) {
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
this.jwsAlgorithmResolver = jwsAlgorithmResolver;
}
}

View File

@ -21,13 +21,15 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
@ -42,8 +44,6 @@ public class OidcIdTokenDecoderFactoryTests {
private OidcIdTokenDecoderFactory idTokenDecoderFactory;
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> defaultJwtValidatorFactory = OidcIdTokenValidator::new;
@Before
public void setUp() {
this.idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
@ -55,6 +55,12 @@ public class OidcIdTokenDecoderFactoryTests {
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void setJwsAlgorithmResolverWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.idTokenDecoderFactory.setJwsAlgorithmResolver(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void createDecoderWhenClientRegistrationNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(null))
@ -62,9 +68,42 @@ public class OidcIdTokenDecoderFactoryTests {
}
@Test
public void createDecoderWhenJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
public void createDecoderWhenJwsAlgorithmDefaultAndJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.jwkSetUri(null).build()))
.isInstanceOf(OAuth2AuthenticationException.class);
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
"for Client Registration: 'registration-id'. " +
"Check to ensure you have configured the JwkSet URI.");
}
@Test
public void createDecoderWhenJwsAlgorithmEcAndJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> SignatureAlgorithm.ES256);
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.jwkSetUri(null).build()))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
"for Client Registration: 'registration-id'. " +
"Check to ensure you have configured the JwkSet URI.");
}
@Test
public void createDecoderWhenJwsAlgorithmHmacAndClientSecretNullThenThrowOAuth2AuthenticationException() {
this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.clientSecret(null).build()))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
"for Client Registration: 'registration-id'. " +
"Check to ensure you have configured the client secret.");
}
@Test
public void createDecoderWhenJwsAlgorithmNullThenThrowOAuth2AuthenticationException() {
this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> null);
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.build()))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
"for Client Registration: 'registration-id'. " +
"Check to ensure you have configured a valid JWS Algorithm: 'null'");
}
@Test
@ -78,11 +117,28 @@ public class OidcIdTokenDecoderFactoryTests {
Function<ClientRegistration, OAuth2TokenValidator<Jwt>> customJwtValidatorFactory = mock(Function.class);
this.idTokenDecoderFactory.setJwtValidatorFactory(customJwtValidatorFactory);
when(customJwtValidatorFactory.apply(any(ClientRegistration.class)))
.thenReturn(this.defaultJwtValidatorFactory.apply(this.registration.build()));
ClientRegistration clientRegistration = this.registration.build();
this.idTokenDecoderFactory.createDecoder(this.registration.build());
when(customJwtValidatorFactory.apply(same(clientRegistration)))
.thenReturn(new OidcIdTokenValidator(clientRegistration));
verify(customJwtValidatorFactory).apply(any(ClientRegistration.class));
this.idTokenDecoderFactory.createDecoder(clientRegistration);
verify(customJwtValidatorFactory).apply(same(clientRegistration));
}
@Test
public void createDecoderWhenCustomJwsAlgorithmResolverSetThenApplied() {
Function<ClientRegistration, JwsAlgorithm> customJwsAlgorithmResolver = mock(Function.class);
this.idTokenDecoderFactory.setJwsAlgorithmResolver(customJwsAlgorithmResolver);
ClientRegistration clientRegistration = this.registration.build();
when(customJwsAlgorithmResolver.apply(same(clientRegistration)))
.thenReturn(MacAlgorithm.HS256);
this.idTokenDecoderFactory.createDecoder(clientRegistration);
verify(customJwsAlgorithmResolver).apply(same(clientRegistration));
}
}

View File

@ -21,13 +21,15 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
/**
@ -42,8 +44,6 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
private ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory;
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> defaultJwtValidatorFactory = OidcIdTokenValidator::new;
@Before
public void setUp() {
this.idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
@ -55,6 +55,12 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void setJwsAlgorithmResolverWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.idTokenDecoderFactory.setJwsAlgorithmResolver(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void createDecoderWhenClientRegistrationNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(null))
@ -62,9 +68,42 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
}
@Test
public void createDecoderWhenJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
public void createDecoderWhenJwsAlgorithmDefaultAndJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.jwkSetUri(null).build()))
.isInstanceOf(OAuth2AuthenticationException.class);
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
"for Client Registration: 'registration-id'. " +
"Check to ensure you have configured the JwkSet URI.");
}
@Test
public void createDecoderWhenJwsAlgorithmEcAndJwkSetUriEmptyThenThrowOAuth2AuthenticationException() {
this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> SignatureAlgorithm.ES256);
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.jwkSetUri(null).build()))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
"for Client Registration: 'registration-id'. " +
"Check to ensure you have configured the JwkSet URI.");
}
@Test
public void createDecoderWhenJwsAlgorithmHmacAndClientSecretNullThenThrowOAuth2AuthenticationException() {
this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.clientSecret(null).build()))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
"for Client Registration: 'registration-id'. " +
"Check to ensure you have configured the client secret.");
}
@Test
public void createDecoderWhenJwsAlgorithmNullThenThrowOAuth2AuthenticationException() {
this.idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> null);
assertThatThrownBy(() -> this.idTokenDecoderFactory.createDecoder(this.registration.build()))
.isInstanceOf(OAuth2AuthenticationException.class)
.hasMessage("[missing_signature_verifier] Failed to find a Signature Verifier " +
"for Client Registration: 'registration-id'. " +
"Check to ensure you have configured a valid JWS Algorithm: 'null'");
}
@Test
@ -78,11 +117,28 @@ public class ReactiveOidcIdTokenDecoderFactoryTests {
Function<ClientRegistration, OAuth2TokenValidator<Jwt>> customJwtValidatorFactory = mock(Function.class);
this.idTokenDecoderFactory.setJwtValidatorFactory(customJwtValidatorFactory);
when(customJwtValidatorFactory.apply(any(ClientRegistration.class)))
.thenReturn(this.defaultJwtValidatorFactory.apply(this.registration.build()));
ClientRegistration clientRegistration = this.registration.build();
this.idTokenDecoderFactory.createDecoder(this.registration.build());
when(customJwtValidatorFactory.apply(same(clientRegistration)))
.thenReturn(new OidcIdTokenValidator(clientRegistration));
verify(customJwtValidatorFactory).apply(any(ClientRegistration.class));
this.idTokenDecoderFactory.createDecoder(clientRegistration);
verify(customJwtValidatorFactory).apply(same(clientRegistration));
}
@Test
public void createDecoderWhenCustomJwsAlgorithmResolverSetThenApplied() {
Function<ClientRegistration, JwsAlgorithm> customJwsAlgorithmResolver = mock(Function.class);
this.idTokenDecoderFactory.setJwsAlgorithmResolver(customJwsAlgorithmResolver);
ClientRegistration clientRegistration = this.registration.build();
when(customJwsAlgorithmResolver.apply(same(clientRegistration)))
.thenReturn(MacAlgorithm.HS256);
this.idTokenDecoderFactory.createDecoder(clientRegistration);
verify(customJwsAlgorithmResolver).apply(same(clientRegistration));
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose.jws;
/**
* Super interface for cryptographic algorithms defined by the JSON Web Algorithms (JWA) specification
* and used by JSON Web Signature (JWS) to digitally sign or create a MAC
* of the contents of the JWS Protected Header and JWS Payload.
*
* @author Joe Grandja
* @since 5.2
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms (JWA)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518#section-3">Cryptographic Algorithms for Digital Signatures and MACs</a>
*/
public interface JwsAlgorithm {
String getName();
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose.jws;
import java.util.stream.Stream;
/**
* An enumeration of the cryptographic algorithms defined by the JSON Web Algorithms (JWA) specification
* and used by JSON Web Signature (JWS) to create a MAC of the contents of the JWS Protected Header and JWS Payload.
*
* @author Joe Grandja
* @since 5.2
* @see JwsAlgorithm
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms (JWA)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518#section-3">Cryptographic Algorithms for Digital Signatures and MACs</a>
*/
public enum MacAlgorithm implements JwsAlgorithm {
/**
* HMAC using SHA-256 (Required)
*/
HS256(JwsAlgorithms.HS256),
/**
* HMAC using SHA-384 (Optional)
*/
HS384(JwsAlgorithms.HS384),
/**
* HMAC using SHA-512 (Optional)
*/
HS512(JwsAlgorithms.HS512);
private final String name;
MacAlgorithm(String name) {
this.name = name;
}
/**
* Attempt to resolve the provided algorithm name to a {@code MacAlgorithm}.
*
* @param name the algorithm name
* @return the resolved {@code MacAlgorithm}, or {@code null} if not found
*/
public static MacAlgorithm from(String name) {
return Stream.of(values())
.filter(algorithm -> algorithm.getName().equals(name))
.findFirst()
.orElse(null);
}
/**
* Returns the algorithm name.
*
* @return the algorithm name
*/
@Override
public String getName() {
return this.name;
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose.jws;
import java.util.stream.Stream;
/**
* An enumeration of the cryptographic algorithms defined by the JSON Web Algorithms (JWA) specification
* and used by JSON Web Signature (JWS) to digitally sign the contents of the JWS Protected Header and JWS Payload.
*
* @author Joe Grandja
* @since 5.2
* @see JwsAlgorithm
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518">JSON Web Algorithms (JWA)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7515">JSON Web Signature (JWS)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7518#section-3">Cryptographic Algorithms for Digital Signatures and MACs</a>
*/
public enum SignatureAlgorithm implements JwsAlgorithm {
/**
* RSASSA-PKCS1-v1_5 using SHA-256 (Recommended)
*/
RS256(JwsAlgorithms.RS256),
/**
* RSASSA-PKCS1-v1_5 using SHA-384 (Optional)
*/
RS384(JwsAlgorithms.RS384),
/**
* RSASSA-PKCS1-v1_5 using SHA-512 (Optional)
*/
RS512(JwsAlgorithms.RS512),
/**
* ECDSA using P-256 and SHA-256 (Recommended+)
*/
ES256(JwsAlgorithms.ES256),
/**
* ECDSA using P-384 and SHA-384 (Optional)
*/
ES384(JwsAlgorithms.ES384),
/**
* ECDSA using P-521 and SHA-512 (Optional)
*/
ES512(JwsAlgorithms.ES512),
/**
* RSASSA-PSS using SHA-256 and MGF1 with SHA-256 (Optional)
*/
PS256(JwsAlgorithms.PS256),
/**
* RSASSA-PSS using SHA-384 and MGF1 with SHA-384 (Optional)
*/
PS384(JwsAlgorithms.PS384),
/**
* RSASSA-PSS using SHA-512 and MGF1 with SHA-512 (Optional)
*/
PS512(JwsAlgorithms.PS512);
private final String name;
SignatureAlgorithm(String name) {
this.name = name;
}
/**
* Attempt to resolve the provided algorithm name to a {@code SignatureAlgorithm}.
*
* @param name the algorithm name
* @return the resolved {@code SignatureAlgorithm}, or {@code null} if not found
*/
public static SignatureAlgorithm from(String name) {
return Stream.of(values())
.filter(algorithm -> algorithm.getName().equals(name))
.findFirst()
.orElse(null);
}
/**
* Returns the algorithm name.
*
* @return the algorithm name
*/
@Override
public String getName() {
return this.name;
}
}

View File

@ -16,21 +16,12 @@
package org.springframework.security.oauth2.jwt;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.RemoteKeySourceException;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.JWSKeySelector;
@ -45,7 +36,6 @@ import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -54,15 +44,29 @@ import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.util.Assert;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A low-level Nimbus implementation of {@link JwtDecoder} which takes a raw Nimbus configuration.
*
* @author Josh Cummings
* @author Joe Grandja
* @since 5.2
*/
public final class NimbusJwtDecoder implements JwtDecoder {
@ -178,8 +182,6 @@ public final class NimbusJwtDecoder implements JwtDecoder {
*
* @param jwkSetUri the JWK Set uri to use
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
*
* @since 5.2
*/
public static JwkSetUriJwtDecoderBuilder withJwkSetUri(String jwkSetUri) {
return new JwkSetUriJwtDecoderBuilder(jwkSetUri);
@ -190,18 +192,24 @@ public final class NimbusJwtDecoder implements JwtDecoder {
*
* @param key the public key to use
* @return a {@link PublicKeyJwtDecoderBuilder} for further configurations
*
* @since 5.2
*/
public static PublicKeyJwtDecoderBuilder withPublicKey(RSAPublicKey key) {
return new PublicKeyJwtDecoderBuilder(key);
}
/**
* Use the given {@code SecretKey} to validate the MAC on a JSON Web Signature (JWS).
*
* @param secretKey the {@code SecretKey} used to validate the MAC
* @return a {@link SecretKeyJwtDecoderBuilder} for further configurations
*/
public static SecretKeyJwtDecoderBuilder withSecretKey(SecretKey secretKey) {
return new SecretKeyJwtDecoderBuilder(secretKey);
}
/**
* A builder for creating {@link NimbusJwtDecoder} instances based on a
* <a target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> uri.
*
* @since 5.2
*/
public static final class JwkSetUriJwtDecoderBuilder {
private String jwkSetUri;
@ -220,9 +228,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
* @param jwsAlgorithm the algorithm to use
* @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations
*/
public JwkSetUriJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
public JwkSetUriJwtDecoderBuilder jwsAlgorithm(JwsAlgorithm jwsAlgorithm) {
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm.getName());
return this;
}
@ -303,10 +311,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
}
/**
* A builder for creating {@link NimbusJwtDecoder} instances based on a
* public key.
*
* @since 5.2
* A builder for creating {@link NimbusJwtDecoder} instances based on a public key.
*/
public static final class PublicKeyJwtDecoderBuilder {
private JWSAlgorithm jwsAlgorithm;
@ -314,7 +319,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private PublicKeyJwtDecoderBuilder(RSAPublicKey key) {
Assert.notNull(key, "key cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(JwsAlgorithms.RS256);
this.jwsAlgorithm = JWSAlgorithm.RS256;
this.key = rsaKey(key);
}
@ -330,12 +335,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
* The value should be one of
* <a href="https://tools.ietf.org/html/rfc7518#section-3.3" target="_blank">RS256, RS384, or RS512</a>.
*
* @param jwsAlgorithm the algorithm to use
* @param signatureAlgorithm the algorithm to use
* @return a {@link PublicKeyJwtDecoderBuilder} for further configurations
*/
public PublicKeyJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
public PublicKeyJwtDecoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
return this;
}
@ -368,4 +373,56 @@ public final class NimbusJwtDecoder implements JwtDecoder {
return new NimbusJwtDecoder(processor());
}
}
/**
* A builder for creating {@link NimbusJwtDecoder} instances based on a {@code SecretKey}.
*/
public static final class SecretKeyJwtDecoderBuilder {
private final SecretKey secretKey;
private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
private SecretKeyJwtDecoderBuilder(SecretKey secretKey) {
Assert.notNull(secretKey, "secretKey cannot be null");
this.secretKey = secretKey;
}
/**
* Use the given
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target="_blank">algorithm</a>
* when generating the MAC.
*
* The value should be one of
* <a href="https://tools.ietf.org/html/rfc7518#section-3.2" target="_blank">HS256, HS384 or HS512</a>.
*
* @param macAlgorithm the MAC algorithm to use
* @return a {@link SecretKeyJwtDecoderBuilder} for further configurations
*/
public SecretKeyJwtDecoderBuilder macAlgorithm(MacAlgorithm macAlgorithm) {
Assert.notNull(macAlgorithm, "macAlgorithm cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(macAlgorithm.getName());
return this;
}
/**
* Build the configured {@link NimbusJwtDecoder}.
*
* @return the configured {@link NimbusJwtDecoder}
*/
public NimbusJwtDecoder build() {
return new NimbusJwtDecoder(processor());
}
JWTProcessor<SecurityContext> processor() {
JWKSource<SecurityContext> jwkSource = new ImmutableSecret<>(this.secretKey);
JWSKeySelector<SecurityContext> jwsKeySelector =
new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource);
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
// Spring Security validates the claim set independent from Nimbus
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { });
return jwtProcessor;
}
}
}

View File

@ -15,15 +15,16 @@
*/
package org.springframework.security.oauth2.jwt;
import java.util.Collections;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.util.Assert;
import org.springframework.web.client.RestOperations;
import java.util.Collections;
import java.util.Map;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
/**
@ -75,7 +76,7 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
Assert.hasText(jwkSetUrl, "jwkSetUrl cannot be empty");
Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
this.jwtDecoderBuilder = withJwkSetUri(jwkSetUrl).jwsAlgorithm(jwsAlgorithm);
this.jwtDecoderBuilder = withJwkSetUri(jwkSetUrl).jwsAlgorithm(SignatureAlgorithm.from(jwsAlgorithm));
this.delegate = makeDelegate();
}

View File

@ -15,13 +15,6 @@
*/
package org.springframework.security.oauth2.jwt;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
@ -31,6 +24,7 @@ import com.nimbusds.jose.jwk.JWKSelector;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import com.nimbusds.jose.jwk.source.JWKSecurityContextJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.BadJOSEException;
@ -44,26 +38,35 @@ import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.jwt.proc.JWTProcessor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.util.Assert;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
/**
* An implementation of a {@link ReactiveJwtDecoder} that &quot;decodes&quot; a
* JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a
* JSON Web Signature (JWS). The public key used for verification is obtained from the
* JSON Web Key (JWK) Set {@code URL} supplied via the constructor.
* JSON Web Signature (JWS).
*
* <p>
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK internally.
*
* @author Rob Winch
* @author Joe Grandja
* @since 5.1
* @see ReactiveJwtDecoder
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
@ -75,22 +78,34 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
private final Converter<SignedJWT, Mono<JWTClaimsSet>> jwtProcessor;
private OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDefault();
private Converter<Map<String, Object>, Map<String, Object>> claimSetConverter = MappedJwtClaimSetConverter
.withDefaults(Collections.emptyMap());
public NimbusReactiveJwtDecoder(RSAPublicKey publicKey) {
this.jwtProcessor = withPublicKey(publicKey).processor();
}
private Converter<Map<String, Object>, Map<String, Object>> claimSetConverter =
MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
/**
* Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters.
* Constructs a {@code NimbusReactiveJwtDecoder} using the provided parameters.
*
* @param jwkSetUrl the JSON Web Key (JWK) Set {@code URL}
*/
public NimbusReactiveJwtDecoder(String jwkSetUrl) {
this.jwtProcessor = withJwkSetUri(jwkSetUrl).processor();
this(withJwkSetUri(jwkSetUrl).processor());
}
/**
* Constructs a {@code NimbusReactiveJwtDecoder} using the provided parameters.
*
* @param publicKey the {@code RSAPublicKey} used to verify the signature
* @since 5.2
*/
public NimbusReactiveJwtDecoder(RSAPublicKey publicKey) {
this(withPublicKey(publicKey).processor());
}
/**
* Constructs a {@code NimbusReactiveJwtDecoder} using the provided parameters.
*
* @param jwtProcessor the {@link Converter} used to process and verify the signed Jwt and return the Jwt Claim Set
* @since 5.2
*/
public NimbusReactiveJwtDecoder(Converter<SignedJWT, Mono<JWTClaimsSet>> jwtProcessor) {
this.jwtProcessor = jwtProcessor;
}
@ -188,6 +203,18 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
return new PublicKeyReactiveJwtDecoderBuilder(key);
}
/**
* Use the given {@code SecretKey} to validate the MAC on a JSON Web Signature (JWS).
*
* @param secretKey the {@code SecretKey} used to validate the MAC
* @return a {@link SecretKeyReactiveJwtDecoderBuilder} for further configurations
*
* @since 5.2
*/
public static SecretKeyReactiveJwtDecoderBuilder withSecretKey(SecretKey secretKey) {
return new SecretKeyReactiveJwtDecoderBuilder(secretKey);
}
/**
* Use the given {@link Function} to validate JWTs
*
@ -207,8 +234,7 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
* @since 5.2
*/
public static final class JwkSetUriReactiveJwtDecoderBuilder {
private String jwkSetUri;
private final String jwkSetUri;
private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;
private WebClient webClient = WebClient.create();
@ -224,9 +250,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
* @param jwsAlgorithm the algorithm to use
* @return a {@link JwkSetUriReactiveJwtDecoderBuilder} for further configurations
*/
public JwkSetUriReactiveJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
public JwkSetUriReactiveJwtDecoderBuilder jwsAlgorithm(JwsAlgorithm jwsAlgorithm) {
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm.getName());
return this;
}
@ -284,19 +310,18 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
}
/**
* A builder for creating Nimbus {@link JWTProcessor} instances based on a
* public key.
* A builder for creating {@link NimbusReactiveJwtDecoder} instances based on a public key.
*
* @since 5.2
*/
public static final class PublicKeyReactiveJwtDecoderBuilder {
private final RSAKey key;
private JWSAlgorithm jwsAlgorithm;
private RSAKey key;
private PublicKeyReactiveJwtDecoderBuilder(RSAPublicKey key) {
Assert.notNull(key, "key cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(JwsAlgorithms.RS256);
this.key = rsaKey(key);
this.jwsAlgorithm = JWSAlgorithm.RS256;
}
private static RSAKey rsaKey(RSAPublicKey publicKey) {
@ -310,12 +335,12 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
* The value should be one of
* <a href="https://tools.ietf.org/html/rfc7518#section-3.3" target="_blank">RS256, RS384, or RS512</a>.
*
* @param jwsAlgorithm the algorithm to use
* @param signatureAlgorithm the algorithm to use
* @return a {@link PublicKeyReactiveJwtDecoderBuilder} for further configurations
*/
public PublicKeyReactiveJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
public PublicKeyReactiveJwtDecoderBuilder signatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
Assert.notNull(signatureAlgorithm, "signatureAlgorithm cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName());
return this;
}
@ -349,17 +374,71 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
}
}
/**
* A builder for creating {@link NimbusReactiveJwtDecoder} instances based on a {@code SecretKey}.
*
* @since 5.2
*/
public static final class SecretKeyReactiveJwtDecoderBuilder {
private final SecretKey secretKey;
private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256;
private SecretKeyReactiveJwtDecoderBuilder(SecretKey secretKey) {
Assert.notNull(secretKey, "secretKey cannot be null");
this.secretKey = secretKey;
}
/**
* Use the given
* <a href="https://tools.ietf.org/html/rfc7515#section-4.1.1" target="_blank">algorithm</a>
* when generating the MAC.
*
* The value should be one of
* <a href="https://tools.ietf.org/html/rfc7518#section-3.2" target="_blank">HS256, HS384 or HS512</a>.
*
* @param macAlgorithm the MAC algorithm to use
* @return a {@link SecretKeyReactiveJwtDecoderBuilder} for further configurations
*/
public SecretKeyReactiveJwtDecoderBuilder macAlgorithm(MacAlgorithm macAlgorithm) {
Assert.notNull(macAlgorithm, "macAlgorithm cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(macAlgorithm.getName());
return this;
}
/**
* Build the configured {@link NimbusReactiveJwtDecoder}.
*
* @return the configured {@link NimbusReactiveJwtDecoder}
*/
public NimbusReactiveJwtDecoder build() {
return new NimbusReactiveJwtDecoder(processor());
}
Converter<SignedJWT, Mono<JWTClaimsSet>> processor() {
JWKSource<SecurityContext> jwkSource = new ImmutableSecret<>(this.secretKey);
JWSKeySelector<SecurityContext> jwsKeySelector =
new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource);
DefaultJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
// Spring Security validates the claim set independent from Nimbus
jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { });
return signedJWT -> Mono.just(signedJWT).map(jwt -> createClaimsSet(jwtProcessor, jwt, null));
}
}
/**
* A builder for creating {@link NimbusReactiveJwtDecoder} instances.
*
* @since 5.2
*/
public static final class JwkSourceReactiveJwtDecoderBuilder {
private Function<JWT, Flux<JWK>> jwkSource;
private final Function<JWT, Flux<JWK>> jwkSource;
private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;
private JwkSourceReactiveJwtDecoderBuilder(Function<JWT, Flux<JWK>> jwkSource) {
Assert.notNull(jwkSource, "jwkSource cannot be empty");
Assert.notNull(jwkSource, "jwkSource cannot be null");
this.jwkSource = jwkSource;
}
@ -370,9 +449,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
* @param jwsAlgorithm the algorithm to use
* @return a {@link JwkSourceReactiveJwtDecoderBuilder} for further configurations
*/
public JwkSourceReactiveJwtDecoderBuilder jwsAlgorithm(String jwsAlgorithm) {
Assert.hasText(jwsAlgorithm, "jwsAlgorithm cannot be empty");
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm);
public JwkSourceReactiveJwtDecoderBuilder jwsAlgorithm(JwsAlgorithm jwsAlgorithm) {
Assert.notNull(jwsAlgorithm, "jwsAlgorithm cannot be null");
this.jwsAlgorithm = JWSAlgorithm.parse(jwsAlgorithm.getName());
return this;
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
/**
* @author Joe Grandja
* @since 5.2
*/
public class TestKeys {
public static final String DEFAULT_ENCODED_SECRET_KEY = "bCzY/M48bbkwBEWjmNSIEPfwApcvXOnkCxORBEbPr+4=";
public static final SecretKey DEFAULT_SECRET_KEY =
new SecretKeySpec(Base64.getDecoder().decode(DEFAULT_ENCODED_SECRET_KEY), "AES");
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose.jws;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MacAlgorithm}
*
* @author Joe Grandja
* @since 5.2
*/
public class MacAlgorithmTests {
@Test
public void fromWhenAlgorithmValidThenResolves() {
assertThat(MacAlgorithm.from(JwsAlgorithms.HS256)).isEqualTo(MacAlgorithm.HS256);
assertThat(MacAlgorithm.from(JwsAlgorithms.HS384)).isEqualTo(MacAlgorithm.HS384);
assertThat(MacAlgorithm.from(JwsAlgorithms.HS512)).isEqualTo(MacAlgorithm.HS512);
}
@Test
public void fromWhenAlgorithmInvalidThenDoesNotResolve() {
assertThat(MacAlgorithm.from("invalid")).isNull();
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jose.jws;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SignatureAlgorithm}
*
* @author Joe Grandja
* @since 5.2
*/
public class SignatureAlgorithmTests {
@Test
public void fromWhenAlgorithmValidThenResolves() {
assertThat(SignatureAlgorithm.from(JwsAlgorithms.RS256)).isEqualTo(SignatureAlgorithm.RS256);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.RS384)).isEqualTo(SignatureAlgorithm.RS384);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.RS512)).isEqualTo(SignatureAlgorithm.RS512);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.ES256)).isEqualTo(SignatureAlgorithm.ES256);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.ES384)).isEqualTo(SignatureAlgorithm.ES384);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.ES512)).isEqualTo(SignatureAlgorithm.ES512);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS256)).isEqualTo(SignatureAlgorithm.PS256);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS384)).isEqualTo(SignatureAlgorithm.PS384);
assertThat(SignatureAlgorithm.from(JwsAlgorithms.PS512)).isEqualTo(SignatureAlgorithm.PS512);
}
@Test
public void fromWhenAlgorithmInvalidThenDoesNotResolve() {
assertThat(SignatureAlgorithm.from("invalid")).isNull();
}
}

View File

@ -16,19 +16,11 @@
package org.springframework.security.oauth2.jwt;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.JWTClaimsSet;
@ -40,7 +32,6 @@ import okhttp3.mockwebserver.MockWebServer;
import org.assertj.core.api.Assertions;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
@ -48,9 +39,26 @@ import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.web.client.RestOperations;
import javax.crypto.SecretKey;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@ -58,13 +66,13 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withPublicKey;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.*;
/**
* Tests for {@link NimbusJwtDecoder}
*
* @author Josh Cummings
* @author Joe Grandja
*/
public class NimbusJwtDecoderTests {
private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
@ -217,11 +225,9 @@ public class NimbusJwtDecoderTests {
}
@Test
public void jwsAlgorithmWhenNullOrEmptyThenThrowsException() {
public void jwsAlgorithmWhenNullThenThrowsException() {
NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = withJwkSetUri(JWK_SET_URI);
Assertions.assertThatCode(() -> builder.jwsAlgorithm(null)).isInstanceOf(IllegalArgumentException.class);
Assertions.assertThatCode(() -> builder.jwsAlgorithm("")).isInstanceOf(IllegalArgumentException.class);
Assertions.assertThatCode(() -> builder.jwsAlgorithm("RS4096")).doesNotThrowAnyException();
}
@Test
@ -239,7 +245,7 @@ public class NimbusJwtDecoderTests {
@Test
public void buildWhenSignatureAlgorithmMismatchesKeyTypeThenThrowsException() {
Assertions.assertThatCode(() -> withPublicKey(key())
.jwsAlgorithm(JwsAlgorithms.ES256)
.signatureAlgorithm(SignatureAlgorithm.ES256)
.build())
.isInstanceOf(IllegalStateException.class);
}
@ -254,7 +260,7 @@ public class NimbusJwtDecoderTests {
@Test
public void decodeWhenUsingPublicKeyWithRs512ThenSuccessfullyDecodes() throws Exception {
NimbusJwtDecoder decoder = withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
NimbusJwtDecoder decoder = withPublicKey(key()).signatureAlgorithm(SignatureAlgorithm.RS512).build();
assertThat(decoder.decode(RS512_SIGNED_JWT))
.extracting(Jwt::getSubject)
.isEqualTo("test-subject");
@ -262,17 +268,69 @@ public class NimbusJwtDecoderTests {
@Test
public void decodeWhenSignatureMismatchesAlgorithmThenThrowsException() throws Exception {
NimbusJwtDecoder decoder = withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
NimbusJwtDecoder decoder = withPublicKey(key()).signatureAlgorithm(SignatureAlgorithm.RS512).build();
Assertions.assertThatCode(() -> decoder.decode(RS256_SIGNED_JWT))
.isInstanceOf(JwtException.class);
}
@Test
public void withSecretKeyWhenNullThenThrowsIllegalArgumentException() {
assertThatThrownBy(() -> withSecretKey(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("secretKey cannot be null");
}
@Test
public void withSecretKeyWhenMacAlgorithmNullThenThrowsIllegalArgumentException() {
SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
assertThatThrownBy(() -> withSecretKey(secretKey).macAlgorithm(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("macAlgorithm cannot be null");
}
@Test
public void decodeWhenUsingSecretKeyThenSuccessfullyDecodes() throws Exception {
SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
MacAlgorithm macAlgorithm = MacAlgorithm.HS256;
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("test-subject")
.expirationTime(Date.from(Instant.now().plusSeconds(60)))
.build();
SignedJWT signedJWT = signedJwt(secretKey, macAlgorithm, claimsSet);
NimbusJwtDecoder decoder = withSecretKey(secretKey).macAlgorithm(macAlgorithm).build();
assertThat(decoder.decode(signedJWT.serialize()))
.extracting(Jwt::getSubject)
.isEqualTo("test-subject");
}
@Test
public void decodeWhenUsingSecretKeyAndIncorrectAlgorithmThenThrowsJwtException() throws Exception {
SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
MacAlgorithm macAlgorithm = MacAlgorithm.HS256;
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("test-subject")
.expirationTime(Date.from(Instant.now().plusSeconds(60)))
.build();
SignedJWT signedJWT = signedJwt(secretKey, macAlgorithm, claimsSet);
NimbusJwtDecoder decoder = withSecretKey(secretKey).macAlgorithm(MacAlgorithm.HS512).build();
assertThatThrownBy(() -> decoder.decode(signedJWT.serialize()))
.isInstanceOf(JwtException.class)
.hasMessage("An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found");
}
private RSAPublicKey key() throws InvalidKeySpecException {
byte[] decoded = Base64.getDecoder().decode(VERIFY_KEY.getBytes());
EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
return (RSAPublicKey) kf.generatePublic(spec);
}
private SignedJWT signedJwt(SecretKey secretKey, MacAlgorithm jwsAlgorithm, JWTClaimsSet claimsSet) throws Exception {
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.parse(jwsAlgorithm.getName())), claimsSet);
JWSSigner signer = new MACSigner(secretKey);
signedJWT.sign(signer);
return signedJWT;
}
private static JWTProcessor<SecurityContext> withSigning(String jwkResponse) {
RestOperations restOperations = mock(RestOperations.class);
when(restOperations.exchange(any(RequestEntity.class), eq(String.class)))

View File

@ -16,6 +16,31 @@
package org.springframework.security.oauth2.jwt;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
import java.net.UnknownHostException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
@ -27,39 +52,19 @@ import java.text.ParseException;
import java.time.Instant;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import com.nimbusds.jose.jwk.JWKSet;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSetUri;
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSource;
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withPublicKey;
import static org.mockito.Mockito.*;
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.*;
/**
* @author Rob Winch
* @author Joe Grandja
* @since 5.1
*/
public class NimbusReactiveJwtDecoderTests {
@ -236,11 +241,9 @@ public class NimbusReactiveJwtDecoderTests {
}
@Test
public void jwsAlgorithmWhenNullOrEmptyThenThrowsException() {
public void jwsAlgorithmWhenNullThenThrowsException() {
NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder builder = withJwkSetUri(this.jwkSetUri);
assertThatCode(() -> builder.jwsAlgorithm(null)).isInstanceOf(IllegalArgumentException.class);
assertThatCode(() -> builder.jwsAlgorithm("")).isInstanceOf(IllegalArgumentException.class);
assertThatCode(() -> builder.jwsAlgorithm("RS4096")).doesNotThrowAnyException();
}
@Test
@ -269,7 +272,7 @@ public class NimbusReactiveJwtDecoderTests {
@Test
public void buildWhenSignatureAlgorithmMismatchesKeyTypeThenThrowsException() {
assertThatCode(() -> withPublicKey(key())
.jwsAlgorithm(JwsAlgorithms.ES256)
.signatureAlgorithm(SignatureAlgorithm.ES256)
.build())
.isInstanceOf(IllegalStateException.class);
}
@ -285,7 +288,7 @@ public class NimbusReactiveJwtDecoderTests {
@Test
public void decodeWhenUsingPublicKeyWithRs512ThenSuccessfullyDecodes() throws Exception {
NimbusReactiveJwtDecoder decoder =
withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
withPublicKey(key()).signatureAlgorithm(SignatureAlgorithm.RS512).build();
assertThat(decoder.decode(this.rsa512).block())
.extracting(Jwt::getSubject)
.isEqualTo("test-subject");
@ -294,7 +297,7 @@ public class NimbusReactiveJwtDecoderTests {
@Test
public void decodeWhenSignatureMismatchesAlgorithmThenThrowsException() throws Exception {
NimbusReactiveJwtDecoder decoder =
withPublicKey(key()).jwsAlgorithm(JwsAlgorithms.RS512).build();
withPublicKey(key()).signatureAlgorithm(SignatureAlgorithm.RS512).build();
assertThatCode(() -> decoder.decode(this.rsa256).block())
.isInstanceOf(JwtException.class);
}
@ -316,6 +319,58 @@ public class NimbusReactiveJwtDecoderTests {
.isNotNull();
}
@Test
public void withSecretKeyWhenSecretKeyNullThenThrowsIllegalArgumentException() {
assertThatThrownBy(() -> withSecretKey(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("secretKey cannot be null");
}
@Test
public void withSecretKeyWhenMacAlgorithmNullThenThrowsIllegalArgumentException() {
SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
assertThatThrownBy(() -> withSecretKey(secretKey).macAlgorithm(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("macAlgorithm cannot be null");
}
@Test
public void decodeWhenSecretKeyThenSuccess() throws Exception {
SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
MacAlgorithm macAlgorithm = MacAlgorithm.HS256;
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("test-subject")
.expirationTime(Date.from(Instant.now().plusSeconds(60)))
.build();
SignedJWT signedJWT = signedJwt(secretKey, macAlgorithm, claimsSet);
this.decoder = withSecretKey(secretKey).macAlgorithm(macAlgorithm).build();
Jwt jwt = this.decoder.decode(signedJWT.serialize()).block();
assertThat(jwt.getSubject()).isEqualTo("test-subject");
}
@Test
public void decodeWhenSecretKeyAndAlgorithmMismatchThenThrowsJwtException() throws Exception {
SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY;
MacAlgorithm macAlgorithm = MacAlgorithm.HS256;
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("test-subject")
.expirationTime(Date.from(Instant.now().plusSeconds(60)))
.build();
SignedJWT signedJWT = signedJwt(secretKey, macAlgorithm, claimsSet);
this.decoder = withSecretKey(secretKey).macAlgorithm(MacAlgorithm.HS512).build();
assertThatThrownBy(() -> this.decoder.decode(signedJWT.serialize()).block())
.isInstanceOf(JwtException.class);
}
private SignedJWT signedJwt(SecretKey secretKey, MacAlgorithm jwsAlgorithm, JWTClaimsSet claimsSet) throws Exception {
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.parse(jwsAlgorithm.getName())), claimsSet);
JWSSigner signer = new MACSigner(secretKey);
signedJWT.sign(signer);
return signedJWT;
}
private JWKSet parseJWKSet(String jwkSet) {
try {
return JWKSet.parse(jwkSet);