Add nonce to OIDC Authentication Request

Fixes gh-4442
This commit is contained in:
Mark Heckler 2019-09-01 18:47:13 -05:00 committed by Joe Grandja
parent adde18b873
commit da9f027fa4
13 changed files with 295 additions and 69 deletions

View File

@ -75,6 +75,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* Tests for {@link OAuth2ClientConfigurer}.
*
* @author Joe Grandja
* @author Mark Heckler
*/
public class OAuth2ClientConfigurerTests {
private static ClientRegistrationRepository clientRegistrationRepository;
@ -138,7 +139,8 @@ public class OAuth2ClientConfigurerTests {
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?" +
"response_type=code&client_id=client-1&" +
"scope=user&state=.{15,}&" +
"redirect_uri=http://localhost/client-1");
"redirect_uri=http://localhost/client-1&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -151,7 +153,8 @@ public class OAuth2ClientConfigurerTests {
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?" +
"response_type=code&client_id=client-1&" +
"scope=user&state=.{15,}&" +
"redirect_uri=http://localhost/client-1");
"redirect_uri=http://localhost/client-1&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -203,7 +206,8 @@ public class OAuth2ClientConfigurerTests {
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?" +
"response_type=code&client_id=client-1&" +
"scope=user&state=.{15,}&" +
"redirect_uri=http://localhost/client-1");
"redirect_uri=http://localhost/client-1&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
verify(requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
}

View File

@ -43,6 +43,10 @@ import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.util.Assert;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collection;
import java.util.Map;
@ -61,6 +65,7 @@ import java.util.Map;
* to complete the authentication.
*
* @author Joe Grandja
* @author Mark Heckler
* @since 5.0
* @see OAuth2LoginAuthenticationToken
* @see OAuth2AccessTokenResponseClient
@ -75,6 +80,7 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private final OAuth2UserService<OidcUserRequest, OidcUser> userService;
private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();
@ -152,7 +158,23 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
null);
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString());
}
OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
String requestNonce = authorizationRequest.getAttribute(OidcParameterNames.NONCE);
if (requestNonce != null) {
String nonceHash;
try {
nonceHash = createHash(requestNonce);
} catch (NoSuchAlgorithmException e) {
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_NONCE_ERROR_CODE));
}
String nonceHashClaim = idToken.getClaim(OidcParameterNames.NONCE);
if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) {
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_NONCE_ERROR_CODE));
}
}
OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(
clientRegistration, accessTokenResponse.getAccessToken(), idToken, additionalParameters));
@ -211,4 +233,10 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims());
return idToken;
}
private String createHash(String nonce) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

View File

