Add Passkeys Support

Closes gh-13305
This commit is contained in:
Rob Winch 2024-10-17 21:40:51 -05:00
parent f280aa390b
commit b0e8730d70
157 changed files with 19203 additions and 5 deletions

View File

@ -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))
}

View File

@ -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'

View File

@ -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>
* &#064;Bean
* SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
* http
* // ...
* .webAuthn((webAuthn) -&gt; 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) {

View File

@ -0,0 +1,194 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
}
}

View File

@ -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.
*

View File

@ -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);
}
}
}

View File

@ -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) }

View File

@ -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)
}
}
}

View File

@ -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]

View File

@ -0,0 +1,289 @@
[[passkeys]]
= Passkeys
Spring Security provides support for https://www.passkeys.com[passkeys].
Passkeys are a more secure method of authenticating than passwords and are built using https://www.w3.org/TR/webauthn-3/[WebAuthn].
In order to use a passkey to authenticate, a user must first xref:servlet/authentication/passkeys.adoc#passkeys-register[Register a New Credential].
After the credential is registered, it can be used to authenticate by xref:servlet/authentication/passkeys.adoc#passkeys-verify[verifying an authentication assertion].
[[passkeys-configuration]]
== Configuration
The following configuration enables passkey authentication.
It provides a way to xref:./passkeys.adoc#passkeys-register[] at `/webauthn/register` and a default log in page that allows xref:./passkeys.adoc#passkeys-verify[authenticating with passkeys].
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(withDefaults())
.webAuthn((webAuthn) -> webAuthn
.rpName("Spring Security Relying Party")
.rpId("example.com")
.allowedOrigins("https://example.com")
);
return http.build();
}
@Bean
UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
webAuthn {
rpName = "Spring Security Relying Party"
rpId = "example.com"
allowedOrigins = setOf("https://example.com")
}
}
}
@Bean
open fun userDetailsService(): UserDetailsService {
val userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
return InMemoryUserDetailsManager(userDetails)
}
----
======
[[passkeys-register]]
== Register a New Credential
In order to use a passkey, a user must first https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential[Register a New Credential].
Registering a new credential is composed of two steps:
1. Requesting the Registration Options
2. Registering the Credential
[[passkeys-register-options]]
=== Request the Registration Options
The first step in registration of a new credential is to request the registration options.
In Spring Security, a request for the registration options is typically done using JavaScript and looks like:
[NOTE]
====
Spring Security provides a default registration page that can be used as a reference on how to register credentials.
====
.Request for Registration Options
[source,http]
----
POST /webauthn/register/options
X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
----
The request above will obtain the registration options for the currently authenticated user.
Since the challenge is persisted (state is changed) to be compared at the time of registration, the request must be a POST and include a CSRF token.
.Response for Registration Options
[source,json]
----
{
"rp": {
"name": "SimpleWebAuthn Example",
"id": "example.localhost"
},
"user": {
"name": "user@example.localhost",
"id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w",
"displayName": "user@example.localhost"
},
"challenge": "q7lCdd3SVQxdC-v8pnRAGEn1B2M-t7ZECWPwCAmhWvc",
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -8
},
{
"type": "public-key",
"alg": -7
},
{
"type": "public-key",
"alg": -257
}
],
"timeout": 300000,
"excludeCredentials": [],
"authenticatorSelection": {
"residentKey": "required",
"userVerification": "preferred"
},
"attestation": "direct",
"extensions": {
"credProps": true
}
}
----
[[passkeys-register-create]]
=== Registering the Credential
After the registration options are obtained, they are used to create the credentials that are registered.
To register a new credential, the application should pass the options to https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create[`navigator.credentials.create`] after base64url decoding the binary values such as `user.id`, `challenge`, and `excludeCredentials[].id`.
The returned value can then be sent to the server as a JSON request.
An example registration request can be found below:
.Example Registration Request
[source,http]
----
POST /webauthn/register
X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
{
"publicKey": { // <1>
"credential": {
"id": "dYF7EGnRFFIXkpXi9XU2wg",
"rawId": "dYF7EGnRFFIXkpXi9XU2wg",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAALraVWanqkAfvZZFYZpVEg0AEHWBexBp0RRSF5KV4vV1NsKlAQIDJiABIVggQjmrekPGzyqtoKK9HPUH-8Z2FLpoqkklFpFPQVICQ3IiWCD6I9Jvmor685fOZOyGXqUd87tXfvJk8rxj9OhuZvUALA",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSl9RTi10SFJYRWVKYjlNcUNrWmFPLUdOVmlibXpGVGVWMk43Z0ptQUdrQSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"transports": [
"internal",
"hybrid"
]
},
"type": "public-key",
"clientExtensionResults": {},
"authenticatorAttachment": "platform"
},
"label": "1password" // <2>
}
}
----
<1> The result of calling `navigator.credentials.create` with binary values base64url encoded.
<2> A label that the user selects to have associated with this credential to help the user distinguish the credential.
.Example Successful Registration Response
[source,http]
----
HTTP/1.1 200 OK
{
"success": true
}
----
[[passkeys-verify]]
== Verifying an Authentication Assertion
After xref:./passkeys.adoc#passkeys-register[] the passkey can be https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion[verified] (authenticated).
Verifying a credential is composed of two steps:
1. Requesting the Verification Options
2. Verifying the Credential
[[passkeys-verify-options]]
=== Request the Verification Options
The first step in verification of a credential is to request the verification options.
In Spring Security, a request for the verification options is typically done using JavaScript and looks like:
[NOTE]
====
Spring Security provides a default log in page that can be used as a reference on how to verify credentials.
====
.Request for Verification Options
[source,http]
----
POST /webauthn/authenticate/options
X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
----
The request above will obtain the verification options.
Since the challenge is persisted (state is changed) to be compared at the time of authentication, the request must be a POST and include a CSRF token.
The response will contain the options for obtaining a credential with binary values such as `challenge` base64url encoded.
.Example Response for Verification Options
[source,json]
----
{
"challenge": "cQfdGrj9zDg3zNBkOH3WPL954FTOShVy0-CoNgSewNM",
"timeout": 300000,
"rpId": "example.localhost",
"allowCredentials": [],
"userVerification": "preferred",
"extensions": {}
}
----
[[passkeys-verify-get]]
=== Verifying the Credential
After the verification options are obtained, they are used to get a credential.
To get a credential, the application should pass the options to https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create[`navigator.credentials.get`] after base64url decoding the binary values such as `challenge`.
The returned value of `navigator.credentials.get` can then be sent to the server as a JSON request.
Binary values such as `rawId` and `response.*` must be base64url encoded.
An example authentication request can be found below:
.Example Authentication Request
[source,http]
----
POST /login/webauthn
X-CSRF-TOKEN: 4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
{
"id": "dYF7EGnRFFIXkpXi9XU2wg",
"rawId": "dYF7EGnRFFIXkpXi9XU2wg",
"response": {
"authenticatorData": "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"signature": "MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z",
"userHandle": "Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo"
},
"clientExtensionResults": {},
"authenticatorAttachment": "platform"
}
----
.Example Successful Authentication Response
[source,http]
----
HTTP/1.1 200 OK
{
"redirectUrl": "/", // <1>
"authenticated": true // <2>
}
----
<1> The URL to redirect to
<2> Indicates that the user is authenticated
.Example Authentication Failure Response
[source,http]
----
HTTP/1.1 401 OK
----

