diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java index ec0322aa5af..06bcea879ea 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java @@ -79,6 +79,7 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; @@ -99,6 +100,8 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntities; +import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.LinkedMultiValueMap; @@ -113,6 +116,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -324,6 +328,52 @@ public void requestWhenRefreshTokenRequestThenIdTokenContainsSidClaim() throws E assertThat(idToken.getClaim("sid")).isEqualTo(sidClaim); } + // gh-19202 + @Test + public void requestWhenWebAuthnAuthenticationThenIdTokenContainsSidAndAuthTimeClaims() throws Exception { + this.spring.register(AuthorizationServerConfigurationWithWebAuthn.class).autowire(); + + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); + this.registeredClientRepository.save(registeredClient); + + WebAuthnAuthentication webAuthnAuthentication = new WebAuthnAuthentication( + TestPublicKeyCredentialUserEntities.userEntity().build(), + List.of(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.WEBAUTHN_AUTHORITY))); + + MultiValueMap authorizationRequestParameters = getAuthorizationRequestParameters( + registeredClient); + MvcResult mvcResult = this.mvc + .perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters) + .with(authentication(webAuthnAuthentication))) + .andExpect(status().is3xxRedirection()) + .andReturn(); + String redirectedUrl = mvcResult.getResponse().getRedirectedUrl(); + assertThat(redirectedUrl).matches(".+\\?code=.{15,}&state=state"); + + String authorizationCode = extractParameterFromRedirectUri(redirectedUrl, "code"); + OAuth2Authorization authorization = this.authorizationService.findByToken(authorizationCode, + AUTHORIZATION_CODE_TOKEN_TYPE); + + mvcResult = this.mvc + .perform(post(DEFAULT_TOKEN_ENDPOINT_URI).params(getTokenRequestParameters(registeredClient, authorization)) + .header(HttpHeaders.AUTHORIZATION, + "Basic " + encodeBasicAuth(registeredClient.getClientId(), registeredClient.getClientSecret()))) + .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); + + Jwt idToken = this.jwtDecoder + .decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN)); + + assertThat(idToken.getClaim("sid")).isNotNull(); + assertThat(idToken.getClaim("auth_time")).isNotNull(); + } + @Test public void requestWhenLogoutRequestThenLogout() throws Exception { this.spring.register(AuthorizationServerConfiguration.class).autowire(); @@ -696,6 +746,23 @@ SessionRegistry sessionRegistry() { } + // gh-19202 + // Uses InMemoryOAuth2AuthorizationService to avoid Jackson serialization complexity + // when storing WebAuthnAuthentication (which requires both Jackson 2 and 3 WebAuthn + // modules to be registered in JdbcOAuth2AuthorizationService). + @EnableWebSecurity + @Configuration + static class AuthorizationServerConfigurationWithWebAuthn extends AuthorizationServerConfiguration { + + @Bean + @Override + OAuth2AuthorizationService authorizationService(JdbcOperations jdbcOperations, + RegisteredClientRepository registeredClientRepository) { + return new InMemoryOAuth2AuthorizationService(); + } + + } + @EnableWebSecurity @Configuration static class AuthorizationServerConfigurationWithTokenGenerator extends AuthorizationServerConfiguration { diff --git a/webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialUserEntity.java b/webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialUserEntity.java index 269cc750e58..b0359bad33a 100644 --- a/webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialUserEntity.java +++ b/webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialUserEntity.java @@ -20,6 +20,7 @@ import org.jspecify.annotations.Nullable; +import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest; import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations; @@ -34,7 +35,7 @@ * @since 6.4 * @see WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest) */ -public interface PublicKeyCredentialUserEntity extends Serializable { +public interface PublicKeyCredentialUserEntity extends Serializable, AuthenticatedPrincipal { /** * The