parent
f280aa390b
commit
b0e8730d70
|
@ -106,7 +106,7 @@ develocity {
|
|||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ dependencies {
|
|||
optional 'org.jetbrains.kotlin:kotlin-reflect'
|
||||
optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
|
||||
optional 'jakarta.annotation:jakarta.annotation-api'
|
||||
optional libs.webauthn4j.core
|
||||
|
||||
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.ServletApiConfigurer;
|
||||
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.oauth2.client.OAuth2ClientConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||
|
@ -3674,6 +3675,31 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
|||
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) {
|
||||
List<RequestMatcher> matchers = new ArrayList<>(patterns.length);
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
@ -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 subjectPrincipalRegex: String? = null
|
||||
|
||||
|
||||
internal fun get(): (X509Configurer<HttpSecurity>) -> Unit {
|
||||
return { x509 ->
|
||||
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/ldap.adoc[LDAP]
|
||||
*** xref:servlet/authentication/persistence.adoc[Persistence]
|
||||
*** xref:servlet/authentication/passkeys.adoc[Passkeys]
|
||||
*** xref:servlet/authentication/onetimetoken.adoc[One-Time Token]
|
||||
*** xref:servlet/authentication/session-management.adoc[Session Management]
|
||||
*** xref:servlet/authentication/rememberme.adoc[Remember Me]
|
||||
|
|
|
@ -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-instancio-instancio-junit = "org.instancio:instancio-junit:3.7.1"
|
||||
|
||||
webauthn4j-core = 'com.webauthn4j:webauthn4j-core:0.27.0.RELEASE'
|
||||
|
||||
[plugins]
|
||||
|
||||
org-gradle-wrapper-upgrade = "org.gradle.wrapper-upgrade:0.11.4"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
dist/
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"printWidth": 120
|
||||
}
|
|
@ -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,
|
||||
];
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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 };
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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")),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
|
||||
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 {
|
||||
javascript project(path: ':spring-security-javascript', configuration: 'javascript')
|
||||
management platform(project(":spring-security-dependencies"))
|
||||
api project(':spring-security-core')
|
||||
api 'org.springframework:spring-core'
|
||||
|
@ -16,6 +41,7 @@ dependencies {
|
|||
optional 'org.springframework:spring-tx'
|
||||
optional 'org.springframework:spring-webflux'
|
||||
optional 'org.springframework:spring-webmvc'
|
||||
optional libs.webauthn4j.core
|
||||
|
||||
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 passkeysEnabled;
|
||||
|
||||
private boolean oneTimeTokenEnabled;
|
||||
|
||||
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>> resolveHeaders = (request) -> Collections.emptyMap();
|
||||
|
||||
public DefaultLoginPageGeneratingFilter() {
|
||||
}
|
||||
|
||||
|
@ -117,6 +121,17 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
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() {
|
||||
return this.formLoginEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled;
|
||||
}
|
||||
|
@ -153,6 +168,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
this.saml2LoginEnabled = saml2LoginEnabled;
|
||||
}
|
||||
|
||||
public void setPasskeysEnabled(boolean passkeysEnabled) {
|
||||
this.passkeysEnabled = passkeysEnabled;
|
||||
}
|
||||
|
||||
public void setAuthenticationUrl(String authenticationUrl) {
|
||||
this.authenticationUrl = authenticationUrl;
|
||||
}
|
||||
|
@ -207,14 +226,46 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
|
||||
return HtmlTemplates.fromTemplate(LOGIN_PAGE_TEMPLATE)
|
||||
.withRawHtml("contextPath", contextPath)
|
||||
.withRawHtml("javaScript", renderJavaScript(request, contextPath))
|
||||
.withRawHtml("formLogin", renderFormLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
||||
.withRawHtml("oneTimeTokenLogin",
|
||||
renderOneTimeTokenLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
|
||||
.withRawHtml("oauth2Login", renderOAuth2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
||||
.withRawHtml("saml2Login", renderSaml2Login(loginError, logoutSuccess, errorMsg, contextPath))
|
||||
.withRawHtml("passkeyLogin", renderPasskeyLogin())
|
||||
.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,
|
||||
String contextPath, String errorMsg) {
|
||||
if (!this.formLoginEnabled) {
|
||||
|
@ -235,6 +286,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
.withValue("passwordParameter", this.passwordParameter)
|
||||
.withRawHtml("rememberMeInput", renderRememberMe(this.rememberMeParameter))
|
||||
.withRawHtml("hiddenInputs", hiddenInputs)
|
||||
.withValue("autocomplete", this.passkeysEnabled ? "autocomplete=\"password webauthn\"" : "")
|
||||
.render();
|
||||
}
|
||||
|
||||
|
@ -383,6 +435,26 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
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 = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
@ -392,12 +464,12 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<title>Please sign in</title>
|
||||
<link href="{{contextPath}}/default-ui.css" rel="stylesheet" />
|
||||
<link href="{{contextPath}}/default-ui.css" rel="stylesheet" />{{javaScript}}
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
{{formLogin}}
|
||||
{{oneTimeTokenLogin}}
|
||||
{{oneTimeTokenLogin}}{{passkeyLogin}}
|
||||
{{oauth2Login}}
|
||||
{{saml2Login}}
|
||||
</div>
|
||||
|
@ -414,7 +486,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
</p>
|
||||
<p>
|
||||
<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>
|
||||
{{rememberMeInput}}
|
||||
{{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…
Reference in New Issue