@ -43,13 +43,17 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collection;
import java.util.Map;
/**
* An implementation of an {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth 2.0 Login,
* which leverages the OAuth 2.0 Authorization Code Grant Flow.
*
* <p>
* This {@link org.springframework.security.authentication.AuthenticationProvider} is responsible for authenticating
* an Authorization Code credential with the Authorization Server's Token Endpoint
* and if valid, exchanging it for an Access Token credential.
@ -77,6 +81,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
private final ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
@ -170,7 +175,8 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
}
return createOidcToken(clientRegistration, accessTokenResponse)
.map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken, additionalParameters))
.doOnNext(idToken -> validateNonce(authorizationCodeAuthentication, idToken))
.map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken, additionalParameters))
.flatMap(this.userService::loadUser)
.map(oauth2User -> {
Collection<? extends GrantedAuthority> mappedAuthorities =
@ -192,4 +198,33 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
return jwtDecoder.decode(rawIdToken)
.map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()));
}
private Mono<OidcIdToken> validateNonce(OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication, OidcIdToken idToken) {
String requestNonce = authorizationCodeAuthentication
.getAuthorizationExchange()
.getAuthorizationRequest()
.getAttribute(OidcParameterNames.NONCE);
if (requestNonce != null) {
String nonceHash;
try {
nonceHash = createHash(requestNonce);
} catch (NoSuchAlgorithmException e) {
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_NONCE_ERROR_CODE));
}
String nonceHashClaim = idToken.getClaim(OidcParameterNames.NONCE);
if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) {
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_NONCE_ERROR_CODE));
}
}
return Mono.just(idToken);
}
private String createHash(String nonce) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

View File

@ -57,6 +57,7 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withSecre
*
* @author Joe Grandja
* @author Rafael Dominguez
* @author Mark Heckler
* @since 5.2
* @see JwtDecoderFactory
* @see ClientRegistration
@ -88,12 +89,14 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
Converter<Object, ?> urlConverter = getConverter(TypeDescriptor.valueOf(URL.class));
Converter<Object, ?> stringConverter = getConverter(TypeDescriptor.valueOf(String.class));
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter);
claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter);
claimTypeConverters.put(IdTokenClaimNames.NONCE, stringConverter);
claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);

View File

@ -107,13 +107,6 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator<Jwt> {
invalidClaims.put(IdTokenClaimNames.IAT, idToken.getIssuedAt());
}
// 11. If a nonce value was sent in the Authentication Request,
// a nonce Claim MUST be present and its value checked to verify
// that it is the same value as the one that was sent in the Authentication Request.
// The Client SHOULD check the nonce value for replay attacks.
// The precise method for detecting replay attacks is Client specific.
// TODO Depends on gh-4442
if (!invalidClaims.isEmpty()) {
return OAuth2TokenValidatorResult.failure(invalidIdToken(invalidClaims));
}

View File

@ -57,6 +57,7 @@ import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.w
*
* @author Joe Grandja
* @author Rafael Dominguez
* @author Mark Heckler
* @since 5.2
* @see ReactiveJwtDecoderFactory
* @see ClientRegistration
@ -88,12 +89,14 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
Converter<Object, ?> urlConverter = getConverter(TypeDescriptor.valueOf(URL.class));
Converter<Object, ?> stringConverter = getConverter(TypeDescriptor.valueOf(String.class));
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter);
claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter);
claimTypeConverters.put(IdTokenClaimNames.NONCE, stringConverter);
claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);

View File

@ -24,6 +24,7 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
@ -51,6 +52,7 @@ import java.util.Map;
* @author Joe Grandja
* @author Rob Winch
* @author Eddú Meléndez
* @author Mark Heckler
* @since 5.1
* @see OAuth2AuthorizationRequestResolver
* @see OAuth2AuthorizationRequestRedirectFilter
@ -61,7 +63,7 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
private final ClientRegistrationRepository clientRegistrationRepository;
private final AntPathRequestMatcher authorizationRequestMatcher;
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private final StringKeyGenerator stringKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
/**
* Constructs a {@code DefaultOAuth2AuthorizationRequestResolver} using the provided parameters.
@ -118,11 +120,15 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
OAuth2AuthorizationRequest.Builder builder;
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.authorizationCode();
Map<String, Object> additionalParameters = new HashMap<>();
addNonceParameters(attributes, additionalParameters);
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
Map<String, Object> additionalParameters = new HashMap<>();
addPkceParameters(attributes, additionalParameters);
builder.additionalParameters(additionalParameters);
}
builder.additionalParameters(additionalParameters);
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.implicit();
} else {
@ -201,6 +207,27 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
.toUriString();
}
/**
* Creates nonce and its hash for use in OpenID Connect Authentication Requests
*
* @param attributes where {@link OidcParameterNames#NONCE} is stored for the token request
* @param additionalParameters where hash of {@link OidcParameterNames#NONCE} is added to the authentication request
*
* @since 5.2
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes">15.5.2. Nonce Implementation Notes</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation">3.1.3.7. ID Token Validation</a>
*/
private void addNonceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
try {
String nonce = this.stringKeyGenerator.generateKey();
attributes.put(OidcParameterNames.NONCE, nonce);
String nonceHash = createHash(nonce);
additionalParameters.put(OidcParameterNames.NONCE, nonceHash);
} catch (NoSuchAlgorithmException ignored) {
}
}
/**
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
*
@ -214,10 +241,10 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
*/
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
String codeVerifier = this.codeVerifierGenerator.generateKey();
String codeVerifier = this.stringKeyGenerator.generateKey();
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
try {
String codeChallenge = createCodeChallenge(codeVerifier);
String codeChallenge = createHash(codeVerifier);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
} catch (NoSuchAlgorithmException e) {
@ -225,9 +252,9 @@ public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2Au
}
}
private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
private String createHash(String value) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

View File

