Add OpenID Connect 1.0 Logout Endpoint

Closes gh-266
This commit is contained in:
Joe Grandja
2023-01-30 10:47:36 -05:00
parent 7c6516bbbb
commit 98e3fe807a
66 changed files with 2718 additions and 84 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -26,6 +26,8 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
@@ -47,6 +49,7 @@ public class InMemoryOAuth2AuthorizationServiceTests {
"code", Instant.now(), Instant.now().plus(5, ChronoUnit.MINUTES));
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private InMemoryOAuth2AuthorizationService authorizationService;
@BeforeEach
@@ -263,6 +266,29 @@ public class InMemoryOAuth2AuthorizationServiceTests {
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenWhenIdTokenExistsThenFound() {
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject("subject")
.issuedAt(Instant.now().minusSeconds(60))
.expiresAt(Instant.now())
.build();
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.id(ID)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.token(idToken)
.build();
this.authorizationService.save(authorization);
OAuth2Authorization result = this.authorizationService.findByToken(
idToken.getTokenValue(), ID_TOKEN_TOKEN_TYPE);
assertThat(authorization).isEqualTo(result);
result = this.authorizationService.findByToken(idToken.getTokenValue(), null);
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenWhenRefreshTokenExistsThenFound() {
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", Instant.now());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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 org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
@@ -76,6 +77,7 @@ public class JdbcOAuth2AuthorizationServiceTests {
private static final String OAUTH2_AUTHORIZATION_SCHEMA_CLOB_DATA_TYPE_SQL_RESOURCE = "org/springframework/security/oauth2/server/authorization/custom-oauth2-authorization-schema-clob-data-type.sql";
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private static final String ID = "id";
private static final RegisteredClient REGISTERED_CLIENT = TestRegisteredClients.registeredClient().build();
private static final String PRINCIPAL_NAME = "principal";
@@ -344,6 +346,32 @@ public class JdbcOAuth2AuthorizationServiceTests {
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenWhenIdTokenExistsThenFound() {
when(this.registeredClientRepository.findById(eq(REGISTERED_CLIENT.getId())))
.thenReturn(REGISTERED_CLIENT);
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject("subject")
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().truncatedTo(ChronoUnit.MILLIS))
.build();
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(REGISTERED_CLIENT)
.id(ID)
.principalName(PRINCIPAL_NAME)
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE)
.token(idToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
this.authorizationService.save(authorization);
OAuth2Authorization result = this.authorizationService.findByToken(
idToken.getTokenValue(), ID_TOKEN_TOKEN_TYPE);
assertThat(authorization).isEqualTo(result);
result = this.authorizationService.findByToken(idToken.getTokenValue(), null);
assertThat(authorization).isEqualTo(result);
}
@Test
public void findByTokenWhenRefreshTokenExistsThenFound() {
when(this.registeredClientRepository.findById(eq(REGISTERED_CLIENT.getId())))
@@ -494,7 +522,7 @@ public class JdbcOAuth2AuthorizationServiceTests {
private static final String PK_FILTER = "id = ?";
private static final String UNKNOWN_TOKEN_TYPE_FILTER = "state = ? OR authorizationCodeValue = ? OR " +
"accessTokenValue = ? OR refreshTokenValue = ?";
"accessTokenValue = ? OR oidcIdTokenValue = ? OR refreshTokenValue = ?";
// @formatter:off
private static final String LOAD_AUTHORIZATION_SQL = "SELECT " + COLUMN_NAMES
@@ -539,7 +567,7 @@ public class JdbcOAuth2AuthorizationServiceTests {
@Override
public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
return findBy(UNKNOWN_TOKEN_TYPE_FILTER, token, token, token, token);
return findBy(UNKNOWN_TOKEN_TYPE_FILTER, token, token, token, token, token);
}
private OAuth2Authorization findBy(String filter, Object... args) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -19,8 +19,11 @@ import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -31,6 +34,8 @@ import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -95,6 +100,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
private OAuth2TokenGenerator<?> tokenGenerator;
private SessionRegistry sessionRegistry;
private OAuth2AuthorizationCodeAuthenticationProvider authenticationProvider;
@BeforeEach
@@ -116,8 +122,10 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
return delegatingTokenGenerator.generate(context);
}
});
this.sessionRegistry = mock(SessionRegistry.class);
this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
this.authorizationService, this.tokenGenerator);
this.authenticationProvider.setSessionRegistry(this.sessionRegistry);
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder().issuer("https://provider.com").build();
AuthorizationServerContextHolder.setContext(new TestAuthorizationServerContext(authorizationServerSettings, null));
}
@@ -146,6 +154,13 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
assertThat(this.authenticationProvider.supports(OAuth2AuthorizationCodeAuthenticationToken.class)).isTrue();
}
@Test
public void setSessionRegistryWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authenticationProvider.setSessionRegistry(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("sessionRegistry cannot be null");
}
@Test
public void authenticateWhenClientPrincipalNotOAuth2ClientAuthenticationTokenThenThrowOAuth2AuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
@@ -456,6 +471,19 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
when(this.jwtEncoder.encode(any())).thenReturn(createJwt());
Authentication principal = authorization.getAttribute(Principal.class.getName());
List<SessionInformation> sessions = new ArrayList<>();
sessions.add(new SessionInformation(principal.getPrincipal(),
"session3", Date.from(Instant.now())));
sessions.add(new SessionInformation(principal.getPrincipal(),
"session2", Date.from(Instant.now().minus(1, ChronoUnit.HOURS))));
sessions.add(new SessionInformation(principal.getPrincipal(),
"session1", Date.from(Instant.now().minus(2, ChronoUnit.HOURS))));
SessionInformation expectedSession = sessions.get(0); // Most recent
when(this.sessionRegistry.getAllSessions(eq(principal.getPrincipal()), eq(false)))
.thenReturn(sessions);
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
(OAuth2AccessTokenAuthenticationToken) this.authenticationProvider.authenticate(authentication);
@@ -464,7 +492,7 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
// Access Token context
JwtEncodingContext accessTokenContext = jwtEncodingContextCaptor.getAllValues().get(0);
assertThat(accessTokenContext.getRegisteredClient()).isEqualTo(registeredClient);
assertThat(accessTokenContext.<Authentication>getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName()));
assertThat(accessTokenContext.<Authentication>getPrincipal()).isEqualTo(principal);
assertThat(accessTokenContext.getAuthorization()).isEqualTo(authorization);
assertThat(accessTokenContext.getAuthorization().getAccessToken()).isNull();
assertThat(accessTokenContext.getAuthorizedScopes()).isEqualTo(authorization.getAuthorizedScopes());
@@ -480,13 +508,15 @@ public class OAuth2AuthorizationCodeAuthenticationProviderTests {
// ID Token context
JwtEncodingContext idTokenContext = jwtEncodingContextCaptor.getAllValues().get(1);
assertThat(idTokenContext.getRegisteredClient()).isEqualTo(registeredClient);
assertThat(idTokenContext.<Authentication>getPrincipal()).isEqualTo(authorization.getAttribute(Principal.class.getName()));
assertThat(idTokenContext.<Authentication>getPrincipal()).isEqualTo(principal);
assertThat(idTokenContext.getAuthorization()).isNotEqualTo(authorization);
assertThat(idTokenContext.getAuthorization().getAccessToken()).isNotNull();
assertThat(idTokenContext.getAuthorizedScopes()).isEqualTo(authorization.getAuthorizedScopes());
assertThat(idTokenContext.getTokenType().getValue()).isEqualTo(OidcParameterNames.ID_TOKEN);
assertThat(idTokenContext.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(idTokenContext.<OAuth2AuthorizationGrantAuthenticationToken>getAuthorizationGrant()).isEqualTo(authentication);
SessionInformation sessionInformation = idTokenContext.get(SessionInformation.class);
assertThat(sessionInformation).isNotNull().isSameAs(expectedSession);
assertThat(idTokenContext.getJwsHeader()).isNotNull();
assertThat(idTokenContext.getClaims()).isNotNull();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -294,6 +294,7 @@ public class JdbcRegisteredClientRepositoryTests {
+ "clientAuthenticationMethods, "
+ "authorizationGrantTypes, "
+ "redirectUris, "
+ "postLogoutRedirectUris, "
+ "scopes, "
+ "clientSettings,"
+ "tokenSettings";
@@ -305,7 +306,7 @@ public class JdbcRegisteredClientRepositoryTests {
// @formatter:off
private static final String INSERT_REGISTERED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
// @formatter:on
private CustomJdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
@@ -353,6 +354,7 @@ public class JdbcRegisteredClientRepositoryTests {
Set<String> clientAuthenticationMethods = StringUtils.commaDelimitedListToSet(rs.getString("clientAuthenticationMethods"));
Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(rs.getString("authorizationGrantTypes"));
Set<String> redirectUris = StringUtils.commaDelimitedListToSet(rs.getString("redirectUris"));
Set<String> postLogoutRedirectUris = StringUtils.commaDelimitedListToSet(rs.getString("postLogoutRedirectUris"));
Set<String> clientScopes = StringUtils.commaDelimitedListToSet(rs.getString("scopes"));
// @formatter:off
@@ -369,6 +371,7 @@ public class JdbcRegisteredClientRepositoryTests {
authorizationGrantTypes.forEach(grantType ->
grantTypes.add(resolveAuthorizationGrantType(grantType))))
.redirectUris((uris) -> uris.addAll(redirectUris))
.postLogoutRedirectUris((uris) -> uris.addAll(postLogoutRedirectUris))
.scopes((scopes) -> scopes.addAll(clientScopes));
// @formatter:on

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -40,6 +40,7 @@ public class RegisteredClientTests {
private static final String CLIENT_ID = "client-1";
private static final String CLIENT_SECRET = "secret";
private static final Set<String> REDIRECT_URIS = Collections.singleton("https://example.com");
private static final Set<String> POST_LOGOUT_REDIRECT_URIS = Collections.singleton("https://example.com/oidc-post-logout");
private static final Set<String> SCOPES = Collections.unmodifiableSet(
Stream.of("openid", "profile", "email").collect(Collectors.toSet()));
private static final Set<ClientAuthenticationMethod> CLIENT_AUTHENTICATION_METHODS =
@@ -71,6 +72,7 @@ public class RegisteredClientTests {
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.redirectUris(redirectUris -> redirectUris.addAll(REDIRECT_URIS))
.postLogoutRedirectUris(postLogoutRedirectUris -> postLogoutRedirectUris.addAll(POST_LOGOUT_REDIRECT_URIS))
.scopes(scopes -> scopes.addAll(SCOPES))
.build();
@@ -84,6 +86,7 @@ public class RegisteredClientTests {
.isEqualTo(Collections.singleton(AuthorizationGrantType.AUTHORIZATION_CODE));
assertThat(registration.getClientAuthenticationMethods()).isEqualTo(CLIENT_AUTHENTICATION_METHODS);
assertThat(registration.getRedirectUris()).isEqualTo(REDIRECT_URIS);
assertThat(registration.getPostLogoutRedirectUris()).isEqualTo(POST_LOGOUT_REDIRECT_URIS);
assertThat(registration.getScopes()).isEqualTo(SCOPES);
}
@@ -229,6 +232,35 @@ public class RegisteredClientTests {
).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void buildWhenPostLogoutRedirectUriInvalidThenThrowIllegalArgumentException() {
assertThatThrownBy(() ->
RegisteredClient.withId(ID)
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.redirectUris(redirectUris -> redirectUris.addAll(REDIRECT_URIS))
.postLogoutRedirectUri("invalid URI")
.build()
).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void buildWhenPostLogoutRedirectUriContainsFragmentThenThrowIllegalArgumentException() {
assertThatThrownBy(() ->
RegisteredClient.withId(ID)
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.redirectUri("https://example.com")
.postLogoutRedirectUri("https://example.com/index#fragment")
.scopes(scopes -> scopes.addAll(SCOPES))
.build()
).isInstanceOf(IllegalArgumentException.class);
}
@Test
public void buildWhenTwoAuthorizationGrantTypesAreProvidedThenBothAreRegistered() {
RegisteredClient registration = RegisteredClient.withId(ID)
@@ -345,6 +377,8 @@ public class RegisteredClientTests {
assertThat(registration.getAuthorizationGrantTypes()).isNotSameAs(updated.getAuthorizationGrantTypes());
assertThat(registration.getRedirectUris()).isEqualTo(updated.getRedirectUris());
assertThat(registration.getRedirectUris()).isNotSameAs(updated.getRedirectUris());
assertThat(registration.getPostLogoutRedirectUris()).isEqualTo(updated.getPostLogoutRedirectUris());
assertThat(registration.getPostLogoutRedirectUris()).isNotSameAs(updated.getPostLogoutRedirectUris());
assertThat(registration.getScopes()).isEqualTo(updated.getScopes());
assertThat(registration.getScopes()).isNotSameAs(updated.getScopes());
assertThat(registration.getClientSettings()).isEqualTo(updated.getClientSettings());
@@ -360,6 +394,7 @@ public class RegisteredClientTests {
String newSecret = "new-secret";
String newScope = "new-scope";
String newRedirectUri = "https://another-redirect-uri.com";
String newPostLogoutRedirectUri = "https://another-post-logout-redirect-uri.com";
RegisteredClient updated = RegisteredClient.from(registration)
.clientName(newName)
.clientSecret(newSecret)
@@ -371,6 +406,10 @@ public class RegisteredClientTests {
redirectUris.clear();
redirectUris.add(newRedirectUri);
})
.postLogoutRedirectUris(postLogoutRedirectUris -> {
postLogoutRedirectUris.clear();
postLogoutRedirectUris.add(newPostLogoutRedirectUri);
})
.build();
assertThat(registration.getClientName()).isNotEqualTo(newName);
@@ -381,6 +420,8 @@ public class RegisteredClientTests {
assertThat(updated.getScopes()).containsExactly(newScope);
assertThat(registration.getRedirectUris()).doesNotContain(newRedirectUri);
assertThat(updated.getRedirectUris()).containsExactly(newRedirectUri);
assertThat(registration.getPostLogoutRedirectUris()).doesNotContain(newPostLogoutRedirectUri);
assertThat(updated.getPostLogoutRedirectUris()).containsExactly(newPostLogoutRedirectUri);
}
@Test

View File

@@ -38,6 +38,7 @@ public class TestRegisteredClients {
.redirectUri("https://example.com/callback-1")
.redirectUri("https://example.com/callback-2")
.redirectUri("https://example.com/callback-3")
.postLogoutRedirectUri("https://example.com/oidc-post-logout")
.scope("scope1");
}
@@ -52,6 +53,7 @@ public class TestRegisteredClients {
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.redirectUri("https://example.com")
.postLogoutRedirectUri("https://example.com/oidc-post-logout")
.scope("scope1")
.scope("scope2");
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -126,6 +126,7 @@ public class OidcProviderConfigurationTests {
jsonPath("$.token_endpoint_auth_methods_supported[3]").value(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()),
jsonPath("jwks_uri").value(ISSUER_URL.concat(this.authorizationServerSettings.getJwkSetEndpoint())),
jsonPath("userinfo_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcUserInfoEndpoint())),
jsonPath("end_session_endpoint").value(ISSUER_URL.concat(this.authorizationServerSettings.getOidcLogoutEndpoint())),
jsonPath("response_types_supported").value(OAuth2AuthorizationResponseType.CODE.getValue()),
jsonPath("$.grant_types_supported[0]").value(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
jsonPath("$.grant_types_supported[1]").value(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()),

View File

@@ -47,6 +47,7 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -123,6 +124,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
public class OidcTests {
private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
private static final String DEFAULT_OIDC_LOGOUT_ENDPOINT_URI = "/connect/logout";
private static final String AUTHORITIES_CLAIM = "authorities";
private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE);
private static EmbeddedDatabase db;
@@ -216,8 +218,9 @@ public class OidcTests {
servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
// Assert user authorities was propagated as claim in ID Token
Jwt idToken = this.jwtDecoder.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
// Assert user authorities was propagated as claim in ID Token
List<String> authoritiesClaim = idToken.getClaim(AUTHORITIES_CLAIM);
Authentication principal = authorization.getAttribute(Principal.class.getName());
Set<String> userAuthorities = new HashSet<>();
@@ -225,6 +228,59 @@ public class OidcTests {
userAuthorities.add(authority.getAuthority());
}
assertThat(authoritiesClaim).containsExactlyInAnyOrderElementsOf(userAuthorities);
// Assert sid claim was added in ID Token
assertThat(idToken.<String>getClaim("sid")).isNotNull();
}
@Test
public void requestWhenLogoutRequestThenLogout() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
this.registeredClientRepository.save(registeredClient);
// Login
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient);
MvcResult mvcResult = this.mvc.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
.params(authorizationRequestParameters)
.with(user("user")))
.andExpect(status().is3xxRedirection())
.andReturn();
MockHttpSession session = (MockHttpSession) mvcResult.getRequest().getSession();
assertThat(session.isNew()).isTrue();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, AUTHORIZATION_CODE_TOKEN_TYPE);
// Get ID Token
mvcResult = this.mvc.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
.params(getTokenRequestParameters(registeredClient, authorization))
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
registeredClient.getClientId(), registeredClient.getClientSecret()))
.session(session))
.andExpect(status().isOk())
.andReturn();
MockHttpServletResponse servletResponse = mvcResult.getResponse();
MockClientHttpResponse httpResponse = new MockClientHttpResponse(
servletResponse.getContentAsByteArray(), HttpStatus.valueOf(servletResponse.getStatus()));
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter.read(OAuth2AccessTokenResponse.class, httpResponse);
String idToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN);
// Logout
mvcResult = this.mvc.perform(post(DEFAULT_OIDC_LOGOUT_ENDPOINT_URI)
.param("id_token_hint", idToken)
.session(session))
.andExpect(status().is3xxRedirection())
.andReturn();
redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
assertThat(redirectedUrl).matches("/");
assertThat(session.isInvalid()).isTrue();
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -58,6 +58,7 @@ public class OidcClientRegistrationTests {
.clientSecretExpiresAt(clientSecretExpiresAt)
.clientName("client-name")
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("https://client.example.com/oidc-post-logout")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
@@ -79,6 +80,7 @@ public class OidcClientRegistrationTests {
assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt);
assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
assertThat(clientRegistration.getPostLogoutRedirectUris()).containsOnly("https://client.example.com/oidc-post-logout");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName());
assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
@@ -108,6 +110,7 @@ public class OidcClientRegistrationTests {
claims.put(OidcClientMetadataClaimNames.CLIENT_SECRET_EXPIRES_AT, clientSecretExpiresAt);
claims.put(OidcClientMetadataClaimNames.CLIENT_NAME, "client-name");
claims.put(OidcClientMetadataClaimNames.REDIRECT_URIS, Collections.singletonList("https://client.example.com"));
claims.put(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, Collections.singletonList("https://client.example.com/oidc-post-logout"));
claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_METHOD, ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
claims.put(OidcClientMetadataClaimNames.TOKEN_ENDPOINT_AUTH_SIGNING_ALG, MacAlgorithm.HS256.getName());
claims.put(OidcClientMetadataClaimNames.GRANT_TYPES, Arrays.asList(
@@ -128,6 +131,7 @@ public class OidcClientRegistrationTests {
assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(clientSecretExpiresAt);
assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
assertThat(clientRegistration.getPostLogoutRedirectUris()).containsOnly("https://client.example.com/oidc-post-logout");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName());
assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
@@ -261,6 +265,41 @@ public class OidcClientRegistrationTests {
assertThat(clientRegistration.getRedirectUris()).containsExactly("https://client2.example.com");
}
@Test
public void buildWhenPostLogoutRedirectUrisNotListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.minimalBuilder
.claim(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, "postLogoutRedirectUris");
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessageStartingWith("post_logout_redirect_uris must be of type List");
}
@Test
public void buildWhenPostLogoutRedirectUrisEmptyListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.minimalBuilder
.claim(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS, Collections.emptyList());
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("post_logout_redirect_uris cannot be empty");
}
@Test
public void buildWhenPostLogoutRedirectUrisAddingOrRemovingThenCorrectValues() {
// @formatter:off
OidcClientRegistration clientRegistration = this.minimalBuilder
.postLogoutRedirectUri("https://client1.example.com/oidc-post-logout")
.postLogoutRedirectUris(postLogoutRedirectUris -> {
postLogoutRedirectUris.clear();
postLogoutRedirectUris.add("https://client2.example.com/oidc-post-logout");
})
.build();
// @formatter:on
assertThat(clientRegistration.getPostLogoutRedirectUris()).containsExactly("https://client2.example.com/oidc-post-logout");
}
@Test
public void buildWhenGrantTypesNotListThenThrowIllegalArgumentException() {
OidcClientRegistration.Builder builder = this.minimalBuilder

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -62,6 +62,7 @@ public class OidcProviderConfigurationTests {
.userInfoEndpoint("https://example.com/issuer1/userinfo")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
.clientRegistrationEndpoint("https://example.com/issuer1/connect/register")
.endSessionEndpoint("https://example.com/issuer1/connect/logout")
.claim("a-claim", "a-value")
.build();
@@ -77,6 +78,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo"));
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
assertThat(providerConfiguration.getEndSessionEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/logout"));
assertThat(providerConfiguration.<String>getClaim("a-claim")).isEqualTo("a-value");
}
@@ -118,6 +120,7 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256"));
claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, "https://example.com/issuer1/userinfo");
claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, "https://example.com/issuer1/connect/register");
claims.put(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT, "https://example.com/issuer1/connect/logout");
claims.put("some-claim", "some-value");
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build();
@@ -134,6 +137,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo"));
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
assertThat(providerConfiguration.getEndSessionEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/logout"));
assertThat(providerConfiguration.<String>getClaim("some-claim")).isEqualTo("some-value");
}
@@ -150,6 +154,7 @@ public class OidcProviderConfigurationTests {
claims.put(OidcProviderMetadataClaimNames.ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED, Collections.singletonList("RS256"));
claims.put(OidcProviderMetadataClaimNames.USER_INFO_ENDPOINT, url("https://example.com/issuer1/userinfo"));
claims.put(OidcProviderMetadataClaimNames.REGISTRATION_ENDPOINT, url("https://example.com/issuer1/connect/register"));
claims.put(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT, url("https://example.com/issuer1/connect/logout"));
claims.put("some-claim", "some-value");
OidcProviderConfiguration providerConfiguration = OidcProviderConfiguration.withClaims(claims).build();
@@ -166,6 +171,7 @@ public class OidcProviderConfigurationTests {
assertThat(providerConfiguration.getUserInfoEndpoint()).isEqualTo(url("https://example.com/issuer1/userinfo"));
assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull();
assertThat(providerConfiguration.getClientRegistrationEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/register"));
assertThat(providerConfiguration.getEndSessionEndpoint()).isEqualTo(url("https://example.com/issuer1/connect/logout"));
assertThat(providerConfiguration.<String>getClaim("some-claim")).isEqualTo("some-value");
}
@@ -412,6 +418,16 @@ public class OidcProviderConfigurationTests {
.withMessage("clientRegistrationEndpoint must be a valid URL");
}
@Test
public void buildWhenEndSessionEndpointNotUrlThenThrowIllegalArgumentException() {
OidcProviderConfiguration.Builder builder = this.minimalConfigurationBuilder
.claims((claims) -> claims.put(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT, "not an url"));
assertThatIllegalArgumentException()
.isThrownBy(builder::build)
.withMessage("endSessionEndpoint must be a valid URL");
}
@Test
public void responseTypesWhenAddingOrRemovingThenCorrectValues() {
OidcProviderConfiguration configuration = this.minimalConfigurationBuilder

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -359,6 +359,78 @@ public class OidcClientRegistrationAuthenticationProviderTests {
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenInvalidPostLogoutRedirectUriThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(),
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
registeredClient, jwtAccessToken, jwt.getClaims()).build();
when(this.authorizationService.findByToken(
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
.thenReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create"));
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("invalid uri")
.build();
// @formatter:on
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo("invalid_client_metadata");
assertThat(error.getDescription()).contains(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
});
verify(this.authorizationService).findByToken(
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenPostLogoutRedirectUriContainsFragmentThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(),
jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(
registeredClient, jwtAccessToken, jwt.getClaims()).build();
when(this.authorizationService.findByToken(
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN)))
.thenReturn(authorization);
JwtAuthenticationToken principal = new JwtAuthenticationToken(
jwt, AuthorityUtils.createAuthorityList("SCOPE_client.create"));
// @formatter:off
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("https://client.example.com/oidc-post-logout#fragment")
.build();
// @formatter:on
OidcClientRegistrationAuthenticationToken authentication = new OidcClientRegistrationAuthenticationToken(
principal, clientRegistration);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo("invalid_client_metadata");
assertThat(error.getDescription()).contains(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
});
verify(this.authorizationService).findByToken(
eq(jwtAccessToken.getTokenValue()), eq(OAuth2TokenType.ACCESS_TOKEN));
}
@Test
public void authenticateWhenInvalidTokenEndpointAuthenticationMethodThenThrowOAuth2AuthenticationException() {
Jwt jwt = createJwtClientRegistration();
@@ -545,6 +617,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
OidcClientRegistration clientRegistration = OidcClientRegistration.builder()
.clientName("client-name")
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("https://client.example.com/oidc-post-logout")
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
.grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.scope("scope1")
@@ -588,6 +661,7 @@ public class OidcClientRegistrationAuthenticationProviderTests {
assertThat(registeredClientResult.getClientName()).isEqualTo(clientRegistration.getClientName());
assertThat(registeredClientResult.getClientAuthenticationMethods()).containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
assertThat(registeredClientResult.getRedirectUris()).containsExactly("https://client.example.com");
assertThat(registeredClientResult.getPostLogoutRedirectUris()).containsExactly("https://client.example.com/oidc-post-logout");
assertThat(registeredClientResult.getAuthorizationGrantTypes())
.containsExactlyInAnyOrder(AuthorizationGrantType.AUTHORIZATION_CODE, AuthorizationGrantType.CLIENT_CREDENTIALS);
assertThat(registeredClientResult.getScopes()).containsExactlyInAnyOrder("scope1", "scope2");
@@ -603,6 +677,8 @@ public class OidcClientRegistrationAuthenticationProviderTests {
assertThat(clientRegistrationResult.getClientName()).isEqualTo(registeredClientResult.getClientName());
assertThat(clientRegistrationResult.getRedirectUris())
.containsExactlyInAnyOrderElementsOf(registeredClientResult.getRedirectUris());
assertThat(clientRegistrationResult.getPostLogoutRedirectUris())
.containsExactlyInAnyOrderElementsOf(registeredClientResult.getPostLogoutRedirectUris());
List<String> grantTypes = new ArrayList<>();
registeredClientResult.getAuthorizationGrantTypes().forEach(authorizationGrantType ->

View File

@@ -0,0 +1,426 @@
/*
* Copyright 2020-2023 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.server.authorization.oidc.authentication;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Tests for {@link OidcLogoutAuthenticationProvider}.
*
* @author Joe Grandja
*/
public class OidcLogoutAuthenticationProviderTests {
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private RegisteredClientRepository registeredClientRepository;
private OAuth2AuthorizationService authorizationService;
private SessionRegistry sessionRegistry;
private AuthorizationServerSettings authorizationServerSettings;
private OidcLogoutAuthenticationProvider authenticationProvider;
@BeforeEach
public void setUp() {
this.registeredClientRepository = mock(RegisteredClientRepository.class);
this.authorizationService = mock(OAuth2AuthorizationService.class);
this.sessionRegistry = mock(SessionRegistry.class);
this.authorizationServerSettings = AuthorizationServerSettings.builder().issuer("https://provider.com").build();
TestAuthorizationServerContext authorizationServerContext =
new TestAuthorizationServerContext(this.authorizationServerSettings, null);
AuthorizationServerContextHolder.setContext(authorizationServerContext);
this.authenticationProvider = new OidcLogoutAuthenticationProvider(
this.registeredClientRepository, this.authorizationService, this.sessionRegistry);
}
@AfterEach
public void cleanup() {
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationProvider(null, this.authorizationService, this.sessionRegistry))
.withMessage("registeredClientRepository cannot be null");
}
@Test
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationProvider(this.registeredClientRepository, null, this.sessionRegistry))
.withMessage("authorizationService cannot be null");
}
@Test
public void constructorWhenSessionRegistryNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationProvider(this.registeredClientRepository, this.authorizationService, null))
.withMessage("sessionRegistry cannot be null");
}
@Test
public void supportsWhenTypeOidcLogoutAuthenticationTokenThenReturnTrue() {
assertThat(this.authenticationProvider.supports(OidcLogoutAuthenticationToken.class)).isTrue();
}
@Test
public void authenticateWhenIdTokenNotFoundThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", principal, "session-1", null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains("id_token_hint");
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
}
@Test
public void authenticateWhenMissingAudienceThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject(principal.getName())
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, "session-1", null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains(IdTokenClaimNames.AUD);
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenInvalidAudienceThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject(principal.getName())
.audience(Collections.singleton(registeredClient.getClientId() + "-invalid"))
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, "session-1", null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains(IdTokenClaimNames.AUD);
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenInvalidClientIdThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject(principal.getName())
.audience(Collections.singleton(registeredClient.getClientId()))
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, "session-1", registeredClient.getClientId() + "-invalid", null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains(OAuth2ParameterNames.CLIENT_ID);
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenInvalidPostLogoutRedirectUriThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject(principal.getName())
.audience(Collections.singleton(registeredClient.getClientId()))
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, "session-1", registeredClient.getClientId(),
"https://example.com/callback-1-invalid", null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
assertThat(error.getDescription()).contains("post_logout_redirect_uri");
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenMissingSidThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject(principal.getName())
.audience(Collections.singleton(registeredClient.getClientId()))
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
String sessionId = "session-1";
List<SessionInformation> sessions = Collections.singletonList(
new SessionInformation(principal.getPrincipal(), sessionId, Date.from(Instant.now())));
when(this.sessionRegistry.getAllSessions(eq(principal.getPrincipal()), eq(true)))
.thenReturn(sessions);
principal.setAuthenticated(true);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, sessionId, null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains("sid");
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenInvalidSidThenThrowOAuth2AuthenticationException() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject(principal.getName())
.audience(Collections.singleton(registeredClient.getClientId()))
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.claim("sid", "other-session")
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
String sessionId = "session-1";
List<SessionInformation> sessions = Collections.singletonList(
new SessionInformation(principal.getPrincipal(), sessionId, Date.from(Instant.now())));
when(this.sessionRegistry.getAllSessions(eq(principal.getPrincipal()), eq(true)))
.thenReturn(sessions);
principal.setAuthenticated(true);
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, sessionId, null, null, null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_TOKEN);
assertThat(error.getDescription()).contains("sid");
});
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
}
@Test
public void authenticateWhenValidIdTokenThenAuthenticated() {
TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
String sessionId = "session-1";
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
.issuer("https://provider.com")
.subject(principal.getName())
.audience(Collections.singleton(registeredClient.getClientId()))
.issuedAt(Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.expiresAt(Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.MILLIS))
.claim("sid", sessionId)
.build();
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.principalName(principal.getName())
.token(idToken,
(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()))
.build();
when(this.authorizationService.findByToken(eq(idToken.getTokenValue()), eq(ID_TOKEN_TOKEN_TYPE)))
.thenReturn(authorization);
when(this.registeredClientRepository.findById(eq(authorization.getRegisteredClientId())))
.thenReturn(registeredClient);
SessionInformation sessionInformation = new SessionInformation(
principal.getPrincipal(), sessionId, Date.from(Instant.now()));
List<SessionInformation> sessions = Collections.singletonList(sessionInformation);
when(this.sessionRegistry.getAllSessions(eq(principal.getPrincipal()), eq(true)))
.thenReturn(sessions);
principal.setAuthenticated(true);
String postLogoutRedirectUri = registeredClient.getPostLogoutRedirectUris().toArray(new String[0])[0];
String state = "state";
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
idToken.getTokenValue(), principal, sessionId, registeredClient.getClientId(), postLogoutRedirectUri, state);
OidcLogoutAuthenticationToken authenticationResult =
(OidcLogoutAuthenticationToken) this.authenticationProvider.authenticate(authentication);
verify(this.authorizationService).findByToken(
eq(authentication.getIdToken()), eq(ID_TOKEN_TOKEN_TYPE));
verify(this.registeredClientRepository).findById(
eq(authorization.getRegisteredClientId()));
assertThat(authenticationResult.getPrincipal()).isEqualTo(principal);
assertThat(authenticationResult.getCredentials().toString()).isEmpty();
assertThat(authenticationResult.getIdToken()).isEqualTo(idToken.getTokenValue());
assertThat(authenticationResult.getSessionId()).isEqualTo(sessionInformation.getSessionId());
assertThat(authenticationResult.getSessionInformation()).isEqualTo(sessionInformation);
assertThat(authenticationResult.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(authenticationResult.getPostLogoutRedirectUri()).isEqualTo(postLogoutRedirectUri);
assertThat(authenticationResult.getState()).isEqualTo(state);
assertThat(authenticationResult.isAuthenticated()).isTrue();
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2020-2023 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.server.authorization.oidc.authentication;
import java.sql.Date;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.session.SessionInformation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link OidcLogoutAuthenticationToken}.
*
* @author Joe Grandja
*/
public class OidcLogoutAuthenticationTokenTests {
private final String idToken = "id-token";
private final TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", "credentials");
private final String sessionId = "session-1";
private final SessionInformation sessionInformation = new SessionInformation(this.principal, "session-2", Date.from(Instant.now()));
private final String clientId = "client-1";
private final String postLogoutRedirectUri = "https://example.com/oidc-post-logout";
private final String state = "state-1";
@Test
public void constructorWhenIdTokenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
null, this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("idToken cannot be empty");
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
null, this.principal, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("idToken cannot be empty");
}
@Test
public void constructorWhenIdTokenEmptyThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
"", this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("idToken cannot be empty");
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
"", this.principal, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("idToken cannot be empty");
}
@Test
public void constructorWhenPrincipalNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
this.idToken, null, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("principal cannot be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> new OidcLogoutAuthenticationToken(
this.idToken, null, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state))
.withMessage("principal cannot be null");
}
@Test
public void constructorWhenSessionIdProvidedThenCreated() {
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
this.idToken, this.principal, this.sessionId, this.clientId, this.postLogoutRedirectUri, this.state);
assertThat(authentication.getPrincipal()).isEqualTo(this.principal);
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.getIdToken()).isEqualTo(this.idToken);
assertThat(authentication.getSessionId()).isEqualTo(this.sessionId);
assertThat(authentication.getSessionInformation()).isNull();
assertThat(authentication.getClientId()).isEqualTo(this.clientId);
assertThat(authentication.getPostLogoutRedirectUri()).isEqualTo(this.postLogoutRedirectUri);
assertThat(authentication.getState()).isEqualTo(this.state);
assertThat(authentication.isAuthenticated()).isFalse();
}
@Test
public void constructorWhenSessionInformationProvidedThenCreated() {
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
this.idToken, this.principal, this.sessionInformation, this.clientId, this.postLogoutRedirectUri, this.state);
assertThat(authentication.getPrincipal()).isEqualTo(this.principal);
assertThat(authentication.getCredentials().toString()).isEmpty();
assertThat(authentication.getIdToken()).isEqualTo(this.idToken);
assertThat(authentication.getSessionId()).isEqualTo(this.sessionInformation.getSessionId());
assertThat(authentication.getSessionInformation()).isEqualTo(this.sessionInformation);
assertThat(authentication.getClientId()).isEqualTo(this.clientId);
assertThat(authentication.getPostLogoutRedirectUri()).isEqualTo(this.postLogoutRedirectUri);
assertThat(authentication.getState()).isEqualTo(this.state);
assertThat(authentication.isAuthenticated()).isTrue();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -99,6 +99,9 @@ public class OidcClientRegistrationHttpMessageConverterTests {
+" \"redirect_uris\": [\n"
+ " \"https://client.example.com\"\n"
+ " ],\n"
+" \"post_logout_redirect_uris\": [\n"
+ " \"https://client.example.com/oidc-post-logout\"\n"
+ " ],\n"
+" \"token_endpoint_auth_method\": \"client_secret_jwt\",\n"
+" \"token_endpoint_auth_signing_alg\": \"HS256\",\n"
+" \"grant_types\": [\n"
@@ -125,6 +128,7 @@ public class OidcClientRegistrationHttpMessageConverterTests {
assertThat(clientRegistration.getClientSecretExpiresAt()).isEqualTo(Instant.ofEpochSecond(1607637467L));
assertThat(clientRegistration.getClientName()).isEqualTo("client-name");
assertThat(clientRegistration.getRedirectUris()).containsOnly("https://client.example.com");
assertThat(clientRegistration.getPostLogoutRedirectUris()).containsOnly("https://client.example.com/oidc-post-logout");
assertThat(clientRegistration.getTokenEndpointAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue());
assertThat(clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm()).isEqualTo(MacAlgorithm.HS256.getName());
assertThat(clientRegistration.getGrantTypes()).containsExactlyInAnyOrder("authorization_code", "client_credentials");
@@ -183,6 +187,7 @@ public class OidcClientRegistrationHttpMessageConverterTests {
.clientSecretExpiresAt(Instant.ofEpochSecond(1607637467))
.clientName("client-name")
.redirectUri("https://client.example.com")
.postLogoutRedirectUri("https://client.example.com/oidc-post-logout")
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT.getValue())
.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256.getName())
.grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue())
@@ -208,6 +213,7 @@ public class OidcClientRegistrationHttpMessageConverterTests {
assertThat(clientRegistrationResponse).contains("\"client_secret_expires_at\":1607637467");
assertThat(clientRegistrationResponse).contains("\"client_name\":\"client-name\"");
assertThat(clientRegistrationResponse).contains("\"redirect_uris\":[\"https://client.example.com\"]");
assertThat(clientRegistrationResponse).contains("\"post_logout_redirect_uris\":[\"https://client.example.com/oidc-post-logout\"]");
assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_method\":\"client_secret_jwt\"");
assertThat(clientRegistrationResponse).contains("\"token_endpoint_auth_signing_alg\":\"HS256\"");
assertThat(clientRegistrationResponse).contains("\"grant_types\":[\"authorization_code\",\"client_credentials\"]");

