Skip to content

Commit f4ce801

Browse files
committed
Fix WebAuthn OIDC id_token missing sid and auth_time claims with SpringSessionBackedSessionRegistry
PublicKeyCredentialUserEntity did not implement AuthenticatedPrincipal, so SpringSessionBackedSessionRegistry fell through to toString() when extracting the principal name, causing session lookup to fail and both sid and auth_time claims to be omitted from the OIDC id_token. Fixes gh-19202 Signed-off-by: jyx-07 <s25069@gsm.hs.kr>
1 parent 6555199 commit f4ce801

3 files changed

Lines changed: 110 additions & 1 deletion

File tree

config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
import org.springframework.security.oauth2.jwt.Jwt;
8080
import org.springframework.security.oauth2.jwt.JwtDecoder;
8181
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
82+
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
8283
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
8384
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
8485
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
@@ -99,6 +100,8 @@
99100
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
100101
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
101102
import org.springframework.security.web.SecurityFilterChain;
103+
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities;
104+
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication;
102105
import org.springframework.test.web.servlet.MockMvc;
103106
import org.springframework.test.web.servlet.MvcResult;
104107
import org.springframework.util.LinkedMultiValueMap;
@@ -113,6 +116,7 @@
113116
import static org.mockito.Mockito.spy;
114117
import static org.mockito.Mockito.times;
115118
import static org.mockito.Mockito.verify;
119+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
116120
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
117121
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
118122
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -324,6 +328,52 @@ public void requestWhenRefreshTokenRequestThenIdTokenContainsSidClaim() throws E
324328
assertThat(idToken.<String>getClaim("sid")).isEqualTo(sidClaim);
325329
}
326330

