Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -324,6 +328,52 @@ public void requestWhenRefreshTokenRequestThenIdTokenContainsSidClaim() throws E
assertThat(idToken.<String>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<String, String> 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.<String>getClaim("sid")).isNotNull();
assertThat(idToken.<Object>getClaim("auth_time")).isNotNull();
}

@Test
public void requestWhenLogoutRequestThenLogout() throws Exception {
this.spring.register(AuthorizationServerConfiguration.class).autowire();
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -34,14 +35,15 @@
* @since 6.4
* @see WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest)
*/
public interface PublicKeyCredentialUserEntity extends Serializable {
public interface PublicKeyCredentialUserEntity extends Serializable, AuthenticatedPrincipal {

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

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

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

import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.junit.jupiter.api.Test;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
Expand Down Expand Up @@ -56,6 +59,43 @@ void setAuthenticationWhenFalseThenNotAuthenticated() {
assertThat(authentication.isAuthenticated()).isFalse();
}

@Test
void getNameReturnsUserEntityName() {
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntities.userEntity().build();
WebAuthnAuthentication authentication = new WebAuthnAuthentication(userEntity,
AuthorityUtils.createAuthorityList("ROLE_USER"));
assertThat(authentication.getName()).isEqualTo(userEntity.getName());
}

// gh-19202
@Test
void principalImplementsAuthenticatedPrincipal() {
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntities.userEntity().build();
assertThat(userEntity).isInstanceOf(AuthenticatedPrincipal.class);
assertThat(((AuthenticatedPrincipal) userEntity).getName()).isEqualTo(userEntity.getName());
}

// gh-19202
// Simulates the name-extraction logic used internally by SpringSessionBackedSessionRegistry.
// Before the fix, AbstractAuthenticationToken falls through to toString() because
// PublicKeyCredentialUserEntity did not implement AuthenticatedPrincipal.
@Test
void principalNameResolvableViaAbstractAuthenticationToken() {
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntities.userEntity().build();
AbstractAuthenticationToken wrapper = new AbstractAuthenticationToken(Collections.emptyList()) {
@Override
public Object getPrincipal() {
return userEntity;
}

@Override
public Object getCredentials() {
return null;
}
};
assertThat(wrapper.getName()).isEqualTo(userEntity.getName());
}

@Test
void toBuilderWhenApplyThenCopies() {
PublicKeyCredentialUserEntity alice = TestPublicKeyCredentialUserEntities.userEntity().build();
Expand Down