mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-01 09:42:13 +00:00
Add Passkeys Support
Closes gh-13305
This commit is contained in:
parent
f280aa390b
commit
b0e8730d70
@ -106,7 +106,7 @@ develocity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nohttp {
|
nohttp {
|
||||||
source.exclude "buildSrc/build/**"
|
source.exclude "buildSrc/build/**", "javascript/.gradle/**", "javascript/package-lock.json", "javascript/node_modules/**", "javascript/build/**", "javascript/dist/**"
|
||||||
source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd))
|
source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@ dependencies {
|
|||||||
optional 'org.jetbrains.kotlin:kotlin-reflect'
|
optional 'org.jetbrains.kotlin:kotlin-reflect'
|
||||||
optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
|
optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
|
||||||
optional 'jakarta.annotation:jakarta.annotation-api'
|
optional 'jakarta.annotation:jakarta.annotation-api'
|
||||||
|
optional libs.webauthn4j.core
|
||||||
|
|
||||||
provided 'jakarta.servlet:jakarta.servlet-api'
|
provided 'jakarta.servlet:jakarta.servlet-api'
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ import org.springframework.security.config.annotation.web.configurers.RequestCac
|
|||||||
import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.WebAuthnConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.X509Configurer;
|
import org.springframework.security.config.annotation.web.configurers.X509Configurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||||
@ -3674,6 +3675,31 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies webAuthn/passkeys based authentication.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* @Bean
|
||||||
|
* SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
* http
|
||||||
|
* // ...
|
||||||
|
* .webAuthn((webAuthn) -> webAuthn
|
||||||
|
* .rpName("Spring Security Relying Party")
|
||||||
|
* .rpId("example.com")
|
||||||
|
* .allowedOrigins("https://example.com")
|
||||||
|
* );
|
||||||
|
* return http.build();
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
* @param webAuthn the customizer to apply
|
||||||
|
* @return the {@link HttpSecurity} for further customizations
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public HttpSecurity webAuthn(Customizer<WebAuthnConfigurer<HttpSecurity>> webAuthn) throws Exception {
|
||||||
|
webAuthn.customize(getOrApply(new WebAuthnConfigurer<HttpSecurity>()));
|
||||||
|
return HttpSecurity.this;
|
||||||
|
}
|
||||||
|
|
||||||
private List<RequestMatcher> createAntMatchers(String... patterns) {
|
private List<RequestMatcher> createAntMatchers(String... patterns) {
|
||||||
List<RequestMatcher> matchers = new ArrayList<>(patterns.length);
|
List<RequestMatcher> matchers = new ArrayList<>(patterns.length);
|
||||||
for (String pattern : patterns) {
|
for (String pattern : patterns) {
|
||||||
|
@ -0,0 +1,194 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.config.annotation.web.configurers;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
|
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||||
|
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||||
|
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
|
||||||
|
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||||
|
import org.springframework.security.web.csrf.CsrfToken;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
|
||||||
|
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
|
||||||
|
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
|
||||||
|
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
|
||||||
|
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
|
||||||
|
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository;
|
||||||
|
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
|
||||||
|
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
|
||||||
|
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||||
|
import org.springframework.security.web.webauthn.management.Webauthn4JRelyingPartyOperations;
|
||||||
|
import org.springframework.security.web.webauthn.registration.DefaultWebAuthnRegistrationPageGeneratingFilter;
|
||||||
|
import org.springframework.security.web.webauthn.registration.PublicKeyCredentialCreationOptionsFilter;
|
||||||
|
import org.springframework.security.web.webauthn.registration.WebAuthnRegistrationFilter;
|
||||||
|
|
||||||
|
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures WebAuthn for Spring Security applications
|
||||||
|
*
|
||||||
|
* @param <H> the type of builder
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
||||||
|
extends AbstractHttpConfigurer<WebAuthnConfigurer<H>, H> {
|
||||||
|
|
||||||
|
private String rpId;
|
||||||
|
|
||||||
|
private String rpName;
|
||||||
|
|
||||||
|
private Set<String> allowedOrigins = new HashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Relying Party id.
|
||||||
|
* @param rpId the relying party id
|
||||||
|
* @return the {@link WebAuthnConfigurer} for further customization
|
||||||
|
*/
|
||||||
|
public WebAuthnConfigurer<H> rpId(String rpId) {
|
||||||
|
this.rpId = rpId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the relying party name
|
||||||
|
* @param rpName the relying party name
|
||||||
|
* @return the {@link WebAuthnConfigurer} for further customization
|
||||||
|
*/
|
||||||
|
public WebAuthnConfigurer<H> rpName(String rpName) {
|
||||||
|
this.rpName = rpName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method for {@link #allowedOrigins(Set)}
|
||||||
|
* @param allowedOrigins the allowed origins
|
||||||
|
* @return the {@link WebAuthnConfigurer} for further customization
|
||||||
|
* @see #allowedOrigins(Set)
|
||||||
|
*/
|
||||||
|
public WebAuthnConfigurer<H> allowedOrigins(String... allowedOrigins) {
|
||||||
|
return allowedOrigins(Set.of(allowedOrigins));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the allowed origins.
|
||||||
|
* @param allowedOrigins the allowed origins
|
||||||
|
* @return the {@link WebAuthnConfigurer} for further customization
|
||||||
|
* @see #allowedOrigins(String...)
|
||||||
|
*/
|
||||||
|
public WebAuthnConfigurer<H> allowedOrigins(Set<String> allowedOrigins) {
|
||||||
|
this.allowedOrigins = allowedOrigins;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(H http) throws Exception {
|
||||||
|
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> {
|
||||||
|
throw new IllegalStateException("Missing UserDetailsService Bean");
|
||||||
|
});
|
||||||
|
PublicKeyCredentialUserEntityRepository userEntities = getSharedOrBean(http,
|
||||||
|
PublicKeyCredentialUserEntityRepository.class)
|
||||||
|
.orElse(userEntityRepository());
|
||||||
|
UserCredentialRepository userCredentials = getSharedOrBean(http, UserCredentialRepository.class)
|
||||||
|
.orElse(userCredentialRepository());
|
||||||
|
WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials);
|
||||||
|
WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter();
|
||||||
|
webAuthnAuthnFilter.setAuthenticationManager(
|
||||||
|
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
|
||||||
|
http.addFilterBefore(webAuthnAuthnFilter, BasicAuthenticationFilter.class);
|
||||||
|
http.addFilterAfter(new WebAuthnRegistrationFilter(userCredentials, rpOperations), AuthorizationFilter.class);
|
||||||
|
http.addFilterBefore(new PublicKeyCredentialCreationOptionsFilter(rpOperations), AuthorizationFilter.class);
|
||||||
|
http.addFilterAfter(new DefaultWebAuthnRegistrationPageGeneratingFilter(userEntities, userCredentials),
|
||||||
|
AuthorizationFilter.class);
|
||||||
|
http.addFilterBefore(new PublicKeyCredentialRequestOptionsFilter(rpOperations), AuthorizationFilter.class);
|
||||||
|
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
|
||||||
|
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
|
||||||
|
if (loginPageGeneratingFilter != null) {
|
||||||
|
ClassPathResource webauthn = new ClassPathResource(
|
||||||
|
"org/springframework/security/spring-security-webauthn.js");
|
||||||
|
AntPathRequestMatcher matcher = antMatcher(HttpMethod.GET, "/login/webauthn.js");
|
||||||
|
|
||||||
|
Constructor<DefaultResourcesFilter> constructor = DefaultResourcesFilter.class
|
||||||
|
.getDeclaredConstructor(RequestMatcher.class, ClassPathResource.class, MediaType.class);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
DefaultResourcesFilter resourcesFilter = constructor.newInstance(matcher, webauthn,
|
||||||
|
MediaType.parseMediaType("text/javascript"));
|
||||||
|
http.addFilter(resourcesFilter);
|
||||||
|
DefaultLoginPageGeneratingFilter loginGeneratingFilter = http
|
||||||
|
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
|
||||||
|
loginGeneratingFilter.setPasskeysEnabled(true);
|
||||||
|
loginGeneratingFilter.setResolveHeaders((request) -> {
|
||||||
|
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||||
|
return Map.of(csrfToken.getHeaderName(), csrfToken.getToken());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <C> Optional<C> getSharedOrBean(H http, Class<C> type) {
|
||||||
|
C shared = http.getSharedObject(type);
|
||||||
|
return Optional.ofNullable(shared).or(() -> getBeanOrNull(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> Optional<T> getBeanOrNull(Class<T> type) {
|
||||||
|
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
|
||||||
|
if (context == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Optional.of(context.getBean(type));
|
||||||
|
}
|
||||||
|
catch (NoSuchBeanDefinitionException ex) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MapUserCredentialRepository userCredentialRepository() {
|
||||||
|
return new MapUserCredentialRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PublicKeyCredentialUserEntityRepository userEntityRepository() {
|
||||||
|
return new MapPublicKeyCredentialUserEntityRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations(
|
||||||
|
PublicKeyCredentialUserEntityRepository userEntities, UserCredentialRepository userCredentials) {
|
||||||
|
Optional<WebAuthnRelyingPartyOperations> webauthnOperationsBean = getBeanOrNull(
|
||||||
|
WebAuthnRelyingPartyOperations.class);
|
||||||
|
if (webauthnOperationsBean.isPresent()) {
|
||||||
|
return webauthnOperationsBean.get();
|
||||||
|
}
|
||||||
|
Webauthn4JRelyingPartyOperations result = new Webauthn4JRelyingPartyOperations(userEntities, userCredentials,
|
||||||
|
PublicKeyCredentialRpEntity.builder().id(this.rpId).name(this.rpName).build(), this.allowedOrigins);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1031,6 +1031,37 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
|
|||||||
this.http.rememberMe(rememberMeCustomizer)
|
this.http.rememberMe(rememberMeCustomizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable WebAuthn configuration.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* @Configuration
|
||||||
|
* @EnableWebSecurity
|
||||||
|
* class SecurityConfig {
|
||||||
|
*
|
||||||
|
* @Bean
|
||||||
|
* fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
* http {
|
||||||
|
* webAuthn {
|
||||||
|
* loginPage = "/log-in"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* return http.build()
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param webAuthnConfiguration custom configurations to be applied
|
||||||
|
* to the WebAuthn authentication
|
||||||
|
* @see [WebAuthnDsl]
|
||||||
|
*/
|
||||||
|
fun webAuthn(webAuthnConfiguration: WebAuthnDsl.() -> Unit) {
|
||||||
|
val webAuthnCustomizer = WebAuthnDsl().apply(webAuthnConfiguration).get()
|
||||||
|
this.http.webAuthn(webAuthnCustomizer)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the [Filter] at the location of the specified [Filter] class.
|
* Adds the [Filter] at the location of the specified [Filter] class.
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2021 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.config.annotation.web
|
||||||
|
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.WebAuthnConfigurer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Kotlin DSL to configure [HttpSecurity] webauthn using idiomatic Kotlin code.
|
||||||
|
* @property rpName the relying party name
|
||||||
|
* @property rpId the relying party id
|
||||||
|
* @property the allowed origins
|
||||||
|
* @since 6.4
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
@SecurityMarker
|
||||||
|
class WebAuthnDsl {
|
||||||
|
var rpName: String? = null
|
||||||
|
var rpId: String? = null
|
||||||
|
var allowedOrigins: Set<String>? = null
|
||||||
|
|
||||||
|
internal fun get(): (WebAuthnConfigurer<HttpSecurity>) -> Unit {
|
||||||
|
return { webAuthn -> webAuthn
|
||||||
|
.rpId(rpId)
|
||||||
|
.rpName(rpName)
|
||||||
|
.allowedOrigins(allowedOrigins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -53,6 +53,7 @@ class X509Dsl {
|
|||||||
var authenticationUserDetailsService: AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>? = null
|
var authenticationUserDetailsService: AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>? = null
|
||||||
var subjectPrincipalRegex: String? = null
|
var subjectPrincipalRegex: String? = null
|
||||||
|
|
||||||
|
|
||||||
internal fun get(): (X509Configurer<HttpSecurity>) -> Unit {
|
internal fun get(): (X509Configurer<HttpSecurity>) -> Unit {
|
||||||
return { x509 ->
|
return { x509 ->
|
||||||
x509AuthenticationFilter?.also { x509.x509AuthenticationFilter(x509AuthenticationFilter) }
|
x509AuthenticationFilter?.also { x509.x509AuthenticationFilter(x509AuthenticationFilter) }
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.config.annotation.web
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.config.test.SpringTestContext
|
||||||
|
import org.springframework.security.config.test.SpringTestContextExtension
|
||||||
|
import org.springframework.security.core.userdetails.User
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService
|
||||||
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
|
import org.springframework.test.web.servlet.post
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for [WebAuthnDsl]
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
@ExtendWith(SpringTestContextExtension::class)
|
||||||
|
class WebAuthnDslTests {
|
||||||
|
@JvmField
|
||||||
|
val spring = SpringTestContext(this)
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `default configuration`() {
|
||||||
|
this.spring.register(WebauthnConfig::class.java).autowire()
|
||||||
|
|
||||||
|
this.mockMvc.post("/test1")
|
||||||
|
.andExpect {
|
||||||
|
status { isForbidden() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
open class WebauthnConfig {
|
||||||
|
@Bean
|
||||||
|
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http {
|
||||||
|
webAuthn {
|
||||||
|
rpName = "Spring Security Relying Party"
|
||||||
|
rpId = "example.com"
|
||||||
|
allowedOrigins = setOf("https://example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return http.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun userDetailsService(): UserDetailsService {
|
||||||
|
val userDetails = User.withDefaultPasswordEncoder()
|
||||||
|
.username("rod")
|
||||||
|
.password("password")
|
||||||
|
.roles("USER")
|
||||||
|
.build()
|
||||||
|
return InMemoryUserDetailsManager(userDetails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,7 @@
|
|||||||
***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
|
***** xref:servlet/authentication/passwords/dao-authentication-provider.adoc[DaoAuthenticationProvider]
|
||||||
***** xref:servlet/authentication/passwords/ldap.adoc[LDAP]
|
***** xref:servlet/authentication/passwords/ldap.adoc[LDAP]
|
||||||
*** xref:servlet/authentication/persistence.adoc[Persistence]
|
*** xref:servlet/authentication/persistence.adoc[Persistence]
|
||||||
|
*** xref:servlet/authentication/passkeys.adoc[Passkeys]
|
||||||
*** xref:servlet/authentication/onetimetoken.adoc[One-Time Token]
|
*** xref:servlet/authentication/onetimetoken.adoc[One-Time Token]
|
||||||
*** xref:servlet/authentication/session-management.adoc[Session Management]
|
*** xref:servlet/authentication/session-management.adoc[Session Management]
|
||||||
*** xref:servlet/authentication/rememberme.adoc[Remember Me]
|
*** xref:servlet/authentication/rememberme.adoc[Remember Me]
|
||||||
|
289
docs/modules/ROOT/pages/servlet/authentication/passkeys.adoc
Normal file
289
docs/modules/ROOT/pages/servlet/authentication/passkeys.adoc
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
[[passkeys]]
|
||||||
|
= Passkeys
|
||||||
|
|
||||||
|
Spring Security provides support for https://www.passkeys.com[passkeys].
|
||||||
|
Passkeys are a more secure method of authenticating than passwords and are built using https://www.w3.org/TR/webauthn-3/[WebAuthn].
|
||||||
|
|
||||||
|
In order to use a passkey to authenticate, a user must first xref:servlet/authentication/passkeys.adoc#passkeys-register[Register a New Credential].
|
||||||
|
After the credential is registered, it can be used to authenticate by xref:servlet/authentication/passkeys.adoc#passkeys-verify[verifying an authentication assertion].
|
||||||
|
|
||||||
|
[[passkeys-configuration]]
|
||||||
|
== Configuration
|
||||||
|
|
||||||
|
The following configuration enables passkey authentication.
|
||||||
|
It provides a way to xref:./passkeys.adoc#passkeys-register[] at `/webauthn/register` and a default log in page that allows xref:./passkeys.adoc#passkeys-verify[authenticating with passkeys].
|
||||||
|
|
||||||
|
[tabs]
|
||||||
|
======
|
||||||
|
Java::
|
||||||
|
+
|
||||||
|
[source,java,role="primary"]
|
||||||
|
----
|
||||||
|
@Bean
|
||||||
|
SecurityFilterChain filterChain(HttpSecurity http) {
|
||||||
|
http
|
||||||
|
// ...
|
||||||
|
.formLogin(withDefaults())
|
||||||
|
.webAuthn((webAuthn) -> webAuthn
|
||||||
|
.rpName("Spring Security Relying Party")
|
||||||
|
.rpId("example.com")
|
||||||
|
.allowedOrigins("https://example.com")
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
UserDetailsService userDetailsService() {
|
||||||
|
UserDetails userDetails = User.withDefaultPasswordEncoder()
|
||||||
|
.username("user")
|
||||||
|
.password("password")
|
||||||
|
.roles("USER")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new InMemoryUserDetailsManager(userDetails);
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
Kotlin::
|
||||||
|
+
|
||||||
|
[source,kotlin,role="secondary"]
|
||||||
|
----
|
||||||
|
@Bean
|
||||||
|
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
http {
|
||||||
|
webAuthn {
|
||||||
|
rpName = "Spring Security Relying Party"
|
||||||
|
rpId = "example.com"
|
||||||
|
allowedOrigins = setOf("https://example.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun userDetailsService(): UserDetailsService {
|
||||||
|
val userDetails = User.withDefaultPasswordEncoder()
|
||||||
|
.username("user")
|
||||||
|
.password("password")
|
||||||
|
.roles("USER")
|
||||||
|
.build()
|
||||||
|
return InMemoryUserDetailsManager(userDetails)
|
||||||
|
}
|
||||||
|
----
|
||||||
|
======
|
||||||
|
|
||||||
|
[[passkeys-register]]
|
||||||
|
== Register a New Credential
|
||||||
|
|
||||||
|
In order to use a passkey, a user must first https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential[Register a New Credential].
|
||||||
|
|
||||||
|
Registering a new credential is composed of two steps:
|
||||||
|
|
||||||
|
1. Requesting the Registration Options
|
||||||
|
2. Registering the Credential
|
||||||
|
|
||||||
|
[[passkeys-register-options]]
|
||||||
|
=== Request the Registration Options
|
||||||
|
|
||||||
|
The first step in registration of a new credential is to request the registration options.
|
||||||
|
In Spring Security, a request for the registration options is typically done using JavaScript and looks like:
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
Spring Security provides a default registration page that can be used as a reference on how to register credentials.
|
||||||
|
====
|
||||||
|
|
||||||
|
.Request for Registration Options
|
||||||
|
[source,http]
|
||||||
|
----
|
||||||
|
POST /webauthn/register/options
|
||||||
|
X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
|
||||||
|
----
|
||||||
|
|
||||||
|
The request above will obtain the registration options for the currently authenticated user.
|
||||||
|
Since the challenge is persisted (state is changed) to be compared at the time of registration, the request must be a POST and include a CSRF token.
|
||||||
|
|
||||||
|
.Response for Registration Options
|
||||||
|
[source,json]
|
||||||
|
----
|
||||||
|
{
|
||||||
|
"rp": {
|
||||||
|
"name": "SimpleWebAuthn Example",
|
||||||
|
"id": "example.localhost"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"name": "user@example.localhost",
|
||||||
|
"id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w",
|
||||||
|
"displayName": "user@example.localhost"
|
||||||
|
},
|
||||||
|
"challenge": "q7lCdd3SVQxdC-v8pnRAGEn1B2M-t7ZECWPwCAmhWvc",
|
||||||
|
"pubKeyCredParams": [
|
||||||
|
{
|
||||||
|
"type": "public-key",
|
||||||
|
"alg": -8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "public-key",
|
||||||
|
"alg": -7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "public-key",
|
||||||
|
"alg": -257
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeout": 300000,
|
||||||
|
"excludeCredentials": [],
|
||||||
|
"authenticatorSelection": {
|
||||||
|
"residentKey": "required",
|
||||||
|
"userVerification": "preferred"
|
||||||
|
},
|
||||||
|
"attestation": "direct",
|
||||||
|
"extensions": {
|
||||||
|
"credProps": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
[[passkeys-register-create]]
|
||||||
|
=== Registering the Credential
|
||||||
|
|
||||||
|
After the registration options are obtained, they are used to create the credentials that are registered.
|
||||||
|
To register a new credential, the application should pass the options to https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create[`navigator.credentials.create`] after base64url decoding the binary values such as `user.id`, `challenge`, and `excludeCredentials[].id`.
|
||||||
|
|
||||||
|
The returned value can then be sent to the server as a JSON request.
|
||||||
|
An example registration request can be found below:
|
||||||
|
|
||||||
|
.Example Registration Request
|
||||||
|
[source,http]
|
||||||
|
----
|
||||||
|
POST /webauthn/register
|
||||||
|
X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
|
||||||
|
|
||||||
|
{
|
||||||
|
"publicKey": { // <1>
|
||||||
|
"credential": {
|
||||||
|
"id": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||||
|
"rawId": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||||
|
"response": {
|
||||||
|
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAALraVWanqkAfvZZFYZpVEg0AEHWBexBp0RRSF5KV4vV1NsKlAQIDJiABIVggQjmrekPGzyqtoKK9HPUH-8Z2FLpoqkklFpFPQVICQ3IiWCD6I9Jvmor685fOZOyGXqUd87tXfvJk8rxj9OhuZvUALA",
|
||||||
|
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSl9RTi10SFJYRWVKYjlNcUNrWmFPLUdOVmlibXpGVGVWMk43Z0ptQUdrQSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
|
||||||
|
"transports": [
|
||||||
|
"internal",
|
||||||
|
"hybrid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "public-key",
|
||||||
|
"clientExtensionResults": {},
|
||||||
|
"authenticatorAttachment": "platform"
|
||||||
|
},
|
||||||
|
"label": "1password" // <2>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
<1> The result of calling `navigator.credentials.create` with binary values base64url encoded.
|
||||||
|
<2> A label that the user selects to have associated with this credential to help the user distinguish the credential.
|
||||||
|
|
||||||
|
.Example Successful Registration Response
|
||||||
|
[source,http]
|
||||||
|
----
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
[[passkeys-verify]]
|
||||||
|
== Verifying an Authentication Assertion
|
||||||
|
|
||||||
|
After xref:./passkeys.adoc#passkeys-register[] the passkey can be https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion[verified] (authenticated).
|
||||||
|
|
||||||
|
Verifying a credential is composed of two steps:
|
||||||
|
|
||||||
|
1. Requesting the Verification Options
|
||||||
|
2. Verifying the Credential
|
||||||
|
|
||||||
|
[[passkeys-verify-options]]
|
||||||
|
=== Request the Verification Options
|
||||||
|
|
||||||
|
The first step in verification of a credential is to request the verification options.
|
||||||
|
In Spring Security, a request for the verification options is typically done using JavaScript and looks like:
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
Spring Security provides a default log in page that can be used as a reference on how to verify credentials.
|
||||||
|
====
|
||||||
|
|
||||||
|
.Request for Verification Options
|
||||||
|
[source,http]
|
||||||
|
----
|
||||||
|
POST /webauthn/authenticate/options
|
||||||
|
X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
|
||||||
|
----
|
||||||
|
|
||||||
|
The request above will obtain the verification options.
|
||||||
|
Since the challenge is persisted (state is changed) to be compared at the time of authentication, the request must be a POST and include a CSRF token.
|
||||||
|
|
||||||
|
The response will contain the options for obtaining a credential with binary values such as `challenge` base64url encoded.
|
||||||
|
|
||||||
|
.Example Response for Verification Options
|
||||||
|
[source,json]
|
||||||
|
----
|
||||||
|
{
|
||||||
|
"challenge": "cQfdGrj9zDg3zNBkOH3WPL954FTOShVy0-CoNgSewNM",
|
||||||
|
"timeout": 300000,
|
||||||
|
"rpId": "example.localhost",
|
||||||
|
"allowCredentials": [],
|
||||||
|
"userVerification": "preferred",
|
||||||
|
"extensions": {}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
[[passkeys-verify-get]]
|
||||||
|
=== Verifying the Credential
|
||||||
|
|
||||||
|
After the verification options are obtained, they are used to get a credential.
|
||||||
|
To get a credential, the application should pass the options to https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create[`navigator.credentials.get`] after base64url decoding the binary values such as `challenge`.
|
||||||
|
|
||||||
|
The returned value of `navigator.credentials.get` can then be sent to the server as a JSON request.
|
||||||
|
Binary values such as `rawId` and `response.*` must be base64url encoded.
|
||||||
|
An example authentication request can be found below:
|
||||||
|
|
||||||
|
.Example Authentication Request
|
||||||
|
[source,http]
|
||||||
|
----
|
||||||
|
POST /login/webauthn
|
||||||
|
X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||||
|
"rawId": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||||
|
"response": {
|
||||||
|
"authenticatorData": "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
|
||||||
|
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
|
||||||
|
"signature": "MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z",
|
||||||
|
"userHandle": "Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo"
|
||||||
|
},
|
||||||
|
"clientExtensionResults": {},
|
||||||
|
"authenticatorAttachment": "platform"
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
.Example Successful Authentication Response
|
||||||
|
[source,http]
|
||||||
|
----
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"redirectUrl": "/", // <1>
|
||||||
|
"authenticated": true // <2>
|
||||||
|
}
|
||||||
|
----
|
||||||
|
<1> The URL to redirect to
|
||||||
|
<2> Indicates that the user is authenticated
|
||||||
|
|
||||||
|
.Example Authentication Failure Response
|
||||||
|
[source,http]
|
||||||
|
----
|
||||||
|
HTTP/1.1 401 OK
|
||||||
|
|
||||||
|
----
|
@ -107,6 +107,8 @@ org-jfrog-buildinfo-build-info-extractor-gradle = "org.jfrog.buildinfo:build-inf
|
|||||||
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969"
|
org-sonarsource-scanner-gradle-sonarqube-gradle-plugin = "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.8.0.1969"
|
||||||
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
org-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
||||||
|
|
||||||
|
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.27.0.RELEASE'
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
|
||||||
org-gradle-wrapper-upgrade = "org.gradle.wrapper-upgrade:0.11.4"
|
org-gradle-wrapper-upgrade = "org.gradle.wrapper-upgrade:0.11.4"
|
||||||
|
2
javascript/.gitignore
vendored
Normal file
2
javascript/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
3
javascript/.prettierrc
Normal file
3
javascript/.prettierrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
30
javascript/eslint.config.js
Normal file
30
javascript/eslint.config.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import eslintConfigPrettier from "eslint-plugin-prettier/recommended";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ["build/**/*"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["lib/**/*.js"],
|
||||||
|
languageOptions: {
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
gobalThis: "readonly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["test/**/*.js"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.mocha,
|
||||||
|
...globals.chai,
|
||||||
|
...globals.nodeBuiltin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
eslintConfigPrettier,
|
||||||
|
];
|
43
javascript/lib/abort-controller.js
Normal file
43
javascript/lib/abort-controller.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const holder = {
|
||||||
|
controller: new AbortController(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new AbortSignal to be used in the options for the registration and authentication ceremonies.
|
||||||
|
* Aborts the existing AbortController if it exists, cancelling any existing ceremony.
|
||||||
|
*
|
||||||
|
* The authentication ceremony, when triggered with conditional mediation, shows a non-modal
|
||||||
|
* interaction. If the user does not interact with the non-modal dialog, the existing ceremony MUST
|
||||||
|
* be cancelled before initiating a new one, hence the need for a singleton AbortController.
|
||||||
|
*
|
||||||
|
* @returns {AbortSignal} a new, non-aborted AbortSignal
|
||||||
|
*/
|
||||||
|
function newSignal() {
|
||||||
|
if (!!holder.controller) {
|
||||||
|
holder.controller.abort("Initiating new WebAuthN ceremony, cancelling current ceremony");
|
||||||
|
}
|
||||||
|
holder.controller = new AbortController();
|
||||||
|
return holder.controller.signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
newSignal,
|
||||||
|
};
|
33
javascript/lib/base64url.js
Normal file
33
javascript/lib/base64url.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
encode: function (buffer) {
|
||||||
|
const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||||
|
return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||||
|
},
|
||||||
|
decode: function (base64url) {
|
||||||
|
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const binStr = window.atob(base64);
|
||||||
|
const bin = new Uint8Array(binStr.length);
|
||||||
|
for (let i = 0; i < binStr.length; i++) {
|
||||||
|
bin[i] = binStr.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bin.buffer;
|
||||||
|
},
|
||||||
|
};
|
33
javascript/lib/http.js
Normal file
33
javascript/lib/http.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
async function post(url, headers, body) {
|
||||||
|
const options = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (body) {
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
return fetch(url, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { post };
|
24
javascript/lib/index.js
Normal file
24
javascript/lib/index.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { setupLogin } from "./webauthn-login.js";
|
||||||
|
import { setupRegistration } from "./webauthn-registration.js";
|
||||||
|
|
||||||
|
// Make "setup" available in the window domain, so it can be run with "setupLogin()"
|
||||||
|
window.setupLogin = setupLogin;
|
||||||
|
window.setupRegistration = setupRegistration;
|
194
javascript/lib/webauthn-core.js
Normal file
194
javascript/lib/webauthn-core.js
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import base64url from "./base64url.js";
|
||||||
|
import http from "./http.js";
|
||||||
|
import abortController from "./abort-controller.js";
|
||||||
|
|
||||||
|
async function isConditionalMediationAvailable() {
|
||||||
|
return !!(
|
||||||
|
window.PublicKeyCredential &&
|
||||||
|
window.PublicKeyCredential.isConditionalMediationAvailable &&
|
||||||
|
(await window.PublicKeyCredential.isConditionalMediationAvailable())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticate(headers, contextPath, useConditionalMediation) {
|
||||||
|
let options;
|
||||||
|
try {
|
||||||
|
const optionsResponse = await http.post(`${contextPath}/webauthn/authenticate/options`, headers);
|
||||||
|
if (!optionsResponse.ok) {
|
||||||
|
throw new Error(`HTTP ${optionsResponse.status}`);
|
||||||
|
}
|
||||||
|
options = await optionsResponse.json();
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Authentication failed. Could not fetch authentication options: ${err.message}`, { cause: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseRequestOptionsFromJSON
|
||||||
|
const decodedOptions = {
|
||||||
|
...options,
|
||||||
|
challenge: base64url.decode(options.challenge),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Invoke the WebAuthn get() method.
|
||||||
|
const credentialOptions = {
|
||||||
|
publicKey: decodedOptions,
|
||||||
|
signal: abortController.newSignal(),
|
||||||
|
};
|
||||||
|
if (useConditionalMediation) {
|
||||||
|
// Request a conditional UI
|
||||||
|
credentialOptions.mediation = "conditional";
|
||||||
|
}
|
||||||
|
|
||||||
|
let cred;
|
||||||
|
try {
|
||||||
|
cred = await navigator.credentials.get(credentialOptions);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Authentication failed. Call to navigator.credentials.get failed: ${err.message}`, { cause: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { response, type: credType } = cred;
|
||||||
|
let userHandle;
|
||||||
|
if (response.userHandle) {
|
||||||
|
userHandle = base64url.encode(response.userHandle);
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
id: cred.id,
|
||||||
|
rawId: base64url.encode(cred.rawId),
|
||||||
|
response: {
|
||||||
|
authenticatorData: base64url.encode(response.authenticatorData),
|
||||||
|
clientDataJSON: base64url.encode(response.clientDataJSON),
|
||||||
|
signature: base64url.encode(response.signature),
|
||||||
|
userHandle,
|
||||||
|
},
|
||||||
|
credType,
|
||||||
|
clientExtensionResults: cred.getClientExtensionResults(),
|
||||||
|
authenticatorAttachment: cred.authenticatorAttachment,
|
||||||
|
};
|
||||||
|
|
||||||
|
let authenticationResponse;
|
||||||
|
try {
|
||||||
|
const authenticationCallResponse = await http.post(`${contextPath}/login/webauthn`, headers, body);
|
||||||
|
if (!authenticationCallResponse.ok) {
|
||||||
|
throw new Error(`HTTP ${authenticationCallResponse.status}`);
|
||||||
|
}
|
||||||
|
authenticationResponse = await authenticationCallResponse.json();
|
||||||
|
// if (authenticationResponse && authenticationResponse.authenticated) {
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Authentication failed. Could not process the authentication request: ${err.message}`, {
|
||||||
|
cause: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(authenticationResponse && authenticationResponse.authenticated && authenticationResponse.redirectUrl)) {
|
||||||
|
throw new Error(
|
||||||
|
`Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: ${JSON.stringify(authenticationResponse)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticationResponse.redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register(headers, contextPath, label) {
|
||||||
|
if (!label) {
|
||||||
|
throw new Error("Error: Passkey Label is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
let options;
|
||||||
|
try {
|
||||||
|
const optionsResponse = await http.post(`${contextPath}/webauthn/register/options`, headers);
|
||||||
|
if (!optionsResponse.ok) {
|
||||||
|
throw new Error(`Server responded with HTTP ${optionsResponse.status}`);
|
||||||
|
}
|
||||||
|
options = await optionsResponse.json();
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Registration failed. Could not fetch registration options: ${e.message}`, { cause: e });
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON
|
||||||
|
const decodedExcludeCredentials = !options.excludeCredentials
|
||||||
|
? []
|
||||||
|
: options.excludeCredentials.map((cred) => ({
|
||||||
|
...cred,
|
||||||
|
id: base64url.decode(cred.id),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const decodedOptions = {
|
||||||
|
...options,
|
||||||
|
user: {
|
||||||
|
...options.user,
|
||||||
|
id: base64url.decode(options.user.id),
|
||||||
|
},
|
||||||
|
challenge: base64url.decode(options.challenge),
|
||||||
|
excludeCredentials: decodedExcludeCredentials,
|
||||||
|
};
|
||||||
|
|
||||||
|
let credentialsContainer;
|
||||||
|
try {
|
||||||
|
credentialsContainer = await navigator.credentials.create({
|
||||||
|
publicKey: decodedOptions,
|
||||||
|
signal: abortController.newSignal(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Registration failed. Call to navigator.credentials.create failed: ${e.message}`, { cause: e });
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Let response be credential.response. If response is not an instance of AuthenticatorAttestationResponse, abort the ceremony with a user-visible error. https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||||
|
const { response } = credentialsContainer;
|
||||||
|
const credential = {
|
||||||
|
id: credentialsContainer.id,
|
||||||
|
rawId: base64url.encode(credentialsContainer.rawId),
|
||||||
|
response: {
|
||||||
|
attestationObject: base64url.encode(response.attestationObject),
|
||||||
|
clientDataJSON: base64url.encode(response.clientDataJSON),
|
||||||
|
transports: response.getTransports ? response.getTransports() : [],
|
||||||
|
},
|
||||||
|
type: credentialsContainer.type,
|
||||||
|
clientExtensionResults: credentialsContainer.getClientExtensionResults(),
|
||||||
|
authenticatorAttachment: credentialsContainer.authenticatorAttachment,
|
||||||
|
};
|
||||||
|
|
||||||
|
const registrationRequest = {
|
||||||
|
publicKey: {
|
||||||
|
credential: credential,
|
||||||
|
label: label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let verificationJSON;
|
||||||
|
try {
|
||||||
|
const verificationResp = await http.post(`${contextPath}/webauthn/register`, headers, registrationRequest);
|
||||||
|
if (!verificationResp.ok) {
|
||||||
|
throw new Error(`HTTP ${verificationResp.status}`);
|
||||||
|
}
|
||||||
|
verificationJSON = await verificationResp.json();
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Registration failed. Could not process the registration request: ${e.message}`, { cause: e });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(verificationJSON && verificationJSON.success)) {
|
||||||
|
throw new Error(`Registration failed. Server responded with: ${JSON.stringify(verificationJSON)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
authenticate,
|
||||||
|
register,
|
||||||
|
isConditionalMediationAvailable,
|
||||||
|
};
|
47
javascript/lib/webauthn-login.js
Normal file
47
javascript/lib/webauthn-login.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import webauthn from "./webauthn-core.js";
|
||||||
|
|
||||||
|
async function authenticateOrError(headers, contextPath, useConditionalMediation) {
|
||||||
|
try {
|
||||||
|
const redirectUrl = await webauthn.authenticate(headers, contextPath, useConditionalMediation);
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
window.location.href = `${contextPath}/login?error`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function conditionalMediation(headers, contextPath) {
|
||||||
|
const available = await webauthn.isConditionalMediationAvailable();
|
||||||
|
if (available) {
|
||||||
|
await authenticateOrError(headers, contextPath, true);
|
||||||
|
}
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupLogin(headers, contextPath, signinButton) {
|
||||||
|
signinButton.addEventListener("click", async () => {
|
||||||
|
await authenticateOrError(headers, contextPath, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: conditional mediation triggers browser crashes
|
||||||
|
// See: https://github.com/rwinch/spring-security-webauthn/issues/73
|
||||||
|
// await conditionalMediation(headers, contextPath);
|
||||||
|
}
|
108
javascript/lib/webauthn-registration.js
Normal file
108
javascript/lib/webauthn-registration.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import webauthn from "./webauthn-core.js";
|
||||||
|
|
||||||
|
function setVisibility(element, value) {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
element.style.display = value ? "block" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setError(ui, msg) {
|
||||||
|
resetPopups(ui);
|
||||||
|
const error = ui.getError();
|
||||||
|
if (!error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error.textContent = msg;
|
||||||
|
setVisibility(error, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSuccess(ui) {
|
||||||
|
resetPopups(ui);
|
||||||
|
const success = ui.getSuccess();
|
||||||
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVisibility(success, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPopups(ui) {
|
||||||
|
const success = ui.getSuccess();
|
||||||
|
const error = ui.getError();
|
||||||
|
setVisibility(success, false);
|
||||||
|
setVisibility(error, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDeleteForm(contextPath, form, headers) {
|
||||||
|
const options = {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await fetch(form.action, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param headers headers added to the credentials creation POST request, typically CSRF
|
||||||
|
* @param contextPath the contextPath from which the app is served
|
||||||
|
* @param ui contains getRegisterButton(), getSuccess(), getError(), getLabelInput(), getDeleteForms()
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function setupRegistration(headers, contextPath, ui) {
|
||||||
|
resetPopups(ui);
|
||||||
|
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
setError(ui, "WebAuthn is not supported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = new URLSearchParams(window.location.search);
|
||||||
|
if (queryString.has("success")) {
|
||||||
|
setSuccess(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.getRegisterButton().addEventListener("click", async () => {
|
||||||
|
resetPopups(ui);
|
||||||
|
const label = ui.getLabelInput().value;
|
||||||
|
try {
|
||||||
|
await webauthn.register(headers, contextPath, label);
|
||||||
|
window.location.href = `${contextPath}/webauthn/register?success`;
|
||||||
|
} catch (err) {
|
||||||
|
setError(ui, err.message);
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.getDeleteForms().forEach((form) =>
|
||||||
|
form.addEventListener("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
await submitDeleteForm(contextPath, form, headers);
|
||||||
|
window.location.href = `${contextPath}/webauthn/register?success`;
|
||||||
|
} catch (err) {
|
||||||
|
setError(ui, err.message);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
5465
javascript/package-lock.json
generated
Normal file
5465
javascript/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
javascript/package.json
Normal file
51
javascript/package.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "@springprojects/spring-security-webauthn",
|
||||||
|
"version": "1.0.0-alpha.9",
|
||||||
|
"description": "WebAuthN JS library for Spring Security",
|
||||||
|
"license": "ASL-2.0",
|
||||||
|
"author": "????",
|
||||||
|
"contributors": [
|
||||||
|
"Rob Winch <rwinch@users.noreply.github.com>",
|
||||||
|
"Daniel Garnier-Moiroux <git@garnier.wf>"
|
||||||
|
],
|
||||||
|
"repository": "github:spring-projects/spring-security",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/spring-projects/spring-security/issues"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "mocha",
|
||||||
|
"check": "npm test && npm run lint",
|
||||||
|
"test:watch": "mocha --watch --parallel",
|
||||||
|
"assemble": "esbuild lib/index.js --bundle --outfile=build/dist/spring-security-webauthn.js",
|
||||||
|
"build": "npm run check && npm run assemble",
|
||||||
|
"lint": "eslint",
|
||||||
|
"format": "npm run lint -- --fix"
|
||||||
|
},
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"files": [
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"Spring Security",
|
||||||
|
"WebAuthn",
|
||||||
|
"passkeys"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.6.0",
|
||||||
|
"@types/sinon": "^17.0.3",
|
||||||
|
"chai": "~4.3",
|
||||||
|
"esbuild": "^0.23.0",
|
||||||
|
"eslint": "^9.6.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"globals": "^15.8.0",
|
||||||
|
"mocha": "~10.2",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"prettier-eslint": "~15.0",
|
||||||
|
"sinon": "^18.0.0"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
49
javascript/spring-security-javascript.gradle
Normal file
49
javascript/spring-security-javascript.gradle
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'base'
|
||||||
|
id 'com.github.node-gradle.node' version '7.1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
node {
|
||||||
|
download = true
|
||||||
|
version = '20.17.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('check') {
|
||||||
|
dependsOn 'npm_run_check'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('dist', Zip) {
|
||||||
|
dependsOn 'npm_run_assemble'
|
||||||
|
from 'build/dist/spring-security.js'
|
||||||
|
into 'org/springframework/security'
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
javascript {
|
||||||
|
canBeConsumed = true
|
||||||
|
canBeResolved = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
artifacts {
|
||||||
|
javascript(project.layout.buildDirectory.dir('dist')) {
|
||||||
|
builtBy(npm_run_assemble)
|
||||||
|
}
|
||||||
|
}
|
49
javascript/test/abort-controller.test.js
Normal file
49
javascript/test/abort-controller.test.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import "./bootstrap.js";
|
||||||
|
import abortController from "../lib/abort-controller.js";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
describe("abort-controller", () => {
|
||||||
|
describe("newSignal", () => {
|
||||||
|
it("returns an AbortSignal", () => {
|
||||||
|
const signal = abortController.newSignal();
|
||||||
|
|
||||||
|
expect(signal).to.be.instanceof(AbortSignal);
|
||||||
|
expect(signal.aborted).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a new signal every time", () => {
|
||||||
|
const initialSignal = abortController.newSignal();
|
||||||
|
|
||||||
|
const newSignal = abortController.newSignal();
|
||||||
|
|
||||||
|
expect(initialSignal).to.not.equal(newSignal);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("aborts the existing signal", () => {
|
||||||
|
const signal = abortController.newSignal();
|
||||||
|
|
||||||
|
abortController.newSignal();
|
||||||
|
|
||||||
|
expect(signal.aborted).to.be.true;
|
||||||
|
expect(signal.reason).to.equal("Initiating new WebAuthN ceremony, cancelling current ceremony");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
76
javascript/test/base64.test.js
Normal file
76
javascript/test/base64.test.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { expect } from "chai";
|
||||||
|
import base64url from "../lib/base64url.js";
|
||||||
|
|
||||||
|
describe("base64url", () => {
|
||||||
|
before(() => {
|
||||||
|
// Emulate the atob / btoa base64 encoding/decoding from the browser
|
||||||
|
global.window = {
|
||||||
|
btoa: (str) => Buffer.from(str, "binary").toString("base64"),
|
||||||
|
atob: (b64) => Buffer.from(b64, "base64").toString("binary"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
// Reset window object
|
||||||
|
global.window = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes", () => {
|
||||||
|
// "Zm9vYmFy" is "foobar" in base 64, i.e. f:102 o:111 o:111 b:98 a:97 r:114
|
||||||
|
const decoded = base64url.decode("Zm9vYmFy");
|
||||||
|
|
||||||
|
expect(new Uint8Array(decoded)).to.be.deep.equal(new Uint8Array([102, 111, 111, 98, 97, 114]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("decodes special characters", () => {
|
||||||
|
// Wrap the decode function for easy testing
|
||||||
|
const decode = (str) => {
|
||||||
|
const decoded = new Uint8Array(base64url.decode(str));
|
||||||
|
return Array.from(decoded);
|
||||||
|
};
|
||||||
|
|
||||||
|
// "Pz8/" is "???" in base64, i.e. ?:63 three times
|
||||||
|
expect(decode("Pz8/")).to.be.deep.equal(decode("Pz8_"));
|
||||||
|
expect(decode("Pz8_")).to.be.deep.equal([63, 63, 63]);
|
||||||
|
// "Pj4+" is ">>>" in base64, ie >:62 three times
|
||||||
|
expect(decode("Pj4+")).to.be.deep.equal(decode("Pj4-"));
|
||||||
|
expect(decode("Pj4-")).to.be.deep.equal([62, 62, 62]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("encodes", () => {
|
||||||
|
const encoded = base64url.encode(Buffer.from("foobar"));
|
||||||
|
|
||||||
|
expect(encoded).to.be.equal("Zm9vYmFy");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("encodes special +/ characters", () => {
|
||||||
|
const encode = (str) => base64url.encode(Buffer.from(str));
|
||||||
|
|
||||||
|
expect(encode("???")).to.be.equal("Pz8_");
|
||||||
|
expect(encode(">>>")).to.be.equal("Pj4-");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is stable", () => {
|
||||||
|
const base = "tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g";
|
||||||
|
|
||||||
|
expect(base64url.encode(base64url.decode(base))).to.be.equal(base);
|
||||||
|
});
|
||||||
|
});
|
22
javascript/test/bootstrap.js
vendored
Normal file
22
javascript/test/bootstrap.js
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chai from "chai";
|
||||||
|
|
||||||
|
// Show full diffs when there is an equality difference an assertion.
|
||||||
|
// By default, chai truncates at 40 characters, making it difficult to
|
||||||
|
// compare e.g. error messages
|
||||||
|
chai.config.truncateThreshold = 0;
|
65
javascript/test/http.test.js
Normal file
65
javascript/test/http.test.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import http from "../lib/http.js";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { fake, assert } from "sinon";
|
||||||
|
|
||||||
|
describe("http", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
global.fetch = fake.resolves({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.fetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("post", () => {
|
||||||
|
it("calls fetch with headers", async () => {
|
||||||
|
const url = "https://example.com/some/path";
|
||||||
|
const headers = { "x-custom": "some-value" };
|
||||||
|
|
||||||
|
const resp = await http.post(url, headers);
|
||||||
|
|
||||||
|
expect(resp.ok).to.be.true;
|
||||||
|
assert.calledOnceWithExactly(global.fetch, url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends the body as a JSON string", async () => {
|
||||||
|
const body = { foo: "bar", baz: 42 };
|
||||||
|
const url = "https://example.com/some/path";
|
||||||
|
|
||||||
|
const resp = await http.post(url, {}, body);
|
||||||
|
|
||||||
|
expect(resp.ok).to.be.true;
|
||||||
|
assert.calledOnceWithExactly(global.fetch, url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: `{"foo":"bar","baz":42}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
697
javascript/test/webauthn-core.test.js
Normal file
697
javascript/test/webauthn-core.test.js
Normal file
@ -0,0 +1,697 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import "./bootstrap.js";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { assert, fake, match, stub } from "sinon";
|
||||||
|
import http from "../lib/http.js";
|
||||||
|
import webauthn from "../lib/webauthn-core.js";
|
||||||
|
import base64url from "../lib/base64url.js";
|
||||||
|
|
||||||
|
describe("webauthn-core", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
global.window = {
|
||||||
|
btoa: (str) => Buffer.from(str, "binary").toString("base64"),
|
||||||
|
atob: (b64) => Buffer.from(b64, "base64").toString("binary"),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.window;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isConditionalMediationAvailable", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.window.PublicKeyCredential;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is available", async () => {
|
||||||
|
global.window = {
|
||||||
|
PublicKeyCredential: {
|
||||||
|
isConditionalMediationAvailable: fake.resolves(true),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await webauthn.isConditionalMediationAvailable();
|
||||||
|
|
||||||
|
expect(result).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("is not available", async () => {
|
||||||
|
it("PublicKeyCredential does not exist", async () => {
|
||||||
|
global.window = {};
|
||||||
|
const result = await webauthn.isConditionalMediationAvailable();
|
||||||
|
expect(result).to.be.false;
|
||||||
|
});
|
||||||
|
it("PublicKeyCredential.isConditionalMediationAvailable undefined", async () => {
|
||||||
|
global.window = {
|
||||||
|
PublicKeyCredential: {},
|
||||||
|
};
|
||||||
|
const result = await webauthn.isConditionalMediationAvailable();
|
||||||
|
expect(result).to.be.false;
|
||||||
|
});
|
||||||
|
it("PublicKeyCredential.isConditionalMediationAvailable false", async () => {
|
||||||
|
global.window = {
|
||||||
|
PublicKeyCredential: {
|
||||||
|
isConditionalMediationAvailable: fake.resolves(false),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await webauthn.isConditionalMediationAvailable();
|
||||||
|
expect(result).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("authenticate", () => {
|
||||||
|
let httpPostStub;
|
||||||
|
const contextPath = "/some/path";
|
||||||
|
|
||||||
|
const credentialsGetOptions = {
|
||||||
|
challenge: "nRbOrtNKTfJ1JaxfUDKs8j3B-JFqyGQw8DO4u6eV3JA",
|
||||||
|
timeout: 300000,
|
||||||
|
rpId: "localhost",
|
||||||
|
allowCredentials: [],
|
||||||
|
userVerification: "preferred",
|
||||||
|
extensions: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is kind of a self-fulfilling prophecy type of test: we produce array buffers by calling
|
||||||
|
// base64url.decode ; they will then be re-encoded to the same string in the production code.
|
||||||
|
// The ArrayBuffer API is not super friendly.
|
||||||
|
beforeEach(() => {
|
||||||
|
httpPostStub = stub(http, "post");
|
||||||
|
httpPostStub.withArgs(contextPath + "/webauthn/authenticate/options", match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.resolves(credentialsGetOptions),
|
||||||
|
});
|
||||||
|
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.resolves({
|
||||||
|
authenticated: true,
|
||||||
|
redirectUrl: "/success",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const validAuthenticatorResponse = {
|
||||||
|
id: "UgghgP5QKozwsSUK1twCj8mpgZs",
|
||||||
|
rawId: base64url.decode("UgghgP5QKozwsSUK1twCj8mpgZs"),
|
||||||
|
response: {
|
||||||
|
authenticatorData: base64url.decode("y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA"),
|
||||||
|
clientDataJSON: base64url.decode(
|
||||||
|
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUTdlR0NkNUw2cG9fa01meWNIQnBWRlR5dmd3RklCV0QxZWg5OUktRFhnWSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
|
||||||
|
),
|
||||||
|
signature: base64url.decode(
|
||||||
|
"MEUCIGT9PAWfU3lMicOXFMpHGcl033dY-sNSJvehlXvvoivyAiEA_D_yOsChERlXX2rFcK6Qx5BaAbx5qdU2hgYDVN6W770",
|
||||||
|
),
|
||||||
|
userHandle: base64url.decode("tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g"),
|
||||||
|
},
|
||||||
|
getClientExtensionResults: () => ({}),
|
||||||
|
authenticatorAttachment: "platform",
|
||||||
|
type: "public-key",
|
||||||
|
};
|
||||||
|
global.navigator = {
|
||||||
|
credentials: {
|
||||||
|
get: fake.resolves(validAuthenticatorResponse),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
http.post.restore();
|
||||||
|
delete global.navigator;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("succeeds", async () => {
|
||||||
|
const redirectUrl = await webauthn.authenticate({ "x-custom": "some-value" }, contextPath, false);
|
||||||
|
|
||||||
|
expect(redirectUrl).to.equal("/success");
|
||||||
|
assert.calledWith(
|
||||||
|
httpPostStub.lastCall,
|
||||||
|
`${contextPath}/login/webauthn`,
|
||||||
|
{ "x-custom": "some-value" },
|
||||||
|
{
|
||||||
|
id: "UgghgP5QKozwsSUK1twCj8mpgZs",
|
||||||
|
rawId: "UgghgP5QKozwsSUK1twCj8mpgZs",
|
||||||
|
credType: "public-key",
|
||||||
|
response: {
|
||||||
|
authenticatorData: "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
|
||||||
|
clientDataJSON:
|
||||||
|
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUTdlR0NkNUw2cG9fa01meWNIQnBWRlR5dmd3RklCV0QxZWg5OUktRFhnWSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
|
||||||
|
signature:
|
||||||
|
"MEUCIGT9PAWfU3lMicOXFMpHGcl033dY-sNSJvehlXvvoivyAiEA_D_yOsChERlXX2rFcK6Qx5BaAbx5qdU2hgYDVN6W770",
|
||||||
|
userHandle: "tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g",
|
||||||
|
},
|
||||||
|
clientExtensionResults: {},
|
||||||
|
authenticatorAttachment: "platform",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls the authenticator with the correct options", async () => {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
|
||||||
|
assert.calledOnceWithMatch(global.navigator.credentials.get, {
|
||||||
|
publicKey: {
|
||||||
|
challenge: base64url.decode("nRbOrtNKTfJ1JaxfUDKs8j3B-JFqyGQw8DO4u6eV3JA"),
|
||||||
|
timeout: 300000,
|
||||||
|
rpId: "localhost",
|
||||||
|
allowCredentials: [],
|
||||||
|
userVerification: "preferred",
|
||||||
|
extensions: {},
|
||||||
|
},
|
||||||
|
signal: match.any,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("authentication failures", () => {
|
||||||
|
it("when authentication options call", async () => {
|
||||||
|
httpPostStub
|
||||||
|
.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any)
|
||||||
|
.rejects(new Error("Connection refused"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Authentication failed. Could not fetch authentication options: Connection refused",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when authentication options call returns does not return HTTP 200 OK", async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any).resolves({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal("Authentication failed. Could not fetch authentication options: HTTP 400");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when authentication options are not valid json", async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.rejects(new Error("Not valid JSON")),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal("Authentication failed. Could not fetch authentication options: Not valid JSON");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when navigator.credentials.get fails", async () => {
|
||||||
|
global.navigator.credentials.get = fake.rejects(new Error("Operation was aborted"));
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Authentication failed. Call to navigator.credentials.get failed: Operation was aborted",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when authentication call fails", async () => {
|
||||||
|
httpPostStub
|
||||||
|
.withArgs(`${contextPath}/login/webauthn`, match.any, match.any)
|
||||||
|
.rejects(new Error("Connection refused"));
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Authentication failed. Could not process the authentication request: Connection refused",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when authentication call does not return HTTP 200 OK", async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal("Authentication failed. Could not process the authentication request: HTTP 400");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when authentication call does not return JSON", async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.rejects(new Error("Not valid JSON")),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Authentication failed. Could not process the authentication request: Not valid JSON",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when authentication call returns null", async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.resolves(null),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: null',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when authentication call returns {"authenticated":false}', async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.resolves({
|
||||||
|
authenticated: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: {"authenticated":false}',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when authentication call returns no redirectUrl", async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.resolves({
|
||||||
|
authenticated: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.authenticate({}, contextPath, false);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: {"authenticated":true}',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("authenticate should throw");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("register", () => {
|
||||||
|
let httpPostStub;
|
||||||
|
const contextPath = "/some/path";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const credentialsCreateOptions = {
|
||||||
|
rp: {
|
||||||
|
name: "Spring Security Relying Party",
|
||||||
|
id: "example.localhost",
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
name: "user",
|
||||||
|
id: "eatPy60xmXG_58JrIiIBa5wq8Y76c7MD6mnY5vW8yP8",
|
||||||
|
displayName: "user",
|
||||||
|
},
|
||||||
|
challenge: "s0hBOfkSaVLXdsbyD8jii6t2IjUd-eiTP1Cmeuo1qUo",
|
||||||
|
pubKeyCredParams: [
|
||||||
|
{
|
||||||
|
type: "public-key",
|
||||||
|
alg: -8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "public-key",
|
||||||
|
alg: -7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "public-key",
|
||||||
|
alg: -257,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timeout: 300000,
|
||||||
|
excludeCredentials: [
|
||||||
|
{
|
||||||
|
id: "nOsjw8eaaqSwVdTBBYE1FqfGdHs",
|
||||||
|
type: "public-key",
|
||||||
|
transports: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authenticatorSelection: {
|
||||||
|
residentKey: "required",
|
||||||
|
userVerification: "preferred",
|
||||||
|
},
|
||||||
|
attestation: "direct",
|
||||||
|
extensions: { credProps: true },
|
||||||
|
};
|
||||||
|
const validAuthenticatorResponse = {
|
||||||
|
authenticatorAttachment: "platform",
|
||||||
|
id: "9wAuex_025BgEQrs7fOypo5SGBA",
|
||||||
|
rawId: base64url.decode("9wAuex_025BgEQrs7fOypo5SGBA"),
|
||||||
|
response: {
|
||||||
|
attestationObject: base64url.decode(
|
||||||
|
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E",
|
||||||
|
),
|
||||||
|
getAuthenticatorData: () =>
|
||||||
|
base64url.decode(
|
||||||
|
"y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E",
|
||||||
|
),
|
||||||
|
clientDataJSON: base64url.decode(
|
||||||
|
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiUVdwd3lUcXJpYVlqbVdnOWFvZ0FxUlRKNVFYMFBGV2JWR2xNeGNsVjZhcyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
|
||||||
|
),
|
||||||
|
getPublicKey: () =>
|
||||||
|
base64url.decode(
|
||||||
|
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwH2kzYF5J4Qbzd8AoVVIsoh-8MEFWjIaAyiIbET7paBrMCiMzmx25DLYzuvPV2jnmdVo0sZeHyTjEEfP47L3UQ",
|
||||||
|
),
|
||||||
|
getPublicKeyAlgorithm: () => -7,
|
||||||
|
getTransports: () => ["internal"],
|
||||||
|
},
|
||||||
|
type: "public-key",
|
||||||
|
getClientExtensionResults: () => ({}),
|
||||||
|
};
|
||||||
|
global.navigator = {
|
||||||
|
credentials: {
|
||||||
|
create: fake.resolves(validAuthenticatorResponse),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
httpPostStub = stub(http, "post");
|
||||||
|
httpPostStub.withArgs(contextPath + "/webauthn/register/options", match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.resolves(credentialsCreateOptions),
|
||||||
|
});
|
||||||
|
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
json: fake.resolves({
|
||||||
|
success: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpPostStub.restore();
|
||||||
|
delete global.navigator;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("succeeds", async () => {
|
||||||
|
const contextPath = "/some/path";
|
||||||
|
const headers = { _csrf: "csrf-value" };
|
||||||
|
|
||||||
|
await webauthn.register(headers, contextPath, "my passkey");
|
||||||
|
assert.calledWithExactly(
|
||||||
|
httpPostStub.lastCall,
|
||||||
|
`${contextPath}/webauthn/register`,
|
||||||
|
headers,
|
||||||
|
match({
|
||||||
|
publicKey: {
|
||||||
|
credential: {
|
||||||
|
id: "9wAuex_025BgEQrs7fOypo5SGBA",
|
||||||
|
rawId: "9wAuex_025BgEQrs7fOypo5SGBA",
|
||||||
|
response: {
|
||||||
|
attestationObject:
|
||||||
|
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E",
|
||||||
|
clientDataJSON:
|
||||||
|
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiUVdwd3lUcXJpYVlqbVdnOWFvZ0FxUlRKNVFYMFBGV2JWR2xNeGNsVjZhcyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
|
||||||
|
transports: ["internal"],
|
||||||
|
},
|
||||||
|
type: "public-key",
|
||||||
|
clientExtensionResults: {},
|
||||||
|
authenticatorAttachment: "platform",
|
||||||
|
},
|
||||||
|
label: "my passkey",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls the authenticator with the correct options", async () => {
|
||||||
|
await webauthn.register({}, contextPath, "my passkey");
|
||||||
|
|
||||||
|
assert.calledOnceWithExactly(
|
||||||
|
global.navigator.credentials.create,
|
||||||
|
match({
|
||||||
|
publicKey: {
|
||||||
|
rp: {
|
||||||
|
name: "Spring Security Relying Party",
|
||||||
|
id: "example.localhost",
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
name: "user",
|
||||||
|
id: base64url.decode("eatPy60xmXG_58JrIiIBa5wq8Y76c7MD6mnY5vW8yP8"),
|
||||||
|
displayName: "user",
|
||||||
|
},
|
||||||
|
challenge: base64url.decode("s0hBOfkSaVLXdsbyD8jii6t2IjUd-eiTP1Cmeuo1qUo"),
|
||||||
|
pubKeyCredParams: [
|
||||||
|
{
|
||||||
|
type: "public-key",
|
||||||
|
alg: -8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "public-key",
|
||||||
|
alg: -7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "public-key",
|
||||||
|
alg: -257,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timeout: 300000,
|
||||||
|
excludeCredentials: [
|
||||||
|
{
|
||||||
|
id: base64url.decode("nOsjw8eaaqSwVdTBBYE1FqfGdHs"),
|
||||||
|
type: "public-key",
|
||||||
|
transports: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
authenticatorSelection: {
|
||||||
|
residentKey: "required",
|
||||||
|
userVerification: "preferred",
|
||||||
|
},
|
||||||
|
attestation: "direct",
|
||||||
|
extensions: { credProps: true },
|
||||||
|
},
|
||||||
|
signal: match.any,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("registration failures", () => {
|
||||||
|
it("when label is missing", async () => {
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, "/", "");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal("Error: Passkey Label is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when cannot get the registration options", async () => {
|
||||||
|
httpPostStub.withArgs(match.any, match.any).rejects(new Error("Server threw an error"));
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, "/", "my passkey");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Registration failed. Could not fetch registration options: Server threw an error",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when registration options call does not return HTTP 200 OK", async () => {
|
||||||
|
httpPostStub.withArgs(match.any, match.any).resolves({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, "/", "my passkey");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Registration failed. Could not fetch registration options: Server responded with HTTP 400",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when registration options are not valid JSON", async () => {
|
||||||
|
httpPostStub.withArgs(match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.rejects(new Error("Not a JSON response")),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, "/", "my passkey");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Registration failed. Could not fetch registration options: Not a JSON response",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when navigator.credentials.create fails", async () => {
|
||||||
|
global.navigator = {
|
||||||
|
credentials: {
|
||||||
|
create: fake.rejects(new Error("authenticator threw an error")),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, contextPath, "my passkey");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Registration failed. Call to navigator.credentials.create failed: authenticator threw an error",
|
||||||
|
);
|
||||||
|
expect(err.cause).to.deep.equal(new Error("authenticator threw an error"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when registration call fails", async () => {
|
||||||
|
httpPostStub
|
||||||
|
.withArgs(`${contextPath}/webauthn/register`, match.any, match.any)
|
||||||
|
.rejects(new Error("Connection refused"));
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, contextPath, "my passkey");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Registration failed. Could not process the registration request: Connection refused",
|
||||||
|
);
|
||||||
|
expect(err.cause).to.deep.equal(new Error("Connection refused"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when registration call does not return HTTP 200 OK", async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, contextPath, "my passkey");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal("Registration failed. Could not process the registration request: HTTP 400");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when registration call does not return JSON", async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.rejects(new Error("Not valid JSON")),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, contextPath, "my passkey");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal(
|
||||||
|
"Registration failed. Could not process the registration request: Not valid JSON",
|
||||||
|
);
|
||||||
|
expect(err.cause).to.deep.equal(new Error("Not valid JSON"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when registration call returns null", async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.resolves(null),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, contextPath, "my passkey");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal("Registration failed. Server responded with: null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when registration call returns {"success":false}', async () => {
|
||||||
|
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: fake.resolves({ success: false }),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await webauthn.register({}, contextPath, "my passkey");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an("error");
|
||||||
|
expect(err.message).to.equal('Registration failed. Server responded with: {"success":false}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect.fail("register should throw");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
106
javascript/test/webauthn-login.test.js
Normal file
106
javascript/test/webauthn-login.test.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import "./bootstrap.js";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { setupLogin } from "../lib/webauthn-login.js";
|
||||||
|
import webauthn from "../lib/webauthn-core.js";
|
||||||
|
import { assert, fake, match, stub } from "sinon";
|
||||||
|
|
||||||
|
describe("webauthn-login", () => {
|
||||||
|
describe("bootstrap", () => {
|
||||||
|
let authenticateStub;
|
||||||
|
let isConditionalMediationAvailableStub;
|
||||||
|
let signinButton;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
isConditionalMediationAvailableStub = stub(webauthn, "isConditionalMediationAvailable").resolves(false);
|
||||||
|
authenticateStub = stub(webauthn, "authenticate").resolves("/success");
|
||||||
|
signinButton = {
|
||||||
|
addEventListener: fake(),
|
||||||
|
};
|
||||||
|
|
||||||
|
global.console = {
|
||||||
|
error: stub(),
|
||||||
|
};
|
||||||
|
global.window = {
|
||||||
|
location: {
|
||||||
|
href: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
authenticateStub.restore();
|
||||||
|
isConditionalMediationAvailableStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets up a click event listener on the signin button", async () => {
|
||||||
|
await setupLogin({}, "/some/path", signinButton);
|
||||||
|
|
||||||
|
assert.calledOnceWithMatch(signinButton.addEventListener, "click", match.typeOf("function"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: conditional mediation triggers browser crashes
|
||||||
|
// See: https://github.com/rwinch/spring-security-webauthn/issues/73
|
||||||
|
xit("uses conditional mediation when available", async () => {
|
||||||
|
isConditionalMediationAvailableStub.resolves(true);
|
||||||
|
|
||||||
|
const headers = { "x-header": "value" };
|
||||||
|
const contextPath = "/some/path";
|
||||||
|
|
||||||
|
await setupLogin(headers, contextPath, signinButton);
|
||||||
|
|
||||||
|
assert.calledOnceWithExactly(authenticateStub, headers, contextPath, true);
|
||||||
|
expect(global.window.location.href).to.equal("/success");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not call authenticate when conditional mediation is not available", async () => {
|
||||||
|
await setupLogin({}, "/", signinButton);
|
||||||
|
|
||||||
|
assert.notCalled(authenticateStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls authenticate when the signin button is clicked", async () => {
|
||||||
|
const headers = { "x-header": "value" };
|
||||||
|
const contextPath = "/some/path";
|
||||||
|
|
||||||
|
await setupLogin(headers, contextPath, signinButton);
|
||||||
|
|
||||||
|
// Call the event listener
|
||||||
|
await signinButton.addEventListener.firstCall.lastArg();
|
||||||
|
|
||||||
|
assert.calledOnceWithExactly(authenticateStub, headers, contextPath, false);
|
||||||
|
expect(global.window.location.href).to.equal("/success");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles authentication errors", async () => {
|
||||||
|
authenticateStub.rejects(new Error("Authentication failed"));
|
||||||
|
await setupLogin({}, "/some/path", signinButton);
|
||||||
|
|
||||||
|
// Call the event listener
|
||||||
|
await signinButton.addEventListener.firstCall.lastArg();
|
||||||
|
|
||||||
|
expect(global.window.location.href).to.equal(`/some/path/login?error`);
|
||||||
|
assert.calledOnceWithMatch(
|
||||||
|
global.console.error,
|
||||||
|
match.instanceOf(Error).and(match.has("message", "Authentication failed")),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
279
javascript/test/webauthn-registration.test.js
Normal file
279
javascript/test/webauthn-registration.test.js
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
import "./bootstrap.js";
|
||||||
|
import { expect, util, Assertion } from "chai";
|
||||||
|
import { setupRegistration } from "../lib/webauthn-registration.js";
|
||||||
|
import webauthn from "../lib/webauthn-core.js";
|
||||||
|
import { assert, fake, match, stub } from "sinon";
|
||||||
|
|
||||||
|
describe("webauthn-registration", () => {
|
||||||
|
before(() => {
|
||||||
|
Assertion.addProperty("visible", function () {
|
||||||
|
const obj = util.flag(this, "object");
|
||||||
|
new Assertion(obj).to.have.nested.property("style.display", "block");
|
||||||
|
});
|
||||||
|
Assertion.addProperty("hidden", function () {
|
||||||
|
const obj = util.flag(this, "object");
|
||||||
|
new Assertion(obj).to.have.nested.property("style.display", "none");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bootstrap", () => {
|
||||||
|
let registerStub;
|
||||||
|
let registerButton;
|
||||||
|
let labelField;
|
||||||
|
let errorPopup;
|
||||||
|
let successPopup;
|
||||||
|
let deleteForms;
|
||||||
|
let ui;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
registerStub = stub(webauthn, "register").resolves(undefined);
|
||||||
|
errorPopup = {
|
||||||
|
style: {
|
||||||
|
display: undefined,
|
||||||
|
},
|
||||||
|
textContent: undefined,
|
||||||
|
};
|
||||||
|
successPopup = {
|
||||||
|
style: {
|
||||||
|
display: undefined,
|
||||||
|
},
|
||||||
|
textContent: undefined,
|
||||||
|
};
|
||||||
|
registerButton = {
|
||||||
|
addEventListener: fake(),
|
||||||
|
};
|
||||||
|
labelField = {
|
||||||
|
value: undefined,
|
||||||
|
};
|
||||||
|
deleteForms = [];
|
||||||
|
ui = {
|
||||||
|
getSuccess: function () {
|
||||||
|
return successPopup;
|
||||||
|
},
|
||||||
|
getError: function () {
|
||||||
|
return errorPopup;
|
||||||
|
},
|
||||||
|
getRegisterButton: function () {
|
||||||
|
return registerButton;
|
||||||
|
},
|
||||||
|
getLabelInput: function () {
|
||||||
|
return labelField;
|
||||||
|
},
|
||||||
|
getDeleteForms: function () {
|
||||||
|
return deleteForms;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
global.window = {
|
||||||
|
location: {
|
||||||
|
href: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
global.console = {
|
||||||
|
error: stub(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
registerStub.restore();
|
||||||
|
delete global.window;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when webauthn is not supported", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
delete global.window.PublicKeyCredential;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not set up a click event listener", async () => {
|
||||||
|
await setupRegistration({}, "/", ui);
|
||||||
|
|
||||||
|
assert.notCalled(registerButton.addEventListener);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an error popup", async () => {
|
||||||
|
await setupRegistration({}, "/", ui);
|
||||||
|
|
||||||
|
expect(errorPopup).to.be.visible;
|
||||||
|
expect(errorPopup.textContent).to.equal("WebAuthn is not supported");
|
||||||
|
expect(successPopup).to.be.hidden;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when webauthn is supported", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
global.window.PublicKeyCredential = fake();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the popups", async () => {
|
||||||
|
await setupRegistration({}, "/", ui);
|
||||||
|
|
||||||
|
expect(successPopup).to.be.hidden;
|
||||||
|
expect(errorPopup).to.be.hidden;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets up a click event listener on the register button", async () => {
|
||||||
|
await setupRegistration({}, "/some/path", ui);
|
||||||
|
|
||||||
|
assert.calledOnceWithMatch(registerButton.addEventListener, "click", match.typeOf("function"));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`when the query string contains "success"`, () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
global.window.location.search = "?success&continue=true";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the success popup", async () => {
|
||||||
|
await setupRegistration({}, "/", ui);
|
||||||
|
|
||||||
|
expect(successPopup).to.be.visible;
|
||||||
|
expect(errorPopup).to.be.hidden;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the register button is clicked", () => {
|
||||||
|
const headers = { "x-header": "value" };
|
||||||
|
const contextPath = "/some/path";
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupRegistration(headers, contextPath, ui);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides all the popups", async () => {
|
||||||
|
successPopup.textContent = "dummy-content";
|
||||||
|
successPopup.style.display = "block";
|
||||||
|
errorPopup.textContent = "dummy-content";
|
||||||
|
errorPopup.style.display = "block";
|
||||||
|
|
||||||
|
await registerButton.addEventListener.firstCall.lastArg();
|
||||||
|
|
||||||
|
expect(successPopup).to.be.hidden;
|
||||||
|
expect(errorPopup).to.be.hidden;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls register", async () => {
|
||||||
|
labelField.value = "passkey name";
|
||||||
|
|
||||||
|
await registerButton.addEventListener.firstCall.lastArg();
|
||||||
|
|
||||||
|
assert.calledOnceWithExactly(registerStub, headers, contextPath, labelField.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("navigates to success page", async () => {
|
||||||
|
labelField.value = "passkey name";
|
||||||
|
|
||||||
|
await registerButton.addEventListener.firstCall.lastArg();
|
||||||
|
|
||||||
|
expect(global.window.location.href).to.equal(`${contextPath}/webauthn/register?success`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles errors", async () => {
|
||||||
|
registerStub.rejects(new Error("The registration failed"));
|
||||||
|
|
||||||
|
await registerButton.addEventListener.firstCall.lastArg();
|
||||||
|
|
||||||
|
expect(errorPopup.textContent).to.equal("The registration failed");
|
||||||
|
expect(errorPopup).to.be.visible;
|
||||||
|
expect(successPopup).to.be.hidden;
|
||||||
|
assert.calledOnceWithMatch(
|
||||||
|
global.console.error,
|
||||||
|
match.instanceOf(Error).and(match.has("message", "The registration failed")),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("delete", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
global.fetch = fake.resolves({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete global.fetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no errors when no forms", async () => {
|
||||||
|
await setupRegistration({}, "/some/path", ui);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets up forms for fetch", async () => {
|
||||||
|
const deleteFormOne = {
|
||||||
|
addEventListener: fake(),
|
||||||
|
};
|
||||||
|
const deleteFormTwo = {
|
||||||
|
addEventListener: fake(),
|
||||||
|
};
|
||||||
|
deleteForms = [deleteFormOne, deleteFormTwo];
|
||||||
|
|
||||||
|
await setupRegistration({}, "", ui);
|
||||||
|
|
||||||
|
assert.calledOnceWithMatch(deleteFormOne.addEventListener, "submit", match.typeOf("function"));
|
||||||
|
assert.calledOnceWithMatch(deleteFormTwo.addEventListener, "submit", match.typeOf("function"));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the delete button is clicked", () => {
|
||||||
|
it("calls POST to the form action", async () => {
|
||||||
|
const contextPath = "/some/path";
|
||||||
|
const deleteForm = {
|
||||||
|
addEventListener: fake(),
|
||||||
|
action: `${contextPath}/webauthn/1234`,
|
||||||
|
};
|
||||||
|
deleteForms = [deleteForm];
|
||||||
|
const headers = {
|
||||||
|
"X-CSRF-TOKEN": "token",
|
||||||
|
};
|
||||||
|
|
||||||
|
await setupRegistration(headers, contextPath, ui);
|
||||||
|
|
||||||
|
const clickEvent = {
|
||||||
|
preventDefault: fake(),
|
||||||
|
};
|
||||||
|
await deleteForm.addEventListener.firstCall.lastArg(clickEvent);
|
||||||
|
assert.calledOnce(clickEvent.preventDefault);
|
||||||
|
assert.calledOnceWithExactly(global.fetch, `/some/path/webauthn/1234`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(global.window.location.href).to.equal(`/some/path/webauthn/register?success`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles errors", async () => {
|
||||||
|
global.fetch = fake.rejects("Server threw an error");
|
||||||
|
global.window.location.href = "/initial/location";
|
||||||
|
const deleteForm = {
|
||||||
|
addEventListener: fake(),
|
||||||
|
};
|
||||||
|
deleteForms = [deleteForm];
|
||||||
|
|
||||||
|
await setupRegistration({}, "", ui);
|
||||||
|
const clickEvent = { preventDefault: fake() };
|
||||||
|
await deleteForm.addEventListener.firstCall.lastArg(clickEvent);
|
||||||
|
|
||||||
|
expect(errorPopup).to.be.visible;
|
||||||
|
expect(errorPopup.textContent).to.equal("Server threw an error");
|
||||||
|
// URL does not change
|
||||||
|
expect(global.window.location.href).to.equal("/initial/location");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,31 @@
|
|||||||
apply plugin: 'io.spring.convention.spring-module'
|
apply plugin: 'io.spring.convention.spring-module'
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
javascript {
|
||||||
|
canBeConsumed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def syncJavascript = tasks.register('syncJavascript', Sync) {
|
||||||
|
group = 'Build'
|
||||||
|
description = 'Syncs the Javascript from the javascript configuration'
|
||||||
|
into project.layout.buildDirectory.dir('spring-security-javascript')
|
||||||
|
from(configurations.javascript) {
|
||||||
|
into 'org/springframework/security'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
resources {
|
||||||
|
srcDirs(syncJavascript)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
javascript project(path: ':spring-security-javascript', configuration: 'javascript')
|
||||||
management platform(project(":spring-security-dependencies"))
|
management platform(project(":spring-security-dependencies"))
|
||||||
api project(':spring-security-core')
|
api project(':spring-security-core')
|
||||||
api 'org.springframework:spring-core'
|
api 'org.springframework:spring-core'
|
||||||
@ -16,6 +41,7 @@ dependencies {
|
|||||||
optional 'org.springframework:spring-tx'
|
optional 'org.springframework:spring-tx'
|
||||||
optional 'org.springframework:spring-webflux'
|
optional 'org.springframework:spring-webflux'
|
||||||
optional 'org.springframework:spring-webmvc'
|
optional 'org.springframework:spring-webmvc'
|
||||||
|
optional libs.webauthn4j.core
|
||||||
|
|
||||||
provided 'jakarta.servlet:jakarta.servlet-api'
|
provided 'jakarta.servlet:jakarta.servlet-api'
|
||||||
|
|
||||||
|
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.authentication;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
|
import org.springframework.http.server.ServletServerHttpResponse;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
|
||||||
|
import org.springframework.security.web.savedrequest.RequestCache;
|
||||||
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
|
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link AuthenticationSuccessHandler} that writes a JSON response with the redirect
|
||||||
|
* URL and an authenticated status similar to:
|
||||||
|
*
|
||||||
|
* <code>
|
||||||
|
* {
|
||||||
|
* "redirectUrl": "/user/profile",
|
||||||
|
* "authenticated": true
|
||||||
|
* }
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class HttpMessageConverterAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||||
|
|
||||||
|
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
|
||||||
|
JsonMapper.builder().addModule(new WebauthnJackson2Module()).build());
|
||||||
|
|
||||||
|
private RequestCache requestCache = new HttpSessionRequestCache();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link GenericHttpMessageConverter} to write to the response. The default
|
||||||
|
* is {@link MappingJackson2HttpMessageConverter}.
|
||||||
|
* @param converter the {@link GenericHttpMessageConverter} to use. Cannot be null.
|
||||||
|
*/
|
||||||
|
public void setConverter(HttpMessageConverter<Object> converter) {
|
||||||
|
Assert.notNull(converter, "converter cannot be null");
|
||||||
|
this.converter = converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link RequestCache} to use. The default is
|
||||||
|
* {@link HttpSessionRequestCache}.
|
||||||
|
* @param requestCache the {@link RequestCache} to use. Cannot be null
|
||||||
|
*/
|
||||||
|
public void setRequestCache(RequestCache requestCache) {
|
||||||
|
Assert.notNull(requestCache, "requestCache cannot be null");
|
||||||
|
this.requestCache = requestCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
Authentication authentication) throws IOException, ServletException {
|
||||||
|
final SavedRequest savedRequest = this.requestCache.getRequest(request, response);
|
||||||
|
final String redirectUrl = (savedRequest != null) ? savedRequest.getRedirectUrl()
|
||||||
|
: request.getContextPath() + "/";
|
||||||
|
this.requestCache.removeRequest(request, response);
|
||||||
|
this.converter.write(new AuthenticationSuccess(redirectUrl), MediaType.APPLICATION_JSON,
|
||||||
|
new ServletServerHttpResponse(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A response object used to write the JSON response for successful authentication.
|
||||||
|
*
|
||||||
|
* NOTE: We should be careful about writing {@link Authentication} or
|
||||||
|
* {@link Authentication#getPrincipal()} to the response since it contains
|
||||||
|
* credentials.
|
||||||
|
*/
|
||||||
|
public static final class AuthenticationSuccess {
|
||||||
|
|
||||||
|
private final String redirectUrl;
|
||||||
|
|
||||||
|
private AuthenticationSuccess(String redirectUrl) {
|
||||||
|
this.redirectUrl = redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRedirectUrl() {
|
||||||
|
return this.redirectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthenticated() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -67,6 +67,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||||||
|
|
||||||
private boolean saml2LoginEnabled;
|
private boolean saml2LoginEnabled;
|
||||||
|
|
||||||
|
private boolean passkeysEnabled;
|
||||||
|
|
||||||
private boolean oneTimeTokenEnabled;
|
private boolean oneTimeTokenEnabled;
|
||||||
|
|
||||||
private String authenticationUrl;
|
private String authenticationUrl;
|
||||||
@ -85,6 +87,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||||||
|
|
||||||
private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = (request) -> Collections.emptyMap();
|
private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = (request) -> Collections.emptyMap();
|
||||||
|
|
||||||
|
private Function<HttpServletRequest, Map<String, String>> resolveHeaders = (request) -> Collections.emptyMap();
|
||||||
|
|
||||||
public DefaultLoginPageGeneratingFilter() {
|
public DefaultLoginPageGeneratingFilter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +121,17 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||||||
this.resolveHiddenInputs = resolveHiddenInputs;
|
this.resolveHiddenInputs = resolveHiddenInputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a Function used to resolve a Map of the HTTP headers where the key is the name
|
||||||
|
* of the header and the value is the value of the header. Typically, this is used to
|
||||||
|
* resolve the CSRF token.
|
||||||
|
* @param resolveHeaders the function to resolve the headers
|
||||||
|
*/
|
||||||
|
public void setResolveHeaders(Function<HttpServletRequest, Map<String, String>> resolveHeaders) {
|
||||||
|
Assert.notNull(resolveHeaders, "resolveHeaders cannot be null");
|
||||||
|
this.resolveHeaders = resolveHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return this.formLoginEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled;
|
return this.formLoginEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled;
|
||||||
}
|
}
|
||||||
@ -153,6 +168,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||||||
this.saml2LoginEnabled = saml2LoginEnabled;
|
this.saml2LoginEnabled = saml2LoginEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPasskeysEnabled(boolean passkeysEnabled) {
|
||||||
|
this.passkeysEnabled = passkeysEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public void setAuthenticationUrl(String authenticationUrl) {
|
public void setAuthenticationUrl(String authenticationUrl) {
|
||||||
this.authenticationUrl = authenticationUrl;
|
this.authenticationUrl = authenticationUrl;
|
||||||
}
|
}
|
||||||
@ -207,14 +226,46 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||||||
|
|
||||||
return HtmlTemplates.fromTemplate(LOGIN_PAGE_TEMPLATE)
|
return HtmlTemplates.fromTemplate(LOGIN_PAGE_TEMPLATE)
|
||||||
.withRawHtml("contextPath", contextPath)
|
.withRawHtml("contextPath", contextPath)
|
||||||
|
.withRawHtml("javaScript", renderJavaScript(request, contextPath))
|
||||||
.withRawHtml("formLogin", renderFormLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
.withRawHtml("formLogin", renderFormLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
||||||
.withRawHtml("oneTimeTokenLogin",
|
.withRawHtml("oneTimeTokenLogin",
|
||||||
renderOneTimeTokenLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
renderOneTimeTokenLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
||||||
.withRawHtml("oauth2Login", renderOAuth2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
.withRawHtml("oauth2Login", renderOAuth2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
||||||
.withRawHtml("saml2Login", renderSaml2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
.withRawHtml("saml2Login", renderSaml2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
||||||
|
.withRawHtml("passkeyLogin", renderPasskeyLogin())
|
||||||
.render();
|
.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String renderJavaScript(HttpServletRequest request, String contextPath) {
|
||||||
|
if (this.passkeysEnabled) {
|
||||||
|
return HtmlTemplates.fromTemplate(PASSKEY_SCRIPT_TEMPLATE)
|
||||||
|
.withValue("loginPageUrl", this.loginPageUrl)
|
||||||
|
.withValue("contextPath", contextPath)
|
||||||
|
.withRawHtml("csrfHeaders", renderHeaders(request))
|
||||||
|
.render();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderPasskeyLogin() {
|
||||||
|
if (this.passkeysEnabled) {
|
||||||
|
return PASSKEY_FORM_TEMPLATE;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderHeaders(HttpServletRequest request) {
|
||||||
|
StringBuffer javascriptHeadersEntries = new StringBuffer();
|
||||||
|
Map<String, String> headers = this.resolveHeaders.apply(request);
|
||||||
|
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||||
|
javascriptHeadersEntries.append(HtmlTemplates.fromTemplate(CSRF_HEADERS)
|
||||||
|
.withValue("headerName", header.getKey())
|
||||||
|
.withValue("headerValue", header.getValue())
|
||||||
|
.render());
|
||||||
|
}
|
||||||
|
return javascriptHeadersEntries.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private String renderFormLogin(HttpServletRequest request, boolean loginError, boolean logoutSuccess,
|
private String renderFormLogin(HttpServletRequest request, boolean loginError, boolean logoutSuccess,
|
||||||
String contextPath, String errorMsg) {
|
String contextPath, String errorMsg) {
|
||||||
if (!this.formLoginEnabled) {
|
if (!this.formLoginEnabled) {
|
||||||
@ -235,6 +286,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||||||
.withValue("passwordParameter", this.passwordParameter)
|
.withValue("passwordParameter", this.passwordParameter)
|
||||||
.withRawHtml("rememberMeInput", renderRememberMe(this.rememberMeParameter))
|
.withRawHtml("rememberMeInput", renderRememberMe(this.rememberMeParameter))
|
||||||
.withRawHtml("hiddenInputs", hiddenInputs)
|
.withRawHtml("hiddenInputs", hiddenInputs)
|
||||||
|
.withValue("autocomplete", this.passkeysEnabled ? "autocomplete=\"password webauthn\"" : "")
|
||||||
.render();
|
.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,6 +435,26 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||||||
return uri.equals(request.getContextPath() + url);
|
return uri.equals(request.getContextPath() + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String CSRF_HEADERS = """
|
||||||
|
{"{{headerName}}" : "{{headerValue}}"}""";
|
||||||
|
|
||||||
|
private static final String PASSKEY_SCRIPT_TEMPLATE = """
|
||||||
|
<script type="text/javascript" src="{{contextPath}}/login/webauthn.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
<!--
|
||||||
|
document.addEventListener("DOMContentLoaded",() => setupLogin({{csrfHeaders}}, "{{contextPath}}", document.getElementById('passkey-signin')));
|
||||||
|
|
||||||
|
//-->
|
||||||
|
</script>
|
||||||
|
""";
|
||||||
|
|
||||||
|
private static final String PASSKEY_FORM_TEMPLATE = """
|
||||||
|
<div class="login-form">
|
||||||
|
<h2>Login with Passkeys</h2>
|
||||||
|
<button id="passkey-signin" type="submit" class="primary">Sign in with a passkey</button>
|
||||||
|
</form>
|
||||||
|
""";
|
||||||
|
|
||||||
private static final String LOGIN_PAGE_TEMPLATE = """
|
private static final String LOGIN_PAGE_TEMPLATE = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@ -392,12 +464,12 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
<title>Please sign in</title>
|
<title>Please sign in</title>
|
||||||
<link href="{{contextPath}}/default-ui.css" rel="stylesheet" />
|
<link href="{{contextPath}}/default-ui.css" rel="stylesheet" />{{javaScript}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{formLogin}}
|
{{formLogin}}
|
||||||
{{oneTimeTokenLogin}}
|
{{oneTimeTokenLogin}}{{passkeyLogin}}
|
||||||
{{oauth2Login}}
|
{{oauth2Login}}
|
||||||
{{saml2Login}}
|
{{saml2Login}}
|
||||||
</div>
|
</div>
|
||||||
@ -414,7 +486,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="password" class="screenreader">Password</label>
|
<label for="password" class="screenreader">Password</label>
|
||||||
<input type="password" id="password" name="{{passwordParameter}}" placeholder="Password" required>
|
<input type="password" id="password" name="{{passwordParameter}}" placeholder="Password" {{autocomplete}}required>
|
||||||
</p>
|
</p>
|
||||||
{{rememberMeInput}}
|
{{rememberMeInput}}
|
||||||
{{hiddenInputs}}
|
{{hiddenInputs}}
|
||||||
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-relying-party">WebAuthn Relying
|
||||||
|
* Parties</a> may use <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#enumdef-attestationconveyancepreference">AttestationConveyancePreference</a>
|
||||||
|
* to specify their preference regarding attestation conveyance during credential
|
||||||
|
* generation.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class AttestationConveyancePreference {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-none">none</a>
|
||||||
|
* preference indicates that the Relying Party is not interested in
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#attestation">attestation</a>.
|
||||||
|
*/
|
||||||
|
public static final AttestationConveyancePreference NONE = new AttestationConveyancePreference("none");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-indirect">indirect</a>
|
||||||
|
* preference indicates that the Relying Party wants to receive a verifiable
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-statement">attestation
|
||||||
|
* statement</a>, but allows the client to decide how to obtain such an attestation
|
||||||
|
* statement.
|
||||||
|
*/
|
||||||
|
public static final AttestationConveyancePreference INDIRECT = new AttestationConveyancePreference("indirect");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-direct">direct</a>
|
||||||
|
* preference indicates that the Relying Party wants to receive the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-statement">attestation
|
||||||
|
* statement</a> as generated by the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>.
|
||||||
|
*/
|
||||||
|
public static final AttestationConveyancePreference DIRECT = new AttestationConveyancePreference("direct");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-enterprise">enterprise</a>
|
||||||
|
* preference indicates that the Relying Party wants to receive an
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-statement">attestation
|
||||||
|
* statement</a> that may include uniquely identifying information.
|
||||||
|
*/
|
||||||
|
public static final AttestationConveyancePreference ENTERPRISE = new AttestationConveyancePreference("enterprise");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
AttestationConveyancePreference(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the String value of the preference.
|
||||||
|
* @return the String value of the preference.
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an instance of {@link AttestationConveyancePreference}
|
||||||
|
* @param value the {@link #getValue()}
|
||||||
|
* @return an {@link AttestationConveyancePreference}
|
||||||
|
*/
|
||||||
|
public static AttestationConveyancePreference valueOf(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case "none":
|
||||||
|
return NONE;
|
||||||
|
case "indirect":
|
||||||
|
return INDIRECT;
|
||||||
|
case "direct":
|
||||||
|
return DIRECT;
|
||||||
|
case "enterprise":
|
||||||
|
return ENTERPRISE;
|
||||||
|
default:
|
||||||
|
return new AttestationConveyancePreference(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client extension
|
||||||
|
* input</a> entry in the {@link AuthenticationExtensionsClientInputs}.
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see ImmutableAuthenticationExtensionsClientInput
|
||||||
|
*/
|
||||||
|
public interface AuthenticationExtensionsClientInput<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension
|
||||||
|
* identifier</a>.
|
||||||
|
* @return the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension
|
||||||
|
* identifier</a>.
|
||||||
|
*/
|
||||||
|
String getExtensionId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client
|
||||||
|
* extension</a>.
|
||||||
|
* @return the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client
|
||||||
|
* extension</a>.
|
||||||
|
*/
|
||||||
|
T getInput();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#iface-authentication-extensions-client-inputs">AuthenticationExtensionsClientInputs</a>
|
||||||
|
* is a dictionary containing the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client extension
|
||||||
|
* input</a> values for zero or more
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-extensions">WebAuthn
|
||||||
|
* Extensions</a>.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see PublicKeyCredentialCreationOptions#getExtensions()
|
||||||
|
*/
|
||||||
|
public interface AuthenticationExtensionsClientInputs {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all of the {@link AuthenticationExtensionsClientInput}.
|
||||||
|
* @return a non-null {@link List} of {@link AuthenticationExtensionsClientInput}.
|
||||||
|
*/
|
||||||
|
List<AuthenticationExtensionsClientInput> getInputs();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client extension
|
||||||
|
* output</a> entry in {@link AuthenticationExtensionsClientOutputs}.
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
* @see AuthenticationExtensionsClientOutputs#getOutputs()
|
||||||
|
* @see CredentialPropertiesOutput
|
||||||
|
*/
|
||||||
|
public interface AuthenticationExtensionsClientOutput<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension
|
||||||
|
* identifier</a>.
|
||||||
|
* @return the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension
|
||||||
|
* identifier</a>.
|
||||||
|
*/
|
||||||
|
String getExtensionId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client
|
||||||
|
* extension output</a>.
|
||||||
|
* @return the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client
|
||||||
|
* extension output</a>.
|
||||||
|
*/
|
||||||
|
T getOutput();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientoutputs">AuthenticationExtensionsClientOutputs</a>
|
||||||
|
* is a dictionary containing the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client extension
|
||||||
|
* output</a> values for zero or more
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-extensions">WebAuthn
|
||||||
|
* Extensions</a>.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see PublicKeyCredential#getClientExtensionResults()
|
||||||
|
*/
|
||||||
|
public interface AuthenticationExtensionsClientOutputs {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all of the {@link AuthenticationExtensionsClientOutput}.
|
||||||
|
* @return a non-null {@link List} of {@link AuthenticationExtensionsClientOutput}.
|
||||||
|
*/
|
||||||
|
List<AuthenticationExtensionsClientOutput<?>> getOutputs();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,205 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse">AuthenticatorAssertionResponse</a>
|
||||||
|
* interface represents an
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>'s response
|
||||||
|
* to a client’s request for generation of a new
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#authentication-assertion">authentication
|
||||||
|
* assertion</a> given the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-relying-party">WebAuthn Relying
|
||||||
|
* Party</a>'s challenge and OPTIONAL list of credentials it is aware of. This response
|
||||||
|
* contains a cryptographic signature proving possession of the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#credential-private-key">credential private
|
||||||
|
* key</a>, and optionally evidence of
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#user-consent">user consent</a> to a specific
|
||||||
|
* transaction.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see PublicKeyCredential#getResponse()
|
||||||
|
*/
|
||||||
|
public final class AuthenticatorAssertionResponse extends AuthenticatorResponse {
|
||||||
|
|
||||||
|
private final Bytes authenticatorData;
|
||||||
|
|
||||||
|
private final Bytes signature;
|
||||||
|
|
||||||
|
private final Bytes userHandle;
|
||||||
|
|
||||||
|
private final Bytes attestationObject;
|
||||||
|
|
||||||
|
private AuthenticatorAssertionResponse(Bytes clientDataJSON, Bytes authenticatorData, Bytes signature,
|
||||||
|
Bytes userHandle, Bytes attestationObject) {
|
||||||
|
super(clientDataJSON);
|
||||||
|
this.authenticatorData = authenticatorData;
|
||||||
|
this.signature = signature;
|
||||||
|
this.userHandle = userHandle;
|
||||||
|
this.attestationObject = attestationObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata">authenticatorData</a>
|
||||||
|
* contains the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator-data">authenticator
|
||||||
|
* data</a> returned by the authenticator. See
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data">6.1
|
||||||
|
* Authenticator Data.</a>.
|
||||||
|
* @return the {@code authenticatorData}
|
||||||
|
*/
|
||||||
|
public Bytes getAuthenticatorData() {
|
||||||
|
return this.authenticatorData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature">signature</a>
|
||||||
|
* contains the raw signature returned from the authenticator. See
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-op-get-assertion">6.3.3 The
|
||||||
|
* authenticatorGetAssertion Operation</a>.
|
||||||
|
* @return the {@code signature}
|
||||||
|
*/
|
||||||
|
public Bytes getSignature() {
|
||||||
|
return this.signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle">userHandle</a>
|
||||||
|
* is the <a href="https://www.w3.org/TR/webauthn-3/#user-handle">user handle</a>
|
||||||
|
* which is returned from the authenticator, or null if the authenticator did not
|
||||||
|
* return a user handle. See
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-op-get-assertion">6.3.3 The
|
||||||
|
* authenticatorGetAssertion Operation</a>. The authenticator MUST always return a
|
||||||
|
* user handle if the <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials">allowCredentials</a>
|
||||||
|
* option used in the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#authentication-ceremony">authentication
|
||||||
|
* ceremony</a> is empty, and MAY return one otherwise.
|
||||||
|
* @return the <a href="https://www.w3.org/TR/webauthn-3/#user-handle">user handle</a>
|
||||||
|
*/
|
||||||
|
public Bytes getUserHandle() {
|
||||||
|
return this.userHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-attestationobject">attestationObject</a>
|
||||||
|
* is an OPTIONAL attribute contains an
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-object">attestation
|
||||||
|
* object</a>, if the authenticator supports attestation in assertions.
|
||||||
|
* @return the {@code attestationObject}
|
||||||
|
*/
|
||||||
|
public Bytes getAttestationObject() {
|
||||||
|
return this.attestationObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link AuthenticatorAssertionResponseBuilder}
|
||||||
|
* @return the {@link AuthenticatorAssertionResponseBuilder}
|
||||||
|
*/
|
||||||
|
public static AuthenticatorAssertionResponseBuilder builder() {
|
||||||
|
return new AuthenticatorAssertionResponseBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link AuthenticatorAssertionResponse}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public static final class AuthenticatorAssertionResponseBuilder {
|
||||||
|
|
||||||
|
private Bytes authenticatorData;
|
||||||
|
|
||||||
|
private Bytes signature;
|
||||||
|
|
||||||
|
private Bytes userHandle;
|
||||||
|
|
||||||
|
private Bytes attestationObject;
|
||||||
|
|
||||||
|
private Bytes clientDataJSON;
|
||||||
|
|
||||||
|
private AuthenticatorAssertionResponseBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link #getAuthenticatorData()} property
|
||||||
|
* @param authenticatorData the authenticator data.
|
||||||
|
* @return the {@link AuthenticatorAssertionResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAssertionResponseBuilder authenticatorData(Bytes authenticatorData) {
|
||||||
|
this.authenticatorData = authenticatorData;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link #getSignature()} property
|
||||||
|
* @param signature the signature
|
||||||
|
* @return the {@link AuthenticatorAssertionResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAssertionResponseBuilder signature(Bytes signature) {
|
||||||
|
this.signature = signature;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link #getUserHandle()} property
|
||||||
|
* @param userHandle the user handle
|
||||||
|
* @return the {@link AuthenticatorAssertionResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAssertionResponseBuilder userHandle(Bytes userHandle) {
|
||||||
|
this.userHandle = userHandle;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link #attestationObject} property
|
||||||
|
* @param attestationObject the attestation object
|
||||||
|
* @return the {@link AuthenticatorAssertionResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAssertionResponseBuilder attestationObject(Bytes attestationObject) {
|
||||||
|
this.attestationObject = attestationObject;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link #getClientDataJSON()} property
|
||||||
|
* @param clientDataJSON the client data JSON
|
||||||
|
* @return the {@link AuthenticatorAssertionResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAssertionResponseBuilder clientDataJSON(Bytes clientDataJSON) {
|
||||||
|
this.clientDataJSON = clientDataJSON;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the {@link AuthenticatorAssertionResponse}
|
||||||
|
* @return the {@link AuthenticatorAssertionResponse}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAssertionResponse build() {
|
||||||
|
return new AuthenticatorAssertionResponse(this.clientDataJSON, this.authenticatorData, this.signature,
|
||||||
|
this.userHandle, this.attestationObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment">AuthenticatorAttachment</a>.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class AuthenticatorAttachment {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates <a href=
|
||||||
|
* "https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#cross-platform-attachment">cross-platform
|
||||||
|
* attachment</a>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Authenticators of this class are removable from, and can "roam" among, client
|
||||||
|
* platforms.
|
||||||
|
*/
|
||||||
|
public static final AuthenticatorAttachment CROSS_PLATFORM = new AuthenticatorAttachment("cross-platform");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates <a href=
|
||||||
|
* "https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#platform-attachment">platform
|
||||||
|
* attachment</a>.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Usually, authenticators of this class are not removable from the platform.
|
||||||
|
*/
|
||||||
|
public static final AuthenticatorAttachment PLATFORM = new AuthenticatorAttachment("platform");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
AuthenticatorAttachment(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value.
|
||||||
|
* @return the value.
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AuthenticatorAttachment [" + this.value + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an instance of {@link AuthenticatorAttachment} based upon the value passed in.
|
||||||
|
* @param value the value to obtain the {@link AuthenticatorAttachment}
|
||||||
|
* @return the {@link AuthenticatorAttachment}
|
||||||
|
*/
|
||||||
|
public static AuthenticatorAttachment valueOf(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case "cross-platform":
|
||||||
|
return CROSS_PLATFORM;
|
||||||
|
case "platform":
|
||||||
|
return PLATFORM;
|
||||||
|
default:
|
||||||
|
return new AuthenticatorAttachment(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthenticatorAttachment[] values() {
|
||||||
|
return new AuthenticatorAttachment[] { CROSS_PLATFORM, PLATFORM };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse">AuthenticatorAttestationResponse</a>
|
||||||
|
* represents the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>'s response
|
||||||
|
* to a client’s request for the creation of a new
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#public-key-credential">public key
|
||||||
|
* credential</a>.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see PublicKeyCredential#getResponse()
|
||||||
|
*/
|
||||||
|
public final class AuthenticatorAttestationResponse extends AuthenticatorResponse {
|
||||||
|
|
||||||
|
private final Bytes attestationObject;
|
||||||
|
|
||||||
|
private final List<AuthenticatorTransport> transports;
|
||||||
|
|
||||||
|
private AuthenticatorAttestationResponse(Bytes clientDataJSON, Bytes attestationObject,
|
||||||
|
List<AuthenticatorTransport> transports) {
|
||||||
|
super(clientDataJSON);
|
||||||
|
this.attestationObject = attestationObject;
|
||||||
|
this.transports = transports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-attestationobject">attestationObject</a>
|
||||||
|
* attribute contains an attestation object, which is opaque to, and cryptographically
|
||||||
|
* protected against tampering by, the client.
|
||||||
|
* @return the attestationObject
|
||||||
|
*/
|
||||||
|
public Bytes getAttestationObject() {
|
||||||
|
return this.attestationObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-gettransports">transports</a>
|
||||||
|
* returns the <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-transports-slot">transports</a>
|
||||||
|
* @return the transports
|
||||||
|
*/
|
||||||
|
public List<AuthenticatorTransport> getTransports() {
|
||||||
|
return this.transports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
* @return the {@link AuthenticatorAttestationResponseBuilder}
|
||||||
|
*/
|
||||||
|
public static AuthenticatorAttestationResponseBuilder builder() {
|
||||||
|
return new AuthenticatorAttestationResponseBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds {@link AuthenticatorAssertionResponse}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public static final class AuthenticatorAttestationResponseBuilder {
|
||||||
|
|
||||||
|
private Bytes attestationObject;
|
||||||
|
|
||||||
|
private List<AuthenticatorTransport> transports;
|
||||||
|
|
||||||
|
private Bytes clientDataJSON;
|
||||||
|
|
||||||
|
private AuthenticatorAttestationResponseBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getAttestationObject()} property.
|
||||||
|
* @param attestationObject the attestation object.
|
||||||
|
* @return the {@link AuthenticatorAttestationResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAttestationResponseBuilder attestationObject(Bytes attestationObject) {
|
||||||
|
this.attestationObject = attestationObject;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getTransports()} property.
|
||||||
|
* @param transports the transports
|
||||||
|
* @return the {@link AuthenticatorAttestationResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAttestationResponseBuilder transports(AuthenticatorTransport... transports) {
|
||||||
|
return transports(Arrays.asList(transports));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getTransports()} property.
|
||||||
|
* @param transports the transports
|
||||||
|
* @return the {@link AuthenticatorAttestationResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAttestationResponseBuilder transports(List<AuthenticatorTransport> transports) {
|
||||||
|
this.transports = transports;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getClientDataJSON()} property.
|
||||||
|
* @param clientDataJSON the client data JSON.
|
||||||
|
* @return the {@link AuthenticatorAttestationResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAttestationResponseBuilder clientDataJSON(Bytes clientDataJSON) {
|
||||||
|
this.clientDataJSON = clientDataJSON;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link AuthenticatorAssertionResponse}.
|
||||||
|
* @return the {@link AuthenticatorAttestationResponseBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorAttestationResponse build() {
|
||||||
|
return new AuthenticatorAttestationResponse(this.clientDataJSON, this.attestationObject, this.transports);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#iface-authenticatorresponse">AuthenticatorResponse</a>
|
||||||
|
* represents <a href="https://www.w3.org/TR/webauthn-3/#authenticator">Authenticators</a>
|
||||||
|
* respond to <a href="https://www.w3.org/TR/webauthn-3/#relying-party">Relying Party</a>
|
||||||
|
* requests.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public abstract class AuthenticatorResponse {
|
||||||
|
|
||||||
|
private final Bytes clientDataJSON;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance
|
||||||
|
* @param clientDataJSON the {@link #getClientDataJSON()}
|
||||||
|
*/
|
||||||
|
AuthenticatorResponse(Bytes clientDataJSON) {
|
||||||
|
this.clientDataJSON = clientDataJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson">clientDataJSON</a>
|
||||||
|
* contains a JSON-compatible serialization of the client data, the hash of which is
|
||||||
|
* passed to the authenticator by the client in its call to either create() or get()
|
||||||
|
* (i.e., the client data itself is not sent to the authenticator).
|
||||||
|
* @return the client data JSON
|
||||||
|
*/
|
||||||
|
public Bytes getClientDataJSON() {
|
||||||
|
return this.clientDataJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria">AuthenticatorAttachment</a>
|
||||||
|
* can be used by
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-relying-party">WebAuthn Relying
|
||||||
|
* Parties</a> to specify their requirements regarding authenticator attributes.
|
||||||
|
*
|
||||||
|
* There is no <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-requireresidentkey">requireResidentKey</a>
|
||||||
|
* property because it is only for backwards compatability with WebAuthn Level 1.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see PublicKeyCredentialCreationOptions#getAuthenticatorSelection()
|
||||||
|
*/
|
||||||
|
public final class AuthenticatorSelectionCriteria {
|
||||||
|
|
||||||
|
private final AuthenticatorAttachment authenticatorAttachment;
|
||||||
|
|
||||||
|
private final ResidentKeyRequirement residentKey;
|
||||||
|
|
||||||
|
private final UserVerificationRequirement userVerification;
|
||||||
|
|
||||||
|
// NOTE: There is no requireResidentKey property because it is only for backward
|
||||||
|
// compatability with WebAuthn Level 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance
|
||||||
|
* @param authenticatorAttachment the authenticator attachment
|
||||||
|
* @param residentKey the resident key requirement
|
||||||
|
* @param userVerification the user verification
|
||||||
|
*/
|
||||||
|
private AuthenticatorSelectionCriteria(AuthenticatorAttachment authenticatorAttachment,
|
||||||
|
ResidentKeyRequirement residentKey, UserVerificationRequirement userVerification) {
|
||||||
|
this.authenticatorAttachment = authenticatorAttachment;
|
||||||
|
this.residentKey = residentKey;
|
||||||
|
this.userVerification = userVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment">
|
||||||
|
* authenticatorAttachment</a> is present, eligible
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticators</a> are
|
||||||
|
* filtered to be only those authenticators attached with the specified
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#enum-attachment">authenticator
|
||||||
|
* attachment modality</a> (see also <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#sctn-authenticator-attachment-modality">6.2.1
|
||||||
|
* Authenticator Attachment Modality</a>).
|
||||||
|
* @return the authenticator attachment
|
||||||
|
*/
|
||||||
|
public AuthenticatorAttachment getAuthenticatorAttachment() {
|
||||||
|
return this.authenticatorAttachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey">residentKey</a>
|
||||||
|
* specifies the extent to which the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#relying-party">Relying Party</a> desires
|
||||||
|
* to create a <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#client-side-discoverable-credential">client-side
|
||||||
|
* discoverable credential</a>.
|
||||||
|
* @return the residenty key requirement
|
||||||
|
*/
|
||||||
|
public ResidentKeyRequirement getResidentKey() {
|
||||||
|
return this.residentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification">userVerification</a>
|
||||||
|
* specifies the <a href="https://www.w3.org/TR/webauthn-3/#relying-party">Relying
|
||||||
|
* Party</a>'s requirements regarding
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#user-verification">user verification</a>
|
||||||
|
* for the <a href=
|
||||||
|
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">create()</a>
|
||||||
|
* operation.
|
||||||
|
* @return the user verification requirement
|
||||||
|
*/
|
||||||
|
public UserVerificationRequirement getUserVerification() {
|
||||||
|
return this.userVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link AuthenticatorSelectionCriteriaBuilder}
|
||||||
|
* @return a new {@link AuthenticatorSelectionCriteriaBuilder}
|
||||||
|
*/
|
||||||
|
public static AuthenticatorSelectionCriteriaBuilder builder() {
|
||||||
|
return new AuthenticatorSelectionCriteriaBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link AuthenticatorSelectionCriteria}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public static final class AuthenticatorSelectionCriteriaBuilder {
|
||||||
|
|
||||||
|
private AuthenticatorAttachment authenticatorAttachment;
|
||||||
|
|
||||||
|
private ResidentKeyRequirement residentKey;
|
||||||
|
|
||||||
|
private UserVerificationRequirement userVerification;
|
||||||
|
|
||||||
|
private AuthenticatorSelectionCriteriaBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getAuthenticatorAttachment()} property.
|
||||||
|
* @param authenticatorAttachment the authenticator attachment
|
||||||
|
* @return the {@link AuthenticatorSelectionCriteriaBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorSelectionCriteriaBuilder authenticatorAttachment(
|
||||||
|
AuthenticatorAttachment authenticatorAttachment) {
|
||||||
|
this.authenticatorAttachment = authenticatorAttachment;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getResidentKey()} property.
|
||||||
|
* @param residentKey the resident key
|
||||||
|
* @return the {@link AuthenticatorSelectionCriteriaBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorSelectionCriteriaBuilder residentKey(ResidentKeyRequirement residentKey) {
|
||||||
|
this.residentKey = residentKey;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getUserVerification()} property.
|
||||||
|
* @param userVerification the user verification requirement
|
||||||
|
* @return the {@link AuthenticatorSelectionCriteriaBuilder}
|
||||||
|
*/
|
||||||
|
public AuthenticatorSelectionCriteriaBuilder userVerification(UserVerificationRequirement userVerification) {
|
||||||
|
this.userVerification = userVerification;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a {@link AuthenticatorSelectionCriteria}
|
||||||
|
* @return a new {@link AuthenticatorSelectionCriteria}
|
||||||
|
*/
|
||||||
|
public AuthenticatorSelectionCriteria build() {
|
||||||
|
return new AuthenticatorSelectionCriteria(this.authenticatorAttachment, this.residentKey,
|
||||||
|
this.userVerification);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport">AuthenticatorTransport</a>
|
||||||
|
* defines hints as to how clients might communicate with a particular authenticator in
|
||||||
|
* order to obtain an assertion for a specific credential.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class AuthenticatorTransport {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-usb">usbc</a>
|
||||||
|
* indicates the respective authenticator can be contacted over removable USB.
|
||||||
|
*/
|
||||||
|
public static final AuthenticatorTransport USB = new AuthenticatorTransport("usb");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-nfc">nfc</a>
|
||||||
|
* indicates the respective authenticator can be contacted over Near Field
|
||||||
|
* Communication (NFC).
|
||||||
|
*/
|
||||||
|
public static final AuthenticatorTransport NFC = new AuthenticatorTransport("nfc");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-ble">ble</a>
|
||||||
|
* Indicates the respective authenticator can be contacted over Bluetooth Smart
|
||||||
|
* (Bluetooth Low Energy / BLE).
|
||||||
|
*/
|
||||||
|
public static final AuthenticatorTransport BLE = new AuthenticatorTransport("ble");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-smart-card">smart-card</a>
|
||||||
|
* indicates the respective authenticator can be contacted over ISO/IEC 7816 smart
|
||||||
|
* card with contacts.
|
||||||
|
*/
|
||||||
|
public static final AuthenticatorTransport SMART_CARD = new AuthenticatorTransport("smart-card");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-hybrid">hybrid</a>
|
||||||
|
* indicates the respective authenticator can be contacted using a combination of
|
||||||
|
* (often separate) data-transport and proximity mechanisms. This supports, for
|
||||||
|
* example, authentication on a desktop computer using a smartphone.
|
||||||
|
*/
|
||||||
|
public static final AuthenticatorTransport HYBRID = new AuthenticatorTransport("hybrid");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-internal">internal</a>
|
||||||
|
* indicates the respective authenticator is contacted using a client device-specific
|
||||||
|
* transport, i.e., it is a platform authenticator. These authenticators are not
|
||||||
|
* removable from the client device.
|
||||||
|
*/
|
||||||
|
public static final AuthenticatorTransport INTERNAL = new AuthenticatorTransport("internal");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
AuthenticatorTransport(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get's the value.
|
||||||
|
* @return the value.
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an instance of {@link AuthenticatorTransport}.
|
||||||
|
* @param value the value of the {@link AuthenticatorTransport}
|
||||||
|
* @return the {@link AuthenticatorTransport}
|
||||||
|
*/
|
||||||
|
public static AuthenticatorTransport valueOf(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case "usb":
|
||||||
|
return USB;
|
||||||
|
case "nfc":
|
||||||
|
return NFC;
|
||||||
|
case "ble":
|
||||||
|
return BLE;
|
||||||
|
case "smart-card":
|
||||||
|
return SMART_CARD;
|
||||||
|
case "hybrid":
|
||||||
|
return HYBRID;
|
||||||
|
case "internal":
|
||||||
|
return INTERNAL;
|
||||||
|
default:
|
||||||
|
return new AuthenticatorTransport(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthenticatorTransport[] values() {
|
||||||
|
return new AuthenticatorTransport[] { USB, NFC, BLE, HYBRID, INTERNAL };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object representation of byte[].
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class Bytes {
|
||||||
|
|
||||||
|
private static final SecureRandom RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding();
|
||||||
|
|
||||||
|
private static final Base64.Decoder DECODER = Base64.getUrlDecoder();
|
||||||
|
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance
|
||||||
|
* @param bytes the raw base64UrlString that will be encoded.
|
||||||
|
*/
|
||||||
|
public Bytes(byte[] bytes) {
|
||||||
|
Assert.notNull(bytes, "bytes cannot be null");
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the raw bytes.
|
||||||
|
* @return the bytes
|
||||||
|
*/
|
||||||
|
public byte[] getBytes() {
|
||||||
|
return Arrays.copyOf(this.bytes, this.bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the bytes as Base64 URL encoded String.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String toBase64UrlString() {
|
||||||
|
return ENCODER.encodeToString(getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof Bytes that) {
|
||||||
|
return that.toBase64UrlString().equals(toBase64UrlString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return toBase64UrlString().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return "Bytes[" + toBase64UrlString() + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a secure random {@link Bytes} with random bytes and sufficient entropy.
|
||||||
|
* @return a new secure random generated {@link Bytes}
|
||||||
|
*/
|
||||||
|
public static Bytes random() {
|
||||||
|
byte[] bytes = new byte[32];
|
||||||
|
RANDOM.nextBytes(bytes);
|
||||||
|
return new Bytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance from a base64 url string.
|
||||||
|
* @param base64UrlString the base64 url string
|
||||||
|
* @return the {@link Bytes}
|
||||||
|
*/
|
||||||
|
public static Bytes fromBase64(String base64UrlString) {
|
||||||
|
byte[] bytes = DECODER.decode(base64UrlString);
|
||||||
|
return new Bytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier">COSEAlgorithmIdentifier</a> is
|
||||||
|
* used to identify a cryptographic algorithm.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see PublicKeyCredentialParameters#getAlg()
|
||||||
|
*/
|
||||||
|
public final class COSEAlgorithmIdentifier {
|
||||||
|
|
||||||
|
public static final COSEAlgorithmIdentifier EdDSA = new COSEAlgorithmIdentifier(-8);
|
||||||
|
|
||||||
|
public static final COSEAlgorithmIdentifier ES256 = new COSEAlgorithmIdentifier(-7);
|
||||||
|
|
||||||
|
public static final COSEAlgorithmIdentifier ES384 = new COSEAlgorithmIdentifier(-35);
|
||||||
|
|
||||||
|
public static final COSEAlgorithmIdentifier ES512 = new COSEAlgorithmIdentifier(-36);
|
||||||
|
|
||||||
|
public static final COSEAlgorithmIdentifier RS256 = new COSEAlgorithmIdentifier(-257);
|
||||||
|
|
||||||
|
public static final COSEAlgorithmIdentifier RS384 = new COSEAlgorithmIdentifier(-258);
|
||||||
|
|
||||||
|
public static final COSEAlgorithmIdentifier RS512 = new COSEAlgorithmIdentifier(-259);
|
||||||
|
|
||||||
|
public static final COSEAlgorithmIdentifier RS1 = new COSEAlgorithmIdentifier(-65535);
|
||||||
|
|
||||||
|
private final long value;
|
||||||
|
|
||||||
|
private COSEAlgorithmIdentifier(long value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.valueOf(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static COSEAlgorithmIdentifier[] values() {
|
||||||
|
return new COSEAlgorithmIdentifier[] { EdDSA, ES256, ES384, ES512, RS256, RS384, RS512, RS1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements <a href=
|
||||||
|
* "https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension">
|
||||||
|
* Credential Protection (credProtect)</a>.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class CredProtectAuthenticationExtensionsClientInput
|
||||||
|
implements AuthenticationExtensionsClientInput<CredProtectAuthenticationExtensionsClientInput.CredProtect> {
|
||||||
|
|
||||||
|
private final CredProtect input;
|
||||||
|
|
||||||
|
public CredProtectAuthenticationExtensionsClientInput(CredProtect input) {
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getExtensionId() {
|
||||||
|
return "credProtect";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CredProtect getInput() {
|
||||||
|
return this.input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CredProtect {
|
||||||
|
|
||||||
|
private final ProtectionPolicy credProtectionPolicy;
|
||||||
|
|
||||||
|
private final boolean enforceCredentialProtectionPolicy;
|
||||||
|
|
||||||
|
public CredProtect(ProtectionPolicy credProtectionPolicy, boolean enforceCredentialProtectionPolicy) {
|
||||||
|
this.enforceCredentialProtectionPolicy = enforceCredentialProtectionPolicy;
|
||||||
|
this.credProtectionPolicy = credProtectionPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnforceCredentialProtectionPolicy() {
|
||||||
|
return this.enforceCredentialProtectionPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProtectionPolicy getCredProtectionPolicy() {
|
||||||
|
return this.credProtectionPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ProtectionPolicy {
|
||||||
|
|
||||||
|
USER_VERIFICATION_OPTIONAL, USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST, USER_VERIFICATION_REQUIRED
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-credentialpropertiesoutput">CredentialPropertiesOutput</a>
|
||||||
|
* is the Client extension output.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class CredentialPropertiesOutput
|
||||||
|
implements AuthenticationExtensionsClientOutput<CredentialPropertiesOutput.ExtensionOutput> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The extension id.
|
||||||
|
*/
|
||||||
|
public static final String EXTENSION_ID = "credProps";
|
||||||
|
|
||||||
|
private final ExtensionOutput output;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
* @param rk is the resident key is discoverable
|
||||||
|
*/
|
||||||
|
public CredentialPropertiesOutput(boolean rk) {
|
||||||
|
this.output = new ExtensionOutput(rk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getExtensionId() {
|
||||||
|
return EXTENSION_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExtensionOutput getOutput() {
|
||||||
|
return this.output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The output for {@link CredentialPropertiesOutput}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see #getOutput()
|
||||||
|
*/
|
||||||
|
public static final class ExtensionOutput {
|
||||||
|
|
||||||
|
private final boolean rk;
|
||||||
|
|
||||||
|
private ExtensionOutput(boolean rk) {
|
||||||
|
this.rk = rk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This OPTIONAL property, known abstractly as the resident key credential
|
||||||
|
* property (i.e., client-side discoverable credential property), is a Boolean
|
||||||
|
* value indicating whether the PublicKeyCredential returned as a result of a
|
||||||
|
* registration ceremony is a client-side discoverable credential.
|
||||||
|
* @return is resident key credential property
|
||||||
|
*/
|
||||||
|
public boolean isRk() {
|
||||||
|
return this.rk;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a <a href="https://www.w3.org/TR/webauthn-3/#credential-record">Credential
|
||||||
|
* Record</a> that is stored by the Relying Party
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#reg-ceremony-store-credential-record">after
|
||||||
|
* successful registration</a>.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public interface CredentialRecord {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-type">credential.type</a>
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
PublicKeyCredentialType getCredentialType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-id">credential.id</a>.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Bytes getCredentialId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-publickey">publicKey</a>
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
PublicKeyCose getPublicKey();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-signcount">authData.signCount</a>
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
long getSignatureCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-uvinitialized">uvInitialized</a>
|
||||||
|
* is the value of the UV (user verified) flag in authData and indicates whether any
|
||||||
|
* credential from this public key credential source has had the UV flag set.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
boolean isUvInitialized();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-transports">transpots</a>
|
||||||
|
* is the value returned from {@code response.getTransports()}.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Set<AuthenticatorTransport> getTransports();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-backupeligible">backupElgible</a>
|
||||||
|
* flag is the same as the BE flag in authData.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
boolean isBackupEligible();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-backupstate">backupState</a>
|
||||||
|
* flag is the same as the BS flag in authData.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
boolean isBackupState();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reference to the associated {@link PublicKeyCredentialUserEntity#getId()}
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Bytes getUserEntityUserId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-attestationobject">attestationObject</a>
|
||||||
|
* is the value of the attestationObject attribute when the public key credential
|
||||||
|
* source was registered.
|
||||||
|
* @return the attestationObject
|
||||||
|
*/
|
||||||
|
Bytes getAttestationObject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-attestationclientdatajson">attestationClientDataJSON</a>
|
||||||
|
* is the value of the attestationObject attribute when the public key credential
|
||||||
|
* source was registered.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Bytes getAttestationClientDataJSON();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A human-readable label for this {@link CredentialRecord} assigned by the user.
|
||||||
|
* @return a label
|
||||||
|
*/
|
||||||
|
String getLabel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last time this {@link CredentialRecord} was used.
|
||||||
|
* @return the last time this {@link CredentialRecord} was used.
|
||||||
|
*/
|
||||||
|
Instant getLastUsed();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When this {@link CredentialRecord} was created.
|
||||||
|
* @return When this {@link CredentialRecord} was created.
|
||||||
|
*/
|
||||||
|
Instant getCreated();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An immutable {@link AuthenticationExtensionsClientInput}.
|
||||||
|
*
|
||||||
|
* @param <T> the input type
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see AuthenticationExtensionsClientInputs
|
||||||
|
*/
|
||||||
|
public class ImmutableAuthenticationExtensionsClientInput<T> implements AuthenticationExtensionsClientInput<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
|
||||||
|
*/
|
||||||
|
public static final AuthenticationExtensionsClientInput<Boolean> credProps = new ImmutableAuthenticationExtensionsClientInput<>(
|
||||||
|
"credProps", true);
|
||||||
|
|
||||||
|
private final String extensionId;
|
||||||
|
|
||||||
|
private final T input;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance
|
||||||
|
* @param extensionId the extension id.
|
||||||
|
* @param input the input.
|
||||||
|
*/
|
||||||
|
public ImmutableAuthenticationExtensionsClientInput(String extensionId, T input) {
|
||||||
|
this.extensionId = extensionId;
|
||||||
|
this.input = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getExtensionId() {
|
||||||
|
return this.extensionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T getInput() {
|
||||||
|
return this.input;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An immutable implementation of {@link AuthenticationExtensionsClientInputs}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class ImmutableAuthenticationExtensionsClientInputs implements AuthenticationExtensionsClientInputs {
|
||||||
|
|
||||||
|
private final List<AuthenticationExtensionsClientInput> inputs;
|
||||||
|
|
||||||
|
public ImmutableAuthenticationExtensionsClientInputs(List<AuthenticationExtensionsClientInput> inputs) {
|
||||||
|
this.inputs = inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableAuthenticationExtensionsClientInputs(AuthenticationExtensionsClientInput... inputs) {
|
||||||
|
this(Arrays.asList(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuthenticationExtensionsClientInput> getInputs() {
|
||||||
|
return this.inputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An immutable implementation of {@link AuthenticationExtensionsClientOutputs}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
public class ImmutableAuthenticationExtensionsClientOutputs implements AuthenticationExtensionsClientOutputs {
|
||||||
|
|
||||||
|
private final List<AuthenticationExtensionsClientOutput<?>> outputs;
|
||||||
|
|
||||||
|
public ImmutableAuthenticationExtensionsClientOutputs(List<AuthenticationExtensionsClientOutput<?>> outputs) {
|
||||||
|
this.outputs = outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableAuthenticationExtensionsClientOutputs(AuthenticationExtensionsClientOutput<?>... outputs) {
|
||||||
|
this(Arrays.asList(outputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AuthenticationExtensionsClientOutput<?>> getOutputs() {
|
||||||
|
return this.outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,285 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An immutable {@link CredentialRecord}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class ImmutableCredentialRecord implements CredentialRecord {
|
||||||
|
|
||||||
|
private final PublicKeyCredentialType credentialType;
|
||||||
|
|
||||||
|
private final Bytes credentialId;
|
||||||
|
|
||||||
|
private final Bytes userEntityUserId;
|
||||||
|
|
||||||
|
private final PublicKeyCose publicKey;
|
||||||
|
|
||||||
|
private final long signatureCount;
|
||||||
|
|
||||||
|
private final boolean uvInitialized;
|
||||||
|
|
||||||
|
private final Set<AuthenticatorTransport> transports;
|
||||||
|
|
||||||
|
private final boolean backupEligible;
|
||||||
|
|
||||||
|
private final boolean backupState;
|
||||||
|
|
||||||
|
private final Bytes attestationObject;
|
||||||
|
|
||||||
|
private final Bytes attestationClientDataJSON;
|
||||||
|
|
||||||
|
private final Instant created;
|
||||||
|
|
||||||
|
private final Instant lastUsed;
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
private ImmutableCredentialRecord(PublicKeyCredentialType credentialType, Bytes credentialId,
|
||||||
|
Bytes userEntityUserId, PublicKeyCose publicKey, long signatureCount, boolean uvInitialized,
|
||||||
|
Set<AuthenticatorTransport> transports, boolean backupEligible, boolean backupState,
|
||||||
|
Bytes attestationObject, Bytes attestationClientDataJSON, Instant created, Instant lastUsed, String label) {
|
||||||
|
this.credentialType = credentialType;
|
||||||
|
this.credentialId = credentialId;
|
||||||
|
this.userEntityUserId = userEntityUserId;
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
this.signatureCount = signatureCount;
|
||||||
|
this.uvInitialized = uvInitialized;
|
||||||
|
this.transports = transports;
|
||||||
|
this.backupEligible = backupEligible;
|
||||||
|
this.backupState = backupState;
|
||||||
|
this.attestationObject = attestationObject;
|
||||||
|
this.attestationClientDataJSON = attestationClientDataJSON;
|
||||||
|
this.created = created;
|
||||||
|
this.lastUsed = lastUsed;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKeyCredentialType getCredentialType() {
|
||||||
|
return this.credentialType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bytes getCredentialId() {
|
||||||
|
return this.credentialId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bytes getUserEntityUserId() {
|
||||||
|
return this.userEntityUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKeyCose getPublicKey() {
|
||||||
|
return this.publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSignatureCount() {
|
||||||
|
return this.signatureCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isUvInitialized() {
|
||||||
|
return this.uvInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<AuthenticatorTransport> getTransports() {
|
||||||
|
return this.transports;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBackupEligible() {
|
||||||
|
return this.backupEligible;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBackupState() {
|
||||||
|
return this.backupState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bytes getAttestationObject() {
|
||||||
|
return this.attestationObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bytes getAttestationClientDataJSON() {
|
||||||
|
return this.attestationClientDataJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant getCreated() {
|
||||||
|
return this.created;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant getLastUsed() {
|
||||||
|
return this.lastUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLabel() {
|
||||||
|
return this.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImmutableCredentialRecordBuilder builder() {
|
||||||
|
return new ImmutableCredentialRecordBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImmutableCredentialRecordBuilder fromCredentialRecord(CredentialRecord credentialRecord) {
|
||||||
|
return new ImmutableCredentialRecordBuilder(credentialRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ImmutableCredentialRecordBuilder {
|
||||||
|
|
||||||
|
private PublicKeyCredentialType credentialType;
|
||||||
|
|
||||||
|
private Bytes credentialId;
|
||||||
|
|
||||||
|
private Bytes userEntityUserId;
|
||||||
|
|
||||||
|
private PublicKeyCose publicKey;
|
||||||
|
|
||||||
|
private long signatureCount;
|
||||||
|
|
||||||
|
private boolean uvInitialized;
|
||||||
|
|
||||||
|
private Set<AuthenticatorTransport> transports;
|
||||||
|
|
||||||
|
private boolean backupEligible;
|
||||||
|
|
||||||
|
private boolean backupState;
|
||||||
|
|
||||||
|
private Bytes attestationObject;
|
||||||
|
|
||||||
|
private Bytes attestationClientDataJSON;
|
||||||
|
|
||||||
|
private Instant created = Instant.now();
|
||||||
|
|
||||||
|
private Instant lastUsed = this.created;
|
||||||
|
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
private ImmutableCredentialRecordBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImmutableCredentialRecordBuilder(CredentialRecord other) {
|
||||||
|
this.credentialType = other.getCredentialType();
|
||||||
|
this.credentialId = other.getCredentialId();
|
||||||
|
this.userEntityUserId = other.getUserEntityUserId();
|
||||||
|
this.publicKey = other.getPublicKey();
|
||||||
|
this.signatureCount = other.getSignatureCount();
|
||||||
|
this.uvInitialized = other.isUvInitialized();
|
||||||
|
this.transports = other.getTransports();
|
||||||
|
this.backupEligible = other.isBackupEligible();
|
||||||
|
this.backupState = other.isBackupState();
|
||||||
|
this.attestationObject = other.getAttestationObject();
|
||||||
|
this.attestationClientDataJSON = other.getAttestationClientDataJSON();
|
||||||
|
this.created = other.getCreated();
|
||||||
|
this.lastUsed = other.getLastUsed();
|
||||||
|
this.label = other.getLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder credentialType(PublicKeyCredentialType credentialType) {
|
||||||
|
this.credentialType = credentialType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder credentialId(Bytes credentialId) {
|
||||||
|
this.credentialId = credentialId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder userEntityUserId(Bytes userEntityUserId) {
|
||||||
|
this.userEntityUserId = userEntityUserId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder publicKey(PublicKeyCose publicKey) {
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder signatureCount(long signatureCount) {
|
||||||
|
this.signatureCount = signatureCount;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder uvInitialized(boolean uvInitialized) {
|
||||||
|
this.uvInitialized = uvInitialized;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder transports(Set<AuthenticatorTransport> transports) {
|
||||||
|
this.transports = transports;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder backupEligible(boolean backupEligible) {
|
||||||
|
this.backupEligible = backupEligible;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder backupState(boolean backupState) {
|
||||||
|
this.backupState = backupState;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder attestationObject(Bytes attestationObject) {
|
||||||
|
this.attestationObject = attestationObject;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder attestationClientDataJSON(Bytes attestationClientDataJSON) {
|
||||||
|
this.attestationClientDataJSON = attestationClientDataJSON;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder created(Instant created) {
|
||||||
|
this.created = created;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder lastUsed(Instant lastUsed) {
|
||||||
|
this.lastUsed = lastUsed;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecordBuilder label(String label) {
|
||||||
|
this.label = label;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImmutableCredentialRecord build() {
|
||||||
|
return new ImmutableCredentialRecord(this.credentialType, this.credentialId, this.userEntityUserId,
|
||||||
|
this.publicKey, this.signatureCount, this.uvInitialized, this.transports, this.backupEligible,
|
||||||
|
this.backupState, this.attestationObject, this.attestationClientDataJSON, this.created,
|
||||||
|
this.lastUsed, this.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An immutable {@link PublicKeyCose}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class ImmutablePublicKeyCose implements PublicKeyCose {
|
||||||
|
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
* @param bytes the raw bytes of the public key.
|
||||||
|
*/
|
||||||
|
public ImmutablePublicKeyCose(byte[] bytes) {
|
||||||
|
this.bytes = Arrays.copyOf(bytes, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getBytes() {
|
||||||
|
return Arrays.copyOf(this.bytes, this.bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance form a Base64 URL encoded String
|
||||||
|
* @param base64EncodedString the base64EncodedString encoded String
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static ImmutablePublicKeyCose fromBase64(String base64EncodedString) {
|
||||||
|
byte[] decode = Base64.getUrlDecoder().decode(base64EncodedString);
|
||||||
|
return new ImmutablePublicKeyCose(decode);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity">PublicKeyCredentialUserEntity</a>
|
||||||
|
* is used to supply additional
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#user-account">user account</a> attributes
|
||||||
|
* when creating a new credential.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class ImmutablePublicKeyCredentialUserEntity implements PublicKeyCredentialUserEntity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When inherited by PublicKeyCredentialUserEntity, it is a human-palatable identifier
|
||||||
|
* for a user account. It is intended only for display, i.e., aiding the user in
|
||||||
|
* determining the difference between user accounts with similar displayNames. For
|
||||||
|
* example, "alexm", "alex.mueller@example.com" or "+14255551234".
|
||||||
|
*
|
||||||
|
* The Relying Party MAY let the user choose this value. The Relying Party SHOULD
|
||||||
|
* perform enforcement, as prescribed in Section 3.4.3 of [RFC8265] for the
|
||||||
|
* UsernameCasePreserved Profile of the PRECIS IdentifierClass [RFC8264], when setting
|
||||||
|
* name's value, or displaying the value to the user.
|
||||||
|
*
|
||||||
|
* This string MAY contain language and direction metadata. Relying Parties SHOULD
|
||||||
|
* consider providing this information. See § 6.4.2 Language and Direction Encoding
|
||||||
|
* about how this metadata is encoded.
|
||||||
|
*
|
||||||
|
* Clients SHOULD perform enforcement, as prescribed in Section 3.4.3 of [RFC8265] for
|
||||||
|
* the UsernameCasePreserved Profile of the PRECIS IdentifierClass [RFC8264], on
|
||||||
|
* name's value prior to displaying the value to the user or including the value as a
|
||||||
|
* parameter of the authenticatorMakeCredential operation.
|
||||||
|
*/
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user handle of the user account entity. A user handle is an opaque byte
|
||||||
|
* sequence with a maximum size of 64 bytes, and is not meant to be displayed to the
|
||||||
|
* user.
|
||||||
|
*
|
||||||
|
* To ensure secure operation, authentication and authorization decisions MUST be made
|
||||||
|
* on the basis of this id member, not the displayName nor name members. See Section
|
||||||
|
* 6.1 of [RFC8266].
|
||||||
|
*
|
||||||
|
* The user handle MUST NOT contain personally identifying information about the user,
|
||||||
|
* such as a username or e-mail address; see § 14.6.1 User Handle Contents for
|
||||||
|
* details. The user handle MUST NOT be empty, though it MAY be null.
|
||||||
|
*
|
||||||
|
* Note: the user handle ought not be a constant value across different accounts, even
|
||||||
|
* for non-discoverable credentials, because some authenticators always create
|
||||||
|
* discoverable credentials. Thus a constant user handle would prevent a user from
|
||||||
|
* using such an authenticator with more than one account at the Relying Party.
|
||||||
|
*/
|
||||||
|
private final Bytes id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A human-palatable name for the user account, intended only for display. For
|
||||||
|
* example, "Alex Müller" or "田中倫". The Relying Party SHOULD let the user choose this,
|
||||||
|
* and SHOULD NOT restrict the choice more than necessary.
|
||||||
|
*
|
||||||
|
* Relying Parties SHOULD perform enforcement, as prescribed in Section 2.3 of
|
||||||
|
* [RFC8266] for the Nickname Profile of the PRECIS FreeformClass [RFC8264], when
|
||||||
|
* setting displayName's value, or displaying the value to the user.
|
||||||
|
*
|
||||||
|
* This string MAY contain language and direction metadata. Relying Parties SHOULD
|
||||||
|
* consider providing this information. See § 6.4.2 Language and Direction Encoding
|
||||||
|
* about how this metadata is encoded.
|
||||||
|
*
|
||||||
|
* Clients SHOULD perform enforcement, as prescribed in Section 2.3 of [RFC8266] for
|
||||||
|
* the Nickname Profile of the PRECIS FreeformClass [RFC8264], on displayName's value
|
||||||
|
* prior to displaying the value to the user or including the value as a parameter of
|
||||||
|
* the authenticatorMakeCredential operation.
|
||||||
|
*
|
||||||
|
* When clients, client platforms, or authenticators display a displayName's value,
|
||||||
|
* they should always use UI elements to provide a clear boundary around the displayed
|
||||||
|
* value, and not allow overflow into other elements [css-overflow-3].
|
||||||
|
*
|
||||||
|
* Authenticators MUST accept and store a 64-byte minimum length for a displayName
|
||||||
|
* member’s value. Authenticators MAY truncate a displayName member’s value so that it
|
||||||
|
* fits within 64 bytes. See § 6.4.1 String Truncation about truncation and other
|
||||||
|
* considerations.
|
||||||
|
*/
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
private ImmutablePublicKeyCredentialUserEntity(String name, Bytes id, String displayName) {
|
||||||
|
this.name = name;
|
||||||
|
this.id = id;
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bytes getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayName() {
|
||||||
|
return this.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link PublicKeyCredentialUserEntityBuilder}
|
||||||
|
* @return a new {@link PublicKeyCredentialUserEntityBuilder}
|
||||||
|
*/
|
||||||
|
public static PublicKeyCredentialUserEntityBuilder builder() {
|
||||||
|
return new PublicKeyCredentialUserEntityBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to build {@link PublicKeyCredentialUserEntity}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public static final class PublicKeyCredentialUserEntityBuilder {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private Bytes id;
|
||||||
|
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
private PublicKeyCredentialUserEntityBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getName()} property.
|
||||||
|
* @param name the name
|
||||||
|
* @return the {@link PublicKeyCredentialUserEntityBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialUserEntityBuilder name(String name) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getId()} property.
|
||||||
|
* @param id the id
|
||||||
|
* @return the {@link PublicKeyCredentialUserEntityBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialUserEntityBuilder id(Bytes id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getDisplayName()} property.
|
||||||
|
* @param displayName the display name
|
||||||
|
* @return the {@link PublicKeyCredentialUserEntityBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialUserEntityBuilder displayName(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new {@link PublicKeyCredentialUserEntity}
|
||||||
|
* @return a new {@link PublicKeyCredentialUserEntity}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialUserEntity build() {
|
||||||
|
return new ImmutablePublicKeyCredentialUserEntity(this.name, this.id, this.displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A <a href="https://www.w3.org/TR/webauthn-3/#sctn-encoded-credPubKey-examples">COSE
|
||||||
|
* encoded public key</a>.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public interface PublicKeyCose {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The byes of a COSE encoded public key.
|
||||||
|
* @return the bytes of a COSE encoded public key.
|
||||||
|
*/
|
||||||
|
byte[] getBytes();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,223 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#iface-pkcredential">PublicKeyCredential</a>
|
||||||
|
* contains the attributes that are returned to the caller when a new credential is
|
||||||
|
* created, or a new assertion is requested.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class PublicKeyCredential<R extends AuthenticatorResponse> {
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
private final PublicKeyCredentialType type;
|
||||||
|
|
||||||
|
private final Bytes rawId;
|
||||||
|
|
||||||
|
private final R response;
|
||||||
|
|
||||||
|
private final AuthenticatorAttachment authenticatorAttachment;
|
||||||
|
|
||||||
|
private final AuthenticationExtensionsClientOutputs clientExtensionResults;
|
||||||
|
|
||||||
|
private PublicKeyCredential(String id, PublicKeyCredentialType type, Bytes rawId, R response,
|
||||||
|
AuthenticatorAttachment authenticatorAttachment,
|
||||||
|
AuthenticationExtensionsClientOutputs clientExtensionResults) {
|
||||||
|
this.id = id;
|
||||||
|
this.type = type;
|
||||||
|
this.rawId = rawId;
|
||||||
|
this.response = response;
|
||||||
|
this.authenticatorAttachment = authenticatorAttachment;
|
||||||
|
this.clientExtensionResults = clientExtensionResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The
|
||||||
|
* <a href="https://www.w3.org/TR/credential-management-1/#dom-credential-id">id</a>
|
||||||
|
* attribute is inherited from Credential, though PublicKeyCredential overrides
|
||||||
|
* Credential's getter, instead returning the base64url encoding of the data contained
|
||||||
|
* in the object’s [[identifier]] internal slot.
|
||||||
|
*/
|
||||||
|
public String getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/credential-management-1/#dom-credential-type">type</a>
|
||||||
|
* attribute returns the value of the object’s interface object's [[type]] slot, which
|
||||||
|
* specifies the credential type represented by this object.
|
||||||
|
* @return the credential type
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialType getType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid">rawId</a>
|
||||||
|
* returns the raw identifier.
|
||||||
|
* @return the raw id
|
||||||
|
*/
|
||||||
|
public Bytes getRawId() {
|
||||||
|
return this.rawId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response">response</a>
|
||||||
|
* to the client's request to either create a public key credential, or generate an
|
||||||
|
* authentication assertion.
|
||||||
|
* @return the response
|
||||||
|
*/
|
||||||
|
public R getResponse() {
|
||||||
|
return this.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment">authenticatorAttachment</a>
|
||||||
|
* reports the <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#authenticator-attachment-modality">authenticator
|
||||||
|
* attachment modality</a> in effect at the time the navigator.credentials.create() or
|
||||||
|
* navigator.credentials.get() methods successfully complete.
|
||||||
|
* @return the authenticator attachment
|
||||||
|
*/
|
||||||
|
public AuthenticatorAttachment getAuthenticatorAttachment() {
|
||||||
|
return this.authenticatorAttachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults">clientExtensionsResults</a>
|
||||||
|
* is a mapping of extension identifier to client extension output.
|
||||||
|
* @return the extension results
|
||||||
|
*/
|
||||||
|
public AuthenticationExtensionsClientOutputs getClientExtensionResults() {
|
||||||
|
return this.clientExtensionResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link PublicKeyCredentialBuilder}
|
||||||
|
* @param <T> the response type
|
||||||
|
* @return the {@link PublicKeyCredentialBuilder}
|
||||||
|
*/
|
||||||
|
public static <T extends AuthenticatorResponse> PublicKeyCredentialBuilder<T> builder() {
|
||||||
|
return new PublicKeyCredentialBuilder<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link PublicKeyCredentialBuilder}
|
||||||
|
*
|
||||||
|
* @param <R> the response type
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public static final class PublicKeyCredentialBuilder<R extends AuthenticatorResponse> {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private PublicKeyCredentialType type;
|
||||||
|
|
||||||
|
private Bytes rawId;
|
||||||
|
|
||||||
|
private R response;
|
||||||
|
|
||||||
|
private AuthenticatorAttachment authenticatorAttachment;
|
||||||
|
|
||||||
|
private AuthenticationExtensionsClientOutputs clientExtensionResults;
|
||||||
|
|
||||||
|
private PublicKeyCredentialBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getId()} property
|
||||||
|
* @param id the id
|
||||||
|
* @return the PublicKeyCredentialBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialBuilder id(String id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getType()} property.
|
||||||
|
* @param type the type
|
||||||
|
* @return the PublicKeyCredentialBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialBuilder type(PublicKeyCredentialType type) {
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getRawId()} property.
|
||||||
|
* @param rawId the raw id
|
||||||
|
* @return the PublicKeyCredentialBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialBuilder rawId(Bytes rawId) {
|
||||||
|
this.rawId = rawId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getResponse()} property.
|
||||||
|
* @param response the response
|
||||||
|
* @return the PublicKeyCredentialBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialBuilder response(R response) {
|
||||||
|
this.response = response;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getAuthenticatorAttachment()} property.
|
||||||
|
* @param authenticatorAttachment the authenticator attachement
|
||||||
|
* @return the PublicKeyCredentialBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialBuilder authenticatorAttachment(AuthenticatorAttachment authenticatorAttachment) {
|
||||||
|
this.authenticatorAttachment = authenticatorAttachment;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getClientExtensionResults()} property.
|
||||||
|
* @param clientExtensionResults the client extension results
|
||||||
|
* @return the PublicKeyCredentialBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialBuilder clientExtensionResults(
|
||||||
|
AuthenticationExtensionsClientOutputs clientExtensionResults) {
|
||||||
|
this.clientExtensionResults = clientExtensionResults;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link PublicKeyCredential}
|
||||||
|
* @return a new {@link PublicKeyCredential}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredential<R> build() {
|
||||||
|
return new PublicKeyCredential(this.id, this.type, this.rawId, this.response, this.authenticatorAttachment,
|
||||||
|
this.clientExtensionResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,332 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions">PublicKeyCredentialCreationOptions</a>
|
||||||
|
* which is an argument to <a href=
|
||||||
|
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">creating</a>
|
||||||
|
* a new credential.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class PublicKeyCredentialCreationOptions {
|
||||||
|
|
||||||
|
private final PublicKeyCredentialRpEntity rp;
|
||||||
|
|
||||||
|
private final PublicKeyCredentialUserEntity user;
|
||||||
|
|
||||||
|
private final Bytes challenge;
|
||||||
|
|
||||||
|
private final List<PublicKeyCredentialParameters> pubKeyCredParams;
|
||||||
|
|
||||||
|
private final Duration timeout;
|
||||||
|
|
||||||
|
private final List<PublicKeyCredentialDescriptor> excludeCredentials;
|
||||||
|
|
||||||
|
private final AuthenticatorSelectionCriteria authenticatorSelection;
|
||||||
|
|
||||||
|
private final AttestationConveyancePreference attestation;
|
||||||
|
|
||||||
|
private final AuthenticationExtensionsClientInputs extensions;
|
||||||
|
|
||||||
|
private PublicKeyCredentialCreationOptions(PublicKeyCredentialRpEntity rp, PublicKeyCredentialUserEntity user,
|
||||||
|
Bytes challenge, List<PublicKeyCredentialParameters> pubKeyCredParams, Duration timeout,
|
||||||
|
List<PublicKeyCredentialDescriptor> excludeCredentials,
|
||||||
|
AuthenticatorSelectionCriteria authenticatorSelection, AttestationConveyancePreference attestation,
|
||||||
|
AuthenticationExtensionsClientInputs extensions) {
|
||||||
|
this.rp = rp;
|
||||||
|
this.user = user;
|
||||||
|
this.challenge = challenge;
|
||||||
|
this.pubKeyCredParams = pubKeyCredParams;
|
||||||
|
this.timeout = timeout;
|
||||||
|
this.excludeCredentials = excludeCredentials;
|
||||||
|
this.authenticatorSelection = authenticatorSelection;
|
||||||
|
this.attestation = attestation;
|
||||||
|
this.extensions = extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-rp">rp</a>
|
||||||
|
* property contains data about the Relying Party responsible for the request.
|
||||||
|
* @return the relying party
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRpEntity getRp() {
|
||||||
|
return this.rp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-user">user</a>
|
||||||
|
* contains names and an identifier for the user account performing the registration.
|
||||||
|
* @return the user
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialUserEntity getUser() {
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge">challenge</a>
|
||||||
|
* specifies the challenge that the authenticator signs, along with other data, when
|
||||||
|
* producing an attestation object for the newly created credential.
|
||||||
|
* @return the challenge
|
||||||
|
*/
|
||||||
|
public Bytes getChallenge() {
|
||||||
|
return this.challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-pubkeycredparams">publicKeyCredParams</a>
|
||||||
|
* params lisst the key types and signature algorithms the Relying Party Supports,
|
||||||
|
* ordered from most preferred to least preferred.
|
||||||
|
* @return the public key credential parameters
|
||||||
|
*/
|
||||||
|
public List<PublicKeyCredentialParameters> getPubKeyCredParams() {
|
||||||
|
return this.pubKeyCredParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-timeout">timeout</a>
|
||||||
|
* property specifies a time, in milliseconds, that the Relying Party is willing to
|
||||||
|
* wait for the call to complete.
|
||||||
|
* @return the timeout
|
||||||
|
*/
|
||||||
|
public Duration getTimeout() {
|
||||||
|
return this.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-excludecredentials">excludeCredentials</a>
|
||||||
|
* property is the OPTIONAL member used by the Relying Party to list any existing
|
||||||
|
* credentials mapped to this user account (as identified by user.id).
|
||||||
|
* @return exclude credentials
|
||||||
|
*/
|
||||||
|
public List<PublicKeyCredentialDescriptor> getExcludeCredentials() {
|
||||||
|
return this.excludeCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection">authenticatorSelection</a>
|
||||||
|
* property is an OPTIONAL member used by the Relying Party to list any existing
|
||||||
|
* credentials mapped to this user account (as identified by user.id).
|
||||||
|
* @return the authenticatorSelection
|
||||||
|
*/
|
||||||
|
public AuthenticatorSelectionCriteria getAuthenticatorSelection() {
|
||||||
|
return this.authenticatorSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-attestation">attestation</a>
|
||||||
|
* property is an OPTIONAL member used by the Relying Party to specify a preference
|
||||||
|
* regarding attestation conveyance.
|
||||||
|
* @return the attestation preference
|
||||||
|
*/
|
||||||
|
public AttestationConveyancePreference getAttestation() {
|
||||||
|
return this.attestation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions">extensions</a>
|
||||||
|
* property is an OPTIONAL member used by the Relying Party to provide client
|
||||||
|
* extension inputs requesting additional processing by the client and authenticator.
|
||||||
|
* @return the extensions
|
||||||
|
*/
|
||||||
|
public AuthenticationExtensionsClientInputs getExtensions() {
|
||||||
|
return this.extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link PublicKeyCredentialCreationOptions}
|
||||||
|
* @return a new {@link PublicKeyCredentialCreationOptions}
|
||||||
|
*/
|
||||||
|
public static PublicKeyCredentialCreationOptionsBuilder builder() {
|
||||||
|
return new PublicKeyCredentialCreationOptionsBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to build {@link PublicKeyCredentialCreationOptions}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public static final class PublicKeyCredentialCreationOptionsBuilder {
|
||||||
|
|
||||||
|
private PublicKeyCredentialRpEntity rp;
|
||||||
|
|
||||||
|
private PublicKeyCredentialUserEntity user;
|
||||||
|
|
||||||
|
private Bytes challenge;
|
||||||
|
|
||||||
|
private List<PublicKeyCredentialParameters> pubKeyCredParams = new ArrayList<>();
|
||||||
|
|
||||||
|
private Duration timeout;
|
||||||
|
|
||||||
|
private List<PublicKeyCredentialDescriptor> excludeCredentials = new ArrayList<>();
|
||||||
|
|
||||||
|
private AuthenticatorSelectionCriteria authenticatorSelection;
|
||||||
|
|
||||||
|
private AttestationConveyancePreference attestation;
|
||||||
|
|
||||||
|
private AuthenticationExtensionsClientInputs extensions;
|
||||||
|
|
||||||
|
private PublicKeyCredentialCreationOptionsBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getRp()} property.
|
||||||
|
* @param rp the relying party
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder rp(PublicKeyCredentialRpEntity rp) {
|
||||||
|
this.rp = rp;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getUser()} property.
|
||||||
|
* @param user the user entity
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder user(PublicKeyCredentialUserEntity user) {
|
||||||
|
this.user = user;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getChallenge()} property.
|
||||||
|
* @param challenge the challenge
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder challenge(Bytes challenge) {
|
||||||
|
this.challenge = challenge;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getPubKeyCredParams()} property.
|
||||||
|
* @param pubKeyCredParams the public key credential parameters
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder pubKeyCredParams(
|
||||||
|
PublicKeyCredentialParameters... pubKeyCredParams) {
|
||||||
|
return pubKeyCredParams(Arrays.asList(pubKeyCredParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getPubKeyCredParams()} property.
|
||||||
|
* @param pubKeyCredParams the public key credential parameters
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder pubKeyCredParams(
|
||||||
|
List<PublicKeyCredentialParameters> pubKeyCredParams) {
|
||||||
|
this.pubKeyCredParams = pubKeyCredParams;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getTimeout()} property.
|
||||||
|
* @param timeout the timeout
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder timeout(Duration timeout) {
|
||||||
|
this.timeout = timeout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getExcludeCredentials()} property.
|
||||||
|
* @param excludeCredentials the excluded credentials.
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder excludeCredentials(
|
||||||
|
List<PublicKeyCredentialDescriptor> excludeCredentials) {
|
||||||
|
this.excludeCredentials = excludeCredentials;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getAuthenticatorSelection()} property.
|
||||||
|
* @param authenticatorSelection the authenticator selection
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder authenticatorSelection(
|
||||||
|
AuthenticatorSelectionCriteria authenticatorSelection) {
|
||||||
|
this.authenticatorSelection = authenticatorSelection;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getAttestation()} property.
|
||||||
|
* @param attestation the attestation
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder attestation(AttestationConveyancePreference attestation) {
|
||||||
|
this.attestation = attestation;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getExtensions()} property.
|
||||||
|
* @param extensions the extensions
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder extensions(AuthenticationExtensionsClientInputs extensions) {
|
||||||
|
this.extensions = extensions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows customizing the builder using the {@link Consumer} that is passed in.
|
||||||
|
* @param customizer the {@link Consumer} that can be used to customize the
|
||||||
|
* {@link PublicKeyCredentialCreationOptionsBuilder}
|
||||||
|
* @return the PublicKeyCredentialCreationOptionsBuilder
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptionsBuilder customize(
|
||||||
|
Consumer<PublicKeyCredentialCreationOptionsBuilder> customizer) {
|
||||||
|
customizer.accept(this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new {@link PublicKeyCredentialCreationOptions}
|
||||||
|
* @return the new {@link PublicKeyCredentialCreationOptions}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialCreationOptions build() {
|
||||||
|
return new PublicKeyCredentialCreationOptions(this.rp, this.user, this.challenge, this.pubKeyCredParams,
|
||||||
|
this.timeout, this.excludeCredentials, this.authenticatorSelection, this.attestation,
|
||||||
|
this.extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptor">PublicKeyCredentialDescriptor</a>
|
||||||
|
* identifies a specific public key credential. It is used in create() to prevent creating
|
||||||
|
* duplicate credentials on the same authenticator, and in get() to determine if and how
|
||||||
|
* the credential can currently be reached by the client. It mirrors some fields of the
|
||||||
|
* PublicKeyCredential object returned by create() and get().
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class PublicKeyCredentialDescriptor {
|
||||||
|
|
||||||
|
private final PublicKeyCredentialType type;
|
||||||
|
|
||||||
|
private final Bytes id;
|
||||||
|
|
||||||
|
private final Set<AuthenticatorTransport> transports;
|
||||||
|
|
||||||
|
private PublicKeyCredentialDescriptor(PublicKeyCredentialType type, Bytes id,
|
||||||
|
Set<AuthenticatorTransport> transports) {
|
||||||
|
this.type = type;
|
||||||
|
this.id = id;
|
||||||
|
this.transports = transports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-type">type</a>
|
||||||
|
* property contains the type of the public key credential the caller is referring to.
|
||||||
|
* @return the type
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialType getType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-id">id</a>
|
||||||
|
* property contains the credential ID of the public key credential the caller is
|
||||||
|
* referring to.
|
||||||
|
* @return the id
|
||||||
|
*/
|
||||||
|
public Bytes getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports">transports</a>
|
||||||
|
* property is an OPTIONAL member that contains a hint as to how the client might
|
||||||
|
* communicate with the managing authenticator of the public key credential the caller
|
||||||
|
* is referring to.
|
||||||
|
* @return the transports
|
||||||
|
*/
|
||||||
|
public Set<AuthenticatorTransport> getTransports() {
|
||||||
|
return this.transports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link PublicKeyCredentialDescriptorBuilder}
|
||||||
|
* @return a new {@link PublicKeyCredentialDescriptorBuilder}
|
||||||
|
*/
|
||||||
|
public static PublicKeyCredentialDescriptorBuilder builder() {
|
||||||
|
return new PublicKeyCredentialDescriptorBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to create {@link PublicKeyCredentialDescriptor}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public static final class PublicKeyCredentialDescriptorBuilder {
|
||||||
|
|
||||||
|
private PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY;
|
||||||
|
|
||||||
|
private Bytes id;
|
||||||
|
|
||||||
|
private Set<AuthenticatorTransport> transports;
|
||||||
|
|
||||||
|
private PublicKeyCredentialDescriptorBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getType()} property.
|
||||||
|
* @param type the type
|
||||||
|
* @return the {@link PublicKeyCredentialDescriptorBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialDescriptorBuilder type(PublicKeyCredentialType type) {
|
||||||
|
this.type = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getId()} property.
|
||||||
|
* @param id the id
|
||||||
|
* @return the {@link PublicKeyCredentialDescriptorBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialDescriptorBuilder id(Bytes id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getTransports()} property.
|
||||||
|
* @param transports the transports
|
||||||
|
* @return the {@link PublicKeyCredentialDescriptorBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialDescriptorBuilder transports(Set<AuthenticatorTransport> transports) {
|
||||||
|
this.transports = transports;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getTransports()} property.
|
||||||
|
* @param transports the transports
|
||||||
|
* @return the {@link PublicKeyCredentialDescriptorBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialDescriptorBuilder transports(AuthenticatorTransport... transports) {
|
||||||
|
return transports(Set.of(transports));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link PublicKeyCredentialDescriptor}
|
||||||
|
* @return a new {@link PublicKeyCredentialDescriptor}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialDescriptor build() {
|
||||||
|
return new PublicKeyCredentialDescriptor(this.type, this.id, this.transports);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialparameters">PublicKeyCredentialParameters</a>
|
||||||
|
* is used to supply additional parameters when creating a new credential.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see PublicKeyCredentialCreationOptions#getPubKeyCredParams()
|
||||||
|
*/
|
||||||
|
public final class PublicKeyCredentialParameters {
|
||||||
|
|
||||||
|
public static final PublicKeyCredentialParameters EdDSA = new PublicKeyCredentialParameters(
|
||||||
|
COSEAlgorithmIdentifier.EdDSA);
|
||||||
|
|
||||||
|
public static final PublicKeyCredentialParameters ES256 = new PublicKeyCredentialParameters(
|
||||||
|
COSEAlgorithmIdentifier.ES256);
|
||||||
|
|
||||||
|
public static final PublicKeyCredentialParameters ES384 = new PublicKeyCredentialParameters(
|
||||||
|
COSEAlgorithmIdentifier.ES384);
|
||||||
|
|
||||||
|
public static final PublicKeyCredentialParameters ES512 = new PublicKeyCredentialParameters(
|
||||||
|
COSEAlgorithmIdentifier.ES512);
|
||||||
|
|
||||||
|
public static final PublicKeyCredentialParameters RS256 = new PublicKeyCredentialParameters(
|
||||||
|
COSEAlgorithmIdentifier.RS256);
|
||||||
|
|
||||||
|
public static final PublicKeyCredentialParameters RS384 = new PublicKeyCredentialParameters(
|
||||||
|
COSEAlgorithmIdentifier.RS384);
|
||||||
|
|
||||||
|
public static final PublicKeyCredentialParameters RS512 = new PublicKeyCredentialParameters(
|
||||||
|
COSEAlgorithmIdentifier.RS512);
|
||||||
|
|
||||||
|
public static final PublicKeyCredentialParameters RS1 = new PublicKeyCredentialParameters(
|
||||||
|
COSEAlgorithmIdentifier.RS1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This member specifies the type of credential to be created. The value SHOULD be a
|
||||||
|
* member of PublicKeyCredentialType but client platforms MUST ignore unknown values,
|
||||||
|
* ignoring any PublicKeyCredentialParameters with an unknown type.
|
||||||
|
*/
|
||||||
|
private final PublicKeyCredentialType type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This member specifies the cryptographic signature algorithm with which the newly
|
||||||
|
* generated credential will be used, and thus also the type of asymmetric key pair to
|
||||||
|
* be generated, e.g., RSA or Elliptic Curve.
|
||||||
|
*/
|
||||||
|
private final COSEAlgorithmIdentifier alg;
|
||||||
|
|
||||||
|
private PublicKeyCredentialParameters(COSEAlgorithmIdentifier alg) {
|
||||||
|
this(PublicKeyCredentialType.PUBLIC_KEY, alg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PublicKeyCredentialParameters(PublicKeyCredentialType type, COSEAlgorithmIdentifier alg) {
|
||||||
|
this.type = type;
|
||||||
|
this.alg = alg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialparameters-type">type</a>
|
||||||
|
* property member specifies the type of credential to be created.
|
||||||
|
* @return the type
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialType getType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialparameters-alg">alg</a>
|
||||||
|
* member specifies the cryptographic signature algorithm with which the newly
|
||||||
|
* generated credential will be used, and thus also the type of asymmetric key pair to
|
||||||
|
* be generated, e.g., RSA or Elliptic Curve.
|
||||||
|
* @return the algorithm
|
||||||
|
*/
|
||||||
|
public COSEAlgorithmIdentifier getAlg() {
|
||||||
|
return this.alg;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions">PublicKeyCredentialRequestOptions</a>
|
||||||
|
* contains the information to create an assertion used for authentication.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class PublicKeyCredentialRequestOptions {
|
||||||
|
|
||||||
|
private final Bytes challenge;
|
||||||
|
|
||||||
|
private final Duration timeout;
|
||||||
|
|
||||||
|
private final String rpId;
|
||||||
|
|
||||||
|
private final List<PublicKeyCredentialDescriptor> allowCredentials;
|
||||||
|
|
||||||
|
private final UserVerificationRequirement userVerification;
|
||||||
|
|
||||||
|
private final AuthenticationExtensionsClientInputs extensions;
|
||||||
|
|
||||||
|
private PublicKeyCredentialRequestOptions(Bytes challenge, Duration timeout, String rpId,
|
||||||
|
List<PublicKeyCredentialDescriptor> allowCredentials, UserVerificationRequirement userVerification,
|
||||||
|
AuthenticationExtensionsClientInputs extensions) {
|
||||||
|
Assert.notNull(challenge, "challenge cannot be null");
|
||||||
|
Assert.hasText(rpId, "rpId cannot be empty");
|
||||||
|
this.challenge = challenge;
|
||||||
|
this.timeout = timeout;
|
||||||
|
this.rpId = rpId;
|
||||||
|
this.allowCredentials = allowCredentials;
|
||||||
|
this.userVerification = userVerification;
|
||||||
|
this.extensions = extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge">challenge</a>
|
||||||
|
* property specifies a challenge that the authenticator signs, along with other data,
|
||||||
|
* when producing an authentication assertion.
|
||||||
|
* @return the challenge
|
||||||
|
*/
|
||||||
|
public Bytes getChallenge() {
|
||||||
|
return this.challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout">timeout</a>
|
||||||
|
* property is an OPTIONAL member specifies a time, in milliseconds, that the Relying
|
||||||
|
* Party is willing to wait for the call to complete.
|
||||||
|
* @return the timeout
|
||||||
|
*/
|
||||||
|
public Duration getTimeout() {
|
||||||
|
return this.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-rpid">rpId</a>
|
||||||
|
* is an OPTIONAL member specifies the RP ID claimed by the Relying Party. The client
|
||||||
|
* MUST verify that the Relying Party's origin matches the scope of this RP ID.
|
||||||
|
* @return the relying party id
|
||||||
|
*/
|
||||||
|
public String getRpId() {
|
||||||
|
return this.rpId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials">allowCredentials</a>
|
||||||
|
* property is an OPTIONAL member is used by the client to find authenticators
|
||||||
|
* eligible for this authentication ceremony.
|
||||||
|
* @return the allowCredentials property
|
||||||
|
*/
|
||||||
|
public List<PublicKeyCredentialDescriptor> getAllowCredentials() {
|
||||||
|
return this.allowCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification">userVerification</a>
|
||||||
|
* property is an OPTIONAL member specifies the Relying Party's requirements regarding
|
||||||
|
* user verification for the get() operation.
|
||||||
|
* @return the user verification
|
||||||
|
*/
|
||||||
|
public UserVerificationRequirement getUserVerification() {
|
||||||
|
return this.userVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions">extensions</a>
|
||||||
|
* is an OPTIONAL property used by the Relying Party to provide client extension
|
||||||
|
* inputs requesting additional processing by the client and authenticator.
|
||||||
|
* @return the extensions
|
||||||
|
*/
|
||||||
|
public AuthenticationExtensionsClientInputs getExtensions() {
|
||||||
|
return this.extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
*/
|
||||||
|
public static PublicKeyCredentialRequestOptionsBuilder builder() {
|
||||||
|
return new PublicKeyCredentialRequestOptionsBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to build a {@link PublicKeyCredentialCreationOptions}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public static final class PublicKeyCredentialRequestOptionsBuilder {
|
||||||
|
|
||||||
|
private Bytes challenge;
|
||||||
|
|
||||||
|
private Duration timeout = Duration.ofMinutes(5);
|
||||||
|
|
||||||
|
private String rpId;
|
||||||
|
|
||||||
|
private List<PublicKeyCredentialDescriptor> allowCredentials = Collections.emptyList();
|
||||||
|
|
||||||
|
private UserVerificationRequirement userVerification;
|
||||||
|
|
||||||
|
private AuthenticationExtensionsClientInputs extensions = new ImmutableAuthenticationExtensionsClientInputs(
|
||||||
|
new ArrayList<>());
|
||||||
|
|
||||||
|
private PublicKeyCredentialRequestOptionsBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getChallenge()} property.
|
||||||
|
* @param challenge the challenge
|
||||||
|
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRequestOptionsBuilder challenge(Bytes challenge) {
|
||||||
|
this.challenge = challenge;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getTimeout()} property.
|
||||||
|
* @param timeout the timeout
|
||||||
|
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRequestOptionsBuilder timeout(Duration timeout) {
|
||||||
|
Assert.notNull(timeout, "timeout cannot be null");
|
||||||
|
this.timeout = timeout;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getRpId()} property.
|
||||||
|
* @param rpId the rpId property
|
||||||
|
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRequestOptionsBuilder rpId(String rpId) {
|
||||||
|
this.rpId = rpId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getAllowCredentials()} property
|
||||||
|
* @param allowCredentials the allowed credentials
|
||||||
|
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRequestOptionsBuilder allowCredentials(
|
||||||
|
List<PublicKeyCredentialDescriptor> allowCredentials) {
|
||||||
|
Assert.notNull(allowCredentials, "allowCredentials cannot be null");
|
||||||
|
this.allowCredentials = allowCredentials;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getUserVerification()} property.
|
||||||
|
* @param userVerification the user verification
|
||||||
|
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRequestOptionsBuilder userVerification(UserVerificationRequirement userVerification) {
|
||||||
|
this.userVerification = userVerification;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getExtensions()} property
|
||||||
|
* @param extensions the extensions
|
||||||
|
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRequestOptionsBuilder extensions(AuthenticationExtensionsClientInputs extensions) {
|
||||||
|
this.extensions = extensions;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows customizing the {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
* @param customizer the {@link Consumer} used to customize the builder
|
||||||
|
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRequestOptionsBuilder customize(
|
||||||
|
Consumer<PublicKeyCredentialRequestOptionsBuilder> customizer) {
|
||||||
|
customizer.accept(this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new {@link PublicKeyCredentialRequestOptions}
|
||||||
|
* @return a new {@link PublicKeyCredentialRequestOptions}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRequestOptions build() {
|
||||||
|
if (this.challenge == null) {
|
||||||
|
this.challenge = Bytes.random();
|
||||||
|
}
|
||||||
|
return new PublicKeyCredentialRequestOptions(this.challenge, this.timeout, this.rpId, this.allowCredentials,
|
||||||
|
this.userVerification, this.extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrpentity">PublicKeyCredentialRpEntity</a>
|
||||||
|
* dictionary is used to supply additional Relying Party attributes when creating a new
|
||||||
|
* credential.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class PublicKeyCredentialRpEntity {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
private PublicKeyCredentialRpEntity(String name, String id) {
|
||||||
|
this.name = name;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name">name</a>
|
||||||
|
* property is a human-palatable name for the entity. Its function depends on what the
|
||||||
|
* PublicKeyCredentialEntity represents for the Relying Party, intended only for
|
||||||
|
* display.
|
||||||
|
* @return the name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrpentity-id">id</a>
|
||||||
|
* property is a unique identifier for the Relying Party entity, which sets the
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#rp-id">RP ID</a>.
|
||||||
|
* @return the relying party id
|
||||||
|
*/
|
||||||
|
public String getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link PublicKeyCredentialRpEntityBuilder}
|
||||||
|
* @return a new {@link PublicKeyCredentialRpEntityBuilder}
|
||||||
|
*/
|
||||||
|
public static PublicKeyCredentialRpEntityBuilder builder() {
|
||||||
|
return new PublicKeyCredentialRpEntityBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to create a {@link PublicKeyCredentialRpEntity}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public static final class PublicKeyCredentialRpEntityBuilder {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private PublicKeyCredentialRpEntityBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getName()} property.
|
||||||
|
* @param name the name property
|
||||||
|
* @return the {@link PublicKeyCredentialRpEntityBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRpEntityBuilder name(String name) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link #getId()} property.
|
||||||
|
* @param id the id
|
||||||
|
* @return the {@link PublicKeyCredentialRpEntityBuilder}
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRpEntityBuilder id(String id) {
|
||||||
|
this.id = id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link PublicKeyCredentialRpEntity}.
|
||||||
|
* @return a new {@link PublicKeyCredentialRpEntity}.
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRpEntity build() {
|
||||||
|
return new PublicKeyCredentialRpEntity(this.name, this.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#enum-credentialType">PublicKeyCredentialType</a>
|
||||||
|
* defines the credential types.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class PublicKeyCredentialType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The only credential type that currently exists.
|
||||||
|
*/
|
||||||
|
public static final PublicKeyCredentialType PUBLIC_KEY = new PublicKeyCredentialType("public-key");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
private PublicKeyCredentialType(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value.
|
||||||
|
* @return the value
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PublicKeyCredentialType valueOf(String value) {
|
||||||
|
if (PUBLIC_KEY.getValue().equals(value)) {
|
||||||
|
return PUBLIC_KEY;
|
||||||
|
}
|
||||||
|
return new PublicKeyCredentialType(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
|
||||||
|
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity">PublicKeyCredentialUserEntity</a>
|
||||||
|
* is used to supply additional
|
||||||
|
* <a href="https://www.w3.org/TR/webauthn-3/#user-account">user account</a> attributes
|
||||||
|
* when creating a new credential.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest)
|
||||||
|
*/
|
||||||
|
public interface PublicKeyCredentialUserEntity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id">id</a> is
|
||||||
|
* the user handle of the user account. A user handle is an opaque byte sequence with
|
||||||
|
* a maximum size of 64 bytes, and is not meant to be displayed to the user.
|
||||||
|
* @return the user handle of the user account
|
||||||
|
*/
|
||||||
|
Bytes getId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname">displayName</a>
|
||||||
|
* is a human-palatable name for the user account, intended only for display.
|
||||||
|
* @return the display name
|
||||||
|
*/
|
||||||
|
String getDisplayName();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement">ResidentKeyRequirement</a>
|
||||||
|
* describes the Relying Partys requirements for client-side discoverable credentials.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class ResidentKeyRequirement {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-discouraged">discouraged</a>
|
||||||
|
* requirement indicates that the Relying Party prefers creating a server-side
|
||||||
|
* credential, but will accept a client-side discoverable credential.
|
||||||
|
*/
|
||||||
|
public static final ResidentKeyRequirement DISCOURAGED = new ResidentKeyRequirement("discouraged");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred">preferred</a>
|
||||||
|
* requirement indicates that the Relying Party strongly prefers creating a
|
||||||
|
* client-side discoverable credential, but will accept a server-side credential.
|
||||||
|
*/
|
||||||
|
public static final ResidentKeyRequirement PREFERRED = new ResidentKeyRequirement("preferred");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-required">required</a>
|
||||||
|
* value indicates that the Relying Party requires a client-side discoverable
|
||||||
|
* credential.
|
||||||
|
*/
|
||||||
|
public static final ResidentKeyRequirement REQUIRED = new ResidentKeyRequirement("required");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
private ResidentKeyRequirement(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value.
|
||||||
|
* @return the value
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResidentKeyRequirement valueOf(String value) {
|
||||||
|
if (DISCOURAGED.getValue().equals(value)) {
|
||||||
|
return DISCOURAGED;
|
||||||
|
}
|
||||||
|
if (PREFERRED.getValue().equals(value)) {
|
||||||
|
return PREFERRED;
|
||||||
|
}
|
||||||
|
if (REQUIRED.getValue().equals(value)) {
|
||||||
|
return REQUIRED;
|
||||||
|
}
|
||||||
|
return new ResidentKeyRequirement(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement">UserVerificationRequirement</a>
|
||||||
|
* is used by the Relying Party to indicate if user verification is needed.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public final class UserVerificationRequirement {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-discouraged">discouraged</a>
|
||||||
|
* value indicates that the Relying Party does not want user verification employed
|
||||||
|
* during the operation (e.g., in the interest of minimizing disruption to the user
|
||||||
|
* interaction flow).
|
||||||
|
*/
|
||||||
|
public static final UserVerificationRequirement DISCOURAGED = new UserVerificationRequirement("discouraged");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred">preferred</a>
|
||||||
|
* value indicates that the Relying Party prefers user verification for the operation
|
||||||
|
* if possible, but will not fail the operation if the response does not have the UV
|
||||||
|
* flag set.
|
||||||
|
*/
|
||||||
|
public static final UserVerificationRequirement PREFERRED = new UserVerificationRequirement("preferred");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The <a href=
|
||||||
|
* "https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-required">required</a>
|
||||||
|
* value indicates that the Relying Party requires user verification for the operation
|
||||||
|
* and will fail the overall ceremony if the response does not have the UV flag set.
|
||||||
|
*/
|
||||||
|
public static final UserVerificationRequirement REQUIRED = new UserVerificationRequirement("required");
|
||||||
|
|
||||||
|
private final String value;
|
||||||
|
|
||||||
|
UserVerificationRequirement(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value
|
||||||
|
* @return the value
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.authentication;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link PublicKeyCredentialRequestOptionsRepository} that stores the
|
||||||
|
* {@link PublicKeyCredentialRequestOptions} in the
|
||||||
|
* {@link jakarta.servlet.http.HttpSession}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class HttpSessionPublicKeyCredentialRequestOptionsRepository
|
||||||
|
implements PublicKeyCredentialRequestOptionsRepository {
|
||||||
|
|
||||||
|
static final String DEFAULT_ATTR_NAME = PublicKeyCredentialRequestOptionsRepository.class.getName()
|
||||||
|
.concat(".ATTR_NAME");
|
||||||
|
|
||||||
|
private String attrName = DEFAULT_ATTR_NAME;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
PublicKeyCredentialRequestOptions options) {
|
||||||
|
HttpSession session = request.getSession();
|
||||||
|
session.setAttribute(this.attrName, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKeyCredentialRequestOptions load(HttpServletRequest request) {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (PublicKeyCredentialRequestOptions) session.getAttribute(this.attrName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttrName(String attrName) {
|
||||||
|
Assert.notNull(attrName, "attrName cannot be null");
|
||||||
|
this.attrName = attrName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.authentication;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
|
import org.springframework.http.server.ServletServerHttpResponse;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||||
|
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module;
|
||||||
|
import org.springframework.security.web.webauthn.management.ImmutablePublicKeyCredentialRequestOptionsRequest;
|
||||||
|
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link jakarta.servlet.Filter} that renders the
|
||||||
|
* {@link PublicKeyCredentialRequestOptions} in order to <a href=
|
||||||
|
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-get">get</a>
|
||||||
|
* a credential.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class PublicKeyCredentialRequestOptionsFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private RequestMatcher matcher = antMatcher(HttpMethod.POST, "/webauthn/authenticate/options");
|
||||||
|
|
||||||
|
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||||
|
.getContextHolderStrategy();
|
||||||
|
|
||||||
|
private final WebAuthnRelyingPartyOperations rpOptions;
|
||||||
|
|
||||||
|
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository = new HttpSessionPublicKeyCredentialRequestOptionsRepository();
|
||||||
|
|
||||||
|
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
|
||||||
|
Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance with the provided {@link WebAuthnRelyingPartyOperations}.
|
||||||
|
* @param rpOptions the {@link WebAuthnRelyingPartyOperations} to use. Cannot be null.
|
||||||
|
*/
|
||||||
|
public PublicKeyCredentialRequestOptionsFilter(WebAuthnRelyingPartyOperations rpOptions) {
|
||||||
|
Assert.notNull(rpOptions, "rpOperations cannot be null");
|
||||||
|
this.rpOptions = rpOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
if (!this.matcher.matches(request)) {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityContext context = this.securityContextHolderStrategy.getContext();
|
||||||
|
ImmutablePublicKeyCredentialRequestOptionsRequest optionsRequest = new ImmutablePublicKeyCredentialRequestOptionsRequest(
|
||||||
|
context.getAuthentication());
|
||||||
|
PublicKeyCredentialRequestOptions credentialRequestOptions = this.rpOptions
|
||||||
|
.createCredentialRequestOptions(optionsRequest);
|
||||||
|
this.requestOptionsRepository.save(request, response, credentialRequestOptions);
|
||||||
|
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
this.converter.write(credentialRequestOptions, MediaType.APPLICATION_JSON,
|
||||||
|
new ServletServerHttpResponse(response));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link PublicKeyCredentialRequestOptionsRepository} to use.
|
||||||
|
* @param requestOptionsRepository the
|
||||||
|
* {@link PublicKeyCredentialRequestOptionsRepository} to use. Cannot be null.
|
||||||
|
*/
|
||||||
|
public void setRequestOptionsRepository(PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) {
|
||||||
|
Assert.notNull(requestOptionsRepository, "requestOptionsRepository cannot be null");
|
||||||
|
this.requestOptionsRepository = requestOptionsRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link HttpMessageConverter} to use.
|
||||||
|
* @param converter the {@link HttpMessageConverter} to use. Cannot be null.
|
||||||
|
*/
|
||||||
|
public void setConverter(HttpMessageConverter<Object> converter) {
|
||||||
|
Assert.notNull(converter, "converter cannot be null");
|
||||||
|
this.converter = converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link SecurityContextHolderStrategy} to use.
|
||||||
|
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
|
||||||
|
* use. Cannot be null.
|
||||||
|
*/
|
||||||
|
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
|
||||||
|
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
|
||||||
|
this.securityContextHolderStrategy = securityContextHolderStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.authentication;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves {@link PublicKeyCredentialRequestOptions} between a request to generate an
|
||||||
|
* assertion and the validation of the assertion.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public interface PublicKeyCredentialRequestOptionsRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the provided {@link PublicKeyCredentialRequestOptions} or clears an existing
|
||||||
|
* {@link PublicKeyCredentialRequestOptions} if {@code options} is null.
|
||||||
|
* @param request the {@link HttpServletRequest}
|
||||||
|
* @param response the {@link HttpServletResponse}
|
||||||
|
* @param options the {@link PublicKeyCredentialRequestOptions} to save or null if an
|
||||||
|
* existing {@link PublicKeyCredentialRequestOptions} should be removed.
|
||||||
|
*/
|
||||||
|
void save(HttpServletRequest request, HttpServletResponse response, PublicKeyCredentialRequestOptions options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a saved {@link PublicKeyCredentialRequestOptions} if it exists, otherwise
|
||||||
|
* null.
|
||||||
|
* @param request the {@link HttpServletRequest}
|
||||||
|
* @return the {@link PublicKeyCredentialRequestOptions} that was saved, otherwise
|
||||||
|
* null.
|
||||||
|
*/
|
||||||
|
PublicKeyCredentialRequestOptions load(HttpServletRequest request);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.authentication;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link WebAuthnAuthentication} is used to represent successful authentication with
|
||||||
|
* WebAuthn.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
* @see WebAuthnAuthenticationRequestToken
|
||||||
|
*/
|
||||||
|
public class WebAuthnAuthentication extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
|
private final PublicKeyCredentialUserEntity principal;
|
||||||
|
|
||||||
|
public WebAuthnAuthentication(PublicKeyCredentialUserEntity principal,
|
||||||
|
Collection<? extends GrantedAuthority> authorities) {
|
||||||
|
super(authorities);
|
||||||
|
this.principal = principal;
|
||||||
|
super.setAuthenticated(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAuthenticated(boolean authenticated) {
|
||||||
|
Assert.isTrue(!authenticated, "Cannot set this token to trusted");
|
||||||
|
super.setAuthenticated(authenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKeyCredentialUserEntity getPrincipal() {
|
||||||
|
return this.principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.principal.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.authentication;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||||
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||||
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
|
import org.springframework.http.server.ServletServerHttpRequest;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||||
|
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
|
||||||
|
import org.springframework.security.web.authentication.HttpMessageConverterAuthenticationSuccessHandler;
|
||||||
|
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
|
||||||
|
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
|
||||||
|
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
|
||||||
|
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
|
||||||
|
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module;
|
||||||
|
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticates {@code PublicKeyCredential<AuthenticatorAssertionResponse>} that is
|
||||||
|
* parsed from the body of the {@link HttpServletRequest} using the
|
||||||
|
* {@link #setConverter(GenericHttpMessageConverter)}. An example request is provided
|
||||||
|
* below:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* {
|
||||||
|
* "id": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||||
|
* "rawId": "dYF7EGnRFFIXkpXi9XU2wg",
|
||||||
|
* "response": {
|
||||||
|
* "authenticatorData": "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
|
||||||
|
* "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
|
||||||
|
* "signature": "MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z",
|
||||||
|
* "userHandle": "Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo"
|
||||||
|
* },
|
||||||
|
* "clientExtensionResults": {},
|
||||||
|
* "authenticatorAttachment": "platform"
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class WebAuthnAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
|
||||||
|
|
||||||
|
private GenericHttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
|
||||||
|
Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build());
|
||||||
|
|
||||||
|
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository = new HttpSessionPublicKeyCredentialRequestOptionsRepository();
|
||||||
|
|
||||||
|
public WebAuthnAuthenticationFilter() {
|
||||||
|
super(antMatcher(HttpMethod.POST, "/login/webauthn"));
|
||||||
|
setSecurityContextRepository(new HttpSessionSecurityContextRepository());
|
||||||
|
setAuthenticationFailureHandler(
|
||||||
|
new AuthenticationEntryPointFailureHandler(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)));
|
||||||
|
setAuthenticationSuccessHandler(new HttpMessageConverterAuthenticationSuccessHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws AuthenticationException, IOException, ServletException {
|
||||||
|
ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request);
|
||||||
|
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(PublicKeyCredential.class,
|
||||||
|
AuthenticatorAssertionResponse.class);
|
||||||
|
PublicKeyCredential<AuthenticatorAssertionResponse> publicKeyCredential = null;
|
||||||
|
try {
|
||||||
|
publicKeyCredential = (PublicKeyCredential<AuthenticatorAssertionResponse>) this.converter
|
||||||
|
.read(resolvableType.getType(), getClass(), httpRequest);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
throw new BadCredentialsException("Unable to authenticate the PublicKeyCredential", ex);
|
||||||
|
}
|
||||||
|
PublicKeyCredentialRequestOptions requestOptions = this.requestOptionsRepository.load(request);
|
||||||
|
if (requestOptions == null) {
|
||||||
|
throw new BadCredentialsException(
|
||||||
|
"Unable to authenticate the PublicKeyCredential. No PublicKeyCredentialRequestOptions found.");
|
||||||
|
}
|
||||||
|
this.requestOptionsRepository.save(request, response, null);
|
||||||
|
RelyingPartyAuthenticationRequest authenticationRequest = new RelyingPartyAuthenticationRequest(requestOptions,
|
||||||
|
publicKeyCredential);
|
||||||
|
WebAuthnAuthenticationRequestToken token = new WebAuthnAuthenticationRequestToken(authenticationRequest);
|
||||||
|
return getAuthenticationManager().authenticate(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link GenericHttpMessageConverter} to use for writing
|
||||||
|
* {@code PublicKeyCredential<AuthenticatorAssertionResponse>} to the response. The
|
||||||
|
* default is @{code MappingJackson2HttpMessageConverter}
|
||||||
|
* @param converter the {@link GenericHttpMessageConverter} to use. Cannot be null.
|
||||||
|
*/
|
||||||
|
public void setConverter(GenericHttpMessageConverter<Object> converter) {
|
||||||
|
Assert.notNull(converter, "converter cannot be null");
|
||||||
|
this.converter = converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link PublicKeyCredentialRequestOptionsRepository} to use. The default is
|
||||||
|
* {@link HttpSessionPublicKeyCredentialRequestOptionsRepository}.
|
||||||
|
* @param requestOptionsRepository the
|
||||||
|
* {@link PublicKeyCredentialRequestOptionsRepository} to use. Cannot be null.
|
||||||
|
*/
|
||||||
|
public void setRequestOptionsRepository(PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) {
|
||||||
|
Assert.notNull(requestOptionsRepository, "requestOptionsRepository cannot be null");
|
||||||
|
this.requestOptionsRepository = requestOptionsRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
|
||||||
|
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
|
||||||
|
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link AuthenticationProvider} that uses {@link WebAuthnRelyingPartyOperations} for
|
||||||
|
* authentication using an {@link WebAuthnAuthenticationRequestToken}. First
|
||||||
|
* {@link WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest)}
|
||||||
|
* is invoked. The result is a username passed into {@link UserDetailsService}. The
|
||||||
|
* {@link UserDetails} is used to create an {@link Authentication}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class WebAuthnAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private final WebAuthnRelyingPartyOperations relyingPartyOperations;
|
||||||
|
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
* @param relyingPartyOperations the {@link WebAuthnRelyingPartyOperations} to use.
|
||||||
|
* Cannot be null.
|
||||||
|
* @param userDetailsService the {@link UserDetailsService} to use. Cannot be null.
|
||||||
|
*/
|
||||||
|
public WebAuthnAuthenticationProvider(WebAuthnRelyingPartyOperations relyingPartyOperations,
|
||||||
|
UserDetailsService userDetailsService) {
|
||||||
|
Assert.notNull(relyingPartyOperations, "relyingPartyOperations cannot be null");
|
||||||
|
Assert.notNull(userDetailsService, "userDetailsService cannot be null");
|
||||||
|
this.relyingPartyOperations = relyingPartyOperations;
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
WebAuthnAuthenticationRequestToken webAuthnRequest = (WebAuthnAuthenticationRequestToken) authentication;
|
||||||
|
try {
|
||||||
|
PublicKeyCredentialUserEntity userEntity = this.relyingPartyOperations
|
||||||
|
.authenticate(webAuthnRequest.getWebAuthnRequest());
|
||||||
|
String username = userEntity.getName();
|
||||||
|
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
|
||||||
|
return new WebAuthnAuthentication(userEntity, userDetails.getAuthorities());
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex) {
|
||||||
|
throw new BadCredentialsException(ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
return WebAuthnAuthenticationRequestToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.authentication;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
|
import org.springframework.security.core.authority.AuthorityUtils;
|
||||||
|
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link org.springframework.security.core.Authentication} used in
|
||||||
|
* {@link WebAuthnAuthenticationProvider} for authenticating via WebAuthn.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
public class WebAuthnAuthenticationRequestToken extends AbstractAuthenticationToken {
|
||||||
|
|
||||||
|
private final RelyingPartyAuthenticationRequest webAuthnRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
* @param webAuthnRequest the {@link RelyingPartyAuthenticationRequest} to use for
|
||||||
|
* authentication. Cannot be null.
|
||||||
|
*/
|
||||||
|
public WebAuthnAuthenticationRequestToken(RelyingPartyAuthenticationRequest webAuthnRequest) {
|
||||||
|
super(AuthorityUtils.NO_AUTHORITIES);
|
||||||
|
Assert.notNull(webAuthnRequest, "webAuthnRequest cannot be null");
|
||||||
|
this.webAuthnRequest = webAuthnRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the {@link RelyingPartyAuthenticationRequest}
|
||||||
|
* @return the {@link RelyingPartyAuthenticationRequest}
|
||||||
|
*/
|
||||||
|
public RelyingPartyAuthenticationRequest getWebAuthnRequest() {
|
||||||
|
return this.webAuthnRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAuthenticated(boolean authenticated) {
|
||||||
|
Assert.isTrue(!authenticated, "Cannot set this token to trusted");
|
||||||
|
super.setAuthenticated(authenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
return this.webAuthnRequest.getPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getPrincipal() {
|
||||||
|
return this.webAuthnRequest.getPublicKey().getResponse().getUserHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link AttestationConveyancePreference}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = AttestationConveyancePreferenceSerializer.class)
|
||||||
|
class AttestationConveyancePreferenceMixin {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson serializer for {@link AttestationConveyancePreference}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class AttestationConveyancePreferenceSerializer extends StdSerializer<AttestationConveyancePreference> {
|
||||||
|
|
||||||
|
AttestationConveyancePreferenceSerializer() {
|
||||||
|
super(AttestationConveyancePreference.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(AttestationConveyancePreference preference, JsonGenerator jgen, SerializerProvider provider)
|
||||||
|
throws IOException {
|
||||||
|
jgen.writeString(preference.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link AuthenticationExtensionsClientInputs}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = AuthenticationExtensionsClientInputSerializer.class)
|
||||||
|
class AuthenticationExtensionsClientInputMixin {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Jackson serialization of {@link AuthenticationExtensionsClientInput}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class AuthenticationExtensionsClientInputSerializer extends StdSerializer<AuthenticationExtensionsClientInput> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*/
|
||||||
|
AuthenticationExtensionsClientInputSerializer() {
|
||||||
|
super(AuthenticationExtensionsClientInput.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(AuthenticationExtensionsClientInput input, JsonGenerator jgen, SerializerProvider provider)
|
||||||
|
throws IOException {
|
||||||
|
jgen.writeObjectField(input.getExtensionId(), input.getInput());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link AuthenticationExtensionsClientInputs}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = AuthenticationExtensionsClientInputsSerializer.class)
|
||||||
|
class AuthenticationExtensionsClientInputsMixin {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInput;
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Jackson serialization of {@link AuthenticationExtensionsClientInputs}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class AuthenticationExtensionsClientInputsSerializer extends StdSerializer<AuthenticationExtensionsClientInputs> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*/
|
||||||
|
AuthenticationExtensionsClientInputsSerializer() {
|
||||||
|
super(AuthenticationExtensionsClientInputs.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(AuthenticationExtensionsClientInputs inputs, JsonGenerator jgen, SerializerProvider provider)
|
||||||
|
throws IOException {
|
||||||
|
jgen.writeStartObject();
|
||||||
|
for (AuthenticationExtensionsClientInput input : inputs.getInputs()) {
|
||||||
|
jgen.writeObject(input);
|
||||||
|
}
|
||||||
|
jgen.writeEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JacksonException;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.core.JsonToken;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutput;
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
|
||||||
|
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput;
|
||||||
|
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientOutputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Jackson deserialization of {@link AuthenticationExtensionsClientOutputs}.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<AuthenticationExtensionsClientOutputs> {
|
||||||
|
|
||||||
|
private static final Log logger = LogFactory.getLog(AuthenticationExtensionsClientOutputsDeserializer.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*/
|
||||||
|
AuthenticationExtensionsClientOutputsDeserializer() {
|
||||||
|
super(AuthenticationExtensionsClientOutputs.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationExtensionsClientOutputs deserialize(JsonParser parser, DeserializationContext ctxt)
|
||||||
|
throws IOException, JacksonException {
|
||||||
|
List<AuthenticationExtensionsClientOutput<?>> outputs = new ArrayList<>();
|
||||||
|
for (String key = parser.nextFieldName(); key != null; key = parser.nextFieldName()) {
|
||||||
|
JsonToken startObject = parser.nextValue();
|
||||||
|
if (startObject != JsonToken.START_OBJECT) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (CredentialPropertiesOutput.EXTENSION_ID.equals(key)) {
|
||||||
|
CredentialPropertiesOutput output = parser.readValueAs(CredentialPropertiesOutput.class);
|
||||||
|
outputs.add(output);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Skipping unknown extension with id " + key);
|
||||||
|
}
|
||||||
|
parser.nextValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ImmutableAuthenticationExtensionsClientOutputs(outputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link AuthenticationExtensionsClientOutputs}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonDeserialize(using = AuthenticationExtensionsClientOutputsDeserializer.class)
|
||||||
|
class AuthenticationExtensionsClientOutputsMixin {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link AuthenticatorAssertionResponse}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonDeserialize(builder = AuthenticatorAssertionResponse.AuthenticatorAssertionResponseBuilder.class)
|
||||||
|
class AuthenticatorAssertionResponseMixin {
|
||||||
|
|
||||||
|
@JsonPOJOBuilder(withPrefix = "")
|
||||||
|
abstract class AuthenticatorAssertionResponseBuilderMixin {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JacksonException;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson deserializer for {@link AuthenticatorAttachment}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class AuthenticatorAttachmentDeserializer extends StdDeserializer<AuthenticatorAttachment> {
|
||||||
|
|
||||||
|
AuthenticatorAttachmentDeserializer() {
|
||||||
|
super(AuthenticatorAttachment.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticatorAttachment deserialize(JsonParser parser, DeserializationContext ctxt)
|
||||||
|
throws IOException, JacksonException {
|
||||||
|
String type = parser.readValueAs(String.class);
|
||||||
|
for (AuthenticatorAttachment publicKeyCredentialType : AuthenticatorAttachment.values()) {
|
||||||
|
if (publicKeyCredentialType.getValue().equals(type)) {
|
||||||
|
return publicKeyCredentialType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link AuthenticatorAttachment}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonDeserialize(using = AuthenticatorAttachmentDeserializer.class)
|
||||||
|
@JsonSerialize(using = AuthenticatorAttachmentSerializer.class)
|
||||||
|
class AuthenticatorAttachmentMixin {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson serializer for {@link AuthenticatorAttachment}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class AuthenticatorAttachmentSerializer extends StdSerializer<AuthenticatorAttachment> {
|
||||||
|
|
||||||
|
AuthenticatorAttachmentSerializer() {
|
||||||
|
super(AuthenticatorAttachment.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(AuthenticatorAttachment attachment, JsonGenerator jgen, SerializerProvider provider)
|
||||||
|
throws IOException {
|
||||||
|
jgen.writeString(attachment.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link AuthenticatorAttestationResponse}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonDeserialize(builder = AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder.class)
|
||||||
|
class AuthenticatorAttestationResponseMixin {
|
||||||
|
|
||||||
|
@JsonPOJOBuilder(withPrefix = "")
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
abstract class AuthenticatorAttestationResponseBuilderMixin {
|
||||||
|
|
||||||
|
@JsonSetter
|
||||||
|
abstract AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder transports(
|
||||||
|
List<AuthenticatorTransport> transports);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link AuthenticatorSelectionCriteria}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
abstract class AuthenticatorSelectionCriteriaMixin {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JacksonException;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson deserializer for {@link AuthenticatorTransport}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class AuthenticatorTransportDeserializer extends StdDeserializer<AuthenticatorTransport> {
|
||||||
|
|
||||||
|
AuthenticatorTransportDeserializer() {
|
||||||
|
super(AuthenticatorTransport.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticatorTransport deserialize(JsonParser parser, DeserializationContext ctxt)
|
||||||
|
throws IOException, JacksonException {
|
||||||
|
String transportValue = parser.readValueAs(String.class);
|
||||||
|
for (AuthenticatorTransport transport : AuthenticatorTransport.values()) {
|
||||||
|
if (transport.getValue().equals(transportValue)) {
|
||||||
|
return transport;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link AuthenticatorTransport}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonDeserialize(using = AuthenticatorTransportDeserializer.class)
|
||||||
|
@JsonSerialize(using = AuthenticatorTransportSerializer.class)
|
||||||
|
class AuthenticatorTransportMixin {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson serializer for {@link AuthenticatorTransport}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class AuthenticatorTransportSerializer extends JsonSerializer<AuthenticatorTransport> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(AuthenticatorTransport transport, JsonGenerator jgen, SerializerProvider provider)
|
||||||
|
throws IOException {
|
||||||
|
jgen.writeString(transport.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.Bytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link Bytes}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = BytesSerializer.class)
|
||||||
|
final class BytesMixin {
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
static Bytes fromBase64(String value) {
|
||||||
|
return Bytes.fromBase64(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BytesMixin() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.Bytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson serializer for {@link Bytes}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class BytesSerializer extends StdSerializer<Bytes> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*/
|
||||||
|
BytesSerializer() {
|
||||||
|
super(Bytes.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(Bytes bytes, JsonGenerator jgen, SerializerProvider provider) throws IOException {
|
||||||
|
jgen.writeString(bytes.toBase64UrlString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JacksonException;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson serializer for {@link COSEAlgorithmIdentifier}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class COSEAlgorithmIdentifierDeserializer extends StdDeserializer<COSEAlgorithmIdentifier> {
|
||||||
|
|
||||||
|
COSEAlgorithmIdentifierDeserializer() {
|
||||||
|
super(COSEAlgorithmIdentifier.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public COSEAlgorithmIdentifier deserialize(JsonParser parser, DeserializationContext ctxt)
|
||||||
|
throws IOException, JacksonException {
|
||||||
|
Long transportValue = parser.readValueAs(Long.class);
|
||||||
|
for (COSEAlgorithmIdentifier identifier : COSEAlgorithmIdentifier.values()) {
|
||||||
|
if (identifier.getValue() == transportValue.longValue()) {
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link COSEAlgorithmIdentifier}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonSerialize(using = COSEAlgorithmIdentifierSerializer.class)
|
||||||
|
@JsonDeserialize(using = COSEAlgorithmIdentifierDeserializer.class)
|
||||||
|
abstract class COSEAlgorithmIdentifierMixin {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson serializer for {@link COSEAlgorithmIdentifier}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class COSEAlgorithmIdentifierSerializer extends StdSerializer<COSEAlgorithmIdentifier> {
|
||||||
|
|
||||||
|
COSEAlgorithmIdentifierSerializer() {
|
||||||
|
super(COSEAlgorithmIdentifier.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(COSEAlgorithmIdentifier identifier, JsonGenerator jgen, SerializerProvider provider)
|
||||||
|
throws IOException {
|
||||||
|
jgen.writeNumber(identifier.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
|
||||||
|
@JsonSerialize(using = CredProtectAuthenticationExtensionsClientInputSerializer.class)
|
||||||
|
class CredProtectAuthenticationExtensionsClientInputMixin {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes <a href=
|
||||||
|
* "https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension">credProtect
|
||||||
|
* extension</a>.
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
*/
|
||||||
|
class CredProtectAuthenticationExtensionsClientInputSerializer
|
||||||
|
extends StdSerializer<CredProtectAuthenticationExtensionsClientInput> {
|
||||||
|
|
||||||
|
protected CredProtectAuthenticationExtensionsClientInputSerializer() {
|
||||||
|
super(CredProtectAuthenticationExtensionsClientInput.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(CredProtectAuthenticationExtensionsClientInput input, JsonGenerator jgen,
|
||||||
|
SerializerProvider provider) throws IOException {
|
||||||
|
CredProtectAuthenticationExtensionsClientInput.CredProtect credProtect = input.getInput();
|
||||||
|
String policy = toString(credProtect.getCredProtectionPolicy());
|
||||||
|
jgen.writeObjectField("credentialProtectionPolicy", policy);
|
||||||
|
jgen.writeObjectField("enforceCredentialProtectionPolicy", credProtect.isEnforceCredentialProtectionPolicy());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toString(CredProtectAuthenticationExtensionsClientInput.CredProtect.ProtectionPolicy policy) {
|
||||||
|
switch (policy) {
|
||||||
|
case USER_VERIFICATION_OPTIONAL:
|
||||||
|
return "userVerificationOptional";
|
||||||
|
case USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST:
|
||||||
|
return "userVerificationOptionalWithCredentialIdList";
|
||||||
|
case USER_VERIFICATION_REQUIRED:
|
||||||
|
return "userVerificationRequired";
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported ProtectionPolicy " + policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson mixin for {@link CredentialPropertiesOutput}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
abstract class CredentialPropertiesOutputMixin {
|
||||||
|
|
||||||
|
CredentialPropertiesOutputMixin(@JsonProperty("rk") boolean rk) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2002-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.security.web.webauthn.jackson;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson serializer for {@link Duration}
|
||||||
|
*
|
||||||
|
* @author Rob Winch
|
||||||
|
* @since 6.4
|
||||||
|
*/
|
||||||
|
class DurationSerializer extends StdSerializer<Duration> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*/
|
||||||
|
DurationSerializer() {
|
||||||
|
super(Duration.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serialize(Duration duration, JsonGenerator jgen, SerializerProvider provider) throws IOException {
|
||||||
|
jgen.writeNumber(duration.toMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user