View File

@ -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"

2
javascript/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
dist/

3
javascript/.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"printWidth": 120
}

View File

@ -0,0 +1,30 @@
import globals from "globals";
import eslintConfigPrettier from "eslint-plugin-prettier/recommended";
export default [
{
ignores: ["build/**/*"],
},
{
files: ["lib/**/*.js"],
languageOptions: {
sourceType: "module",
globals: {
...globals.browser,
gobalThis: "readonly",
},
},
},
{
files: ["test/**/*.js"],
languageOptions: {
globals: {
...globals.browser,
...globals.mocha,
...globals.chai,
...globals.nodeBuiltin,
},
},
},
eslintConfigPrettier,
];

View File

@ -0,0 +1,43 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
const holder = {
controller: new AbortController(),
};
/**
* Returns a new AbortSignal to be used in the options for the registration and authentication ceremonies.
* Aborts the existing AbortController if it exists, cancelling any existing ceremony.
*
* The authentication ceremony, when triggered with conditional mediation, shows a non-modal
* interaction. If the user does not interact with the non-modal dialog, the existing ceremony MUST
* be cancelled before initiating a new one, hence the need for a singleton AbortController.
*
* @returns {AbortSignal} a new, non-aborted AbortSignal
*/
function newSignal() {
if (!!holder.controller) {
holder.controller.abort("Initiating new WebAuthN ceremony, cancelling current ceremony");
}
holder.controller = new AbortController();
return holder.controller.signal;
}
export default {
newSignal,
};

View File

@ -0,0 +1,33 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
export default {
encode: function (buffer) {
const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer)));
return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
},
decode: function (base64url) {
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
const binStr = window.atob(base64);
const bin = new Uint8Array(binStr.length);
for (let i = 0; i < binStr.length; i++) {
bin[i] = binStr.charCodeAt(i);
}
return bin.buffer;
},
};

33
javascript/lib/http.js Normal file
View File

@ -0,0 +1,33 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
async function post(url, headers, body) {
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
...headers,
},
};
if (body) {
options.body = JSON.stringify(body);
}
return fetch(url, options);
}
export default { post };

24
javascript/lib/index.js Normal file
View File

@ -0,0 +1,24 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import { setupLogin } from "./webauthn-login.js";
import { setupRegistration } from "./webauthn-registration.js";
// Make "setup" available in the window domain, so it can be run with "setupLogin()"
window.setupLogin = setupLogin;
window.setupRegistration = setupRegistration;

View File