@ -27,6 +27,7 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
@ -52,6 +53,7 @@ import java.util.Map;
* used to resolve the {@link ClientRegistration} and create the {@link OAuth2AuthorizationRequest}.
*
* @author Rob Winch
* @author Mark Heckler
* @since 5.1
*/
public class DefaultServerOAuth2AuthorizationRequestResolver
@ -75,7 +77,7 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
private final StringKeyGenerator codeVerifierGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private final StringKeyGenerator stringKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
/**
* Creates a new instance
@ -132,16 +134,18 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
OAuth2AuthorizationRequest.Builder builder;
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.authorizationCode();
Map<String, Object> additionalParameters = new HashMap<>();
addNonceParameters(attributes, additionalParameters);
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
Map<String, Object> additionalParameters = new HashMap<>();
addPkceParameters(attributes, additionalParameters);
builder.additionalParameters(additionalParameters);
}
}
else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
builder.additionalParameters(additionalParameters);
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
builder = OAuth2AuthorizationRequest.implicit();
}
else {
} else {
throw new IllegalArgumentException(
"Invalid Authorization Grant Type (" + clientRegistration.getAuthorizationGrantType().getValue()
+ ") for Client Registration with Id: " + clientRegistration.getRegistrationId());
@ -207,6 +211,27 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
.toUriString();
}
/**
* Creates nonce and its hash for use in OpenID Connect Authentication Requests
*
* @param attributes where {@link OidcParameterNames#NONCE} is stored for the token request
* @param additionalParameters where hash of {@link OidcParameterNames#NONCE} is added to the authentication request
*
* @since 5.2
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes">15.5.2. Nonce Implementation Notes</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation">3.1.3.7. ID Token Validation</a>
*/
private void addNonceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
try {
String nonce = this.stringKeyGenerator.generateKey();
attributes.put(OidcParameterNames.NONCE, nonce);
String nonceHash = createHash(nonce);
additionalParameters.put(OidcParameterNames.NONCE, nonceHash);
} catch (NoSuchAlgorithmException ignored) {
}
}
/**
* Creates and adds additional PKCE parameters for use in the OAuth 2.0 Authorization and Access Token Requests
*
@ -220,10 +245,10 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">4.2. Client Creates the Code Challenge</a>
*/
private void addPkceParameters(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
String codeVerifier = this.codeVerifierGenerator.generateKey();
String codeVerifier = this.stringKeyGenerator.generateKey();
attributes.put(PkceParameterNames.CODE_VERIFIER, codeVerifier);
try {
String codeChallenge = createCodeChallenge(codeVerifier);
String codeChallenge = createHash(codeVerifier);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, codeChallenge);
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
} catch (NoSuchAlgorithmException e) {
@ -231,9 +256,9 @@ public class DefaultServerOAuth2AuthorizationRequestResolver
}
}
private String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
private String createHash(String value) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
byte[] digest = md.digest(value.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@ -15,25 +15,17 @@
*/
package org.springframework.security.oauth2.client.oidc.authentication;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.mockito.stubbing.Answer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
@ -54,23 +46,34 @@ import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientRegistration;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests.request;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses.error;
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses.success;
import static org.springframework.security.oauth2.jwt.TestJwts.jwt;
/**
* Tests for {@link OidcAuthorizationCodeAuthenticationProvider}.
*
* @author Joe Grandja
* @author Mark Heckler
*/
public class OidcAuthorizationCodeAuthenticationProviderTests {
private ClientRegistration clientRegistration;
@ -81,6 +84,9 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
private OAuth2AccessTokenResponse accessTokenResponse;
private OAuth2UserService<OidcUserRequest, OidcUser> userService;
private OidcAuthorizationCodeAuthenticationProvider authenticationProvider;
private StringKeyGenerator stringKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private String nonce = this.stringKeyGenerator.generateKey();
private String nonceHash;
@Rule
public ExpectedException exception = ExpectedException.none();
@ -88,8 +94,21 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
@Before
@SuppressWarnings("unchecked")
public void setUp() {
try {
nonceHash = createHash(nonce);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
Map<String, Object> attributes = new HashMap<>();
Map<String, Object> additionalParameters = new HashMap<>();
addNonceToRequest(attributes, additionalParameters);
this.clientRegistration = clientRegistration().clientId("client1").build();
this.authorizationRequest = request().scope("openid", "profile", "email").build();
this.authorizationRequest = request()
.scope("openid", "profile", "email")
.attributes(attributes)
.additionalParameters(additionalParameters)
.build();
this.authorizationResponse = success().build();
this.authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest, this.authorizationResponse);
this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
@ -228,6 +247,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
claims.put(IdTokenClaimNames.SUB, "subject1");
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
claims.put(IdTokenClaimNames.AZP, "client1");
claims.put(IdTokenClaimNames.NONCE, nonceHash);
this.setUpIdToken(claims);
OidcUser principal = mock(OidcUser.class);
@ -257,6 +277,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
claims.put(IdTokenClaimNames.SUB, "subject1");
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
claims.put(IdTokenClaimNames.AZP, "client1");
claims.put(IdTokenClaimNames.NONCE, nonceHash);
this.setUpIdToken(claims);
OidcUser principal = mock(OidcUser.class);
@ -286,6 +307,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
claims.put(IdTokenClaimNames.SUB, "subject1");
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
claims.put(IdTokenClaimNames.AZP, "client1");
claims.put(IdTokenClaimNames.NONCE, nonceHash);
this.setUpIdToken(claims);
OidcUser principal = mock(OidcUser.class);
@ -302,9 +324,49 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
this.accessTokenResponse.getAdditionalParameters());
}
private void setUpIdToken(Map<String, Object> claims) {
Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
// gh-4442
@Test
public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToUserRequestNoNonce() {
OAuth2AuthorizationRequest authorizationRequestNoNonce = request()
.scope("openid", "profile", "email")
.attributes(new HashMap<>())
.additionalParameters(new HashMap<>())
.build();
OAuth2AuthorizationExchange authorizationExchangeNoNonce = new OAuth2AuthorizationExchange(authorizationRequestNoNonce, this.authorizationResponse);
Map<String, Object> claims = new HashMap<>();
claims.put(IdTokenClaimNames.ISS, "https://provider.com");
claims.put(IdTokenClaimNames.SUB, "subject1");
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
claims.put(IdTokenClaimNames.AZP, "client1");
this.setUpIdToken(claims);
OidcUser principal = mock(OidcUser.class);
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
when(principal.getAuthorities()).thenAnswer(
(Answer<List<GrantedAuthority>>) invocation -> authorities);
ArgumentCaptor<OidcUserRequest> userRequestArgCaptor = ArgumentCaptor.forClass(OidcUserRequest.class);
when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(principal);
this.authenticationProvider.authenticate(new OAuth2LoginAuthenticationToken(
this.clientRegistration, authorizationExchangeNoNonce));
assertThat(userRequestArgCaptor.getValue().getAdditionalParameters()).containsAllEntriesOf(
this.accessTokenResponse.getAdditionalParameters());
}
private void setUpIdToken(Map<String, Object> claims) {
Jwt idToken = Jwt.withTokenValue("token")
.header("alg", "none")
.audience(Collections.singletonList("https://audience.example.org"))
.expiresAt(Instant.MAX)
.issuedAt(Instant.MIN)
.issuer("https://issuer.example.org")
.jti("jti")
.notBefore(Instant.MIN)
.subject("mock-test-subject")
.claims(c -> c.putAll(claims))
.build();
JwtDecoder jwtDecoder = mock(JwtDecoder.class);
when(jwtDecoder.decode(anyString())).thenReturn(idToken);
this.authenticationProvider.setJwtDecoderFactory(registration -> jwtDecoder);
@ -317,6 +379,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
additionalParameters.put("param1", "value1");
additionalParameters.put("param2", "value2");
additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token");
additionalParameters.put(IdTokenClaimNames.NONCE, nonceHash);
return OAuth2AccessTokenResponse
.withToken("access-token-1234")
@ -328,4 +391,25 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
.build();
}
/**
* Adds nonce for use in OpenID Connect Authentication Requests
*
* @param attributes where {@link IdTokenClaimNames#NONCE} is stored for the token request
* @param additionalParameters where the hash of {@link IdTokenClaimNames#NONCE} is added to be used in the authentication request
*
* @since 5.2
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes">15.5.2. Nonce Implementation Notes</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation">3.1.3.7. ID Token Validation</a>
*/
private void addNonceToRequest(Map<String, Object> attributes, Map<String, Object> additionalParameters) {
attributes.put(IdTokenClaimNames.NONCE, nonce);
additionalParameters.put(IdTokenClaimNames.NONCE, nonceHash);
}
private String createHash(String nonce) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

View File

@ -24,10 +24,8 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.endpoint.*;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -37,6 +35,7 @@ import static org.assertj.core.api.Assertions.entry;
* Tests for {@link DefaultOAuth2AuthorizationRequestResolver}.
*
* @author Joe Grandja
* @author Mark Heckler
*/
public class DefaultOAuth2AuthorizationRequestResolverTests {
private ClientRegistration registration1;
@ -119,12 +118,14 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
assertThat(authorizationRequest.getState()).isNotNull();
assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(OAuth2ParameterNames.REGISTRATION_ID);
assertThat(authorizationRequest.getAttributes())
.containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
.containsOnlyKeys(OAuth2ParameterNames.REGISTRATION_ID, IdTokenClaimNames.NONCE)
.contains(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
assertThat(authorizationRequest.getAuthorizationRequestUri())
.matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/login/oauth2/code/registration-id");
"redirect_uri=http://localhost/login/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -137,7 +138,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request, clientRegistration.getRegistrationId());
assertThat(authorizationRequest).isNotNull();
assertThat(authorizationRequest.getAttributes())
.containsExactly(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
.containsOnlyKeys(OAuth2ParameterNames.REGISTRATION_ID, IdTokenClaimNames.NONCE)
.contains(entry(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()));
}
@Test
@ -259,7 +261,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
.matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/login/oauth2/code/registration-id");
"redirect_uri=http://localhost/login/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -277,7 +280,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
.matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=https://example.com/login/oauth2/code/registration-id");
"redirect_uri=https://example.com/login/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -292,7 +296,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
.matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/authorize/oauth2/code/registration-id");
"redirect_uri=http://localhost/authorize/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -307,7 +312,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
.matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id-2&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/login/oauth2/code/registration-id-2");
"redirect_uri=http://localhost/login/oauth2/code/registration-id-2&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -323,7 +329,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
.matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/authorize/oauth2/code/registration-id");
"redirect_uri=http://localhost/authorize/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -339,7 +346,8 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
.matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id-2&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/login/oauth2/code/registration-id-2");
"redirect_uri=http://localhost/login/oauth2/code/registration-id-2&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -375,6 +383,7 @@ public class DefaultOAuth2AuthorizationRequestResolverTests {
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/login/oauth2/code/pkce-client-registration-id&" +
"code_challenge_method=S256&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" +
"code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* 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.
@ -49,6 +49,7 @@ import static org.mockito.Mockito.*;
* Tests for {@link OAuth2AuthorizationRequestRedirectFilter}.
*
* @author Joe Grandja
* @author Mark Heckler
*/
public class OAuth2AuthorizationRequestRedirectFilterTests {
private ClientRegistration registration1;
@ -154,7 +155,8 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/login/oauth2/code/registration-id");
"redirect_uri=http://localhost/login/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -234,7 +236,8 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/login/oauth2/code/registration-id");
"redirect_uri=http://localhost/login/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -255,7 +258,8 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
assertThat(response.getRedirectedUrl()).matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/authorize/oauth2/code/registration-id");
"redirect_uri=http://localhost/authorize/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
verify(this.requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@ -359,6 +363,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.{15,}&" +
"redirect_uri=http://localhost/login/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" +
"login_hint=user@provider\\.com");
}
}

View File

@ -41,6 +41,7 @@ import static org.mockito.Mockito.when;
/**
* @author Rob Winch
* @author Mark Heckler
* @since 5.1
*/
@RunWith(MockitoJUnitRunner.class)
@ -82,7 +83,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests {
assertThat(request.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.*?&" +
"redirect_uri=/login/oauth2/code/registration-id");
"redirect_uri=/login/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
private OAuth2AuthorizationRequest resolve(String path) {
@ -101,7 +103,8 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests {
assertThat(request.getAuthorizationRequestUri()).matches("https://example.com/login/oauth/authorize\\?" +
"response_type=code&client_id=client-id&" +
"scope=read:user&state=.*?&" +
"redirect_uri=/login/oauth2/code/registration-id");
"redirect_uri=/login/oauth2/code/registration-id&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
@Test
@ -121,6 +124,7 @@ public class DefaultServerOAuth2AuthorizationRequestResolverTests {
"scope=read:user&state=.*?&" +
"redirect_uri=/login/oauth2/code/registration-id&" +
"code_challenge_method=S256&" +
"nonce=([a-zA-Z0-9\\-\\.\\_\\~]){43}&" +
"code_challenge=([a-zA-Z0-9\\-\\.\\_\\~]){43}");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@ -20,6 +20,7 @@ package org.springframework.security.oauth2.core.oidc.endpoint;
* and used by the authorization endpoint and token endpoint.
*
* @author Joe Grandja
* @author Mark Heckler
* @since 5.0
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#OAuthParametersRegistry">18.2 OAuth Parameters Registration</a>
*/
@ -30,4 +31,9 @@ public interface OidcParameterNames {
*/
String ID_TOKEN = "id_token";
/**
* {@code nonce} - used in the Access Token Request and Response.
*/
String NONCE = "nonce";
}