View File

@@ -0,0 +1,363 @@
/*
* Copyright 2020-2023 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.server.authorization.oidc.web;
import java.time.Instant;
import java.util.Date;
import java.util.function.Consumer;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
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.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* Tests for {@link OidcLogoutEndpointFilter}.
*
* @author Joe Grandja
*/
public class OidcLogoutEndpointFilterTests {
private static final String DEFAULT_OIDC_LOGOUT_ENDPOINT_URI = "/connect/logout";
private AuthenticationManager authenticationManager;
private OidcLogoutEndpointFilter filter;
private TestingAuthenticationToken principal;
@BeforeEach
public void setUp() {
this.authenticationManager = mock(AuthenticationManager.class);
this.filter = new OidcLogoutEndpointFilter(this.authenticationManager);
this.principal = new TestingAuthenticationToken("principal", "credentials");
this.principal.setAuthenticated(true);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(this.principal);
SecurityContextHolder.setContext(securityContext);
}
@AfterEach
public void cleanup() {
SecurityContextHolder.clearContext();
}
@Test
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcLogoutEndpointFilter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationManager cannot be null");
}
@Test
public void constructorWhenLogoutEndpointUriNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OidcLogoutEndpointFilter(this.authenticationManager, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("logoutEndpointUri cannot be empty");
}
@Test
public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationConverter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationConverter cannot be null");
}
@Test
public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationSuccessHandler cannot be null");
}
@Test
public void setAuthenticationFailureHandlerWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationFailureHandler(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationFailureHandler cannot be null");
}
@Test
public void doFilterWhenNotLogoutRequestThenNotProcessed() throws Exception {
String requestUri = "/path";
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenLogoutRequestMissingIdTokenHintThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
"id_token_hint",
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.removeParameter("id_token_hint"));
}
@Test
public void doFilterWhenLogoutRequestMultipleIdTokenHintThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
"id_token_hint",
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.addParameter("id_token_hint", "id-token-2"));
}
@Test
public void doFilterWhenLogoutRequestMultipleClientIdThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
OAuth2ParameterNames.CLIENT_ID,
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2"));
}
@Test
public void doFilterWhenLogoutRequestMultiplePostLogoutRedirectUriThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
"post_logout_redirect_uri",
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.addParameter("post_logout_redirect_uri", "https://example.com/callback-4"));
}
@Test
public void doFilterWhenLogoutRequestMultipleStateThenInvalidRequestError() throws Exception {
doFilterWhenRequestInvalidParameterThenError(
createLogoutRequest(TestRegisteredClients.registeredClient().build()),
OAuth2ParameterNames.STATE,
OAuth2ErrorCodes.INVALID_REQUEST,
request -> request.addParameter(OAuth2ParameterNames.STATE, "state-2"));
}
private void doFilterWhenRequestInvalidParameterThenError(MockHttpServletRequest request,
String parameterName, String errorCode, Consumer<MockHttpServletRequest> requestConsumer) throws Exception {
requestConsumer.accept(request);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
assertThat(response.getErrorMessage()).isEqualTo("[" + errorCode + "] OpenID Connect 1.0 Logout Request Parameter: " + parameterName);
}
@Test
public void doFilterWhenLogoutRequestAuthenticationExceptionThenErrorResponse() throws Exception {
OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri");
when(this.authenticationManager.authenticate(any()))
.thenThrow(new OAuth2AuthenticationException(error));
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
assertThat(response.getErrorMessage()).isEqualTo(error.toString());
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.principal);
}
@Test
public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception {
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", this.principal, (SessionInformation) null, null, null, null);
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
when(authenticationConverter.convert(any())).thenReturn(authentication);
this.filter.setAuthenticationConverter(authenticationConverter);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authentication);
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(authenticationConverter).convert(any());
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
}
@Test
public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", this.principal, (SessionInformation) null, null, null, null);
AuthenticationSuccessHandler authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
this.filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authentication);
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), same(authentication));
verifyNoInteractions(filterChain);
}
@Test
public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception {
AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
this.filter.setAuthenticationFailureHandler(authenticationFailureHandler);
when(this.authenticationManager.authenticate(any()))
.thenThrow(new AuthenticationServiceException("AuthenticationServiceException"));
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
ArgumentCaptor<AuthenticationException> authenticationExceptionCaptor = ArgumentCaptor.forClass(AuthenticationException.class);
verify(this.authenticationManager).authenticate(any());
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), authenticationExceptionCaptor.capture());
verifyNoInteractions(filterChain);
assertThat(authenticationExceptionCaptor.getValue())
.isInstanceOf(OAuth2AuthenticationException.class)
.extracting(ex -> ((OAuth2AuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_REQUEST);
assertThat(error.getDescription()).contains("AuthenticationServiceException");
});
}
@Test
public void doFilterWhenLogoutRequestAuthenticatedThenLogout() throws Exception {
MockHttpServletRequest request = createLogoutRequest(TestRegisteredClients.registeredClient().build());
MockHttpSession session = (MockHttpSession) request.getSession(true);
SessionInformation sessionInformation = new SessionInformation(
this.principal, session.getId(), Date.from(Instant.now()));
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", this.principal, sessionInformation, null, null, null);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authentication);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).isEqualTo("/");
assertThat(session.isInvalid()).isTrue();
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
}
@Test
public void doFilterWhenLogoutRequestAuthenticatedWithPostLogoutRedirectUriThenPostLogoutRedirect() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
MockHttpServletRequest request = createLogoutRequest(registeredClient);
MockHttpSession session = (MockHttpSession) request.getSession(true);
SessionInformation sessionInformation = new SessionInformation(
this.principal, session.getId(), Date.from(Instant.now()));
String postLogoutRedirectUri = registeredClient.getPostLogoutRedirectUris().iterator().next();
String state = "state-1";
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken(
"id-token", this.principal, sessionInformation,
registeredClient.getClientId(), postLogoutRedirectUri, state);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authentication);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
assertThat(response.getRedirectedUrl()).isEqualTo(postLogoutRedirectUri + "?state=" + state);
assertThat(session.isInvalid()).isTrue();
assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull();
}
private static MockHttpServletRequest createLogoutRequest(RegisteredClient registeredClient) {
String requestUri = DEFAULT_OIDC_LOGOUT_ENDPOINT_URI;
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.addParameter("id_token_hint", "id-token");
request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
request.addParameter("post_logout_redirect_uri", registeredClient.getPostLogoutRedirectUris().iterator().next());
request.addParameter(OAuth2ParameterNames.STATE, "state");
return request;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -92,6 +92,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
String tokenEndpoint = "/oauth2/v1/token";
String jwkSetEndpoint = "/oauth2/v1/jwks";
String userInfoEndpoint = "/userinfo";
String logoutEndpoint = "/connect/logout";
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
@@ -101,6 +102,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
.tokenEndpoint(tokenEndpoint)
.jwkSetEndpoint(jwkSetEndpoint)
.oidcUserInfoEndpoint(userInfoEndpoint)
.oidcLogoutEndpoint(logoutEndpoint)
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.tokenIntrospectionEndpoint(tokenIntrospectionEndpoint)
.build();
@@ -132,6 +134,7 @@ public class OidcProviderConfigurationEndpointFilterTests {
assertThat(providerConfigurationResponse).contains("\"subject_types_supported\":[\"public\"]");
assertThat(providerConfigurationResponse).contains("\"id_token_signing_alg_values_supported\":[\"RS256\"]");
assertThat(providerConfigurationResponse).contains("\"userinfo_endpoint\":\"https://example.com/issuer1/userinfo\"");
assertThat(providerConfigurationResponse).contains("\"end_session_endpoint\":\"https://example.com/issuer1/connect/logout\"");
assertThat(providerConfigurationResponse).contains("\"token_endpoint_auth_methods_supported\":[\"client_secret_basic\",\"client_secret_post\",\"client_secret_jwt\",\"private_key_jwt\"]");
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* Tests for {@link AuthorizationServerSettings}.
*
* @author Daniel Garnier-Moiroux
* @author Joe Grandja
*/
public class AuthorizationServerSettingsTests {
@@ -39,6 +40,7 @@ public class AuthorizationServerSettingsTests {
assertThat(authorizationServerSettings.getTokenIntrospectionEndpoint()).isEqualTo("/oauth2/introspect");
assertThat(authorizationServerSettings.getOidcClientRegistrationEndpoint()).isEqualTo("/connect/register");
assertThat(authorizationServerSettings.getOidcUserInfoEndpoint()).isEqualTo("/userinfo");
assertThat(authorizationServerSettings.getOidcLogoutEndpoint()).isEqualTo("/connect/logout");
}
@Test
@@ -50,6 +52,7 @@ public class AuthorizationServerSettingsTests {
String tokenIntrospectionEndpoint = "/oauth2/v1/introspect";
String oidcClientRegistrationEndpoint = "/connect/v1/register";
String oidcUserInfoEndpoint = "/connect/v1/userinfo";
String oidcLogoutEndpoint = "/connect/v1/logout";
String issuer = "https://example.com:9000";
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
@@ -62,6 +65,7 @@ public class AuthorizationServerSettingsTests {
.tokenRevocationEndpoint(tokenRevocationEndpoint)
.oidcClientRegistrationEndpoint(oidcClientRegistrationEndpoint)
.oidcUserInfoEndpoint(oidcUserInfoEndpoint)
.oidcLogoutEndpoint(oidcLogoutEndpoint)
.build();
assertThat(authorizationServerSettings.getIssuer()).isEqualTo(issuer);
@@ -72,6 +76,7 @@ public class AuthorizationServerSettingsTests {
assertThat(authorizationServerSettings.getTokenIntrospectionEndpoint()).isEqualTo(tokenIntrospectionEndpoint);
assertThat(authorizationServerSettings.getOidcClientRegistrationEndpoint()).isEqualTo(oidcClientRegistrationEndpoint);
assertThat(authorizationServerSettings.getOidcUserInfoEndpoint()).isEqualTo(oidcUserInfoEndpoint);
assertThat(authorizationServerSettings.getOidcLogoutEndpoint()).isEqualTo(oidcLogoutEndpoint);
}
@Test
@@ -81,7 +86,7 @@ public class AuthorizationServerSettingsTests {
.settings(settings -> settings.put("name2", "value2"))
.build();
assertThat(authorizationServerSettings.getSettings()).hasSize(9);
assertThat(authorizationServerSettings.getSettings()).hasSize(10);
assertThat(authorizationServerSettings.<String>getSetting("name1")).isEqualTo("value1");
assertThat(authorizationServerSettings.<String>getSetting("name2")).isEqualTo("value2");
}
@@ -142,4 +147,11 @@ public class AuthorizationServerSettingsTests {
.withMessage("value cannot be null");
}
@Test
public void oidcLogoutEndpointWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> AuthorizationServerSettings.builder().oidcLogoutEndpoint(null))
.withMessage("value cannot be null");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 the original author or authors.
* Copyright 2020-2023 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.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.token;
import java.security.Principal;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -27,6 +28,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
@@ -46,7 +48,6 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
@@ -67,7 +68,7 @@ public class JwtGeneratorTests {
private JwtEncoder jwtEncoder;
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
private JwtGenerator jwtGenerator;
private AuthorizationServerContext authorizationServerContext;
private TestAuthorizationServerContext authorizationServerContext;
@BeforeEach
public void setUp() {
@@ -168,16 +169,21 @@ public class JwtGeneratorTests {
OAuth2AuthorizationCodeAuthenticationToken authentication =
new OAuth2AuthorizationCodeAuthenticationToken("code", clientPrincipal, authorizationRequest.getRedirectUri(), null);
Authentication principal = authorization.getAttribute(Principal.class.getName());
SessionInformation sessionInformation = new SessionInformation(
principal.getPrincipal(), "session1", Date.from(Instant.now().minus(2, ChronoUnit.HOURS)));
// @formatter:off
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(authorization.getAttribute(Principal.class.getName()))
.principal(principal)
.authorizationServerContext(this.authorizationServerContext)
.authorization(authorization)
.authorizedScopes(authorization.getAuthorizedScopes())
.tokenType(ID_TOKEN_TOKEN_TYPE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrant(authentication)
.put(SessionInformation.class, sessionInformation)
.build();
// @formatter:on
@@ -238,6 +244,10 @@ public class JwtGeneratorTests {
OAuth2AuthorizationRequest.class.getName());
String nonce = (String) authorizationRequest.getAdditionalParameters().get(OidcParameterNames.NONCE);
assertThat(jwtClaimsSet.<String>getClaim(IdTokenClaimNames.NONCE)).isEqualTo(nonce);
SessionInformation sessionInformation = tokenContext.get(SessionInformation.class);
assertThat(jwtClaimsSet.<String>getClaim("sid")).isEqualTo(sessionInformation.getSessionId());
assertThat(jwtClaimsSet.<Date>getClaim(IdTokenClaimNames.AUTH_TIME)).isEqualTo(sessionInformation.getLastRequest());
}
}

View File

@@ -58,6 +58,7 @@ import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -151,6 +152,13 @@ public class OAuth2AuthorizationEndpointFilterTests {
.hasMessage("authenticationFailureHandler cannot be null");
}
@Test
public void setSessionAuthenticationStrategyWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setSessionAuthenticationStrategy(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("sessionAuthenticationStrategy cannot be null");
}
@Test
public void doFilterWhenNotAuthorizationRequestThenNotProcessed() throws Exception {
String requestUri = "/path";
@@ -383,6 +391,31 @@ public class OAuth2AuthorizationEndpointFilterTests {
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), same(authenticationException));
}
@Test
public void doFilterWhenCustomSessionAuthenticationStrategyThenUsed() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult =
new OAuth2AuthorizationCodeRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), principal, this.authorizationCode,
registeredClient.getRedirectUris().iterator().next(), STATE, registeredClient.getScopes());
authorizationCodeRequestAuthenticationResult.setAuthenticated(true);
when(this.authenticationManager.authenticate(any()))
.thenReturn(authorizationCodeRequestAuthenticationResult);
SessionAuthenticationStrategy sessionAuthenticationStrategy = mock(SessionAuthenticationStrategy.class);
this.filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
verify(sessionAuthenticationStrategy).onAuthentication(same(authorizationCodeRequestAuthenticationResult), any(), any());
}
@Test
public void doFilterWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();

View File

@@ -8,6 +8,7 @@ CREATE TABLE oauth2RegisteredClient (
clientAuthenticationMethods varchar(1000) NOT NULL,
authorizationGrantTypes varchar(1000) NOT NULL,
redirectUris varchar(1000) DEFAULT NULL,
postLogoutRedirectUris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
clientSettings varchar(2000) NOT NULL,
tokenSettings varchar(2000) NOT NULL,