@ -0,0 +1,194 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import base64url from "./base64url.js";
import http from "./http.js";
import abortController from "./abort-controller.js";
async function isConditionalMediationAvailable() {
return !!(
window.PublicKeyCredential &&
window.PublicKeyCredential.isConditionalMediationAvailable &&
(await window.PublicKeyCredential.isConditionalMediationAvailable())
);
}
async function authenticate(headers, contextPath, useConditionalMediation) {
let options;
try {
const optionsResponse = await http.post(`${contextPath}/webauthn/authenticate/options`, headers);
if (!optionsResponse.ok) {
throw new Error(`HTTP ${optionsResponse.status}`);
}
options = await optionsResponse.json();
} catch (err) {
throw new Error(`Authentication failed. Could not fetch authentication options: ${err.message}`, { cause: err });
}
// FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseRequestOptionsFromJSON
const decodedOptions = {
...options,
challenge: base64url.decode(options.challenge),
};
// Invoke the WebAuthn get() method.
const credentialOptions = {
publicKey: decodedOptions,
signal: abortController.newSignal(),
};
if (useConditionalMediation) {
// Request a conditional UI
credentialOptions.mediation = "conditional";
}
let cred;
try {
cred = await navigator.credentials.get(credentialOptions);
} catch (err) {
throw new Error(`Authentication failed. Call to navigator.credentials.get failed: ${err.message}`, { cause: err });
}
const { response, type: credType } = cred;
let userHandle;
if (response.userHandle) {
userHandle = base64url.encode(response.userHandle);
}
const body = {
id: cred.id,
rawId: base64url.encode(cred.rawId),
response: {
authenticatorData: base64url.encode(response.authenticatorData),
clientDataJSON: base64url.encode(response.clientDataJSON),
signature: base64url.encode(response.signature),
userHandle,
},
credType,
clientExtensionResults: cred.getClientExtensionResults(),
authenticatorAttachment: cred.authenticatorAttachment,
};
let authenticationResponse;
try {
const authenticationCallResponse = await http.post(`${contextPath}/login/webauthn`, headers, body);
if (!authenticationCallResponse.ok) {
throw new Error(`HTTP ${authenticationCallResponse.status}`);
}
authenticationResponse = await authenticationCallResponse.json();
// if (authenticationResponse && authenticationResponse.authenticated) {
} catch (err) {
throw new Error(`Authentication failed. Could not process the authentication request: ${err.message}`, {
cause: err,
});
}
if (!(authenticationResponse && authenticationResponse.authenticated && authenticationResponse.redirectUrl)) {
throw new Error(
`Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: ${JSON.stringify(authenticationResponse)}`,
);
}
return authenticationResponse.redirectUrl;
}
async function register(headers, contextPath, label) {
if (!label) {
throw new Error("Error: Passkey Label is required");
}
let options;
try {
const optionsResponse = await http.post(`${contextPath}/webauthn/register/options`, headers);
if (!optionsResponse.ok) {
throw new Error(`Server responded with HTTP ${optionsResponse.status}`);
}
options = await optionsResponse.json();
} catch (e) {
throw new Error(`Registration failed. Could not fetch registration options: ${e.message}`, { cause: e });
}
// FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON
const decodedExcludeCredentials = !options.excludeCredentials
? []
: options.excludeCredentials.map((cred) => ({
...cred,
id: base64url.decode(cred.id),
}));
const decodedOptions = {
...options,
user: {
...options.user,
id: base64url.decode(options.user.id),
},
challenge: base64url.decode(options.challenge),
excludeCredentials: decodedExcludeCredentials,
};
let credentialsContainer;
try {
credentialsContainer = await navigator.credentials.create({
publicKey: decodedOptions,
signal: abortController.newSignal(),
});
} catch (e) {
throw new Error(`Registration failed. Call to navigator.credentials.create failed: ${e.message}`, { cause: e });
}
// FIXME: Let response be credential.response. If response is not an instance of AuthenticatorAttestationResponse, abort the ceremony with a user-visible error. https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
const { response } = credentialsContainer;
const credential = {
id: credentialsContainer.id,
rawId: base64url.encode(credentialsContainer.rawId),
response: {
attestationObject: base64url.encode(response.attestationObject),
clientDataJSON: base64url.encode(response.clientDataJSON),
transports: response.getTransports ? response.getTransports() : [],
},
type: credentialsContainer.type,
clientExtensionResults: credentialsContainer.getClientExtensionResults(),
authenticatorAttachment: credentialsContainer.authenticatorAttachment,
};
const registrationRequest = {
publicKey: {
credential: credential,
label: label,
},
};
let verificationJSON;
try {
const verificationResp = await http.post(`${contextPath}/webauthn/register`, headers, registrationRequest);
if (!verificationResp.ok) {
throw new Error(`HTTP ${verificationResp.status}`);
}
verificationJSON = await verificationResp.json();
} catch (e) {
throw new Error(`Registration failed. Could not process the registration request: ${e.message}`, { cause: e });
}
if (!(verificationJSON && verificationJSON.success)) {
throw new Error(`Registration failed. Server responded with: ${JSON.stringify(verificationJSON)}`);
}
}
export default {
authenticate,
register,
isConditionalMediationAvailable,
};

View File

@ -0,0 +1,47 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import webauthn from "./webauthn-core.js";
async function authenticateOrError(headers, contextPath, useConditionalMediation) {
try {
const redirectUrl = await webauthn.authenticate(headers, contextPath, useConditionalMediation);
window.location.href = redirectUrl;
} catch (err) {
console.error(err);
window.location.href = `${contextPath}/login?error`;
}
}
async function conditionalMediation(headers, contextPath) {
const available = await webauthn.isConditionalMediationAvailable();
if (available) {
await authenticateOrError(headers, contextPath, true);
}
return available;
}
export async function setupLogin(headers, contextPath, signinButton) {
signinButton.addEventListener("click", async () => {
await authenticateOrError(headers, contextPath, false);
});
// FIXME: conditional mediation triggers browser crashes
// See: https://github.com/rwinch/spring-security-webauthn/issues/73
// await conditionalMediation(headers, contextPath);
}

View File

@ -0,0 +1,108 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import webauthn from "./webauthn-core.js";
function setVisibility(element, value) {
if (!element) {
return;
}
element.style.display = value ? "block" : "none";
}
function setError(ui, msg) {
resetPopups(ui);
const error = ui.getError();
if (!error) {
return;
}
error.textContent = msg;
setVisibility(error, true);
}
function setSuccess(ui) {
resetPopups(ui);
const success = ui.getSuccess();
if (!success) {
return;
}
setVisibility(success, true);
}
function resetPopups(ui) {
const success = ui.getSuccess();
const error = ui.getError();
setVisibility(success, false);
setVisibility(error, false);
}
async function submitDeleteForm(contextPath, form, headers) {
const options = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
...headers,
},
};
await fetch(form.action, options);
}
/**
*
* @param headers headers added to the credentials creation POST request, typically CSRF
* @param contextPath the contextPath from which the app is served
* @param ui contains getRegisterButton(), getSuccess(), getError(), getLabelInput(), getDeleteForms()
* @returns {Promise<void>}
*/
export async function setupRegistration(headers, contextPath, ui) {
resetPopups(ui);
if (!window.PublicKeyCredential) {
setError(ui, "WebAuthn is not supported");
return;
}
const queryString = new URLSearchParams(window.location.search);
if (queryString.has("success")) {
setSuccess(ui);
}
ui.getRegisterButton().addEventListener("click", async () => {
resetPopups(ui);
const label = ui.getLabelInput().value;
try {
await webauthn.register(headers, contextPath, label);
window.location.href = `${contextPath}/webauthn/register?success`;
} catch (err) {
setError(ui, err.message);
console.error(err);
}
});
ui.getDeleteForms().forEach((form) =>
form.addEventListener("submit", async function (e) {
e.preventDefault();
try {
await submitDeleteForm(contextPath, form, headers);
window.location.href = `${contextPath}/webauthn/register?success`;
} catch (err) {
setError(ui, err.message);
}
}),
);
}

