Skip to content

Commit 746464e

Browse files
committed
Merge branch '6.2.x' into 6.3.x
2 parents b49051a + c1857c0 commit 746464e

File tree

4 files changed

+171
-8
lines changed

4 files changed

+171
-8
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutTokenValidator.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
3131
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
3232
import org.springframework.security.oauth2.jwt.Jwt;
33+
import org.springframework.util.Assert;
3334

3435
/**
3536
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
@@ -57,7 +58,9 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<
5758

5859
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
5960
this.audience = clientRegistration.getClientId();
60-
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
61+
String issuer = clientRegistration.getProviderDetails().getIssuerUri();
62+
Assert.hasText(issuer, "Provider issuer cannot be null");
63+
this.issuer = issuer;
6164
}
6265

6366
@Override

config/src/main/java/org/springframework/security/config/web/server/OidcBackChannelLogoutTokenValidator.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
3131
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
3232
import org.springframework.security.oauth2.jwt.Jwt;
33+
import org.springframework.util.Assert;
3334

3435
/**
3536
* A {@link OAuth2TokenValidator} that validates OIDC Logout Token claims in conformance
@@ -57,7 +58,9 @@ final class OidcBackChannelLogoutTokenValidator implements OAuth2TokenValidator<
5758

5859
OidcBackChannelLogoutTokenValidator(ClientRegistration clientRegistration) {
5960
this.audience = clientRegistration.getClientId();
60-
this.issuer = clientRegistration.getProviderDetails().getIssuerUri();
61+
String issuer = clientRegistration.getProviderDetails().getIssuerUri();
62+
Assert.hasText(issuer, "Provider issuer cannot be null");
63+
this.issuer = issuer;
6164
}
6265

6366
@Override

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcLogoutConfigurerTests.java

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
import org.springframework.web.bind.annotation.RestController;
9292
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
9393

94+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
9495
import static org.hamcrest.Matchers.containsString;
9596
import static org.mockito.ArgumentMatchers.any;
9697
import static org.mockito.BDDMockito.willThrow;
@@ -259,6 +260,22 @@ void logoutWhenCustomComponentsThenUses() throws Exception {
259260
verify(sessionRegistry).removeSessionInformation(any(OidcLogoutToken.class));
260261
}
261262

263+
@Test
264+
void logoutWhenProviderIssuerMissingThenThrowIllegalArgumentException() throws Exception {
265+
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class)
266+
.autowire();
267+
String registrationId = this.clientRegistration.getRegistrationId();
268+
MockHttpSession session = login();
269+
String logoutToken = this.mvc.perform(get("/token/logout").session(session))
270+
.andExpect(status().isOk())
271+
.andReturn()
272+
.getResponse()
273+
.getContentAsString();
274+
assertThatIllegalArgumentException().isThrownBy(
275+
() -> this.mvc.perform(post(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
276+
.param("logout_token", logoutToken)));
277+
}
278+
262279
private MockHttpSession login() throws Exception {
263280
MockMvcDispatcher dispatcher = (MockMvcDispatcher) this.web.getDispatcher();
264281
this.mvc.perform(get("/token/logout")).andExpect(status().isUnauthorized());
@@ -410,6 +427,54 @@ LogoutHandler logoutHandler() {
410427

411428
}
412429

430+
@Configuration
431+
static class ProviderIssuerMissingRegistrationConfig {
432+
433+
@Autowired(required = false)
434+
MockWebServer web;
435+
436+
@Bean
437+
ClientRegistration clientRegistration() {
438+
if (this.web == null) {
439+
return TestClientRegistrations.clientRegistration().issuerUri(null).build();
440+
}
441+
String issuer = this.web.url("/").toString();
442+
return TestClientRegistrations.clientRegistration()
443+
.issuerUri(null)
444+
.jwkSetUri(issuer + "jwks")
445+
.tokenUri(issuer + "token")
446+
.userInfoUri(issuer + "user")
447+
.scope("openid")
448+
.build();
449+
}
450+
451+
@Bean
452+
ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
453+
return new InMemoryClientRegistrationRepository(clientRegistration);
454+
}
455+
456+
}
457+
458+
@Configuration
459+
@EnableWebSecurity
460+
@Import(ProviderIssuerMissingRegistrationConfig.class)
461+
static class ProviderIssuerMissingConfig {
462+
463+
@Bean
464+
@Order(1)
465+
SecurityFilterChain filters(HttpSecurity http) throws Exception {
466+
// @formatter:off
467+
http
468+
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
469+
.oauth2Login(Customizer.withDefaults())
470+
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
471+
// @formatter:on
472+
473+
return http.build();
474+
}
475+
476+
}
477+
413478
@Configuration
414479
@EnableWebSecurity
415480
@EnableWebMvc
@@ -448,6 +513,9 @@ private static JWKSource<SecurityContext> jwks(RSAKey key) {
448513
@Autowired
449514
ClientRegistration registration;
450515

516+
@Autowired(required = false)
517+
MockWebServer web;
518+
451519
@Bean
452520
@Order(0)
453521
SecurityFilterChain authorizationServer(HttpSecurity http, ClientRegistration registration) throws Exception {
@@ -484,15 +552,15 @@ Map<String, Object> accessToken(HttpServletRequest request) {
484552
HttpSession session = request.getSession();
485553
JwtEncoderParameters parameters = JwtEncoderParameters
486554
.from(JwtClaimsSet.builder().id("id").subject(this.username)
487-
.issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now())
555+
.issuer(getIssuerUri()).issuedAt(Instant.now())
488556
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
489557
String token = this.encoder.encode(parameters).getTokenValue();
490558
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
491559
.toJSONObject();
492560
}
493561

494562
String idToken(String sessionId) {
495-
OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri())
563+
OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri())
496564
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
497565
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
498566
.claim(LogoutTokenClaimNames.SID, sessionId).build();
@@ -501,6 +569,13 @@ String idToken(String sessionId) {
501569
return this.encoder.encode(parameters).getTokenValue();
502570
}
503571

572+
private String getIssuerUri() {
573+
if (this.web == null) {
574+
return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri();
575+
}
576+
return this.web.url("/").toString();
577+
}
578+
504579
@GetMapping("/user")
505580
Map<String, Object> userinfo() {
506581
return Map.of("sub", this.username, "id", this.username);

config/src/test/java/org/springframework/security/config/web/server/OidcLogoutSpecTests.java

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,30 @@ void logoutWhenCustomComponentsThenUses() {
323323
verify(sessionRegistry, atLeastOnce()).removeSessionInformation(any(OidcLogoutToken.class));
324324
}
325325

326+
@Test
327+
void logoutWhenProviderIssuerMissingThen5xxServerError() {
328+
this.spring.register(WebServerConfig.class, OidcProviderConfig.class, ProviderIssuerMissingConfig.class)
329+
.autowire();
330+
String registrationId = this.clientRegistration.getRegistrationId();
331+
String session = login();
332+
String logoutToken = this.test.mutateWith(session(session))
333+
.get()
334+
.uri("/token/logout")
335+
.exchange()
336+
.expectStatus()
337+
.isOk()
338+
.returnResult(String.class)
339+
.getResponseBody()
340+
.blockFirst();
341+
this.test.post()
342+
.uri(this.web.url("/logout/connect/back-channel/" + registrationId).toString())
343+
.body(BodyInserters.fromFormData("logout_token", logoutToken))
344+
.exchange()
345+
.expectStatus()
346+
.is5xxServerError();
347+
this.test.mutateWith(session(session)).get().uri("/token/logout").exchange().expectStatus().isOk();
348+
}
349+
326350
private String login() {
327351
this.test.get().uri("/token/logout").exchange().expectStatus().isUnauthorized();
328352
String registrationId = this.clientRegistration.getRegistrationId();
@@ -499,6 +523,54 @@ ServerLogoutHandler logoutHandler() {
499523

500524
}
501525

526+
@Configuration
527+
static class ProviderIssuerMissingRegistrationConfig {
528+
529+
@Autowired(required = false)
530+
MockWebServer web;
531+
532+
@Bean
533+
ClientRegistration clientRegistration() {
534+
if (this.web == null) {
535+
return TestClientRegistrations.clientRegistration().issuerUri(null).build();
536+
}
537+
String issuer = this.web.url("/").toString();
538+
return TestClientRegistrations.clientRegistration()
539+
.issuerUri(null)
540+
.jwkSetUri(issuer + "jwks")
541+
.tokenUri(issuer + "token")
542+
.userInfoUri(issuer + "user")
543+
.scope("openid")
544+
.build();
545+
}
546+
547+
@Bean
548+
ReactiveClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) {
549+
return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
550+
}
551+
552+
}
553+
554+
@Configuration
555+
@EnableWebFluxSecurity
556+
@Import(ProviderIssuerMissingRegistrationConfig.class)
557+
static class ProviderIssuerMissingConfig {
558+
559+
@Bean
560+
@Order(1)
561+
SecurityWebFilterChain filters(ServerHttpSecurity http) throws Exception {
562+
// @formatter:off
563+
http
564+
.authorizeExchange((authorize) -> authorize.anyExchange().authenticated())
565+
.oauth2Login(Customizer.withDefaults())
566+
.oidcLogout((oidc) -> oidc.backChannel(Customizer.withDefaults()));
567+
// @formatter:on
568+
569+
return http.build();
570+
}
571+
572+
}
573+
502574
@Configuration
503575
@EnableWebFluxSecurity
504576
@EnableWebFlux
@@ -537,6 +609,9 @@ private static JWKSource<SecurityContext> jwks(RSAKey key) {
537609
@Autowired
538610
ClientRegistration registration;
539611

612+
@Autowired(required = false)
613+
MockWebServer web;
614+
540615
static ServerWebExchangeMatcher or(String... patterns) {
541616
List<ServerWebExchangeMatcher> matchers = new ArrayList<>();
542617
for (String pattern : patterns) {
@@ -581,15 +656,15 @@ String nonce(@RequestParam("nonce") String nonce, @RequestParam("state") String
581656
Map<String, Object> accessToken(WebSession session) {
582657
JwtEncoderParameters parameters = JwtEncoderParameters
583658
.from(JwtClaimsSet.builder().id("id").subject(this.username)
584-
.issuer(this.registration.getProviderDetails().getIssuerUri()).issuedAt(Instant.now())
659+
.issuer(getIssuerUri()).issuedAt(Instant.now())
585660
.expiresAt(Instant.now().plusSeconds(86400)).claim("scope", "openid").build());
586661
String token = this.encoder.encode(parameters).getTokenValue();
587662
return new OIDCTokens(idToken(session.getId()), new BearerAccessToken(token, 86400, new Scope("openid")), null)
588663
.toJSONObject();
589664
}
590665

591666
String idToken(String sessionId) {
592-
OidcIdToken token = TestOidcIdTokens.idToken().issuer(this.registration.getProviderDetails().getIssuerUri())
667+
OidcIdToken token = TestOidcIdTokens.idToken().issuer(getIssuerUri())
593668
.subject(this.username).expiresAt(Instant.now().plusSeconds(86400))
594669
.audience(List.of(this.registration.getClientId())).nonce(this.nonce)
595670
.claim(LogoutTokenClaimNames.SID, sessionId).build();
@@ -598,6 +673,13 @@ String idToken(String sessionId) {
598673
return this.encoder.encode(parameters).getTokenValue();
599674
}
600675

676+
private String getIssuerUri() {
677+
if (this.web == null) {
678+
return TestClientRegistrations.clientRegistration().build().getProviderDetails().getIssuerUri();
679+
}
680+
return this.web.url("/").toString();
681+
}
682+
601683
@GetMapping("/user")
602684
Map<String, Object> userinfo() {
603685
return Map.of("sub", this.username, "id", this.username);

0 commit comments

Comments
 (0)