mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-03-01 10:59:16 +00:00
Merge branch '6.3.x'
This commit is contained in:
commit
29331a0d8c
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
|||||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
|
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
|
||||||
@ -57,7 +58,9 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<
|
|||||||
|
|
||||||
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
|
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
|
||||||
this.audience = clientRegistration.getClientId();
|
this.audience = clientRegistration.getClientId();
|
||||||
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
|
String issuer = clientRegistration.getProviderDetails().getIssuerUri();
|
||||||
|
Assert.hasText(issuer, "Provider issuer cannot be null");
|
||||||
|
this.issuer = issuer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -30,6 +30,7 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
|||||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
|
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
|
||||||
@ -57,7 +58,9 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<
|
|||||||
|
|
||||||
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
|
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
|
||||||
this.audience = clientRegistration.getClientId();
|
this.audience = clientRegistration.getClientId();
|
||||||
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
|
String issuer = clientRegistration.getProviderDetails().getIssuerUri();
|
||||||
|
Assert.hasText(issuer, "Provider issuer cannot be null");
|
||||||
|
this.issuer = issuer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -93,6 +93,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.BDDMockito.willThrow;
|
import static org.mockito.BDDMockito.willThrow;
|
||||||
@ -295,6 +296,22 @@ public class OidcLogoutConfigurerTests {
|
|||||||
verify(sessionRegistry).removeSessionInformation(any(OidcLogoutToken.class));
|
verify(sessionRegistry).removeSessionInformation(any(OidcLogoutToken.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logoutWhenProviderIssuerMissingThenThrowIllegalArgumentException() throws Exception {
|
||||||
|
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class)
|
||||||
|
.autowire();
|
||||||
|
String registrationId = this.clientRegistration.getRegistrationId();
|
||||||
|
MockHttpSession session = login();
|
||||||
|
String logoutToken = this.mvc.perform(get("/token/logout").session(session))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andReturn()
|
||||||
|
.getResponse()
|
||||||
|
.getContentAsString();
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(
|
||||||
|
() -> this.mvc.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||||
|
.param("logout_token", logoutToken)));
|
||||||
|
}
|
||||||
|
|
||||||
private MockHttpSession login() throws Exception {
|
private MockHttpSession login() throws Exception {
|
||||||
MockMvcDispatcher dispatcher = (MockMvcDispatcher) this.web.getDispatcher();
|
MockMvcDispatcher dispatcher = (MockMvcDispatcher) this.web.getDispatcher();
|
||||||
this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized());
|
this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized());
|
||||||
@ -523,6 +540,54 @@ public class OidcLogoutConfigurerTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class ProviderIssuerMissingRegistrationConfig {
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
MockWebServer web;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ClientRegistration clientRegistration() {
|
||||||
|
if (this.web == null) {
|
||||||
|
return TestClientRegistrations.clientRegistration().issuerUri(null).build();
|
||||||
|
}
|
||||||
|
String issuer = this.web.url("/").toString();
|
||||||
|
return TestClientRegistrations.clientRegistration()
|
||||||
|
.issuerUri(null)
|
||||||
|
.jwkSetUri(issuer + "jwks")
|
||||||
|
.tokenUri(issuer + "token")
|
||||||
|
.userInfoUri(issuer + "user")
|
||||||
|
.scope("openid")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
|
||||||
|
return new InMemoryClientRegistrationRepository(clientRegistration);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@Import(ProviderIssuerMissingRegistrationConfig.class)
|
||||||
|
static class ProviderIssuerMissingConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(1)
|
||||||
|
SecurityFilterChain filters(HttpSecurity http) throws Exception {
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
|
||||||
|
.oauth2Login(Customizer.withDefaults())
|
||||||
|
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableWebMvc
|
@EnableWebMvc
|
||||||
@ -561,6 +626,9 @@ public class OidcLogoutConfigurerTests {
|
|||||||
@Autowired
|
@Autowired
|
||||||
ClientRegistration registration;
|
ClientRegistration registration;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
MockWebServer web;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Order(0)
|
@Order(0)
|
||||||
SecurityFilterChain authorizationServer(HttpSecurity http, ClientRegistration registration) throws Exception {
|
SecurityFilterChain authorizationServer(HttpSecurity http, ClientRegistration registration) throws Exception {
|
||||||
@ -597,7 +665,7 @@ public class OidcLogoutConfigurerTests {
|
|||||||
HttpSession session = request.getSession();
|
HttpSession session = request.getSession();
|
||||||
JwtEncoderParameters parameters = JwtEncoderParameters
|
JwtEncoderParameters parameters = JwtEncoderParameters
|
||||||
.from(JwtClaimsSet.builder().id("id").subject(this.username)
|
.from(JwtClaimsSet.builder().id("id").subject(this.username)
|
||||||
.issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now())
|
.issuer(getIssuerUri()).issuedAt(Instant.now())
|
||||||
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
|
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
|
||||||
String token = this.encoder.encode(parameters).getTokenValue();
|
String token = this.encoder.encode(parameters).getTokenValue();
|
||||||
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
|
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
|
||||||
@ -605,7 +673,7 @@ public class OidcLogoutConfigurerTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String idToken(String sessionId) {
|
String idToken(String sessionId) {
|
||||||
OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri())
|
OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri())
|
||||||
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
|
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
|
||||||
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
|
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
|
||||||
.claim(LogoutTokenClaimNames.SID, sessionId).build();
|
.claim(LogoutTokenClaimNames.SID, sessionId).build();
|
||||||
@ -614,6 +682,13 @@ public class OidcLogoutConfigurerTests {
|
|||||||
return this.encoder.encode(parameters).getTokenValue();
|
return this.encoder.encode(parameters).getTokenValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getIssuerUri() {
|
||||||
|
if (this.web == null) {
|
||||||
|
return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri();
|
||||||
|
}
|
||||||
|
return this.web.url("/").toString();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/user")
|
@GetMapping("/user")
|
||||||
Map<String, Object> userinfo() {
|
Map<String, Object> userinfo() {
|
||||||
return Map.of("sub", this.username, "id", this.username);
|
return Map.of("sub", this.username, "id", this.username);
|
||||||
|
@ -371,6 +371,30 @@ public class OidcLogoutSpecTests {
|
|||||||
verify(sessionRegistry, atLeastOnce()).removeSessionInformation(any(OidcLogoutToken.class));
|
verify(sessionRegistry, atLeastOnce()).removeSessionInformation(any(OidcLogoutToken.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void logoutWhenProviderIssuerMissingThen5xxServerError() {
|
||||||
|
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class)
|
||||||
|
.autowire();
|
||||||
|
String registrationId = this.clientRegistration.getRegistrationId();
|
||||||
|
String session = login();
|
||||||
|
String logoutToken = this.test.mutateWith(session(session))
|
||||||
|
.get()
|
||||||
|
.uri("/token/logout")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk()
|
||||||
|
.returnResult(String.class)
|
||||||
|
.getResponseBody()
|
||||||
|
.blockFirst();
|
||||||
|
this.test.post()
|
||||||
|
.uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
|
||||||
|
.body(BodyInserters.fromFormData("logout_token", logoutToken))
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.is5xxServerError();
|
||||||
|
this.test.mutateWith(session(session)).get().uri("/token/logout").exchange().expectStatus().isOk();
|
||||||
|
}
|
||||||
|
|
||||||
private String login() {
|
private String login() {
|
||||||
this.test.get().uri("/token/logout").exchange().expectStatus().isUnauthorized();
|
this.test.get().uri("/token/logout").exchange().expectStatus().isUnauthorized();
|
||||||
String registrationId = this.clientRegistration.getRegistrationId();
|
String registrationId = this.clientRegistration.getRegistrationId();
|
||||||
@ -624,6 +648,54 @@ public class OidcLogoutSpecTests {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class ProviderIssuerMissingRegistrationConfig {
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
MockWebServer web;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ClientRegistration clientRegistration() {
|
||||||
|
if (this.web == null) {
|
||||||
|
return TestClientRegistrations.clientRegistration().issuerUri(null).build();
|
||||||
|
}
|
||||||
|
String issuer = this.web.url("/").toString();
|
||||||
|
return TestClientRegistrations.clientRegistration()
|
||||||
|
.issuerUri(null)
|
||||||
|
.jwkSetUri(issuer + "jwks")
|
||||||
|
.tokenUri(issuer + "token")
|
||||||
|
.userInfoUri(issuer + "user")
|
||||||
|
.scope("openid")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
ReactiveClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
|
||||||
|
return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFluxSecurity
|
||||||
|
@Import(ProviderIssuerMissingRegistrationConfig.class)
|
||||||
|
static class ProviderIssuerMissingConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Order(1)
|
||||||
|
SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception {
|
||||||
|
// @formatter:off
|
||||||
|
http
|
||||||
|
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
|
||||||
|
.oauth2Login(Customizer.withDefaults())
|
||||||
|
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
@EnableWebFlux
|
@EnableWebFlux
|
||||||
@ -662,6 +734,9 @@ public class OidcLogoutSpecTests {
|
|||||||
@Autowired
|
@Autowired
|
||||||
ClientRegistration registration;
|
ClientRegistration registration;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
MockWebServer web;
|
||||||
|
|
||||||
static ServerWebExchangeMatcher or(String... patterns) {
|
static ServerWebExchangeMatcher or(String... patterns) {
|
||||||
List<ServerWebExchangeMatcher> matchers = new ArrayList<>();
|
List<ServerWebExchangeMatcher> matchers = new ArrayList<>();
|
||||||
for (String pattern : patterns) {
|
for (String pattern : patterns) {
|
||||||
@ -706,7 +781,7 @@ public class OidcLogoutSpecTests {
|
|||||||
Map<String, Object> accessToken(WebSession session) {
|
Map<String, Object> accessToken(WebSession session) {
|
||||||
JwtEncoderParameters parameters = JwtEncoderParameters
|
JwtEncoderParameters parameters = JwtEncoderParameters
|
||||||
.from(JwtClaimsSet.builder().id("id").subject(this.username)
|
.from(JwtClaimsSet.builder().id("id").subject(this.username)
|
||||||
.issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now())
|
.issuer(getIssuerUri()).issuedAt(Instant.now())
|
||||||
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
|
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
|
||||||
String token = this.encoder.encode(parameters).getTokenValue();
|
String token = this.encoder.encode(parameters).getTokenValue();
|
||||||
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
|
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
|
||||||
@ -714,7 +789,7 @@ public class OidcLogoutSpecTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String idToken(String sessionId) {
|
String idToken(String sessionId) {
|
||||||
OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri())
|
OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri())
|
||||||
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
|
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
|
||||||
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
|
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
|
||||||
.claim(LogoutTokenClaimNames.SID, sessionId).build();
|
.claim(LogoutTokenClaimNames.SID, sessionId).build();
|
||||||
@ -723,6 +798,13 @@ public class OidcLogoutSpecTests {
|
|||||||
return this.encoder.encode(parameters).getTokenValue();
|
return this.encoder.encode(parameters).getTokenValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getIssuerUri() {
|
||||||
|
if (this.web == null) {
|
||||||
|
return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri();
|
||||||
|
}
|
||||||
|
return this.web.url("/").toString();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/user")
|
@GetMapping("/user")
|
||||||
Map<String, Object> userinfo() {
|
Map<String, Object> userinfo() {
|
||||||
return Map.of("sub", this.username, "id", this.username);
|
return Map.of("sub", this.username, "id", this.username);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user