5465
javascript/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
javascript/package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "@springprojects/spring-security-webauthn",
"version": "1.0.0-alpha.9",
"description": "WebAuthN JS library for Spring Security",
"license": "ASL-2.0",
"author": "????",
"contributors": [
"Rob Winch <rwinch@users.noreply.github.com>",
"Daniel Garnier-Moiroux <git@garnier.wf>"
],
"repository": "github:spring-projects/spring-security",
"bugs": {
"url": "https://github.com/spring-projects/spring-security/issues"
},
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"test": "mocha",
"check": "npm test && npm run lint",
"test:watch": "mocha --watch --parallel",
"assemble": "esbuild lib/index.js --bundle --outfile=build/dist/spring-security-webauthn.js",
"build": "npm run check && npm run assemble",
"lint": "eslint",
"format": "npm run lint -- --fix"
},
"main": "lib/index.js",
"files": [
"lib"
],
"keywords": [
"Spring Security",
"WebAuthn",
"passkeys"
],
"devDependencies": {
"@eslint/js": "^9.6.0",
"@types/sinon": "^17.0.3",
"chai": "~4.3",
"esbuild": "^0.23.0",
"eslint": "^9.6.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"globals": "^15.8.0",
"mocha": "~10.2",
"prettier": "^3.3.2",
"prettier-eslint": "~15.0",
"sinon": "^18.0.0"
},
"type": "module"
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id 'base'
id 'com.github.node-gradle.node' version '7.1.0'
}
node {
download = true
version = '20.17.0'
}
tasks.named('check') {
dependsOn 'npm_run_check'
}
tasks.register('dist', Zip) {
dependsOn 'npm_run_assemble'
from 'build/dist/spring-security.js'
into 'org/springframework/security'
}
configurations {
javascript {
canBeConsumed = true
canBeResolved = false
}
}
artifacts {
javascript(project.layout.buildDirectory.dir('dist')) {
builtBy(npm_run_assemble)
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import "./bootstrap.js";
import abortController from "../lib/abort-controller.js";
import { expect } from "chai";
describe("abort-controller", () => {
describe("newSignal", () => {
it("returns an AbortSignal", () => {
const signal = abortController.newSignal();
expect(signal).to.be.instanceof(AbortSignal);
expect(signal.aborted).to.be.false;
});
it("returns a new signal every time", () => {
const initialSignal = abortController.newSignal();
const newSignal = abortController.newSignal();
expect(initialSignal).to.not.equal(newSignal);
});
it("aborts the existing signal", () => {
const signal = abortController.newSignal();
abortController.newSignal();
expect(signal.aborted).to.be.true;
expect(signal.reason).to.equal("Initiating new WebAuthN ceremony, cancelling current ceremony");
});
});
});

View File

@ -0,0 +1,76 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import { expect } from "chai";
import base64url from "../lib/base64url.js";
describe("base64url", () => {
before(() => {
// Emulate the atob / btoa base64 encoding/decoding from the browser
global.window = {
btoa: (str) => Buffer.from(str, "binary").toString("base64"),
atob: (b64) => Buffer.from(b64, "base64").toString("binary"),
};
});
after(() => {
// Reset window object
global.window = {};
});
it("decodes", () => {
// "Zm9vYmFy" is "foobar" in base 64, i.e. f:102 o:111 o:111 b:98 a:97 r:114
const decoded = base64url.decode("Zm9vYmFy");
expect(new Uint8Array(decoded)).to.be.deep.equal(new Uint8Array([102, 111, 111, 98, 97, 114]));
});
it("decodes special characters", () => {
// Wrap the decode function for easy testing
const decode = (str) => {
const decoded = new Uint8Array(base64url.decode(str));
return Array.from(decoded);
};
// "Pz8/" is "???" in base64, i.e. ?:63 three times
expect(decode("Pz8/")).to.be.deep.equal(decode("Pz8_"));
expect(decode("Pz8_")).to.be.deep.equal([63, 63, 63]);
// "Pj4+" is ">>>" in base64, ie >:62 three times
expect(decode("Pj4+")).to.be.deep.equal(decode("Pj4-"));
expect(decode("Pj4-")).to.be.deep.equal([62, 62, 62]);
});
it("encodes", () => {
const encoded = base64url.encode(Buffer.from("foobar"));
expect(encoded).to.be.equal("Zm9vYmFy");
});
it("encodes special +/ characters", () => {
const encode = (str) => base64url.encode(Buffer.from(str));
expect(encode("???")).to.be.equal("Pz8_");
expect(encode(">>>")).to.be.equal("Pj4-");
});
it("is stable", () => {
const base = "tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g";
expect(base64url.encode(base64url.decode(base))).to.be.equal(base);
});
});

22
javascript/test/bootstrap.js vendored Normal file
View File

@ -0,0 +1,22 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import chai from "chai";
// Show full diffs when there is an equality difference an assertion.
// By default, chai truncates at 40 characters, making it difficult to
// compare e.g. error messages
chai.config.truncateThreshold = 0;

View File

@ -0,0 +1,65 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import http from "../lib/http.js";
import { expect } from "chai";
import { fake, assert } from "sinon";
describe("http", () => {
beforeEach(() => {
global.fetch = fake.resolves({ ok: true });
});
afterEach(() => {
delete global.fetch;
});
describe("post", () => {
it("calls fetch with headers", async () => {
const url = "https://example.com/some/path";
const headers = { "x-custom": "some-value" };
const resp = await http.post(url, headers);
expect(resp.ok).to.be.true;
assert.calledOnceWithExactly(global.fetch, url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...headers,
},
});
});
it("sends the body as a JSON string", async () => {
const body = { foo: "bar", baz: 42 };
const url = "https://example.com/some/path";
const resp = await http.post(url, {}, body);
expect(resp.ok).to.be.true;
assert.calledOnceWithExactly(global.fetch, url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: `{"foo":"bar","baz":42}`,
});
});
});
});

View File

