Browse Source

Default WebAuthnConfigurer#rpName to rpId

In WebAuthn L3 spec, PublicKeyCredentialEntity.name is deprecated:

> This member is deprecated because many clients do not display it,
> but it remains a required dictionary member for backwards compatibility.
> Relying Parties MAY, as a safe default, set this equal to the RP ID.

Source: https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialentity

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
pull/18139/head
Daniel Garnier-Moiroux 2 months ago committed by Rob Winch
parent
commit
fed6df5167
  1. 1
      config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java
  2. 6
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java
  3. 64
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java
  4. 2
      docs/modules/ROOT/pages/servlet/authentication/passkeys.adoc

1
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

@ -3701,7 +3701,6 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
* http * http
* // ... * // ...
* .webAuthn((webAuthn) -&gt; webAuthn * .webAuthn((webAuthn) -&gt; webAuthn
* .rpName("Spring Security Relying Party")
* .rpId("example.com") * .rpId("example.com")
* .allowedOrigins("https://example.com") * .allowedOrigins("https://example.com")
* ); * );

6
config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java

@ -192,9 +192,9 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
if (webauthnOperationsBean.isPresent()) { if (webauthnOperationsBean.isPresent()) {
return webauthnOperationsBean.get(); return webauthnOperationsBean.get();
} }
Webauthn4JRelyingPartyOperations result = new Webauthn4JRelyingPartyOperations(userEntities, userCredentials, String rpName = (this.rpName != null) ? this.rpName : this.rpId;
PublicKeyCredentialRpEntity.builder().id(this.rpId).name(this.rpName).build(), this.allowedOrigins); return new Webauthn4JRelyingPartyOperations(userEntities, userCredentials,
return result; PublicKeyCredentialRpEntity.builder().id(this.rpId).name(rpName).build(), this.allowedOrigins);
} }
} }

64
config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java

@ -18,12 +18,15 @@ package org.springframework.security.config.annotation.web.configurers;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.Customizer; import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@ -38,7 +41,10 @@ import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -111,6 +117,42 @@ public class WebAuthnConfigurerTests {
.hasSize(1); .hasSize(1);
} }
@Test
void webauthnWhenConfiguredDefaultsRpNameToRpId() throws Exception {
ObjectMapper mapper = new ObjectMapper();
this.spring.register(DefaultWebauthnConfiguration.class).autowire();
String response = this.mvc
.perform(post("/webauthn/register/options").with(csrf())
.with(authentication(new TestingAuthenticationToken("test", "ignored", "ROLE_user"))))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
JsonNode parsedResponse = mapper.readTree(response);
assertThat(parsedResponse.get("rp").get("id").asText()).isEqualTo("example.com");
assertThat(parsedResponse.get("rp").get("name").asText()).isEqualTo("example.com");
}
@Test
void webauthnWhenRpNameConfiguredUsesRpName() throws Exception {
ObjectMapper mapper = new ObjectMapper();
this.spring.register(CustomRpNameWebauthnConfiguration.class).autowire();
String response = this.mvc
.perform(post("/webauthn/register/options").with(csrf())
.with(authentication(new TestingAuthenticationToken("test", "ignored", "ROLE_user"))))
.andExpect(status().is2xxSuccessful())
.andReturn()
.getResponse()
.getContentAsString();
JsonNode parsedResponse = mapper.readTree(response);
assertThat(parsedResponse.get("rp").get("id").asText()).isEqualTo("example.com");
assertThat(parsedResponse.get("rp").get("name").asText()).isEqualTo("Test RP Name");
}
@Test @Test
public void webauthnWhenConfiguredAndFormLoginThenDoesServesJavascript() throws Exception { public void webauthnWhenConfiguredAndFormLoginThenDoesServesJavascript() throws Exception {
this.spring.register(FormLoginAndNoDefaultRegistrationPageConfiguration.class).autowire(); this.spring.register(FormLoginAndNoDefaultRegistrationPageConfiguration.class).autowire();
@ -137,7 +179,27 @@ public class WebAuthnConfigurerTests {
@Bean @Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.formLogin(Customizer.withDefaults()).webAuthn(Customizer.withDefaults()).build(); return http.formLogin(Customizer.withDefaults())
.webAuthn((webauthn) -> webauthn.rpId("example.com"))
.build();
}
}
@Configuration
@EnableWebSecurity
static class CustomRpNameWebauthnConfiguration {
@Bean
UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager();
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.formLogin(Customizer.withDefaults())
.webAuthn((webauthn) -> webauthn.rpId("example.com").rpName("Test RP Name"))
.build();
} }
} }

2
docs/modules/ROOT/pages/servlet/authentication/passkeys.adoc

@ -64,7 +64,6 @@ SecurityFilterChain filterChain(HttpSecurity http) {
// ... // ...
.formLogin(withDefaults()) .formLogin(withDefaults())
.webAuthn((webAuthn) -> webAuthn .webAuthn((webAuthn) -> webAuthn
.rpName("Spring Security Relying Party")
.rpId("example.com") .rpId("example.com")
.allowedOrigins("https://example.com") .allowedOrigins("https://example.com")
); );
@ -91,7 +90,6 @@ Kotlin::
open fun filterChain(http: HttpSecurity): SecurityFilterChain { open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http { http {
webAuthn { webAuthn {
rpName = "Spring Security Relying Party"
rpId = "example.com" rpId = "example.com"
allowedOrigins = setOf("https://example.com") allowedOrigins = setOf("https://example.com")
} }

Loading…
Cancel
Save