|
@@ -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");
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with 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;
|
|
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.Before;
|
|
import org.junit.Rule;
|
|
import org.junit.Rule;
|
|
import org.junit.Test;
|
|
import org.junit.Test;
|
|
import org.junit.rules.ExpectedException;
|
|
import org.junit.rules.ExpectedException;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.mockito.stubbing.Answer;
|
|
import org.mockito.stubbing.Answer;
|
|
-
|
|
|
|
import org.springframework.security.core.GrantedAuthority;
|
|
import org.springframework.security.core.GrantedAuthority;
|
|
import org.springframework.security.core.authority.AuthorityUtils;
|
|
import org.springframework.security.core.authority.AuthorityUtils;
|
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
|
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.authentication.OAuth2LoginAuthenticationToken;
|
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
|
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.JwtDecoder;
|
|
import org.springframework.security.oauth2.jwt.JwtException;
|
|
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.assertj.core.api.Assertions.assertThat;
|
|
import static org.hamcrest.CoreMatchers.containsString;
|
|
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.mock;
|
|
import static org.mockito.Mockito.when;
|
|
import static org.mockito.Mockito.when;
|
|
import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientRegistration;
|
|
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.TestOAuth2AuthorizationRequests.request;
|
|
import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationResponses.error;
|
|
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.core.endpoint.TestOAuth2AuthorizationResponses.success;
|
|
-import static org.springframework.security.oauth2.jwt.TestJwts.jwt;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
* Tests for {@link OidcAuthorizationCodeAuthenticationProvider}.
|
|
* Tests for {@link OidcAuthorizationCodeAuthenticationProvider}.
|
|
*
|
|
*
|
|
* @author Joe Grandja
|
|
* @author Joe Grandja
|
|
|
|
+ * @author Mark Heckler
|
|
*/
|
|
*/
|
|
public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
private ClientRegistration clientRegistration;
|
|
private ClientRegistration clientRegistration;
|
|
@@ -81,6 +84,9 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
private OAuth2AccessTokenResponse accessTokenResponse;
|
|
private OAuth2AccessTokenResponse accessTokenResponse;
|
|
private OAuth2UserService<OidcUserRequest, OidcUser> userService;
|
|
private OAuth2UserService<OidcUserRequest, OidcUser> userService;
|
|
private OidcAuthorizationCodeAuthenticationProvider authenticationProvider;
|
|
private OidcAuthorizationCodeAuthenticationProvider authenticationProvider;
|
|
|
|
+ private StringKeyGenerator stringKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
|
|
|
+ private String nonce = this.stringKeyGenerator.generateKey();
|
|
|
|
+ private String nonceHash;
|
|
|
|
|
|
@Rule
|
|
@Rule
|
|
public ExpectedException exception = ExpectedException.none();
|
|
public ExpectedException exception = ExpectedException.none();
|
|
@@ -88,8 +94,21 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
@Before
|
|
@Before
|
|
@SuppressWarnings("unchecked")
|
|
@SuppressWarnings("unchecked")
|
|
public void setUp() {
|
|
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.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.authorizationResponse = success().build();
|
|
this.authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest, this.authorizationResponse);
|
|
this.authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest, this.authorizationResponse);
|
|
this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
|
|
this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
|
|
@@ -228,6 +247,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
claims.put(IdTokenClaimNames.SUB, "subject1");
|
|
claims.put(IdTokenClaimNames.SUB, "subject1");
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
|
|
claims.put(IdTokenClaimNames.AZP, "client1");
|
|
claims.put(IdTokenClaimNames.AZP, "client1");
|
|
|
|
+ claims.put(IdTokenClaimNames.NONCE, nonceHash);
|
|
this.setUpIdToken(claims);
|
|
this.setUpIdToken(claims);
|
|
|
|
|
|
OidcUser principal = mock(OidcUser.class);
|
|
OidcUser principal = mock(OidcUser.class);
|
|
@@ -257,6 +277,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
claims.put(IdTokenClaimNames.SUB, "subject1");
|
|
claims.put(IdTokenClaimNames.SUB, "subject1");
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
|
|
claims.put(IdTokenClaimNames.AZP, "client1");
|
|
claims.put(IdTokenClaimNames.AZP, "client1");
|
|
|
|
+ claims.put(IdTokenClaimNames.NONCE, nonceHash);
|
|
this.setUpIdToken(claims);
|
|
this.setUpIdToken(claims);
|
|
|
|
|
|
OidcUser principal = mock(OidcUser.class);
|
|
OidcUser principal = mock(OidcUser.class);
|
|
@@ -286,6 +307,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
claims.put(IdTokenClaimNames.SUB, "subject1");
|
|
claims.put(IdTokenClaimNames.SUB, "subject1");
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client1", "client2"));
|
|
claims.put(IdTokenClaimNames.AZP, "client1");
|
|
claims.put(IdTokenClaimNames.AZP, "client1");
|
|
|
|
+ claims.put(IdTokenClaimNames.NONCE, nonceHash);
|
|
this.setUpIdToken(claims);
|
|
this.setUpIdToken(claims);
|
|
|
|
|
|
OidcUser principal = mock(OidcUser.class);
|
|
OidcUser principal = mock(OidcUser.class);
|
|
@@ -302,9 +324,49 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
this.accessTokenResponse.getAdditionalParameters());
|
|
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);
|
|
JwtDecoder jwtDecoder = mock(JwtDecoder.class);
|
|
when(jwtDecoder.decode(anyString())).thenReturn(idToken);
|
|
when(jwtDecoder.decode(anyString())).thenReturn(idToken);
|
|
this.authenticationProvider.setJwtDecoderFactory(registration -> jwtDecoder);
|
|
this.authenticationProvider.setJwtDecoderFactory(registration -> jwtDecoder);
|
|
@@ -317,6 +379,7 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
additionalParameters.put("param1", "value1");
|
|
additionalParameters.put("param1", "value1");
|
|
additionalParameters.put("param2", "value2");
|
|
additionalParameters.put("param2", "value2");
|
|
additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token");
|
|
additionalParameters.put(OidcParameterNames.ID_TOKEN, "id-token");
|
|
|
|
+ additionalParameters.put(IdTokenClaimNames.NONCE, nonceHash);
|
|
|
|
|
|
return OAuth2AccessTokenResponse
|
|
return OAuth2AccessTokenResponse
|
|
.withToken("access-token-1234")
|
|
.withToken("access-token-1234")
|
|
@@ -328,4 +391,25 @@ public class OidcAuthorizationCodeAuthenticationProviderTests {
|
|
.build();
|
|
.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);
|
|
|
|
+ }
|
|
}
|
|
}
|