@ -0,0 +1,697 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import "./bootstrap.js";
import { expect } from "chai";
import { assert, fake, match, stub } from "sinon";
import http from "../lib/http.js";
import webauthn from "../lib/webauthn-core.js";
import base64url from "../lib/base64url.js";
describe("webauthn-core", () => {
beforeEach(() => {
global.window = {
btoa: (str) => Buffer.from(str, "binary").toString("base64"),
atob: (b64) => Buffer.from(b64, "base64").toString("binary"),
};
});
afterEach(() => {
delete global.window;
});
describe("isConditionalMediationAvailable", () => {
afterEach(() => {
delete global.window.PublicKeyCredential;
});
it("is available", async () => {
global.window = {
PublicKeyCredential: {
isConditionalMediationAvailable: fake.resolves(true),
},
};
const result = await webauthn.isConditionalMediationAvailable();
expect(result).to.be.true;
});
describe("is not available", async () => {
it("PublicKeyCredential does not exist", async () => {
global.window = {};
const result = await webauthn.isConditionalMediationAvailable();
expect(result).to.be.false;
});
it("PublicKeyCredential.isConditionalMediationAvailable undefined", async () => {
global.window = {
PublicKeyCredential: {},
};
const result = await webauthn.isConditionalMediationAvailable();
expect(result).to.be.false;
});
it("PublicKeyCredential.isConditionalMediationAvailable false", async () => {
global.window = {
PublicKeyCredential: {
isConditionalMediationAvailable: fake.resolves(false),
},
};
const result = await webauthn.isConditionalMediationAvailable();
expect(result).to.be.false;
});
});
});
describe("authenticate", () => {
let httpPostStub;
const contextPath = "/some/path";
const credentialsGetOptions = {
challenge: "nRbOrtNKTfJ1JaxfUDKs8j3B-JFqyGQw8DO4u6eV3JA",
timeout: 300000,
rpId: "localhost",
allowCredentials: [],
userVerification: "preferred",
extensions: {},
};
// This is kind of a self-fulfilling prophecy type of test: we produce array buffers by calling
// base64url.decode ; they will then be re-encoded to the same string in the production code.
// The ArrayBuffer API is not super friendly.
beforeEach(() => {
httpPostStub = stub(http, "post");
httpPostStub.withArgs(contextPath + "/webauthn/authenticate/options", match.any).resolves({
ok: true,
status: 200,
json: fake.resolves(credentialsGetOptions),
});
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
ok: true,
status: 200,
json: fake.resolves({
authenticated: true,
redirectUrl: "/success",
}),
});
const validAuthenticatorResponse = {
id: "UgghgP5QKozwsSUK1twCj8mpgZs",
rawId: base64url.decode("UgghgP5QKozwsSUK1twCj8mpgZs"),
response: {
authenticatorData: base64url.decode("y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA"),
clientDataJSON: base64url.decode(
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUTdlR0NkNUw2cG9fa01meWNIQnBWRlR5dmd3RklCV0QxZWg5OUktRFhnWSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
),
signature: base64url.decode(
"MEUCIGT9PAWfU3lMicOXFMpHGcl033dY-sNSJvehlXvvoivyAiEA_D_yOsChERlXX2rFcK6Qx5BaAbx5qdU2hgYDVN6W770",
),
userHandle: base64url.decode("tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g"),
},
getClientExtensionResults: () => ({}),
authenticatorAttachment: "platform",
type: "public-key",
};
global.navigator = {
credentials: {
get: fake.resolves(validAuthenticatorResponse),
},
};
});
afterEach(() => {
http.post.restore();
delete global.navigator;
});
it("succeeds", async () => {
const redirectUrl = await webauthn.authenticate({ "x-custom": "some-value" }, contextPath, false);
expect(redirectUrl).to.equal("/success");
assert.calledWith(
httpPostStub.lastCall,
`${contextPath}/login/webauthn`,
{ "x-custom": "some-value" },
{
id: "UgghgP5QKozwsSUK1twCj8mpgZs",
rawId: "UgghgP5QKozwsSUK1twCj8mpgZs",
credType: "public-key",
response: {
authenticatorData: "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
clientDataJSON:
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUTdlR0NkNUw2cG9fa01meWNIQnBWRlR5dmd3RklCV0QxZWg5OUktRFhnWSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
signature:
"MEUCIGT9PAWfU3lMicOXFMpHGcl033dY-sNSJvehlXvvoivyAiEA_D_yOsChERlXX2rFcK6Qx5BaAbx5qdU2hgYDVN6W770",
userHandle: "tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g",
},
clientExtensionResults: {},
authenticatorAttachment: "platform",
},
);
});
it("calls the authenticator with the correct options", async () => {
await webauthn.authenticate({}, contextPath, false);
assert.calledOnceWithMatch(global.navigator.credentials.get, {
publicKey: {
challenge: base64url.decode("nRbOrtNKTfJ1JaxfUDKs8j3B-JFqyGQw8DO4u6eV3JA"),
timeout: 300000,
rpId: "localhost",
allowCredentials: [],
userVerification: "preferred",
extensions: {},
},
signal: match.any,
});
});
describe("authentication failures", () => {
it("when authentication options call", async () => {
httpPostStub
.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any)
.rejects(new Error("Connection refused"));
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Authentication failed. Could not fetch authentication options: Connection refused",
);
return;
}
expect.fail("authenticate should throw");
});
it("when authentication options call returns does not return HTTP 200 OK", async () => {
httpPostStub.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any).resolves({
ok: false,
status: 400,
});
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal("Authentication failed. Could not fetch authentication options: HTTP 400");
return;
}
expect.fail("authenticate should throw");
});
it("when authentication options are not valid json", async () => {
httpPostStub.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any).resolves({
ok: true,
status: 200,
json: fake.rejects(new Error("Not valid JSON")),
});
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal("Authentication failed. Could not fetch authentication options: Not valid JSON");
return;
}
expect.fail("authenticate should throw");
});
it("when navigator.credentials.get fails", async () => {
global.navigator.credentials.get = fake.rejects(new Error("Operation was aborted"));
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Authentication failed. Call to navigator.credentials.get failed: Operation was aborted",
);
return;
}
expect.fail("authenticate should throw");
});
it("when authentication call fails", async () => {
httpPostStub
.withArgs(`${contextPath}/login/webauthn`, match.any, match.any)
.rejects(new Error("Connection refused"));
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Authentication failed. Could not process the authentication request: Connection refused",
);
return;
}
expect.fail("authenticate should throw");
});
it("when authentication call does not return HTTP 200 OK", async () => {
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
ok: false,
status: 400,
});
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal("Authentication failed. Could not process the authentication request: HTTP 400");
return;
}
expect.fail("authenticate should throw");
});
it("when authentication call does not return JSON", async () => {
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
ok: true,
status: 200,
json: fake.rejects(new Error("Not valid JSON")),
});
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Authentication failed. Could not process the authentication request: Not valid JSON",
);
return;
}
expect.fail("authenticate should throw");
});
it("when authentication call returns null", async () => {
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
ok: true,
status: 200,
json: fake.resolves(null),
});
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: null',
);
return;
}
expect.fail("authenticate should throw");
});
it('when authentication call returns {"authenticated":false}', async () => {
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
ok: true,
status: 200,
json: fake.resolves({
authenticated: false,
}),
});
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: {"authenticated":false}',
);
return;
}
expect.fail("authenticate should throw");
});
it("when authentication call returns no redirectUrl", async () => {
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({
ok: true,
status: 200,
json: fake.resolves({
authenticated: true,
}),
});
try {
await webauthn.authenticate({}, contextPath, false);
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: {"authenticated":true}',
);
return;
}
expect.fail("authenticate should throw");
});
});
});
describe("register", () => {
let httpPostStub;
const contextPath = "/some/path";
beforeEach(() => {
const credentialsCreateOptions = {
rp: {
name: "Spring Security Relying Party",
id: "example.localhost",
},
user: {
name: "user",
id: "eatPy60xmXG_58JrIiIBa5wq8Y76c7MD6mnY5vW8yP8",
displayName: "user",
},
challenge: "s0hBOfkSaVLXdsbyD8jii6t2IjUd-eiTP1Cmeuo1qUo",
pubKeyCredParams: [
{
type: "public-key",
alg: -8,
},
{
type: "public-key",
alg: -7,
},
{
type: "public-key",
alg: -257,
},
],
timeout: 300000,
excludeCredentials: [
{
id: "nOsjw8eaaqSwVdTBBYE1FqfGdHs",
type: "public-key",
transports: [],
},
],
authenticatorSelection: {
residentKey: "required",
userVerification: "preferred",
},
attestation: "direct",
extensions: { credProps: true },
};
const validAuthenticatorResponse = {
authenticatorAttachment: "platform",
id: "9wAuex_025BgEQrs7fOypo5SGBA",
rawId: base64url.decode("9wAuex_025BgEQrs7fOypo5SGBA"),
response: {
attestationObject: base64url.decode(
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E",
),
getAuthenticatorData: () =>
base64url.decode(
"y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E",
),
clientDataJSON: base64url.decode(
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiUVdwd3lUcXJpYVlqbVdnOWFvZ0FxUlRKNVFYMFBGV2JWR2xNeGNsVjZhcyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
),
getPublicKey: () =>
base64url.decode(
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwH2kzYF5J4Qbzd8AoVVIsoh-8MEFWjIaAyiIbET7paBrMCiMzmx25DLYzuvPV2jnmdVo0sZeHyTjEEfP47L3UQ",
),
getPublicKeyAlgorithm: () => -7,
getTransports: () => ["internal"],
},
type: "public-key",
getClientExtensionResults: () => ({}),
};
global.navigator = {
credentials: {
create: fake.resolves(validAuthenticatorResponse),
},
};
httpPostStub = stub(http, "post");
httpPostStub.withArgs(contextPath + "/webauthn/register/options", match.any).resolves({
ok: true,
status: 200,
json: fake.resolves(credentialsCreateOptions),
});
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
ok: true,
json: fake.resolves({
success: true,
}),
});
});
afterEach(() => {
httpPostStub.restore();
delete global.navigator;
});
it("succeeds", async () => {
const contextPath = "/some/path";
const headers = { _csrf: "csrf-value" };
await webauthn.register(headers, contextPath, "my passkey");
assert.calledWithExactly(
httpPostStub.lastCall,
`${contextPath}/webauthn/register`,
headers,
match({
publicKey: {
credential: {
id: "9wAuex_025BgEQrs7fOypo5SGBA",
rawId: "9wAuex_025BgEQrs7fOypo5SGBA",
response: {
attestationObject:
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E",
clientDataJSON:
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiUVdwd3lUcXJpYVlqbVdnOWFvZ0FxUlRKNVFYMFBGV2JWR2xNeGNsVjZhcyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9",
transports: ["internal"],
},
type: "public-key",
clientExtensionResults: {},
authenticatorAttachment: "platform",
},
label: "my passkey",
},
}),
);
});
it("calls the authenticator with the correct options", async () => {
await webauthn.register({}, contextPath, "my passkey");
assert.calledOnceWithExactly(
global.navigator.credentials.create,
match({
publicKey: {
rp: {
name: "Spring Security Relying Party",
id: "example.localhost",
},
user: {
name: "user",
id: base64url.decode("eatPy60xmXG_58JrIiIBa5wq8Y76c7MD6mnY5vW8yP8"),
displayName: "user",
},
challenge: base64url.decode("s0hBOfkSaVLXdsbyD8jii6t2IjUd-eiTP1Cmeuo1qUo"),
pubKeyCredParams: [
{
type: "public-key",
alg: -8,
},
{
type: "public-key",
alg: -7,
},
{
type: "public-key",
alg: -257,
},
],
timeout: 300000,
excludeCredentials: [
{
id: base64url.decode("nOsjw8eaaqSwVdTBBYE1FqfGdHs"),
type: "public-key",
transports: [],
},
],
authenticatorSelection: {
residentKey: "required",
userVerification: "preferred",
},
attestation: "direct",
extensions: { credProps: true },
},
signal: match.any,
}),
);
});
describe("registration failures", () => {
it("when label is missing", async () => {
try {
await webauthn.register({}, "/", "");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal("Error: Passkey Label is required");
return;
}
expect.fail("register should throw");
});
it("when cannot get the registration options", async () => {
httpPostStub.withArgs(match.any, match.any).rejects(new Error("Server threw an error"));
try {
await webauthn.register({}, "/", "my passkey");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Registration failed. Could not fetch registration options: Server threw an error",
);
return;
}
expect.fail("register should throw");
});
it("when registration options call does not return HTTP 200 OK", async () => {
httpPostStub.withArgs(match.any, match.any).resolves({
ok: false,
status: 400,
});
try {
await webauthn.register({}, "/", "my passkey");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Registration failed. Could not fetch registration options: Server responded with HTTP 400",
);
return;
}
expect.fail("register should throw");
});
it("when registration options are not valid JSON", async () => {
httpPostStub.withArgs(match.any, match.any).resolves({
ok: true,
status: 200,
json: fake.rejects(new Error("Not a JSON response")),
});
try {
await webauthn.register({}, "/", "my passkey");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Registration failed. Could not fetch registration options: Not a JSON response",
);
return;
}
expect.fail("register should throw");
});
it("when navigator.credentials.create fails", async () => {
global.navigator = {
credentials: {
create: fake.rejects(new Error("authenticator threw an error")),
},
};
try {
await webauthn.register({}, contextPath, "my passkey");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Registration failed. Call to navigator.credentials.create failed: authenticator threw an error",
);
expect(err.cause).to.deep.equal(new Error("authenticator threw an error"));
return;
}
expect.fail("register should throw");
});
it("when registration call fails", async () => {
httpPostStub
.withArgs(`${contextPath}/webauthn/register`, match.any, match.any)
.rejects(new Error("Connection refused"));
try {
await webauthn.register({}, contextPath, "my passkey");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Registration failed. Could not process the registration request: Connection refused",
);
expect(err.cause).to.deep.equal(new Error("Connection refused"));
return;
}
expect.fail("register should throw");
});
it("when registration call does not return HTTP 200 OK", async () => {
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
ok: false,
status: 400,
});
try {
await webauthn.register({}, contextPath, "my passkey");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal("Registration failed. Could not process the registration request: HTTP 400");
return;
}
expect.fail("register should throw");
});
it("when registration call does not return JSON", async () => {
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
ok: true,
status: 200,
json: fake.rejects(new Error("Not valid JSON")),
});
try {
await webauthn.register({}, contextPath, "my passkey");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal(
"Registration failed. Could not process the registration request: Not valid JSON",
);
expect(err.cause).to.deep.equal(new Error("Not valid JSON"));
return;
}
expect.fail("register should throw");
});
it("when registration call returns null", async () => {
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
ok: true,
status: 200,
json: fake.resolves(null),
});
try {
await webauthn.register({}, contextPath, "my passkey");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal("Registration failed. Server responded with: null");
return;
}
expect.fail("register should throw");
});
it('when registration call returns {"success":false}', async () => {
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({
ok: true,
status: 200,
json: fake.resolves({ success: false }),
});
try {
await webauthn.register({}, contextPath, "my passkey");
} catch (err) {
expect(err).to.be.an("error");
expect(err.message).to.equal('Registration failed. Server responded with: {"success":false}');
return;
}
expect.fail("register should throw");
});
});
});
});

