|
@@ -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.
|
|
@@ -16,22 +16,16 @@
|
|
|
|
|
|
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.Map;
|
|
|
|
-
|
|
|
|
import org.junit.Before;
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.junit.runner.RunWith;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.mockito.Mock;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.MockitoJUnitRunner;
|
|
import org.mockito.junit.MockitoJUnitRunner;
|
|
-import reactor.core.publisher.Mono;
|
|
|
|
-
|
|
|
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
|
import org.springframework.security.authentication.TestingAuthenticationToken;
|
|
import org.springframework.security.core.authority.AuthorityUtils;
|
|
import org.springframework.security.core.authority.AuthorityUtils;
|
|
|
|
+import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
|
|
|
|
+import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
|
|
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
|
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
|
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
|
@@ -54,16 +48,25 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
|
import org.springframework.security.oauth2.jwt.Jwt;
|
|
import org.springframework.security.oauth2.jwt.Jwt;
|
|
import org.springframework.security.oauth2.jwt.JwtException;
|
|
import org.springframework.security.oauth2.jwt.JwtException;
|
|
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
|
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
|
|
|
+import reactor.core.publisher.Mono;
|
|
|
|
+
|
|
|
|
+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.Map;
|
|
|
|
|
|
-import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
-import static org.assertj.core.api.Assertions.assertThatCode;
|
|
|
|
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
|
|
|
|
+import static org.assertj.core.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.when;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
+import static org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager.createHash;
|
|
import static org.springframework.security.oauth2.jwt.TestJwts.jwt;
|
|
import static org.springframework.security.oauth2.jwt.TestJwts.jwt;
|
|
|
|
|
|
/**
|
|
/**
|
|
* @author Rob Winch
|
|
* @author Rob Winch
|
|
|
|
+ * @author Joe Grandja
|
|
* @since 5.1
|
|
* @since 5.1
|
|
*/
|
|
*/
|
|
@RunWith(MockitoJUnitRunner.class)
|
|
@RunWith(MockitoJUnitRunner.class)
|
|
@@ -89,6 +92,10 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
|
|
|
|
private OidcAuthorizationCodeReactiveAuthenticationManager manager;
|
|
private OidcAuthorizationCodeReactiveAuthenticationManager manager;
|
|
|
|
|
|
|
|
+ private StringKeyGenerator secureKeyGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
|
|
|
|
+
|
|
|
|
+ private String nonceHash;
|
|
|
|
+
|
|
@Before
|
|
@Before
|
|
public void setup() {
|
|
public void setup() {
|
|
this.manager = new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService);
|
|
this.manager = new OidcAuthorizationCodeReactiveAuthenticationManager(this.accessTokenResponseClient, this.userService);
|
|
@@ -164,6 +171,31 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
.hasMessageContaining("[invalid_id_token] ID Token Validation Error");
|
|
.hasMessageContaining("[invalid_id_token] ID Token Validation Error");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ @Test
|
|
|
|
+ public void authenticateWhenIdTokenInvalidNonceThenOAuth2AuthenticationException() {
|
|
|
|
+ OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo")
|
|
|
|
+ .tokenType(OAuth2AccessToken.TokenType.BEARER)
|
|
|
|
+ .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue()))
|
|
|
|
+ .build();
|
|
|
|
+
|
|
|
|
+ OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = loginToken();
|
|
|
|
+
|
|
|
|
+ Map<String, Object> claims = new HashMap<>();
|
|
|
|
+ claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
|
|
|
|
+ claims.put(IdTokenClaimNames.SUB, "sub");
|
|
|
|
+ claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id"));
|
|
|
|
+ claims.put(IdTokenClaimNames.NONCE, "invalid-nonce-hash");
|
|
|
|
+ Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
|
|
|
|
+
|
|
|
|
+ when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
|
|
+ when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken));
|
|
|
|
+ this.manager.setJwtDecoderFactory(c -> this.jwtDecoder);
|
|
|
|
+
|
|
|
|
+ assertThatThrownBy(() -> this.manager.authenticate(authorizationCodeAuthentication).block())
|
|
|
|
+ .isInstanceOf(OAuth2AuthenticationException.class)
|
|
|
|
+ .hasMessageContaining("[invalid_nonce]");
|
|
|
|
+ }
|
|
|
|
+
|
|
@Test
|
|
@Test
|
|
public void authenticationWhenOAuth2UserNotFoundThenEmpty() {
|
|
public void authenticationWhenOAuth2UserNotFoundThenEmpty() {
|
|
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo")
|
|
OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo")
|
|
@@ -171,17 +203,20 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
.additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."))
|
|
.additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."))
|
|
.build();
|
|
.build();
|
|
|
|
|
|
|
|
+ OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = loginToken();
|
|
|
|
+
|
|
Map<String, Object> claims = new HashMap<>();
|
|
Map<String, Object> claims = new HashMap<>();
|
|
claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
|
|
claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
|
|
claims.put(IdTokenClaimNames.SUB, "rob");
|
|
claims.put(IdTokenClaimNames.SUB, "rob");
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id"));
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id"));
|
|
|
|
+ claims.put(IdTokenClaimNames.NONCE, this.nonceHash);
|
|
Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
|
|
Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
|
|
|
|
|
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
when(this.userService.loadUser(any())).thenReturn(Mono.empty());
|
|
when(this.userService.loadUser(any())).thenReturn(Mono.empty());
|
|
when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken));
|
|
when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken));
|
|
this.manager.setJwtDecoderFactory(c -> this.jwtDecoder);
|
|
this.manager.setJwtDecoderFactory(c -> this.jwtDecoder);
|
|
- assertThat(this.manager.authenticate(loginToken()).block()).isNull();
|
|
|
|
|
|
+ assertThat(this.manager.authenticate(authorizationCodeAuthentication).block()).isNull();
|
|
}
|
|
}
|
|
|
|
|
|
@Test
|
|
@Test
|
|
@@ -191,10 +226,13 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
.additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue()))
|
|
.additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue()))
|
|
.build();
|
|
.build();
|
|
|
|
|
|
|
|
+ OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = loginToken();
|
|
|
|
+
|
|
Map<String, Object> claims = new HashMap<>();
|
|
Map<String, Object> claims = new HashMap<>();
|
|
claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
|
|
claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
|
|
claims.put(IdTokenClaimNames.SUB, "rob");
|
|
claims.put(IdTokenClaimNames.SUB, "rob");
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id"));
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id"));
|
|
|
|
+ claims.put(IdTokenClaimNames.NONCE, this.nonceHash);
|
|
Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
|
|
Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
|
|
|
|
|
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
@@ -203,7 +241,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken));
|
|
when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken));
|
|
this.manager.setJwtDecoderFactory(c -> this.jwtDecoder);
|
|
this.manager.setJwtDecoderFactory(c -> this.jwtDecoder);
|
|
|
|
|
|
- OAuth2LoginAuthenticationToken result = (OAuth2LoginAuthenticationToken) this.manager.authenticate(loginToken()).block();
|
|
|
|
|
|
+ OAuth2LoginAuthenticationToken result = (OAuth2LoginAuthenticationToken) this.manager.authenticate(authorizationCodeAuthentication).block();
|
|
|
|
|
|
assertThat(result.getPrincipal()).isEqualTo(user);
|
|
assertThat(result.getPrincipal()).isEqualTo(user);
|
|
assertThat(result.getAuthorities()).containsOnlyElementsOf(user.getAuthorities());
|
|
assertThat(result.getAuthorities()).containsOnlyElementsOf(user.getAuthorities());
|
|
@@ -218,10 +256,13 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
.refreshToken("refresh-token")
|
|
.refreshToken("refresh-token")
|
|
.build();
|
|
.build();
|
|
|
|
|
|
|
|
+ OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = loginToken();
|
|
|
|
+
|
|
Map<String, Object> claims = new HashMap<>();
|
|
Map<String, Object> claims = new HashMap<>();
|
|
claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
|
|
claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
|
|
claims.put(IdTokenClaimNames.SUB, "rob");
|
|
claims.put(IdTokenClaimNames.SUB, "rob");
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id"));
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList("client-id"));
|
|
|
|
+ claims.put(IdTokenClaimNames.NONCE, this.nonceHash);
|
|
Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
|
|
Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
|
|
|
|
|
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
@@ -230,7 +271,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken));
|
|
when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken));
|
|
this.manager.setJwtDecoderFactory(c -> this.jwtDecoder);
|
|
this.manager.setJwtDecoderFactory(c -> this.jwtDecoder);
|
|
|
|
|
|
- OAuth2LoginAuthenticationToken result = (OAuth2LoginAuthenticationToken) this.manager.authenticate(loginToken()).block();
|
|
|
|
|
|
+ OAuth2LoginAuthenticationToken result = (OAuth2LoginAuthenticationToken) this.manager.authenticate(authorizationCodeAuthentication).block();
|
|
|
|
|
|
assertThat(result.getPrincipal()).isEqualTo(user);
|
|
assertThat(result.getPrincipal()).isEqualTo(user);
|
|
assertThat(result.getAuthorities()).containsOnlyElementsOf(user.getAuthorities());
|
|
assertThat(result.getAuthorities()).containsOnlyElementsOf(user.getAuthorities());
|
|
@@ -251,10 +292,13 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
.additionalParameters(additionalParameters)
|
|
.additionalParameters(additionalParameters)
|
|
.build();
|
|
.build();
|
|
|
|
|
|
|
|
+ OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = loginToken();
|
|
|
|
+
|
|
Map<String, Object> claims = new HashMap<>();
|
|
Map<String, Object> claims = new HashMap<>();
|
|
claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
|
|
claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com");
|
|
claims.put(IdTokenClaimNames.SUB, "rob");
|
|
claims.put(IdTokenClaimNames.SUB, "rob");
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList(clientRegistration.getClientId()));
|
|
claims.put(IdTokenClaimNames.AUD, Arrays.asList(clientRegistration.getClientId()));
|
|
|
|
+ claims.put(IdTokenClaimNames.NONCE, this.nonceHash);
|
|
Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
|
|
Jwt idToken = jwt().claims(c -> c.putAll(claims)).build();
|
|
|
|
|
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse));
|
|
@@ -264,7 +308,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken));
|
|
when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken));
|
|
this.manager.setJwtDecoderFactory(c -> this.jwtDecoder);
|
|
this.manager.setJwtDecoderFactory(c -> this.jwtDecoder);
|
|
|
|
|
|
- this.manager.authenticate(loginToken()).block();
|
|
|
|
|
|
+ this.manager.authenticate(authorizationCodeAuthentication).block();
|
|
|
|
|
|
assertThat(userRequestArgCaptor.getValue().getAdditionalParameters())
|
|
assertThat(userRequestArgCaptor.getValue().getAdditionalParameters())
|
|
.containsAllEntriesOf(accessTokenResponse.getAdditionalParameters());
|
|
.containsAllEntriesOf(accessTokenResponse.getAdditionalParameters());
|
|
@@ -272,6 +316,14 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
|
|
|
|
private OAuth2AuthorizationCodeAuthenticationToken loginToken() {
|
|
private OAuth2AuthorizationCodeAuthenticationToken loginToken() {
|
|
ClientRegistration clientRegistration = this.registration.build();
|
|
ClientRegistration clientRegistration = this.registration.build();
|
|
|
|
+ Map<String, Object> attributes = new HashMap<>();
|
|
|
|
+ Map<String, Object> additionalParameters = new HashMap<>();
|
|
|
|
+ try {
|
|
|
|
+ String nonce = this.secureKeyGenerator.generateKey();
|
|
|
|
+ this.nonceHash = createHash(nonce);
|
|
|
|
+ attributes.put(OidcParameterNames.NONCE, nonce);
|
|
|
|
+ additionalParameters.put(OidcParameterNames.NONCE, this.nonceHash);
|
|
|
|
+ } catch (NoSuchAlgorithmException e) { }
|
|
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest
|
|
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest
|
|
.authorizationCode()
|
|
.authorizationCode()
|
|
.state("state")
|
|
.state("state")
|
|
@@ -279,6 +331,8 @@ public class OidcAuthorizationCodeReactiveAuthenticationManagerTests {
|
|
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
|
.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
|
.redirectUri(clientRegistration.getRedirectUriTemplate())
|
|
.redirectUri(clientRegistration.getRedirectUriTemplate())
|
|
.scopes(clientRegistration.getScopes())
|
|
.scopes(clientRegistration.getScopes())
|
|
|
|
+ .additionalParameters(additionalParameters)
|
|
|
|
+ .attributes(attributes)
|
|
.build();
|
|
.build();
|
|
OAuth2AuthorizationResponse authorizationResponse = this.authorizationResponseBldr
|
|
OAuth2AuthorizationResponse authorizationResponse = this.authorizationResponseBldr
|
|
.redirectUri(clientRegistration.getRedirectUriTemplate())
|
|
.redirectUri(clientRegistration.getRedirectUriTemplate())
|