331+
// gh-19202
332+
@Test
333+
public void requestWhenWebAuthnAuthenticationThenIdTokenContainsSidAndAuthTimeClaims() throws Exception {
334+
this.spring.register(AuthorizationServerConfigurationWithWebAuthn.class).autowire();
335+
336+
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
337+
this.registeredClientRepository.save(registeredClient);
338+
339+
WebAuthnAuthentication webAuthnAuthentication = new WebAuthnAuthentication(
340+
TestPublicKeyCredentialUserEntities.userEntity().build(),
341+
List.of(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.WEBAUTHN_AUTHORITY)));
342+
343+
MultiValueMap<String, String> authorizationRequestParameters = getAuthorizationRequestParameters(
344+
registeredClient);
345+
MvcResult mvcResult = this.mvc
346+
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
347+
.with(authentication(webAuthnAuthentication)))
348+
.andExpect(status().is3xxRedirection())
349+
.andReturn();
350+
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
351+
assertThat(redirectedUrl).matches(".+\\?code=.{15,}&state=state");
352+
353+
String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code");
354+
OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode,
355+
AUTHORIZATION_CODE_TOKEN_TYPE);
356+
357+
mvcResult = this.mvc
358+
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getTokenRequestParameters(registeredClient, authorization))
359+
.header(HttpHeaders.AUTHORIZATION,
360+
"Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret())))
361+
.andExpect(status().isOk())
362+
.andReturn();
363+
364+
MockHttpServletResponse servletResponse = mvcResult.getResponse();
365+
MockClientHttpResponse httpResponse = new MockClientHttpResponse(servletResponse.getContentAsByteArray(),
366+
HttpStatus.valueOf(servletResponse.getStatus()));
367+
OAuth2AccessTokenResponse accessTokenResponse = accessTokenHttpResponseConverter
368+
.read(OAuth2AccessTokenResponse.class, httpResponse);
369+
370+
Jwt idToken = this.jwtDecoder
371+
.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
372+
373+
assertThat(idToken.<String>getClaim("sid")).isNotNull();
374+
assertThat(idToken.<Object>getClaim("auth_time")).isNotNull();
375+
}
376+
327377
@Test
328378
public void requestWhenLogoutRequestThenLogout() throws Exception {
329379
this.spring.register(AuthorizationServerConfiguration.class).autowire();
@@ -696,6 +746,23 @@ SessionRegistry sessionRegistry() {
696746

697747
}
698748

749+
// gh-19202
750+
// Uses InMemoryOAuth2AuthorizationService to avoid Jackson serialization complexity
751+
// when storing WebAuthnAuthentication (which requires both Jackson 2 and 3 WebAuthn
752+
// modules to be registered in JdbcOAuth2AuthorizationService).
753+
@EnableWebSecurity
754+
@Configuration
755+
static class AuthorizationServerConfigurationWithWebAuthn extends AuthorizationServerConfiguration {
756+
757+
@Bean
758+
@Override
759+
OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations,
760+
RegisteredClientRepository registeredClientRepository) {
761+
return new InMemoryOAuth2AuthorizationService();
762+
}
763+
764+
}
765+
699766
@EnableWebSecurity
700767
@Configuration
701768
static class AuthorizationServerConfigurationWithTokenGenerator extends AuthorizationServerConfiguration {

webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialUserEntity.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.jspecify.annotations.Nullable;
2222

23+
import org.springframework.security.core.AuthenticatedPrincipal;
2324
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
2425
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
2526

@@ -34,14 +35,15 @@
3435
* @since 6.4
3536
* @see WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest)
3637
*/
37-
public interface PublicKeyCredentialUserEntity extends Serializable {
38+
public interface PublicKeyCredentialUserEntity extends Serializable, AuthenticatedPrincipal {
3839

3940
/**
4041
* The <a href=
4142
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name">name</a>
4243
* property is a human-palatable identifier for a user account.
4344
* @return the name
4445
*/
46+
@Override
4547
String getName();
4648

4749
/**

webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationTests.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
package org.springframework.security.web.webauthn.authentication;
1818

19+
import java.util.Collections;
1920
import java.util.List;
2021
import java.util.Set;
2122

2223
import org.junit.jupiter.api.Test;
2324

25+
import org.springframework.security.authentication.AbstractAuthenticationToken;
26+
import org.springframework.security.core.AuthenticatedPrincipal;
2427
import org.springframework.security.core.GrantedAuthority;
2528
import org.springframework.security.core.authority.AuthorityUtils;
2629
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
@@ -56,6 +59,43 @@ void setAuthenticationWhenFalseThenNotAuthenticated() {
5659
assertThat(authentication.isAuthenticated()).isFalse();
5760
}
5861

62+
@Test
63+
void getNameReturnsUserEntityName() {
64+
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntities.userEntity().build();
65+
WebAuthnAuthentication authentication = new WebAuthnAuthentication(userEntity,
66+
AuthorityUtils.createAuthorityList("ROLE_USER"));
67+
assertThat(authentication.getName()).isEqualTo(userEntity.getName());
68+
}
69+
70+
// gh-19202
71+
@Test
72+
void principalImplementsAuthenticatedPrincipal() {
73+
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntities.userEntity().build();
74+
assertThat(userEntity).isInstanceOf(AuthenticatedPrincipal.class);
75+
assertThat(((AuthenticatedPrincipal) userEntity).getName()).isEqualTo(userEntity.getName());
76+
}
77+
78+
// gh-19202
79+
// Simulates the name-extraction logic used internally by SpringSessionBackedSessionRegistry.
80+
// Before the fix, AbstractAuthenticationToken falls through to toString() because
81+
// PublicKeyCredentialUserEntity did not implement AuthenticatedPrincipal.
82+
@Test
83+
void principalNameResolvableViaAbstractAuthenticationToken() {
84+
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntities.userEntity().build();
85+
AbstractAuthenticationToken wrapper = new AbstractAuthenticationToken(Collections.emptyList()) {
86+
@Override
87+
public Object getPrincipal() {
88+
return userEntity;
89+
}
90+
91+
@Override
92+
public Object getCredentials() {
93+
return null;
94+
}
95+
};
96+
assertThat(wrapper.getName()).isEqualTo(userEntity.getName());
97+
}
98+
5999
@Test
60100
void toBuilderWhenApplyThenCopies() {
61101
PublicKeyCredentialUserEntity alice = TestPublicKeyCredentialUserEntities.userEntity().build();

0 commit comments

Comments
 (0)