View File

@ -0,0 +1,106 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import "./bootstrap.js";
import { expect } from "chai";
import { setupLogin } from "../lib/webauthn-login.js";
import webauthn from "../lib/webauthn-core.js";
import { assert, fake, match, stub } from "sinon";
describe("webauthn-login", () => {
describe("bootstrap", () => {
let authenticateStub;
let isConditionalMediationAvailableStub;
let signinButton;
beforeEach(() => {
isConditionalMediationAvailableStub = stub(webauthn, "isConditionalMediationAvailable").resolves(false);
authenticateStub = stub(webauthn, "authenticate").resolves("/success");
signinButton = {
addEventListener: fake(),
};
global.console = {
error: stub(),
};
global.window = {
location: {
href: {},
},
};
});
afterEach(() => {
authenticateStub.restore();
isConditionalMediationAvailableStub.restore();
});
it("sets up a click event listener on the signin button", async () => {
await setupLogin({}, "/some/path", signinButton);
assert.calledOnceWithMatch(signinButton.addEventListener, "click", match.typeOf("function"));
});
// FIXME: conditional mediation triggers browser crashes
// See: https://github.com/rwinch/spring-security-webauthn/issues/73
xit("uses conditional mediation when available", async () => {
isConditionalMediationAvailableStub.resolves(true);
const headers = { "x-header": "value" };
const contextPath = "/some/path";
await setupLogin(headers, contextPath, signinButton);
assert.calledOnceWithExactly(authenticateStub, headers, contextPath, true);
expect(global.window.location.href).to.equal("/success");
});
it("does not call authenticate when conditional mediation is not available", async () => {
await setupLogin({}, "/", signinButton);
assert.notCalled(authenticateStub);
});
it("calls authenticate when the signin button is clicked", async () => {
const headers = { "x-header": "value" };
const contextPath = "/some/path";
await setupLogin(headers, contextPath, signinButton);
// Call the event listener
await signinButton.addEventListener.firstCall.lastArg();
assert.calledOnceWithExactly(authenticateStub, headers, contextPath, false);
expect(global.window.location.href).to.equal("/success");
});
it("handles authentication errors", async () => {
authenticateStub.rejects(new Error("Authentication failed"));
await setupLogin({}, "/some/path", signinButton);
// Call the event listener
await signinButton.addEventListener.firstCall.lastArg();
expect(global.window.location.href).to.equal(`/some/path/login?error`);
assert.calledOnceWithMatch(
global.console.error,
match.instanceOf(Error).and(match.has("message", "Authentication failed")),
);
});
});
});

View File

@ -0,0 +1,279 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict";
import "./bootstrap.js";
import { expect, util, Assertion } from "chai";
import { setupRegistration } from "../lib/webauthn-registration.js";
import webauthn from "../lib/webauthn-core.js";
import { assert, fake, match, stub } from "sinon";
describe("webauthn-registration", () => {
before(() => {
Assertion.addProperty("visible", function () {
const obj = util.flag(this, "object");
new Assertion(obj).to.have.nested.property("style.display", "block");
});
Assertion.addProperty("hidden", function () {
const obj = util.flag(this, "object");
new Assertion(obj).to.have.nested.property("style.display", "none");
});
});
describe("bootstrap", () => {
let registerStub;
let registerButton;
let labelField;
let errorPopup;
let successPopup;
let deleteForms;
let ui;
beforeEach(() => {
registerStub = stub(webauthn, "register").resolves(undefined);
errorPopup = {
style: {
display: undefined,
},
textContent: undefined,
};
successPopup = {
style: {
display: undefined,
},
textContent: undefined,
};
registerButton = {
addEventListener: fake(),
};
labelField = {
value: undefined,
};
deleteForms = [];
ui = {
getSuccess: function () {
return successPopup;
},
getError: function () {
return errorPopup;
},
getRegisterButton: function () {
return registerButton;
},
getLabelInput: function () {
return labelField;
},
getDeleteForms: function () {
return deleteForms;
},
};
global.window = {
location: {
href: {},
},
};
global.console = {
error: stub(),
};
});
afterEach(() => {
registerStub.restore();
delete global.window;
});
describe("when webauthn is not supported", () => {
beforeEach(() => {
delete global.window.PublicKeyCredential;
});
it("does not set up a click event listener", async () => {
await setupRegistration({}, "/", ui);
assert.notCalled(registerButton.addEventListener);
});
it("shows an error popup", async () => {
await setupRegistration({}, "/", ui);
expect(errorPopup).to.be.visible;
expect(errorPopup.textContent).to.equal("WebAuthn is not supported");
expect(successPopup).to.be.hidden;
});
});
describe("when webauthn is supported", () => {
beforeEach(() => {
global.window.PublicKeyCredential = fake();
});
it("hides the popups", async () => {
await setupRegistration({}, "/", ui);
expect(successPopup).to.be.hidden;
expect(errorPopup).to.be.hidden;
});
it("sets up a click event listener on the register button", async () => {
await setupRegistration({}, "/some/path", ui);
assert.calledOnceWithMatch(registerButton.addEventListener, "click", match.typeOf("function"));
});
describe(`when the query string contains "success"`, () => {
beforeEach(() => {
global.window.location.search = "?success&continue=true";
});
it("shows the success popup", async () => {
await setupRegistration({}, "/", ui);
expect(successPopup).to.be.visible;
expect(errorPopup).to.be.hidden;
});
});
describe("when the register button is clicked", () => {
const headers = { "x-header": "value" };
const contextPath = "/some/path";
beforeEach(async () => {
await setupRegistration(headers, contextPath, ui);
});
it("hides all the popups", async () => {
successPopup.textContent = "dummy-content";
successPopup.style.display = "block";
errorPopup.textContent = "dummy-content";
errorPopup.style.display = "block";
await registerButton.addEventListener.firstCall.lastArg();
expect(successPopup).to.be.hidden;
expect(errorPopup).to.be.hidden;
});
it("calls register", async () => {
labelField.value = "passkey name";
await registerButton.addEventListener.firstCall.lastArg();
assert.calledOnceWithExactly(registerStub, headers, contextPath, labelField.value);
});
it("navigates to success page", async () => {
labelField.value = "passkey name";
await registerButton.addEventListener.firstCall.lastArg();
expect(global.window.location.href).to.equal(`${contextPath}/webauthn/register?success`);
});
it("handles errors", async () => {
registerStub.rejects(new Error("The registration failed"));
await registerButton.addEventListener.firstCall.lastArg();
expect(errorPopup.textContent).to.equal("The registration failed");
expect(errorPopup).to.be.visible;
expect(successPopup).to.be.hidden;
assert.calledOnceWithMatch(
global.console.error,
match.instanceOf(Error).and(match.has("message", "The registration failed")),
);
});
});
describe("delete", () => {
beforeEach(() => {
global.fetch = fake.resolves({ ok: true });
});
afterEach(() => {
delete global.fetch;
});
it("no errors when no forms", async () => {
await setupRegistration({}, "/some/path", ui);
});
it("sets up forms for fetch", async () => {
const deleteFormOne = {
addEventListener: fake(),
};
const deleteFormTwo = {
addEventListener: fake(),
};
deleteForms = [deleteFormOne, deleteFormTwo];
await setupRegistration({}, "", ui);
assert.calledOnceWithMatch(deleteFormOne.addEventListener, "submit", match.typeOf("function"));
assert.calledOnceWithMatch(deleteFormTwo.addEventListener, "submit", match.typeOf("function"));
});
describe("when the delete button is clicked", () => {
it("calls POST to the form action", async () => {
const contextPath = "/some/path";
const deleteForm = {
addEventListener: fake(),
action: `${contextPath}/webauthn/1234`,
};
deleteForms = [deleteForm];
const headers = {
"X-CSRF-TOKEN": "token",
};
await setupRegistration(headers, contextPath, ui);
const clickEvent = {
preventDefault: fake(),
};
await deleteForm.addEventListener.firstCall.lastArg(clickEvent);
assert.calledOnce(clickEvent.preventDefault);
assert.calledOnceWithExactly(global.fetch, `/some/path/webauthn/1234`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
...headers,
},
});
expect(global.window.location.href).to.equal(`/some/path/webauthn/register?success`);
});
});
it("handles errors", async () => {
global.fetch = fake.rejects("Server threw an error");
global.window.location.href = "/initial/location";
const deleteForm = {
addEventListener: fake(),
};
deleteForms = [deleteForm];
await setupRegistration({}, "", ui);
const clickEvent = { preventDefault: fake() };
await deleteForm.addEventListener.firstCall.lastArg(clickEvent);
expect(errorPopup).to.be.visible;
expect(errorPopup.textContent).to.equal("Server threw an error");
// URL does not change
expect(global.window.location.href).to.equal("/initial/location");
});
});
});
});
});

View File

@ -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'

View File

@ -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;
}
}
}

View File

@ -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}}

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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();
}

View File

@ -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();
}

View File

@ -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 clients 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);
}
}
}

View File

@ -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 };
}
}

View File

@ -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 clients 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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 };
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 };
}
}

View File

@ -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
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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
* members value. Authenticators MAY truncate a displayName members 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);
}
}
}

View File

@ -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();
}

View File

@ -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 objects [[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 objects 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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 {
}

View File

@ -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());
}
}

View File

@ -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 {
}

View File

@ -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());
}
}

View File

@ -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 {
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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 {
}

View File

@ -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 {
}
}

View File

@ -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;
}
}

View File

@ -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 {
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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 {
}

View File

@ -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;
}
}

View File

@ -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 {
}

View File

@ -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());
}
}

View File

@ -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() {
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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());
}
}

View File

@ -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;
}
}

View File

@ -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 {
}

View File

@ -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());
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(using = CredProtectAuthenticationExtensionsClientInputSerializer.class)
class CredProtectAuthenticationExtensionsClientInputMixin {
}

View File

@ -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);
}
}
}

View File

@ -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) {
}
}

View File

@ -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