157 changed files with 19203 additions and 5 deletions
@ -0,0 +1,194 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,43 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,83 @@
@@ -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) |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,289 @@
@@ -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 |
||||
|
||||
---- |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
node_modules/ |
||||
dist/ |
||||
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
import globals from "globals"; |
||||
import eslintConfigPrettier from "eslint-plugin-prettier/recommended"; |
||||
|
||||
export default [ |
||||
{ |
||||
ignores: ["build/**/*"], |
||||
}, |
||||
{ |
||||
files: ["lib/**/*.js"], |
||||
languageOptions: { |
||||
sourceType: "module", |
||||
globals: { |
||||
...globals.browser, |
||||
gobalThis: "readonly", |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
files: ["test/**/*.js"], |
||||
languageOptions: { |
||||
globals: { |
||||
...globals.browser, |
||||
...globals.mocha, |
||||
...globals.chai, |
||||
...globals.nodeBuiltin, |
||||
}, |
||||
}, |
||||
}, |
||||
eslintConfigPrettier, |
||||
]; |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
const holder = { |
||||
controller: new AbortController(), |
||||
}; |
||||
|
||||
/** |
||||
* Returns a new AbortSignal to be used in the options for the registration and authentication ceremonies. |
||||
* Aborts the existing AbortController if it exists, cancelling any existing ceremony. |
||||
* |
||||
* The authentication ceremony, when triggered with conditional mediation, shows a non-modal |
||||
* interaction. If the user does not interact with the non-modal dialog, the existing ceremony MUST |
||||
* be cancelled before initiating a new one, hence the need for a singleton AbortController. |
||||
* |
||||
* @returns {AbortSignal} a new, non-aborted AbortSignal |
||||
*/ |
||||
function newSignal() { |
||||
if (!!holder.controller) { |
||||
holder.controller.abort("Initiating new WebAuthN ceremony, cancelling current ceremony"); |
||||
} |
||||
holder.controller = new AbortController(); |
||||
return holder.controller.signal; |
||||
} |
||||
|
||||
export default { |
||||
newSignal, |
||||
}; |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
export default { |
||||
encode: function (buffer) { |
||||
const base64 = window.btoa(String.fromCharCode(...new Uint8Array(buffer))); |
||||
return base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); |
||||
}, |
||||
decode: function (base64url) { |
||||
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/"); |
||||
const binStr = window.atob(base64); |
||||
const bin = new Uint8Array(binStr.length); |
||||
for (let i = 0; i < binStr.length; i++) { |
||||
bin[i] = binStr.charCodeAt(i); |
||||
} |
||||
return bin.buffer; |
||||
}, |
||||
}; |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
async function post(url, headers, body) { |
||||
const options = { |
||||
method: "POST", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
...headers, |
||||
}, |
||||
}; |
||||
if (body) { |
||||
options.body = JSON.stringify(body); |
||||
} |
||||
return fetch(url, options); |
||||
} |
||||
|
||||
export default { post }; |
||||
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
import { setupLogin } from "./webauthn-login.js"; |
||||
import { setupRegistration } from "./webauthn-registration.js"; |
||||
|
||||
// Make "setup" available in the window domain, so it can be run with "setupLogin()"
|
||||
window.setupLogin = setupLogin; |
||||
window.setupRegistration = setupRegistration; |
||||
@ -0,0 +1,194 @@
@@ -0,0 +1,194 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
import base64url from "./base64url.js"; |
||||
import http from "./http.js"; |
||||
import abortController from "./abort-controller.js"; |
||||
|
||||
async function isConditionalMediationAvailable() { |
||||
return !!( |
||||
window.PublicKeyCredential && |
||||
window.PublicKeyCredential.isConditionalMediationAvailable && |
||||
(await window.PublicKeyCredential.isConditionalMediationAvailable()) |
||||
); |
||||
} |
||||
|
||||
async function authenticate(headers, contextPath, useConditionalMediation) { |
||||
let options; |
||||
try { |
||||
const optionsResponse = await http.post(`${contextPath}/webauthn/authenticate/options`, headers); |
||||
if (!optionsResponse.ok) { |
||||
throw new Error(`HTTP ${optionsResponse.status}`); |
||||
} |
||||
options = await optionsResponse.json(); |
||||
} catch (err) { |
||||
throw new Error(`Authentication failed. Could not fetch authentication options: ${err.message}`, { cause: err }); |
||||
} |
||||
|
||||
// FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseRequestOptionsFromJSON
|
||||
const decodedOptions = { |
||||
...options, |
||||
challenge: base64url.decode(options.challenge), |
||||
}; |
||||
|
||||
// Invoke the WebAuthn get() method.
|
||||
const credentialOptions = { |
||||
publicKey: decodedOptions, |
||||
signal: abortController.newSignal(), |
||||
}; |
||||
if (useConditionalMediation) { |
||||
// Request a conditional UI
|
||||
credentialOptions.mediation = "conditional"; |
||||
} |
||||
|
||||
let cred; |
||||
try { |
||||
cred = await navigator.credentials.get(credentialOptions); |
||||
} catch (err) { |
||||
throw new Error(`Authentication failed. Call to navigator.credentials.get failed: ${err.message}`, { cause: err }); |
||||
} |
||||
|
||||
const { response, type: credType } = cred; |
||||
let userHandle; |
||||
if (response.userHandle) { |
||||
userHandle = base64url.encode(response.userHandle); |
||||
} |
||||
const body = { |
||||
id: cred.id, |
||||
rawId: base64url.encode(cred.rawId), |
||||
response: { |
||||
authenticatorData: base64url.encode(response.authenticatorData), |
||||
clientDataJSON: base64url.encode(response.clientDataJSON), |
||||
signature: base64url.encode(response.signature), |
||||
userHandle, |
||||
}, |
||||
credType, |
||||
clientExtensionResults: cred.getClientExtensionResults(), |
||||
authenticatorAttachment: cred.authenticatorAttachment, |
||||
}; |
||||
|
||||
let authenticationResponse; |
||||
try { |
||||
const authenticationCallResponse = await http.post(`${contextPath}/login/webauthn`, headers, body); |
||||
if (!authenticationCallResponse.ok) { |
||||
throw new Error(`HTTP ${authenticationCallResponse.status}`); |
||||
} |
||||
authenticationResponse = await authenticationCallResponse.json(); |
||||
// if (authenticationResponse && authenticationResponse.authenticated) {
|
||||
} catch (err) { |
||||
throw new Error(`Authentication failed. Could not process the authentication request: ${err.message}`, { |
||||
cause: err, |
||||
}); |
||||
} |
||||
|
||||
if (!(authenticationResponse && authenticationResponse.authenticated && authenticationResponse.redirectUrl)) { |
||||
throw new Error( |
||||
`Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: ${JSON.stringify(authenticationResponse)}`, |
||||
); |
||||
} |
||||
|
||||
return authenticationResponse.redirectUrl; |
||||
} |
||||
|
||||
async function register(headers, contextPath, label) { |
||||
if (!label) { |
||||
throw new Error("Error: Passkey Label is required"); |
||||
} |
||||
|
||||
let options; |
||||
try { |
||||
const optionsResponse = await http.post(`${contextPath}/webauthn/register/options`, headers); |
||||
if (!optionsResponse.ok) { |
||||
throw new Error(`Server responded with HTTP ${optionsResponse.status}`); |
||||
} |
||||
options = await optionsResponse.json(); |
||||
} catch (e) { |
||||
throw new Error(`Registration failed. Could not fetch registration options: ${e.message}`, { cause: e }); |
||||
} |
||||
|
||||
// FIXME: Use https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON
|
||||
const decodedExcludeCredentials = !options.excludeCredentials |
||||
? [] |
||||
: options.excludeCredentials.map((cred) => ({ |
||||
...cred, |
||||
id: base64url.decode(cred.id), |
||||
})); |
||||
|
||||
const decodedOptions = { |
||||
...options, |
||||
user: { |
||||
...options.user, |
||||
id: base64url.decode(options.user.id), |
||||
}, |
||||
challenge: base64url.decode(options.challenge), |
||||
excludeCredentials: decodedExcludeCredentials, |
||||
}; |
||||
|
||||
let credentialsContainer; |
||||
try { |
||||
credentialsContainer = await navigator.credentials.create({ |
||||
publicKey: decodedOptions, |
||||
signal: abortController.newSignal(), |
||||
}); |
||||
} catch (e) { |
||||
throw new Error(`Registration failed. Call to navigator.credentials.create failed: ${e.message}`, { cause: e }); |
||||
} |
||||
|
||||
// FIXME: Let response be credential.response. If response is not an instance of AuthenticatorAttestationResponse, abort the ceremony with a user-visible error. https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
|
||||
const { response } = credentialsContainer; |
||||
const credential = { |
||||
id: credentialsContainer.id, |
||||
rawId: base64url.encode(credentialsContainer.rawId), |
||||
response: { |
||||
attestationObject: base64url.encode(response.attestationObject), |
||||
clientDataJSON: base64url.encode(response.clientDataJSON), |
||||
transports: response.getTransports ? response.getTransports() : [], |
||||
}, |
||||
type: credentialsContainer.type, |
||||
clientExtensionResults: credentialsContainer.getClientExtensionResults(), |
||||
authenticatorAttachment: credentialsContainer.authenticatorAttachment, |
||||
}; |
||||
|
||||
const registrationRequest = { |
||||
publicKey: { |
||||
credential: credential, |
||||
label: label, |
||||
}, |
||||
}; |
||||
|
||||
let verificationJSON; |
||||
try { |
||||
const verificationResp = await http.post(`${contextPath}/webauthn/register`, headers, registrationRequest); |
||||
if (!verificationResp.ok) { |
||||
throw new Error(`HTTP ${verificationResp.status}`); |
||||
} |
||||
verificationJSON = await verificationResp.json(); |
||||
} catch (e) { |
||||
throw new Error(`Registration failed. Could not process the registration request: ${e.message}`, { cause: e }); |
||||
} |
||||
|
||||
if (!(verificationJSON && verificationJSON.success)) { |
||||
throw new Error(`Registration failed. Server responded with: ${JSON.stringify(verificationJSON)}`); |
||||
} |
||||
} |
||||
|
||||
export default { |
||||
authenticate, |
||||
register, |
||||
isConditionalMediationAvailable, |
||||
}; |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
import webauthn from "./webauthn-core.js"; |
||||
|
||||
async function authenticateOrError(headers, contextPath, useConditionalMediation) { |
||||
try { |
||||
const redirectUrl = await webauthn.authenticate(headers, contextPath, useConditionalMediation); |
||||
window.location.href = redirectUrl; |
||||
} catch (err) { |
||||
console.error(err); |
||||
window.location.href = `${contextPath}/login?error`; |
||||
} |
||||
} |
||||
|
||||
async function conditionalMediation(headers, contextPath) { |
||||
const available = await webauthn.isConditionalMediationAvailable(); |
||||
if (available) { |
||||
await authenticateOrError(headers, contextPath, true); |
||||
} |
||||
return available; |
||||
} |
||||
|
||||
export async function setupLogin(headers, contextPath, signinButton) { |
||||
signinButton.addEventListener("click", async () => { |
||||
await authenticateOrError(headers, contextPath, false); |
||||
}); |
||||
|
||||
// FIXME: conditional mediation triggers browser crashes
|
||||
// See: https://github.com/rwinch/spring-security-webauthn/issues/73
|
||||
// await conditionalMediation(headers, contextPath);
|
||||
} |
||||
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
import webauthn from "./webauthn-core.js"; |
||||
|
||||
function setVisibility(element, value) { |
||||
if (!element) { |
||||
return; |
||||
} |
||||
element.style.display = value ? "block" : "none"; |
||||
} |
||||
|
||||
function setError(ui, msg) { |
||||
resetPopups(ui); |
||||
const error = ui.getError(); |
||||
if (!error) { |
||||
return; |
||||
} |
||||
error.textContent = msg; |
||||
setVisibility(error, true); |
||||
} |
||||
|
||||
function setSuccess(ui) { |
||||
resetPopups(ui); |
||||
const success = ui.getSuccess(); |
||||
if (!success) { |
||||
return; |
||||
} |
||||
setVisibility(success, true); |
||||
} |
||||
|
||||
function resetPopups(ui) { |
||||
const success = ui.getSuccess(); |
||||
const error = ui.getError(); |
||||
setVisibility(success, false); |
||||
setVisibility(error, false); |
||||
} |
||||
|
||||
async function submitDeleteForm(contextPath, form, headers) { |
||||
const options = { |
||||
method: "DELETE", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
...headers, |
||||
}, |
||||
}; |
||||
await fetch(form.action, options); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param headers headers added to the credentials creation POST request, typically CSRF |
||||
* @param contextPath the contextPath from which the app is served |
||||
* @param ui contains getRegisterButton(), getSuccess(), getError(), getLabelInput(), getDeleteForms() |
||||
* @returns {Promise<void>} |
||||
*/ |
||||
export async function setupRegistration(headers, contextPath, ui) { |
||||
resetPopups(ui); |
||||
|
||||
if (!window.PublicKeyCredential) { |
||||
setError(ui, "WebAuthn is not supported"); |
||||
return; |
||||
} |
||||
|
||||
const queryString = new URLSearchParams(window.location.search); |
||||
if (queryString.has("success")) { |
||||
setSuccess(ui); |
||||
} |
||||
|
||||
ui.getRegisterButton().addEventListener("click", async () => { |
||||
resetPopups(ui); |
||||
const label = ui.getLabelInput().value; |
||||
try { |
||||
await webauthn.register(headers, contextPath, label); |
||||
window.location.href = `${contextPath}/webauthn/register?success`; |
||||
} catch (err) { |
||||
setError(ui, err.message); |
||||
console.error(err); |
||||
} |
||||
}); |
||||
|
||||
ui.getDeleteForms().forEach((form) => |
||||
form.addEventListener("submit", async function (e) { |
||||
e.preventDefault(); |
||||
try { |
||||
await submitDeleteForm(contextPath, form, headers); |
||||
window.location.href = `${contextPath}/webauthn/register?success`; |
||||
} catch (err) { |
||||
setError(ui, err.message); |
||||
} |
||||
}), |
||||
); |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
{ |
||||
"name": "@springprojects/spring-security-webauthn", |
||||
"version": "1.0.0-alpha.9", |
||||
"description": "WebAuthN JS library for Spring Security", |
||||
"license": "ASL-2.0", |
||||
"author": "????", |
||||
"contributors": [ |
||||
"Rob Winch <rwinch@users.noreply.github.com>", |
||||
"Daniel Garnier-Moiroux <git@garnier.wf>" |
||||
], |
||||
"repository": "github:spring-projects/spring-security", |
||||
"bugs": { |
||||
"url": "https://github.com/spring-projects/spring-security/issues" |
||||
}, |
||||
"engines": { |
||||
"node": ">=20.0.0" |
||||
}, |
||||
"scripts": { |
||||
"test": "mocha", |
||||
"check": "npm test && npm run lint", |
||||
"test:watch": "mocha --watch --parallel", |
||||
"assemble": "esbuild lib/index.js --bundle --outfile=build/dist/spring-security-webauthn.js", |
||||
"build": "npm run check && npm run assemble", |
||||
"lint": "eslint", |
||||
"format": "npm run lint -- --fix" |
||||
}, |
||||
"main": "lib/index.js", |
||||
"files": [ |
||||
"lib" |
||||
], |
||||
"keywords": [ |
||||
"Spring Security", |
||||
"WebAuthn", |
||||
"passkeys" |
||||
], |
||||
"devDependencies": { |
||||
"@eslint/js": "^9.6.0", |
||||
"@types/sinon": "^17.0.3", |
||||
"chai": "~4.3", |
||||
"esbuild": "^0.23.0", |
||||
"eslint": "^9.6.0", |
||||
"eslint-config-prettier": "^9.1.0", |
||||
"eslint-plugin-prettier": "^5.1.3", |
||||
"globals": "^15.8.0", |
||||
"mocha": "~10.2", |
||||
"prettier": "^3.3.2", |
||||
"prettier-eslint": "~15.0", |
||||
"sinon": "^18.0.0" |
||||
}, |
||||
"type": "module" |
||||
} |
||||
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
plugins { |
||||
id 'base' |
||||
id 'com.github.node-gradle.node' version '7.1.0' |
||||
} |
||||
|
||||
node { |
||||
download = true |
||||
version = '20.17.0' |
||||
} |
||||
|
||||
tasks.named('check') { |
||||
dependsOn 'npm_run_check' |
||||
} |
||||
|
||||
tasks.register('dist', Zip) { |
||||
dependsOn 'npm_run_assemble' |
||||
from 'build/dist/spring-security.js' |
||||
into 'org/springframework/security' |
||||
} |
||||
|
||||
configurations { |
||||
javascript { |
||||
canBeConsumed = true |
||||
canBeResolved = false |
||||
} |
||||
} |
||||
|
||||
artifacts { |
||||
javascript(project.layout.buildDirectory.dir('dist')) { |
||||
builtBy(npm_run_assemble) |
||||
} |
||||
} |
||||
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
import "./bootstrap.js"; |
||||
import abortController from "../lib/abort-controller.js"; |
||||
import { expect } from "chai"; |
||||
|
||||
describe("abort-controller", () => { |
||||
describe("newSignal", () => { |
||||
it("returns an AbortSignal", () => { |
||||
const signal = abortController.newSignal(); |
||||
|
||||
expect(signal).to.be.instanceof(AbortSignal); |
||||
expect(signal.aborted).to.be.false; |
||||
}); |
||||
|
||||
it("returns a new signal every time", () => { |
||||
const initialSignal = abortController.newSignal(); |
||||
|
||||
const newSignal = abortController.newSignal(); |
||||
|
||||
expect(initialSignal).to.not.equal(newSignal); |
||||
}); |
||||
|
||||
it("aborts the existing signal", () => { |
||||
const signal = abortController.newSignal(); |
||||
|
||||
abortController.newSignal(); |
||||
|
||||
expect(signal.aborted).to.be.true; |
||||
expect(signal.reason).to.equal("Initiating new WebAuthN ceremony, cancelling current ceremony"); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
import { expect } from "chai"; |
||||
import base64url from "../lib/base64url.js"; |
||||
|
||||
describe("base64url", () => { |
||||
before(() => { |
||||
// Emulate the atob / btoa base64 encoding/decoding from the browser
|
||||
global.window = { |
||||
btoa: (str) => Buffer.from(str, "binary").toString("base64"), |
||||
atob: (b64) => Buffer.from(b64, "base64").toString("binary"), |
||||
}; |
||||
}); |
||||
|
||||
after(() => { |
||||
// Reset window object
|
||||
global.window = {}; |
||||
}); |
||||
|
||||
it("decodes", () => { |
||||
// "Zm9vYmFy" is "foobar" in base 64, i.e. f:102 o:111 o:111 b:98 a:97 r:114
|
||||
const decoded = base64url.decode("Zm9vYmFy"); |
||||
|
||||
expect(new Uint8Array(decoded)).to.be.deep.equal(new Uint8Array([102, 111, 111, 98, 97, 114])); |
||||
}); |
||||
|
||||
it("decodes special characters", () => { |
||||
// Wrap the decode function for easy testing
|
||||
const decode = (str) => { |
||||
const decoded = new Uint8Array(base64url.decode(str)); |
||||
return Array.from(decoded); |
||||
}; |
||||
|
||||
// "Pz8/" is "???" in base64, i.e. ?:63 three times
|
||||
expect(decode("Pz8/")).to.be.deep.equal(decode("Pz8_")); |
||||
expect(decode("Pz8_")).to.be.deep.equal([63, 63, 63]); |
||||
// "Pj4+" is ">>>" in base64, ie >:62 three times
|
||||
expect(decode("Pj4+")).to.be.deep.equal(decode("Pj4-")); |
||||
expect(decode("Pj4-")).to.be.deep.equal([62, 62, 62]); |
||||
}); |
||||
|
||||
it("encodes", () => { |
||||
const encoded = base64url.encode(Buffer.from("foobar")); |
||||
|
||||
expect(encoded).to.be.equal("Zm9vYmFy"); |
||||
}); |
||||
|
||||
it("encodes special +/ characters", () => { |
||||
const encode = (str) => base64url.encode(Buffer.from(str)); |
||||
|
||||
expect(encode("???")).to.be.equal("Pz8_"); |
||||
expect(encode(">>>")).to.be.equal("Pj4-"); |
||||
}); |
||||
|
||||
it("is stable", () => { |
||||
const base = "tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g"; |
||||
|
||||
expect(base64url.encode(base64url.decode(base))).to.be.equal(base); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
import chai from "chai"; |
||||
|
||||
// Show full diffs when there is an equality difference an assertion.
|
||||
// By default, chai truncates at 40 characters, making it difficult to
|
||||
// compare e.g. error messages
|
||||
chai.config.truncateThreshold = 0; |
||||
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
import http from "../lib/http.js"; |
||||
import { expect } from "chai"; |
||||
import { fake, assert } from "sinon"; |
||||
|
||||
describe("http", () => { |
||||
beforeEach(() => { |
||||
global.fetch = fake.resolves({ ok: true }); |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
delete global.fetch; |
||||
}); |
||||
|
||||
describe("post", () => { |
||||
it("calls fetch with headers", async () => { |
||||
const url = "https://example.com/some/path"; |
||||
const headers = { "x-custom": "some-value" }; |
||||
|
||||
const resp = await http.post(url, headers); |
||||
|
||||
expect(resp.ok).to.be.true; |
||||
assert.calledOnceWithExactly(global.fetch, url, { |
||||
method: "POST", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
...headers, |
||||
}, |
||||
}); |
||||
}); |
||||
|
||||
it("sends the body as a JSON string", async () => { |
||||
const body = { foo: "bar", baz: 42 }; |
||||
const url = "https://example.com/some/path"; |
||||
|
||||
const resp = await http.post(url, {}, body); |
||||
|
||||
expect(resp.ok).to.be.true; |
||||
assert.calledOnceWithExactly(global.fetch, url, { |
||||
method: "POST", |
||||
headers: { |
||||
"Content-Type": "application/json", |
||||
}, |
||||
body: `{"foo":"bar","baz":42}`, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,697 @@
@@ -0,0 +1,697 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
import "./bootstrap.js"; |
||||
import { expect } from "chai"; |
||||
import { assert, fake, match, stub } from "sinon"; |
||||
import http from "../lib/http.js"; |
||||
import webauthn from "../lib/webauthn-core.js"; |
||||
import base64url from "../lib/base64url.js"; |
||||
|
||||
describe("webauthn-core", () => { |
||||
beforeEach(() => { |
||||
global.window = { |
||||
btoa: (str) => Buffer.from(str, "binary").toString("base64"), |
||||
atob: (b64) => Buffer.from(b64, "base64").toString("binary"), |
||||
}; |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
delete global.window; |
||||
}); |
||||
|
||||
describe("isConditionalMediationAvailable", () => { |
||||
afterEach(() => { |
||||
delete global.window.PublicKeyCredential; |
||||
}); |
||||
|
||||
it("is available", async () => { |
||||
global.window = { |
||||
PublicKeyCredential: { |
||||
isConditionalMediationAvailable: fake.resolves(true), |
||||
}, |
||||
}; |
||||
|
||||
const result = await webauthn.isConditionalMediationAvailable(); |
||||
|
||||
expect(result).to.be.true; |
||||
}); |
||||
|
||||
describe("is not available", async () => { |
||||
it("PublicKeyCredential does not exist", async () => { |
||||
global.window = {}; |
||||
const result = await webauthn.isConditionalMediationAvailable(); |
||||
expect(result).to.be.false; |
||||
}); |
||||
it("PublicKeyCredential.isConditionalMediationAvailable undefined", async () => { |
||||
global.window = { |
||||
PublicKeyCredential: {}, |
||||
}; |
||||
const result = await webauthn.isConditionalMediationAvailable(); |
||||
expect(result).to.be.false; |
||||
}); |
||||
it("PublicKeyCredential.isConditionalMediationAvailable false", async () => { |
||||
global.window = { |
||||
PublicKeyCredential: { |
||||
isConditionalMediationAvailable: fake.resolves(false), |
||||
}, |
||||
}; |
||||
const result = await webauthn.isConditionalMediationAvailable(); |
||||
expect(result).to.be.false; |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe("authenticate", () => { |
||||
let httpPostStub; |
||||
const contextPath = "/some/path"; |
||||
|
||||
const credentialsGetOptions = { |
||||
challenge: "nRbOrtNKTfJ1JaxfUDKs8j3B-JFqyGQw8DO4u6eV3JA", |
||||
timeout: 300000, |
||||
rpId: "localhost", |
||||
allowCredentials: [], |
||||
userVerification: "preferred", |
||||
extensions: {}, |
||||
}; |
||||
|
||||
// This is kind of a self-fulfilling prophecy type of test: we produce array buffers by calling
|
||||
// base64url.decode ; they will then be re-encoded to the same string in the production code.
|
||||
// The ArrayBuffer API is not super friendly.
|
||||
beforeEach(() => { |
||||
httpPostStub = stub(http, "post"); |
||||
httpPostStub.withArgs(contextPath + "/webauthn/authenticate/options", match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.resolves(credentialsGetOptions), |
||||
}); |
||||
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.resolves({ |
||||
authenticated: true, |
||||
redirectUrl: "/success", |
||||
}), |
||||
}); |
||||
|
||||
const validAuthenticatorResponse = { |
||||
id: "UgghgP5QKozwsSUK1twCj8mpgZs", |
||||
rawId: base64url.decode("UgghgP5QKozwsSUK1twCj8mpgZs"), |
||||
response: { |
||||
authenticatorData: base64url.decode("y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA"), |
||||
clientDataJSON: base64url.decode( |
||||
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUTdlR0NkNUw2cG9fa01meWNIQnBWRlR5dmd3RklCV0QxZWg5OUktRFhnWSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9", |
||||
), |
||||
signature: base64url.decode( |
||||
"MEUCIGT9PAWfU3lMicOXFMpHGcl033dY-sNSJvehlXvvoivyAiEA_D_yOsChERlXX2rFcK6Qx5BaAbx5qdU2hgYDVN6W770", |
||||
), |
||||
userHandle: base64url.decode("tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g"), |
||||
}, |
||||
getClientExtensionResults: () => ({}), |
||||
authenticatorAttachment: "platform", |
||||
type: "public-key", |
||||
}; |
||||
global.navigator = { |
||||
credentials: { |
||||
get: fake.resolves(validAuthenticatorResponse), |
||||
}, |
||||
}; |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
http.post.restore(); |
||||
delete global.navigator; |
||||
}); |
||||
|
||||
it("succeeds", async () => { |
||||
const redirectUrl = await webauthn.authenticate({ "x-custom": "some-value" }, contextPath, false); |
||||
|
||||
expect(redirectUrl).to.equal("/success"); |
||||
assert.calledWith( |
||||
httpPostStub.lastCall, |
||||
`${contextPath}/login/webauthn`, |
||||
{ "x-custom": "some-value" }, |
||||
{ |
||||
id: "UgghgP5QKozwsSUK1twCj8mpgZs", |
||||
rawId: "UgghgP5QKozwsSUK1twCj8mpgZs", |
||||
credType: "public-key", |
||||
response: { |
||||
authenticatorData: "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA", |
||||
clientDataJSON: |
||||
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiUTdlR0NkNUw2cG9fa01meWNIQnBWRlR5dmd3RklCV0QxZWg5OUktRFhnWSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9", |
||||
signature: |
||||
"MEUCIGT9PAWfU3lMicOXFMpHGcl033dY-sNSJvehlXvvoivyAiEA_D_yOsChERlXX2rFcK6Qx5BaAbx5qdU2hgYDVN6W770", |
||||
userHandle: "tyRDnKxdj7uWOT5jrchXu54lo6nf3bWOUvMQnGOXk7g", |
||||
}, |
||||
clientExtensionResults: {}, |
||||
authenticatorAttachment: "platform", |
||||
}, |
||||
); |
||||
}); |
||||
|
||||
it("calls the authenticator with the correct options", async () => { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
|
||||
assert.calledOnceWithMatch(global.navigator.credentials.get, { |
||||
publicKey: { |
||||
challenge: base64url.decode("nRbOrtNKTfJ1JaxfUDKs8j3B-JFqyGQw8DO4u6eV3JA"), |
||||
timeout: 300000, |
||||
rpId: "localhost", |
||||
allowCredentials: [], |
||||
userVerification: "preferred", |
||||
extensions: {}, |
||||
}, |
||||
signal: match.any, |
||||
}); |
||||
}); |
||||
|
||||
describe("authentication failures", () => { |
||||
it("when authentication options call", async () => { |
||||
httpPostStub |
||||
.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any) |
||||
.rejects(new Error("Connection refused")); |
||||
|
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Authentication failed. Could not fetch authentication options: Connection refused", |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
|
||||
it("when authentication options call returns does not return HTTP 200 OK", async () => { |
||||
httpPostStub.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any).resolves({ |
||||
ok: false, |
||||
status: 400, |
||||
}); |
||||
|
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal("Authentication failed. Could not fetch authentication options: HTTP 400"); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
|
||||
it("when authentication options are not valid json", async () => { |
||||
httpPostStub.withArgs(`${contextPath}/webauthn/authenticate/options`, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.rejects(new Error("Not valid JSON")), |
||||
}); |
||||
|
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal("Authentication failed. Could not fetch authentication options: Not valid JSON"); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
|
||||
it("when navigator.credentials.get fails", async () => { |
||||
global.navigator.credentials.get = fake.rejects(new Error("Operation was aborted")); |
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Authentication failed. Call to navigator.credentials.get failed: Operation was aborted", |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
|
||||
it("when authentication call fails", async () => { |
||||
httpPostStub |
||||
.withArgs(`${contextPath}/login/webauthn`, match.any, match.any) |
||||
.rejects(new Error("Connection refused")); |
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Authentication failed. Could not process the authentication request: Connection refused", |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
|
||||
it("when authentication call does not return HTTP 200 OK", async () => { |
||||
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({ |
||||
ok: false, |
||||
status: 400, |
||||
}); |
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal("Authentication failed. Could not process the authentication request: HTTP 400"); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
|
||||
it("when authentication call does not return JSON", async () => { |
||||
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.rejects(new Error("Not valid JSON")), |
||||
}); |
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Authentication failed. Could not process the authentication request: Not valid JSON", |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
|
||||
it("when authentication call returns null", async () => { |
||||
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.resolves(null), |
||||
}); |
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: null', |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
|
||||
it('when authentication call returns {"authenticated":false}', async () => { |
||||
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.resolves({ |
||||
authenticated: false, |
||||
}), |
||||
}); |
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: {"authenticated":false}', |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
|
||||
it("when authentication call returns no redirectUrl", async () => { |
||||
httpPostStub.withArgs(`${contextPath}/login/webauthn`, match.any, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.resolves({ |
||||
authenticated: true, |
||||
}), |
||||
}); |
||||
try { |
||||
await webauthn.authenticate({}, contextPath, false); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
'Authentication failed. Expected {"authenticated": true, "redirectUrl": "..."}, server responded with: {"authenticated":true}', |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("authenticate should throw"); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe("register", () => { |
||||
let httpPostStub; |
||||
const contextPath = "/some/path"; |
||||
|
||||
beforeEach(() => { |
||||
const credentialsCreateOptions = { |
||||
rp: { |
||||
name: "Spring Security Relying Party", |
||||
id: "example.localhost", |
||||
}, |
||||
user: { |
||||
name: "user", |
||||
id: "eatPy60xmXG_58JrIiIBa5wq8Y76c7MD6mnY5vW8yP8", |
||||
displayName: "user", |
||||
}, |
||||
challenge: "s0hBOfkSaVLXdsbyD8jii6t2IjUd-eiTP1Cmeuo1qUo", |
||||
pubKeyCredParams: [ |
||||
{ |
||||
type: "public-key", |
||||
alg: -8, |
||||
}, |
||||
{ |
||||
type: "public-key", |
||||
alg: -7, |
||||
}, |
||||
{ |
||||
type: "public-key", |
||||
alg: -257, |
||||
}, |
||||
], |
||||
timeout: 300000, |
||||
excludeCredentials: [ |
||||
{ |
||||
id: "nOsjw8eaaqSwVdTBBYE1FqfGdHs", |
||||
type: "public-key", |
||||
transports: [], |
||||
}, |
||||
], |
||||
authenticatorSelection: { |
||||
residentKey: "required", |
||||
userVerification: "preferred", |
||||
}, |
||||
attestation: "direct", |
||||
extensions: { credProps: true }, |
||||
}; |
||||
const validAuthenticatorResponse = { |
||||
authenticatorAttachment: "platform", |
||||
id: "9wAuex_025BgEQrs7fOypo5SGBA", |
||||
rawId: base64url.decode("9wAuex_025BgEQrs7fOypo5SGBA"), |
||||
response: { |
||||
attestationObject: base64url.decode( |
||||
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E", |
||||
), |
||||
getAuthenticatorData: () => |
||||
base64url.decode( |
||||
"y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E", |
||||
), |
||||
clientDataJSON: base64url.decode( |
||||
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiUVdwd3lUcXJpYVlqbVdnOWFvZ0FxUlRKNVFYMFBGV2JWR2xNeGNsVjZhcyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9", |
||||
), |
||||
getPublicKey: () => |
||||
base64url.decode( |
||||
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwH2kzYF5J4Qbzd8AoVVIsoh-8MEFWjIaAyiIbET7paBrMCiMzmx25DLYzuvPV2jnmdVo0sZeHyTjEEfP47L3UQ", |
||||
), |
||||
getPublicKeyAlgorithm: () => -7, |
||||
getTransports: () => ["internal"], |
||||
}, |
||||
type: "public-key", |
||||
getClientExtensionResults: () => ({}), |
||||
}; |
||||
global.navigator = { |
||||
credentials: { |
||||
create: fake.resolves(validAuthenticatorResponse), |
||||
}, |
||||
}; |
||||
httpPostStub = stub(http, "post"); |
||||
httpPostStub.withArgs(contextPath + "/webauthn/register/options", match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.resolves(credentialsCreateOptions), |
||||
}); |
||||
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({ |
||||
ok: true, |
||||
json: fake.resolves({ |
||||
success: true, |
||||
}), |
||||
}); |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
httpPostStub.restore(); |
||||
delete global.navigator; |
||||
}); |
||||
|
||||
it("succeeds", async () => { |
||||
const contextPath = "/some/path"; |
||||
const headers = { _csrf: "csrf-value" }; |
||||
|
||||
await webauthn.register(headers, contextPath, "my passkey"); |
||||
assert.calledWithExactly( |
||||
httpPostStub.lastCall, |
||||
`${contextPath}/webauthn/register`, |
||||
headers, |
||||
match({ |
||||
publicKey: { |
||||
credential: { |
||||
id: "9wAuex_025BgEQrs7fOypo5SGBA", |
||||
rawId: "9wAuex_025BgEQrs7fOypo5SGBA", |
||||
response: { |
||||
attestationObject: |
||||
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViYy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAAPv8MAcVTk7MjAtuAgVX170AFPcALnsf9NuQYBEK7O3zsqaOUhgQpQECAyYgASFYIMB9pM2BeSeEG83fAKFVSLKIfvDBBVoyGgMoiGxE-6WgIlggazAojM5sduQy2M7rz1do55nVaNLGXh8k4xBHz-Oy91E", |
||||
clientDataJSON: |
||||
"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiUVdwd3lUcXJpYVlqbVdnOWFvZ0FxUlRKNVFYMFBGV2JWR2xNeGNsVjZhcyIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyJ9", |
||||
transports: ["internal"], |
||||
}, |
||||
type: "public-key", |
||||
clientExtensionResults: {}, |
||||
authenticatorAttachment: "platform", |
||||
}, |
||||
label: "my passkey", |
||||
}, |
||||
}), |
||||
); |
||||
}); |
||||
|
||||
it("calls the authenticator with the correct options", async () => { |
||||
await webauthn.register({}, contextPath, "my passkey"); |
||||
|
||||
assert.calledOnceWithExactly( |
||||
global.navigator.credentials.create, |
||||
match({ |
||||
publicKey: { |
||||
rp: { |
||||
name: "Spring Security Relying Party", |
||||
id: "example.localhost", |
||||
}, |
||||
user: { |
||||
name: "user", |
||||
id: base64url.decode("eatPy60xmXG_58JrIiIBa5wq8Y76c7MD6mnY5vW8yP8"), |
||||
displayName: "user", |
||||
}, |
||||
challenge: base64url.decode("s0hBOfkSaVLXdsbyD8jii6t2IjUd-eiTP1Cmeuo1qUo"), |
||||
pubKeyCredParams: [ |
||||
{ |
||||
type: "public-key", |
||||
alg: -8, |
||||
}, |
||||
{ |
||||
type: "public-key", |
||||
alg: -7, |
||||
}, |
||||
{ |
||||
type: "public-key", |
||||
alg: -257, |
||||
}, |
||||
], |
||||
timeout: 300000, |
||||
excludeCredentials: [ |
||||
{ |
||||
id: base64url.decode("nOsjw8eaaqSwVdTBBYE1FqfGdHs"), |
||||
type: "public-key", |
||||
transports: [], |
||||
}, |
||||
], |
||||
authenticatorSelection: { |
||||
residentKey: "required", |
||||
userVerification: "preferred", |
||||
}, |
||||
attestation: "direct", |
||||
extensions: { credProps: true }, |
||||
}, |
||||
signal: match.any, |
||||
}), |
||||
); |
||||
}); |
||||
|
||||
describe("registration failures", () => { |
||||
it("when label is missing", async () => { |
||||
try { |
||||
await webauthn.register({}, "/", ""); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal("Error: Passkey Label is required"); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
|
||||
it("when cannot get the registration options", async () => { |
||||
httpPostStub.withArgs(match.any, match.any).rejects(new Error("Server threw an error")); |
||||
try { |
||||
await webauthn.register({}, "/", "my passkey"); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Registration failed. Could not fetch registration options: Server threw an error", |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
|
||||
it("when registration options call does not return HTTP 200 OK", async () => { |
||||
httpPostStub.withArgs(match.any, match.any).resolves({ |
||||
ok: false, |
||||
status: 400, |
||||
}); |
||||
try { |
||||
await webauthn.register({}, "/", "my passkey"); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Registration failed. Could not fetch registration options: Server responded with HTTP 400", |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
|
||||
it("when registration options are not valid JSON", async () => { |
||||
httpPostStub.withArgs(match.any, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.rejects(new Error("Not a JSON response")), |
||||
}); |
||||
try { |
||||
await webauthn.register({}, "/", "my passkey"); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Registration failed. Could not fetch registration options: Not a JSON response", |
||||
); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
|
||||
it("when navigator.credentials.create fails", async () => { |
||||
global.navigator = { |
||||
credentials: { |
||||
create: fake.rejects(new Error("authenticator threw an error")), |
||||
}, |
||||
}; |
||||
try { |
||||
await webauthn.register({}, contextPath, "my passkey"); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Registration failed. Call to navigator.credentials.create failed: authenticator threw an error", |
||||
); |
||||
expect(err.cause).to.deep.equal(new Error("authenticator threw an error")); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
|
||||
it("when registration call fails", async () => { |
||||
httpPostStub |
||||
.withArgs(`${contextPath}/webauthn/register`, match.any, match.any) |
||||
.rejects(new Error("Connection refused")); |
||||
try { |
||||
await webauthn.register({}, contextPath, "my passkey"); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Registration failed. Could not process the registration request: Connection refused", |
||||
); |
||||
expect(err.cause).to.deep.equal(new Error("Connection refused")); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
|
||||
it("when registration call does not return HTTP 200 OK", async () => { |
||||
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({ |
||||
ok: false, |
||||
status: 400, |
||||
}); |
||||
try { |
||||
await webauthn.register({}, contextPath, "my passkey"); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal("Registration failed. Could not process the registration request: HTTP 400"); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
|
||||
it("when registration call does not return JSON", async () => { |
||||
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.rejects(new Error("Not valid JSON")), |
||||
}); |
||||
try { |
||||
await webauthn.register({}, contextPath, "my passkey"); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal( |
||||
"Registration failed. Could not process the registration request: Not valid JSON", |
||||
); |
||||
expect(err.cause).to.deep.equal(new Error("Not valid JSON")); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
|
||||
it("when registration call returns null", async () => { |
||||
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.resolves(null), |
||||
}); |
||||
try { |
||||
await webauthn.register({}, contextPath, "my passkey"); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal("Registration failed. Server responded with: null"); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
|
||||
it('when registration call returns {"success":false}', async () => { |
||||
httpPostStub.withArgs(`${contextPath}/webauthn/register`, match.any, match.any).resolves({ |
||||
ok: true, |
||||
status: 200, |
||||
json: fake.resolves({ success: false }), |
||||
}); |
||||
try { |
||||
await webauthn.register({}, contextPath, "my passkey"); |
||||
} catch (err) { |
||||
expect(err).to.be.an("error"); |
||||
expect(err.message).to.equal('Registration failed. Server responded with: {"success":false}'); |
||||
return; |
||||
} |
||||
expect.fail("register should throw"); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,106 @@
@@ -0,0 +1,106 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
import "./bootstrap.js"; |
||||
import { expect } from "chai"; |
||||
import { setupLogin } from "../lib/webauthn-login.js"; |
||||
import webauthn from "../lib/webauthn-core.js"; |
||||
import { assert, fake, match, stub } from "sinon"; |
||||
|
||||
describe("webauthn-login", () => { |
||||
describe("bootstrap", () => { |
||||
let authenticateStub; |
||||
let isConditionalMediationAvailableStub; |
||||
let signinButton; |
||||
|
||||
beforeEach(() => { |
||||
isConditionalMediationAvailableStub = stub(webauthn, "isConditionalMediationAvailable").resolves(false); |
||||
authenticateStub = stub(webauthn, "authenticate").resolves("/success"); |
||||
signinButton = { |
||||
addEventListener: fake(), |
||||
}; |
||||
|
||||
global.console = { |
||||
error: stub(), |
||||
}; |
||||
global.window = { |
||||
location: { |
||||
href: {}, |
||||
}, |
||||
}; |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
authenticateStub.restore(); |
||||
isConditionalMediationAvailableStub.restore(); |
||||
}); |
||||
|
||||
it("sets up a click event listener on the signin button", async () => { |
||||
await setupLogin({}, "/some/path", signinButton); |
||||
|
||||
assert.calledOnceWithMatch(signinButton.addEventListener, "click", match.typeOf("function")); |
||||
}); |
||||
|
||||
// FIXME: conditional mediation triggers browser crashes
|
||||
// See: https://github.com/rwinch/spring-security-webauthn/issues/73
|
||||
xit("uses conditional mediation when available", async () => { |
||||
isConditionalMediationAvailableStub.resolves(true); |
||||
|
||||
const headers = { "x-header": "value" }; |
||||
const contextPath = "/some/path"; |
||||
|
||||
await setupLogin(headers, contextPath, signinButton); |
||||
|
||||
assert.calledOnceWithExactly(authenticateStub, headers, contextPath, true); |
||||
expect(global.window.location.href).to.equal("/success"); |
||||
}); |
||||
|
||||
it("does not call authenticate when conditional mediation is not available", async () => { |
||||
await setupLogin({}, "/", signinButton); |
||||
|
||||
assert.notCalled(authenticateStub); |
||||
}); |
||||
|
||||
it("calls authenticate when the signin button is clicked", async () => { |
||||
const headers = { "x-header": "value" }; |
||||
const contextPath = "/some/path"; |
||||
|
||||
await setupLogin(headers, contextPath, signinButton); |
||||
|
||||
// Call the event listener
|
||||
await signinButton.addEventListener.firstCall.lastArg(); |
||||
|
||||
assert.calledOnceWithExactly(authenticateStub, headers, contextPath, false); |
||||
expect(global.window.location.href).to.equal("/success"); |
||||
}); |
||||
|
||||
it("handles authentication errors", async () => { |
||||
authenticateStub.rejects(new Error("Authentication failed")); |
||||
await setupLogin({}, "/some/path", signinButton); |
||||
|
||||
// Call the event listener
|
||||
await signinButton.addEventListener.firstCall.lastArg(); |
||||
|
||||
expect(global.window.location.href).to.equal(`/some/path/login?error`); |
||||
assert.calledOnceWithMatch( |
||||
global.console.error, |
||||
match.instanceOf(Error).and(match.has("message", "Authentication failed")), |
||||
); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,279 @@
@@ -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"); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,115 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-relying-party">WebAuthn Relying |
||||
* Parties</a> may use <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#enumdef-attestationconveyancepreference">AttestationConveyancePreference</a> |
||||
* to specify their preference regarding attestation conveyance during credential |
||||
* generation. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class AttestationConveyancePreference { |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-none">none</a> |
||||
* preference indicates that the Relying Party is not interested in |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a> |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#attestation">attestation</a>. |
||||
*/ |
||||
public static final AttestationConveyancePreference NONE = new AttestationConveyancePreference("none"); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-indirect">indirect</a> |
||||
* preference indicates that the Relying Party wants to receive a verifiable |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-statement">attestation |
||||
* statement</a>, but allows the client to decide how to obtain such an attestation |
||||
* statement. |
||||
*/ |
||||
public static final AttestationConveyancePreference INDIRECT = new AttestationConveyancePreference("indirect"); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-direct">direct</a> |
||||
* preference indicates that the Relying Party wants to receive the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-statement">attestation |
||||
* statement</a> as generated by the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>. |
||||
*/ |
||||
public static final AttestationConveyancePreference DIRECT = new AttestationConveyancePreference("direct"); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-enterprise">enterprise</a> |
||||
* preference indicates that the Relying Party wants to receive an |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-statement">attestation |
||||
* statement</a> that may include uniquely identifying information. |
||||
*/ |
||||
public static final AttestationConveyancePreference ENTERPRISE = new AttestationConveyancePreference("enterprise"); |
||||
|
||||
private final String value; |
||||
|
||||
AttestationConveyancePreference(String value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
/** |
||||
* Gets the String value of the preference. |
||||
* @return the String value of the preference. |
||||
*/ |
||||
public String getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
/** |
||||
* Gets an instance of {@link AttestationConveyancePreference} |
||||
* @param value the {@link #getValue()} |
||||
* @return an {@link AttestationConveyancePreference} |
||||
*/ |
||||
public static AttestationConveyancePreference valueOf(String value) { |
||||
switch (value) { |
||||
case "none": |
||||
return NONE; |
||||
case "indirect": |
||||
return INDIRECT; |
||||
case "direct": |
||||
return DIRECT; |
||||
case "enterprise": |
||||
return ENTERPRISE; |
||||
default: |
||||
return new AttestationConveyancePreference(value); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* A <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client extension |
||||
* input</a> entry in the {@link AuthenticationExtensionsClientInputs}. |
||||
* |
||||
* @param <T> |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see ImmutableAuthenticationExtensionsClientInput |
||||
*/ |
||||
public interface AuthenticationExtensionsClientInput<T> { |
||||
|
||||
/** |
||||
* Gets the <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension |
||||
* identifier</a>. |
||||
* @return the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension |
||||
* identifier</a>. |
||||
*/ |
||||
String getExtensionId(); |
||||
|
||||
/** |
||||
* Gets the <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client |
||||
* extension</a>. |
||||
* @return the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client |
||||
* extension</a>. |
||||
*/ |
||||
T getInput(); |
||||
|
||||
} |
||||
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#iface-authentication-extensions-client-inputs">AuthenticationExtensionsClientInputs</a> |
||||
* is a dictionary containing the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client extension |
||||
* input</a> values for zero or more |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-extensions">WebAuthn |
||||
* Extensions</a>. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see PublicKeyCredentialCreationOptions#getExtensions() |
||||
*/ |
||||
public interface AuthenticationExtensionsClientInputs { |
||||
|
||||
/** |
||||
* Gets all of the {@link AuthenticationExtensionsClientInput}. |
||||
* @return a non-null {@link List} of {@link AuthenticationExtensionsClientInput}. |
||||
*/ |
||||
List<AuthenticationExtensionsClientInput> getInputs(); |
||||
|
||||
} |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* A <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client extension |
||||
* output</a> entry in {@link AuthenticationExtensionsClientOutputs}. |
||||
* |
||||
* @param <T> |
||||
* @see AuthenticationExtensionsClientOutputs#getOutputs() |
||||
* @see CredentialPropertiesOutput |
||||
*/ |
||||
public interface AuthenticationExtensionsClientOutput<T> { |
||||
|
||||
/** |
||||
* Gets the <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension |
||||
* identifier</a>. |
||||
* @return the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension |
||||
* identifier</a>. |
||||
*/ |
||||
String getExtensionId(); |
||||
|
||||
/** |
||||
* The <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client |
||||
* extension output</a>. |
||||
* @return the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client |
||||
* extension output</a>. |
||||
*/ |
||||
T getOutput(); |
||||
|
||||
} |
||||
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientoutputs">AuthenticationExtensionsClientOutputs</a> |
||||
* is a dictionary containing the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client extension |
||||
* output</a> values for zero or more |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-extensions">WebAuthn |
||||
* Extensions</a>. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see PublicKeyCredential#getClientExtensionResults() |
||||
*/ |
||||
public interface AuthenticationExtensionsClientOutputs { |
||||
|
||||
/** |
||||
* Gets all of the {@link AuthenticationExtensionsClientOutput}. |
||||
* @return a non-null {@link List} of {@link AuthenticationExtensionsClientOutput}. |
||||
*/ |
||||
List<AuthenticationExtensionsClientOutput<?>> getOutputs(); |
||||
|
||||
} |
||||
@ -0,0 +1,205 @@
@@ -0,0 +1,205 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse">AuthenticatorAssertionResponse</a> |
||||
* interface represents an |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>'s response |
||||
* to a client’s request for generation of a new |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#authentication-assertion">authentication |
||||
* assertion</a> given the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-relying-party">WebAuthn Relying |
||||
* Party</a>'s challenge and OPTIONAL list of credentials it is aware of. This response |
||||
* contains a cryptographic signature proving possession of the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#credential-private-key">credential private |
||||
* key</a>, and optionally evidence of |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#user-consent">user consent</a> to a specific |
||||
* transaction. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see PublicKeyCredential#getResponse() |
||||
*/ |
||||
public final class AuthenticatorAssertionResponse extends AuthenticatorResponse { |
||||
|
||||
private final Bytes authenticatorData; |
||||
|
||||
private final Bytes signature; |
||||
|
||||
private final Bytes userHandle; |
||||
|
||||
private final Bytes attestationObject; |
||||
|
||||
private AuthenticatorAssertionResponse(Bytes clientDataJSON, Bytes authenticatorData, Bytes signature, |
||||
Bytes userHandle, Bytes attestationObject) { |
||||
super(clientDataJSON); |
||||
this.authenticatorData = authenticatorData; |
||||
this.signature = signature; |
||||
this.userHandle = userHandle; |
||||
this.attestationObject = attestationObject; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata">authenticatorData</a> |
||||
* contains the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator-data">authenticator |
||||
* data</a> returned by the authenticator. See |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data">6.1 |
||||
* Authenticator Data.</a>. |
||||
* @return the {@code authenticatorData} |
||||
*/ |
||||
public Bytes getAuthenticatorData() { |
||||
return this.authenticatorData; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature">signature</a> |
||||
* contains the raw signature returned from the authenticator. See |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-op-get-assertion">6.3.3 The |
||||
* authenticatorGetAssertion Operation</a>. |
||||
* @return the {@code signature} |
||||
*/ |
||||
public Bytes getSignature() { |
||||
return this.signature; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle">userHandle</a> |
||||
* is the <a href="https://www.w3.org/TR/webauthn-3/#user-handle">user handle</a> |
||||
* which is returned from the authenticator, or null if the authenticator did not |
||||
* return a user handle. See |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-op-get-assertion">6.3.3 The |
||||
* authenticatorGetAssertion Operation</a>. The authenticator MUST always return a |
||||
* user handle if the <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials">allowCredentials</a> |
||||
* option used in the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#authentication-ceremony">authentication |
||||
* ceremony</a> is empty, and MAY return one otherwise. |
||||
* @return the <a href="https://www.w3.org/TR/webauthn-3/#user-handle">user handle</a> |
||||
*/ |
||||
public Bytes getUserHandle() { |
||||
return this.userHandle; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-attestationobject">attestationObject</a> |
||||
* is an OPTIONAL attribute contains an |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-object">attestation |
||||
* object</a>, if the authenticator supports attestation in assertions. |
||||
* @return the {@code attestationObject} |
||||
*/ |
||||
public Bytes getAttestationObject() { |
||||
return this.attestationObject; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link AuthenticatorAssertionResponseBuilder} |
||||
* @return the {@link AuthenticatorAssertionResponseBuilder} |
||||
*/ |
||||
public static AuthenticatorAssertionResponseBuilder builder() { |
||||
return new AuthenticatorAssertionResponseBuilder(); |
||||
} |
||||
|
||||
/** |
||||
* Builds a {@link AuthenticatorAssertionResponse}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public static final class AuthenticatorAssertionResponseBuilder { |
||||
|
||||
private Bytes authenticatorData; |
||||
|
||||
private Bytes signature; |
||||
|
||||
private Bytes userHandle; |
||||
|
||||
private Bytes attestationObject; |
||||
|
||||
private Bytes clientDataJSON; |
||||
|
||||
private AuthenticatorAssertionResponseBuilder() { |
||||
} |
||||
|
||||
/** |
||||
* Set the {@link #getAuthenticatorData()} property |
||||
* @param authenticatorData the authenticator data. |
||||
* @return the {@link AuthenticatorAssertionResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAssertionResponseBuilder authenticatorData(Bytes authenticatorData) { |
||||
this.authenticatorData = authenticatorData; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Set the {@link #getSignature()} property |
||||
* @param signature the signature |
||||
* @return the {@link AuthenticatorAssertionResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAssertionResponseBuilder signature(Bytes signature) { |
||||
this.signature = signature; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Set the {@link #getUserHandle()} property |
||||
* @param userHandle the user handle |
||||
* @return the {@link AuthenticatorAssertionResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAssertionResponseBuilder userHandle(Bytes userHandle) { |
||||
this.userHandle = userHandle; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Set the {@link #attestationObject} property |
||||
* @param attestationObject the attestation object |
||||
* @return the {@link AuthenticatorAssertionResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAssertionResponseBuilder attestationObject(Bytes attestationObject) { |
||||
this.attestationObject = attestationObject; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Set the {@link #getClientDataJSON()} property |
||||
* @param clientDataJSON the client data JSON |
||||
* @return the {@link AuthenticatorAssertionResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAssertionResponseBuilder clientDataJSON(Bytes clientDataJSON) { |
||||
this.clientDataJSON = clientDataJSON; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Builds the {@link AuthenticatorAssertionResponse} |
||||
* @return the {@link AuthenticatorAssertionResponse} |
||||
*/ |
||||
public AuthenticatorAssertionResponse build() { |
||||
return new AuthenticatorAssertionResponse(this.clientDataJSON, this.authenticatorData, this.signature, |
||||
this.userHandle, this.attestationObject); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment">AuthenticatorAttachment</a>. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class AuthenticatorAttachment { |
||||
|
||||
/** |
||||
* Indicates <a href= |
||||
* "https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#cross-platform-attachment">cross-platform |
||||
* attachment</a>. |
||||
* |
||||
* <p> |
||||
* Authenticators of this class are removable from, and can "roam" among, client |
||||
* platforms. |
||||
*/ |
||||
public static final AuthenticatorAttachment CROSS_PLATFORM = new AuthenticatorAttachment("cross-platform"); |
||||
|
||||
/** |
||||
* Indicates <a href= |
||||
* "https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#platform-attachment">platform |
||||
* attachment</a>. |
||||
* |
||||
* <p> |
||||
* Usually, authenticators of this class are not removable from the platform. |
||||
*/ |
||||
public static final AuthenticatorAttachment PLATFORM = new AuthenticatorAttachment("platform"); |
||||
|
||||
private final String value; |
||||
|
||||
AuthenticatorAttachment(String value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
/** |
||||
* Gets the value. |
||||
* @return the value. |
||||
*/ |
||||
public String getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "AuthenticatorAttachment [" + this.value + "]"; |
||||
} |
||||
|
||||
/** |
||||
* Gets an instance of {@link AuthenticatorAttachment} based upon the value passed in. |
||||
* @param value the value to obtain the {@link AuthenticatorAttachment} |
||||
* @return the {@link AuthenticatorAttachment} |
||||
*/ |
||||
public static AuthenticatorAttachment valueOf(String value) { |
||||
switch (value) { |
||||
case "cross-platform": |
||||
return CROSS_PLATFORM; |
||||
case "platform": |
||||
return PLATFORM; |
||||
default: |
||||
return new AuthenticatorAttachment(value); |
||||
} |
||||
} |
||||
|
||||
public static AuthenticatorAttachment[] values() { |
||||
return new AuthenticatorAttachment[] { CROSS_PLATFORM, PLATFORM }; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,144 @@
@@ -0,0 +1,144 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse">AuthenticatorAttestationResponse</a> |
||||
* represents the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>'s response |
||||
* to a client’s request for the creation of a new |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#public-key-credential">public key |
||||
* credential</a>. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see PublicKeyCredential#getResponse() |
||||
*/ |
||||
public final class AuthenticatorAttestationResponse extends AuthenticatorResponse { |
||||
|
||||
private final Bytes attestationObject; |
||||
|
||||
private final List<AuthenticatorTransport> transports; |
||||
|
||||
private AuthenticatorAttestationResponse(Bytes clientDataJSON, Bytes attestationObject, |
||||
List<AuthenticatorTransport> transports) { |
||||
super(clientDataJSON); |
||||
this.attestationObject = attestationObject; |
||||
this.transports = transports; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-attestationobject">attestationObject</a> |
||||
* attribute contains an attestation object, which is opaque to, and cryptographically |
||||
* protected against tampering by, the client. |
||||
* @return the attestationObject |
||||
*/ |
||||
public Bytes getAttestationObject() { |
||||
return this.attestationObject; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-gettransports">transports</a> |
||||
* returns the <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-transports-slot">transports</a> |
||||
* @return the transports |
||||
*/ |
||||
public List<AuthenticatorTransport> getTransports() { |
||||
return this.transports; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new instance. |
||||
* @return the {@link AuthenticatorAttestationResponseBuilder} |
||||
*/ |
||||
public static AuthenticatorAttestationResponseBuilder builder() { |
||||
return new AuthenticatorAttestationResponseBuilder(); |
||||
} |
||||
|
||||
/** |
||||
* Builds {@link AuthenticatorAssertionResponse}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public static final class AuthenticatorAttestationResponseBuilder { |
||||
|
||||
private Bytes attestationObject; |
||||
|
||||
private List<AuthenticatorTransport> transports; |
||||
|
||||
private Bytes clientDataJSON; |
||||
|
||||
private AuthenticatorAttestationResponseBuilder() { |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getAttestationObject()} property. |
||||
* @param attestationObject the attestation object. |
||||
* @return the {@link AuthenticatorAttestationResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAttestationResponseBuilder attestationObject(Bytes attestationObject) { |
||||
this.attestationObject = attestationObject; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getTransports()} property. |
||||
* @param transports the transports |
||||
* @return the {@link AuthenticatorAttestationResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAttestationResponseBuilder transports(AuthenticatorTransport... transports) { |
||||
return transports(Arrays.asList(transports)); |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getTransports()} property. |
||||
* @param transports the transports |
||||
* @return the {@link AuthenticatorAttestationResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAttestationResponseBuilder transports(List<AuthenticatorTransport> transports) { |
||||
this.transports = transports; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getClientDataJSON()} property. |
||||
* @param clientDataJSON the client data JSON. |
||||
* @return the {@link AuthenticatorAttestationResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAttestationResponseBuilder clientDataJSON(Bytes clientDataJSON) { |
||||
this.clientDataJSON = clientDataJSON; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Builds a {@link AuthenticatorAssertionResponse}. |
||||
* @return the {@link AuthenticatorAttestationResponseBuilder} |
||||
*/ |
||||
public AuthenticatorAttestationResponse build() { |
||||
return new AuthenticatorAttestationResponse(this.clientDataJSON, this.attestationObject, this.transports); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#iface-authenticatorresponse">AuthenticatorResponse</a> |
||||
* represents <a href="https://www.w3.org/TR/webauthn-3/#authenticator">Authenticators</a> |
||||
* respond to <a href="https://www.w3.org/TR/webauthn-3/#relying-party">Relying Party</a> |
||||
* requests. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public abstract class AuthenticatorResponse { |
||||
|
||||
private final Bytes clientDataJSON; |
||||
|
||||
/** |
||||
* Creates a new instance |
||||
* @param clientDataJSON the {@link #getClientDataJSON()} |
||||
*/ |
||||
AuthenticatorResponse(Bytes clientDataJSON) { |
||||
this.clientDataJSON = clientDataJSON; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson">clientDataJSON</a> |
||||
* contains a JSON-compatible serialization of the client data, the hash of which is |
||||
* passed to the authenticator by the client in its call to either create() or get() |
||||
* (i.e., the client data itself is not sent to the authenticator). |
||||
* @return the client data JSON |
||||
*/ |
||||
public Bytes getClientDataJSON() { |
||||
return this.clientDataJSON; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria">AuthenticatorAttachment</a> |
||||
* can be used by |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-relying-party">WebAuthn Relying |
||||
* Parties</a> to specify their requirements regarding authenticator attributes. |
||||
* |
||||
* There is no <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-requireresidentkey">requireResidentKey</a> |
||||
* property because it is only for backwards compatability with WebAuthn Level 1. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see PublicKeyCredentialCreationOptions#getAuthenticatorSelection() |
||||
*/ |
||||
public final class AuthenticatorSelectionCriteria { |
||||
|
||||
private final AuthenticatorAttachment authenticatorAttachment; |
||||
|
||||
private final ResidentKeyRequirement residentKey; |
||||
|
||||
private final UserVerificationRequirement userVerification; |
||||
|
||||
// NOTE: There is no requireResidentKey property because it is only for backward
|
||||
// compatability with WebAuthn Level 1
|
||||
|
||||
/** |
||||
* Creates a new instance |
||||
* @param authenticatorAttachment the authenticator attachment |
||||
* @param residentKey the resident key requirement |
||||
* @param userVerification the user verification |
||||
*/ |
||||
private AuthenticatorSelectionCriteria(AuthenticatorAttachment authenticatorAttachment, |
||||
ResidentKeyRequirement residentKey, UserVerificationRequirement userVerification) { |
||||
this.authenticatorAttachment = authenticatorAttachment; |
||||
this.residentKey = residentKey; |
||||
this.userVerification = userVerification; |
||||
} |
||||
|
||||
/** |
||||
* If <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment"> |
||||
* authenticatorAttachment</a> is present, eligible |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticators</a> are |
||||
* filtered to be only those authenticators attached with the specified |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#enum-attachment">authenticator |
||||
* attachment modality</a> (see also <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#sctn-authenticator-attachment-modality">6.2.1 |
||||
* Authenticator Attachment Modality</a>). |
||||
* @return the authenticator attachment |
||||
*/ |
||||
public AuthenticatorAttachment getAuthenticatorAttachment() { |
||||
return this.authenticatorAttachment; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey">residentKey</a> |
||||
* specifies the extent to which the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#relying-party">Relying Party</a> desires |
||||
* to create a <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#client-side-discoverable-credential">client-side |
||||
* discoverable credential</a>. |
||||
* @return the residenty key requirement |
||||
*/ |
||||
public ResidentKeyRequirement getResidentKey() { |
||||
return this.residentKey; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification">userVerification</a> |
||||
* specifies the <a href="https://www.w3.org/TR/webauthn-3/#relying-party">Relying |
||||
* Party</a>'s requirements regarding |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#user-verification">user verification</a> |
||||
* for the <a href= |
||||
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">create()</a> |
||||
* operation. |
||||
* @return the user verification requirement |
||||
*/ |
||||
public UserVerificationRequirement getUserVerification() { |
||||
return this.userVerification; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link AuthenticatorSelectionCriteriaBuilder} |
||||
* @return a new {@link AuthenticatorSelectionCriteriaBuilder} |
||||
*/ |
||||
public static AuthenticatorSelectionCriteriaBuilder builder() { |
||||
return new AuthenticatorSelectionCriteriaBuilder(); |
||||
} |
||||
|
||||
/** |
||||
* Creates a {@link AuthenticatorSelectionCriteria} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public static final class AuthenticatorSelectionCriteriaBuilder { |
||||
|
||||
private AuthenticatorAttachment authenticatorAttachment; |
||||
|
||||
private ResidentKeyRequirement residentKey; |
||||
|
||||
private UserVerificationRequirement userVerification; |
||||
|
||||
private AuthenticatorSelectionCriteriaBuilder() { |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getAuthenticatorAttachment()} property. |
||||
* @param authenticatorAttachment the authenticator attachment |
||||
* @return the {@link AuthenticatorSelectionCriteriaBuilder} |
||||
*/ |
||||
public AuthenticatorSelectionCriteriaBuilder authenticatorAttachment( |
||||
AuthenticatorAttachment authenticatorAttachment) { |
||||
this.authenticatorAttachment = authenticatorAttachment; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getResidentKey()} property. |
||||
* @param residentKey the resident key |
||||
* @return the {@link AuthenticatorSelectionCriteriaBuilder} |
||||
*/ |
||||
public AuthenticatorSelectionCriteriaBuilder residentKey(ResidentKeyRequirement residentKey) { |
||||
this.residentKey = residentKey; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getUserVerification()} property. |
||||
* @param userVerification the user verification requirement |
||||
* @return the {@link AuthenticatorSelectionCriteriaBuilder} |
||||
*/ |
||||
public AuthenticatorSelectionCriteriaBuilder userVerification(UserVerificationRequirement userVerification) { |
||||
this.userVerification = userVerification; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Builds a {@link AuthenticatorSelectionCriteria} |
||||
* @return a new {@link AuthenticatorSelectionCriteria} |
||||
*/ |
||||
public AuthenticatorSelectionCriteria build() { |
||||
return new AuthenticatorSelectionCriteria(this.authenticatorAttachment, this.residentKey, |
||||
this.userVerification); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport">AuthenticatorTransport</a> |
||||
* defines hints as to how clients might communicate with a particular authenticator in |
||||
* order to obtain an assertion for a specific credential. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class AuthenticatorTransport { |
||||
|
||||
/** |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-usb">usbc</a> |
||||
* indicates the respective authenticator can be contacted over removable USB. |
||||
*/ |
||||
public static final AuthenticatorTransport USB = new AuthenticatorTransport("usb"); |
||||
|
||||
/** |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-nfc">nfc</a> |
||||
* indicates the respective authenticator can be contacted over Near Field |
||||
* Communication (NFC). |
||||
*/ |
||||
public static final AuthenticatorTransport NFC = new AuthenticatorTransport("nfc"); |
||||
|
||||
/** |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-ble">ble</a> |
||||
* Indicates the respective authenticator can be contacted over Bluetooth Smart |
||||
* (Bluetooth Low Energy / BLE). |
||||
*/ |
||||
public static final AuthenticatorTransport BLE = new AuthenticatorTransport("ble"); |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-smart-card">smart-card</a> |
||||
* indicates the respective authenticator can be contacted over ISO/IEC 7816 smart |
||||
* card with contacts. |
||||
*/ |
||||
public static final AuthenticatorTransport SMART_CARD = new AuthenticatorTransport("smart-card"); |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-hybrid">hybrid</a> |
||||
* indicates the respective authenticator can be contacted using a combination of |
||||
* (often separate) data-transport and proximity mechanisms. This supports, for |
||||
* example, authentication on a desktop computer using a smartphone. |
||||
*/ |
||||
public static final AuthenticatorTransport HYBRID = new AuthenticatorTransport("hybrid"); |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-internal">internal</a> |
||||
* indicates the respective authenticator is contacted using a client device-specific |
||||
* transport, i.e., it is a platform authenticator. These authenticators are not |
||||
* removable from the client device. |
||||
*/ |
||||
public static final AuthenticatorTransport INTERNAL = new AuthenticatorTransport("internal"); |
||||
|
||||
private final String value; |
||||
|
||||
AuthenticatorTransport(String value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
/** |
||||
* Get's the value. |
||||
* @return the value. |
||||
*/ |
||||
public String getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
/** |
||||
* Gets an instance of {@link AuthenticatorTransport}. |
||||
* @param value the value of the {@link AuthenticatorTransport} |
||||
* @return the {@link AuthenticatorTransport} |
||||
*/ |
||||
public static AuthenticatorTransport valueOf(String value) { |
||||
switch (value) { |
||||
case "usb": |
||||
return USB; |
||||
case "nfc": |
||||
return NFC; |
||||
case "ble": |
||||
return BLE; |
||||
case "smart-card": |
||||
return SMART_CARD; |
||||
case "hybrid": |
||||
return HYBRID; |
||||
case "internal": |
||||
return INTERNAL; |
||||
default: |
||||
return new AuthenticatorTransport(value); |
||||
} |
||||
} |
||||
|
||||
public static AuthenticatorTransport[] values() { |
||||
return new AuthenticatorTransport[] { USB, NFC, BLE, HYBRID, INTERNAL }; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.security.SecureRandom; |
||||
import java.util.Arrays; |
||||
import java.util.Base64; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An object representation of byte[]. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class Bytes { |
||||
|
||||
private static final SecureRandom RANDOM = new SecureRandom(); |
||||
|
||||
private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding(); |
||||
|
||||
private static final Base64.Decoder DECODER = Base64.getUrlDecoder(); |
||||
|
||||
private final byte[] bytes; |
||||
|
||||
/** |
||||
* Creates a new instance |
||||
* @param bytes the raw base64UrlString that will be encoded. |
||||
*/ |
||||
public Bytes(byte[] bytes) { |
||||
Assert.notNull(bytes, "bytes cannot be null"); |
||||
this.bytes = bytes; |
||||
} |
||||
|
||||
/** |
||||
* Gets the raw bytes. |
||||
* @return the bytes |
||||
*/ |
||||
public byte[] getBytes() { |
||||
return Arrays.copyOf(this.bytes, this.bytes.length); |
||||
} |
||||
|
||||
/** |
||||
* Gets the bytes as Base64 URL encoded String. |
||||
* @return |
||||
*/ |
||||
public String toBase64UrlString() { |
||||
return ENCODER.encodeToString(getBytes()); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object obj) { |
||||
if (obj instanceof Bytes that) { |
||||
return that.toBase64UrlString().equals(toBase64UrlString()); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return toBase64UrlString().hashCode(); |
||||
} |
||||
|
||||
public String toString() { |
||||
return "Bytes[" + toBase64UrlString() + "]"; |
||||
} |
||||
|
||||
/** |
||||
* Creates a secure random {@link Bytes} with random bytes and sufficient entropy. |
||||
* @return a new secure random generated {@link Bytes} |
||||
*/ |
||||
public static Bytes random() { |
||||
byte[] bytes = new byte[32]; |
||||
RANDOM.nextBytes(bytes); |
||||
return new Bytes(bytes); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new instance from a base64 url string. |
||||
* @param base64UrlString the base64 url string |
||||
* @return the {@link Bytes} |
||||
*/ |
||||
public static Bytes fromBase64(String base64UrlString) { |
||||
byte[] bytes = DECODER.decode(base64UrlString); |
||||
return new Bytes(bytes); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier">COSEAlgorithmIdentifier</a> is |
||||
* used to identify a cryptographic algorithm. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see PublicKeyCredentialParameters#getAlg() |
||||
*/ |
||||
public final class COSEAlgorithmIdentifier { |
||||
|
||||
public static final COSEAlgorithmIdentifier EdDSA = new COSEAlgorithmIdentifier(-8); |
||||
|
||||
public static final COSEAlgorithmIdentifier ES256 = new COSEAlgorithmIdentifier(-7); |
||||
|
||||
public static final COSEAlgorithmIdentifier ES384 = new COSEAlgorithmIdentifier(-35); |
||||
|
||||
public static final COSEAlgorithmIdentifier ES512 = new COSEAlgorithmIdentifier(-36); |
||||
|
||||
public static final COSEAlgorithmIdentifier RS256 = new COSEAlgorithmIdentifier(-257); |
||||
|
||||
public static final COSEAlgorithmIdentifier RS384 = new COSEAlgorithmIdentifier(-258); |
||||
|
||||
public static final COSEAlgorithmIdentifier RS512 = new COSEAlgorithmIdentifier(-259); |
||||
|
||||
public static final COSEAlgorithmIdentifier RS1 = new COSEAlgorithmIdentifier(-65535); |
||||
|
||||
private final long value; |
||||
|
||||
private COSEAlgorithmIdentifier(long value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
public long getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return String.valueOf(this.value); |
||||
} |
||||
|
||||
public static COSEAlgorithmIdentifier[] values() { |
||||
return new COSEAlgorithmIdentifier[] { EdDSA, ES256, ES384, ES512, RS256, RS384, RS512, RS1 }; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* Implements <a href= |
||||
* "https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension"> |
||||
* Credential Protection (credProtect)</a>. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public class CredProtectAuthenticationExtensionsClientInput |
||||
implements AuthenticationExtensionsClientInput<CredProtectAuthenticationExtensionsClientInput.CredProtect> { |
||||
|
||||
private final CredProtect input; |
||||
|
||||
public CredProtectAuthenticationExtensionsClientInput(CredProtect input) { |
||||
this.input = input; |
||||
} |
||||
|
||||
@Override |
||||
public String getExtensionId() { |
||||
return "credProtect"; |
||||
} |
||||
|
||||
@Override |
||||
public CredProtect getInput() { |
||||
return this.input; |
||||
} |
||||
|
||||
public static class CredProtect { |
||||
|
||||
private final ProtectionPolicy credProtectionPolicy; |
||||
|
||||
private final boolean enforceCredentialProtectionPolicy; |
||||
|
||||
public CredProtect(ProtectionPolicy credProtectionPolicy, boolean enforceCredentialProtectionPolicy) { |
||||
this.enforceCredentialProtectionPolicy = enforceCredentialProtectionPolicy; |
||||
this.credProtectionPolicy = credProtectionPolicy; |
||||
} |
||||
|
||||
public boolean isEnforceCredentialProtectionPolicy() { |
||||
return this.enforceCredentialProtectionPolicy; |
||||
} |
||||
|
||||
public ProtectionPolicy getCredProtectionPolicy() { |
||||
return this.credProtectionPolicy; |
||||
} |
||||
|
||||
public enum ProtectionPolicy { |
||||
|
||||
USER_VERIFICATION_OPTIONAL, USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST, USER_VERIFICATION_REQUIRED |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dictdef-credentialpropertiesoutput">CredentialPropertiesOutput</a> |
||||
* is the Client extension output. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public class CredentialPropertiesOutput |
||||
implements AuthenticationExtensionsClientOutput<CredentialPropertiesOutput.ExtensionOutput> { |
||||
|
||||
/** |
||||
* The extension id. |
||||
*/ |
||||
public static final String EXTENSION_ID = "credProps"; |
||||
|
||||
private final ExtensionOutput output; |
||||
|
||||
/** |
||||
* Creates a new instance. |
||||
* @param rk is the resident key is discoverable |
||||
*/ |
||||
public CredentialPropertiesOutput(boolean rk) { |
||||
this.output = new ExtensionOutput(rk); |
||||
} |
||||
|
||||
@Override |
||||
public String getExtensionId() { |
||||
return EXTENSION_ID; |
||||
} |
||||
|
||||
@Override |
||||
public ExtensionOutput getOutput() { |
||||
return this.output; |
||||
} |
||||
|
||||
/** |
||||
* The output for {@link CredentialPropertiesOutput} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see #getOutput() |
||||
*/ |
||||
public static final class ExtensionOutput { |
||||
|
||||
private final boolean rk; |
||||
|
||||
private ExtensionOutput(boolean rk) { |
||||
this.rk = rk; |
||||
} |
||||
|
||||
/** |
||||
* This OPTIONAL property, known abstractly as the resident key credential |
||||
* property (i.e., client-side discoverable credential property), is a Boolean |
||||
* value indicating whether the PublicKeyCredential returned as a result of a |
||||
* registration ceremony is a client-side discoverable credential. |
||||
* @return is resident key credential property |
||||
*/ |
||||
public boolean isRk() { |
||||
return this.rk; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,136 @@
@@ -0,0 +1,136 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.time.Instant; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* Represents a <a href="https://www.w3.org/TR/webauthn-3/#credential-record">Credential |
||||
* Record</a> that is stored by the Relying Party |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#reg-ceremony-store-credential-record">after |
||||
* successful registration</a>. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public interface CredentialRecord { |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-type">credential.type</a> |
||||
* @return |
||||
*/ |
||||
PublicKeyCredentialType getCredentialType(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-id">credential.id</a>. |
||||
* @return |
||||
*/ |
||||
Bytes getCredentialId(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-publickey">publicKey</a> |
||||
* @return |
||||
*/ |
||||
PublicKeyCose getPublicKey(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-signcount">authData.signCount</a> |
||||
* @return |
||||
*/ |
||||
long getSignatureCount(); |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-uvinitialized">uvInitialized</a> |
||||
* is the value of the UV (user verified) flag in authData and indicates whether any |
||||
* credential from this public key credential source has had the UV flag set. |
||||
* @return |
||||
*/ |
||||
boolean isUvInitialized(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-transports">transpots</a> |
||||
* is the value returned from {@code response.getTransports()}. |
||||
* @return |
||||
*/ |
||||
Set<AuthenticatorTransport> getTransports(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-backupeligible">backupElgible</a> |
||||
* flag is the same as the BE flag in authData. |
||||
* @return |
||||
*/ |
||||
boolean isBackupEligible(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-backupstate">backupState</a> |
||||
* flag is the same as the BS flag in authData. |
||||
* @return |
||||
*/ |
||||
boolean isBackupState(); |
||||
|
||||
/** |
||||
* A reference to the associated {@link PublicKeyCredentialUserEntity#getId()} |
||||
* @return |
||||
*/ |
||||
Bytes getUserEntityUserId(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-attestationobject">attestationObject</a> |
||||
* is the value of the attestationObject attribute when the public key credential |
||||
* source was registered. |
||||
* @return the attestationObject |
||||
*/ |
||||
Bytes getAttestationObject(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-attestationclientdatajson">attestationClientDataJSON</a> |
||||
* is the value of the attestationObject attribute when the public key credential |
||||
* source was registered. |
||||
* @return |
||||
*/ |
||||
Bytes getAttestationClientDataJSON(); |
||||
|
||||
/** |
||||
* A human-readable label for this {@link CredentialRecord} assigned by the user. |
||||
* @return a label |
||||
*/ |
||||
String getLabel(); |
||||
|
||||
/** |
||||
* The last time this {@link CredentialRecord} was used. |
||||
* @return the last time this {@link CredentialRecord} was used. |
||||
*/ |
||||
Instant getLastUsed(); |
||||
|
||||
/** |
||||
* When this {@link CredentialRecord} was created. |
||||
* @return When this {@link CredentialRecord} was created. |
||||
*/ |
||||
Instant getCreated(); |
||||
|
||||
} |
||||
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* An immutable {@link AuthenticationExtensionsClientInput}. |
||||
* |
||||
* @param <T> the input type |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see AuthenticationExtensionsClientInputs |
||||
*/ |
||||
public class ImmutableAuthenticationExtensionsClientInput<T> implements AuthenticationExtensionsClientInput<T> { |
||||
|
||||
/** |
||||
* https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
|
||||
*/ |
||||
public static final AuthenticationExtensionsClientInput<Boolean> credProps = new ImmutableAuthenticationExtensionsClientInput<>( |
||||
"credProps", true); |
||||
|
||||
private final String extensionId; |
||||
|
||||
private final T input; |
||||
|
||||
/** |
||||
* Creates a new instance |
||||
* @param extensionId the extension id. |
||||
* @param input the input. |
||||
*/ |
||||
public ImmutableAuthenticationExtensionsClientInput(String extensionId, T input) { |
||||
this.extensionId = extensionId; |
||||
this.input = input; |
||||
} |
||||
|
||||
@Override |
||||
public String getExtensionId() { |
||||
return this.extensionId; |
||||
} |
||||
|
||||
@Override |
||||
public T getInput() { |
||||
return this.input; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* An immutable implementation of {@link AuthenticationExtensionsClientInputs}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public class ImmutableAuthenticationExtensionsClientInputs implements AuthenticationExtensionsClientInputs { |
||||
|
||||
private final List<AuthenticationExtensionsClientInput> inputs; |
||||
|
||||
public ImmutableAuthenticationExtensionsClientInputs(List<AuthenticationExtensionsClientInput> inputs) { |
||||
this.inputs = inputs; |
||||
} |
||||
|
||||
public ImmutableAuthenticationExtensionsClientInputs(AuthenticationExtensionsClientInput... inputs) { |
||||
this(Arrays.asList(inputs)); |
||||
} |
||||
|
||||
@Override |
||||
public List<AuthenticationExtensionsClientInput> getInputs() { |
||||
return this.inputs; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* An immutable implementation of {@link AuthenticationExtensionsClientOutputs}. |
||||
* |
||||
* @author Rob Winch |
||||
*/ |
||||
public class ImmutableAuthenticationExtensionsClientOutputs implements AuthenticationExtensionsClientOutputs { |
||||
|
||||
private final List<AuthenticationExtensionsClientOutput<?>> outputs; |
||||
|
||||
public ImmutableAuthenticationExtensionsClientOutputs(List<AuthenticationExtensionsClientOutput<?>> outputs) { |
||||
this.outputs = outputs; |
||||
} |
||||
|
||||
public ImmutableAuthenticationExtensionsClientOutputs(AuthenticationExtensionsClientOutput<?>... outputs) { |
||||
this(Arrays.asList(outputs)); |
||||
} |
||||
|
||||
@Override |
||||
public List<AuthenticationExtensionsClientOutput<?>> getOutputs() { |
||||
return this.outputs; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,285 @@
@@ -0,0 +1,285 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.time.Instant; |
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* An immutable {@link CredentialRecord}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class ImmutableCredentialRecord implements CredentialRecord { |
||||
|
||||
private final PublicKeyCredentialType credentialType; |
||||
|
||||
private final Bytes credentialId; |
||||
|
||||
private final Bytes userEntityUserId; |
||||
|
||||
private final PublicKeyCose publicKey; |
||||
|
||||
private final long signatureCount; |
||||
|
||||
private final boolean uvInitialized; |
||||
|
||||
private final Set<AuthenticatorTransport> transports; |
||||
|
||||
private final boolean backupEligible; |
||||
|
||||
private final boolean backupState; |
||||
|
||||
private final Bytes attestationObject; |
||||
|
||||
private final Bytes attestationClientDataJSON; |
||||
|
||||
private final Instant created; |
||||
|
||||
private final Instant lastUsed; |
||||
|
||||
private final String label; |
||||
|
||||
private ImmutableCredentialRecord(PublicKeyCredentialType credentialType, Bytes credentialId, |
||||
Bytes userEntityUserId, PublicKeyCose publicKey, long signatureCount, boolean uvInitialized, |
||||
Set<AuthenticatorTransport> transports, boolean backupEligible, boolean backupState, |
||||
Bytes attestationObject, Bytes attestationClientDataJSON, Instant created, Instant lastUsed, String label) { |
||||
this.credentialType = credentialType; |
||||
this.credentialId = credentialId; |
||||
this.userEntityUserId = userEntityUserId; |
||||
this.publicKey = publicKey; |
||||
this.signatureCount = signatureCount; |
||||
this.uvInitialized = uvInitialized; |
||||
this.transports = transports; |
||||
this.backupEligible = backupEligible; |
||||
this.backupState = backupState; |
||||
this.attestationObject = attestationObject; |
||||
this.attestationClientDataJSON = attestationClientDataJSON; |
||||
this.created = created; |
||||
this.lastUsed = lastUsed; |
||||
this.label = label; |
||||
} |
||||
|
||||
@Override |
||||
public PublicKeyCredentialType getCredentialType() { |
||||
return this.credentialType; |
||||
} |
||||
|
||||
@Override |
||||
public Bytes getCredentialId() { |
||||
return this.credentialId; |
||||
} |
||||
|
||||
@Override |
||||
public Bytes getUserEntityUserId() { |
||||
return this.userEntityUserId; |
||||
} |
||||
|
||||
@Override |
||||
public PublicKeyCose getPublicKey() { |
||||
return this.publicKey; |
||||
} |
||||
|
||||
@Override |
||||
public long getSignatureCount() { |
||||
return this.signatureCount; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isUvInitialized() { |
||||
return this.uvInitialized; |
||||
} |
||||
|
||||
@Override |
||||
public Set<AuthenticatorTransport> getTransports() { |
||||
return this.transports; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isBackupEligible() { |
||||
return this.backupEligible; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isBackupState() { |
||||
return this.backupState; |
||||
} |
||||
|
||||
@Override |
||||
public Bytes getAttestationObject() { |
||||
return this.attestationObject; |
||||
} |
||||
|
||||
@Override |
||||
public Bytes getAttestationClientDataJSON() { |
||||
return this.attestationClientDataJSON; |
||||
} |
||||
|
||||
@Override |
||||
public Instant getCreated() { |
||||
return this.created; |
||||
} |
||||
|
||||
@Override |
||||
public Instant getLastUsed() { |
||||
return this.lastUsed; |
||||
} |
||||
|
||||
@Override |
||||
public String getLabel() { |
||||
return this.label; |
||||
} |
||||
|
||||
public static ImmutableCredentialRecordBuilder builder() { |
||||
return new ImmutableCredentialRecordBuilder(); |
||||
} |
||||
|
||||
public static ImmutableCredentialRecordBuilder fromCredentialRecord(CredentialRecord credentialRecord) { |
||||
return new ImmutableCredentialRecordBuilder(credentialRecord); |
||||
} |
||||
|
||||
public static final class ImmutableCredentialRecordBuilder { |
||||
|
||||
private PublicKeyCredentialType credentialType; |
||||
|
||||
private Bytes credentialId; |
||||
|
||||
private Bytes userEntityUserId; |
||||
|
||||
private PublicKeyCose publicKey; |
||||
|
||||
private long signatureCount; |
||||
|
||||
private boolean uvInitialized; |
||||
|
||||
private Set<AuthenticatorTransport> transports; |
||||
|
||||
private boolean backupEligible; |
||||
|
||||
private boolean backupState; |
||||
|
||||
private Bytes attestationObject; |
||||
|
||||
private Bytes attestationClientDataJSON; |
||||
|
||||
private Instant created = Instant.now(); |
||||
|
||||
private Instant lastUsed = this.created; |
||||
|
||||
private String label; |
||||
|
||||
private ImmutableCredentialRecordBuilder() { |
||||
} |
||||
|
||||
private ImmutableCredentialRecordBuilder(CredentialRecord other) { |
||||
this.credentialType = other.getCredentialType(); |
||||
this.credentialId = other.getCredentialId(); |
||||
this.userEntityUserId = other.getUserEntityUserId(); |
||||
this.publicKey = other.getPublicKey(); |
||||
this.signatureCount = other.getSignatureCount(); |
||||
this.uvInitialized = other.isUvInitialized(); |
||||
this.transports = other.getTransports(); |
||||
this.backupEligible = other.isBackupEligible(); |
||||
this.backupState = other.isBackupState(); |
||||
this.attestationObject = other.getAttestationObject(); |
||||
this.attestationClientDataJSON = other.getAttestationClientDataJSON(); |
||||
this.created = other.getCreated(); |
||||
this.lastUsed = other.getLastUsed(); |
||||
this.label = other.getLabel(); |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder credentialType(PublicKeyCredentialType credentialType) { |
||||
this.credentialType = credentialType; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder credentialId(Bytes credentialId) { |
||||
this.credentialId = credentialId; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder userEntityUserId(Bytes userEntityUserId) { |
||||
this.userEntityUserId = userEntityUserId; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder publicKey(PublicKeyCose publicKey) { |
||||
this.publicKey = publicKey; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder signatureCount(long signatureCount) { |
||||
this.signatureCount = signatureCount; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder uvInitialized(boolean uvInitialized) { |
||||
this.uvInitialized = uvInitialized; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder transports(Set<AuthenticatorTransport> transports) { |
||||
this.transports = transports; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder backupEligible(boolean backupEligible) { |
||||
this.backupEligible = backupEligible; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder backupState(boolean backupState) { |
||||
this.backupState = backupState; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder attestationObject(Bytes attestationObject) { |
||||
this.attestationObject = attestationObject; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder attestationClientDataJSON(Bytes attestationClientDataJSON) { |
||||
this.attestationClientDataJSON = attestationClientDataJSON; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder created(Instant created) { |
||||
this.created = created; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder lastUsed(Instant lastUsed) { |
||||
this.lastUsed = lastUsed; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecordBuilder label(String label) { |
||||
this.label = label; |
||||
return this; |
||||
} |
||||
|
||||
public ImmutableCredentialRecord build() { |
||||
return new ImmutableCredentialRecord(this.credentialType, this.credentialId, this.userEntityUserId, |
||||
this.publicKey, this.signatureCount, this.uvInitialized, this.transports, this.backupEligible, |
||||
this.backupState, this.attestationObject, this.attestationClientDataJSON, this.created, |
||||
this.lastUsed, this.label); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.Base64; |
||||
|
||||
/** |
||||
* An immutable {@link PublicKeyCose} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public class ImmutablePublicKeyCose implements PublicKeyCose { |
||||
|
||||
private final byte[] bytes; |
||||
|
||||
/** |
||||
* Creates a new instance. |
||||
* @param bytes the raw bytes of the public key. |
||||
*/ |
||||
public ImmutablePublicKeyCose(byte[] bytes) { |
||||
this.bytes = Arrays.copyOf(bytes, bytes.length); |
||||
} |
||||
|
||||
@Override |
||||
public byte[] getBytes() { |
||||
return Arrays.copyOf(this.bytes, this.bytes.length); |
||||
} |
||||
|
||||
/** |
||||
* Creates a new instance form a Base64 URL encoded String |
||||
* @param base64EncodedString the base64EncodedString encoded String |
||||
* @return |
||||
*/ |
||||
public static ImmutablePublicKeyCose fromBase64(String base64EncodedString) { |
||||
byte[] decode = Base64.getUrlDecoder().decode(base64EncodedString); |
||||
return new ImmutablePublicKeyCose(decode); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* A <a href="https://www.w3.org/TR/webauthn-3/#sctn-encoded-credPubKey-examples">COSE |
||||
* encoded public key</a>. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public interface PublicKeyCose { |
||||
|
||||
/** |
||||
* The byes of a COSE encoded public key. |
||||
* @return the bytes of a COSE encoded public key. |
||||
*/ |
||||
byte[] getBytes(); |
||||
|
||||
} |
||||
@ -0,0 +1,223 @@
@@ -0,0 +1,223 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#iface-pkcredential">PublicKeyCredential</a> |
||||
* contains the attributes that are returned to the caller when a new credential is |
||||
* created, or a new assertion is requested. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class PublicKeyCredential<R extends AuthenticatorResponse> { |
||||
|
||||
private final String id; |
||||
|
||||
private final PublicKeyCredentialType type; |
||||
|
||||
private final Bytes rawId; |
||||
|
||||
private final R response; |
||||
|
||||
private final AuthenticatorAttachment authenticatorAttachment; |
||||
|
||||
private final AuthenticationExtensionsClientOutputs clientExtensionResults; |
||||
|
||||
private PublicKeyCredential(String id, PublicKeyCredentialType type, Bytes rawId, R response, |
||||
AuthenticatorAttachment authenticatorAttachment, |
||||
AuthenticationExtensionsClientOutputs clientExtensionResults) { |
||||
this.id = id; |
||||
this.type = type; |
||||
this.rawId = rawId; |
||||
this.response = response; |
||||
this.authenticatorAttachment = authenticatorAttachment; |
||||
this.clientExtensionResults = clientExtensionResults; |
||||
} |
||||
|
||||
/** |
||||
* The |
||||
* <a href="https://www.w3.org/TR/credential-management-1/#dom-credential-id">id</a> |
||||
* attribute is inherited from Credential, though PublicKeyCredential overrides |
||||
* Credential's getter, instead returning the base64url encoding of the data contained |
||||
* in the object’s [[identifier]] internal slot. |
||||
*/ |
||||
public String getId() { |
||||
return this.id; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/credential-management-1/#dom-credential-type">type</a> |
||||
* attribute returns the value of the object’s interface object's [[type]] slot, which |
||||
* specifies the credential type represented by this object. |
||||
* @return the credential type |
||||
*/ |
||||
public PublicKeyCredentialType getType() { |
||||
return this.type; |
||||
} |
||||
|
||||
/** |
||||
* The |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid">rawId</a> |
||||
* returns the raw identifier. |
||||
* @return the raw id |
||||
*/ |
||||
public Bytes getRawId() { |
||||
return this.rawId; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response">response</a> |
||||
* to the client's request to either create a public key credential, or generate an |
||||
* authentication assertion. |
||||
* @return the response |
||||
*/ |
||||
public R getResponse() { |
||||
return this.response; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment">authenticatorAttachment</a> |
||||
* reports the <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#authenticator-attachment-modality">authenticator |
||||
* attachment modality</a> in effect at the time the navigator.credentials.create() or |
||||
* navigator.credentials.get() methods successfully complete. |
||||
* @return the authenticator attachment |
||||
*/ |
||||
public AuthenticatorAttachment getAuthenticatorAttachment() { |
||||
return this.authenticatorAttachment; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults">clientExtensionsResults</a> |
||||
* is a mapping of extension identifier to client extension output. |
||||
* @return the extension results |
||||
*/ |
||||
public AuthenticationExtensionsClientOutputs getClientExtensionResults() { |
||||
return this.clientExtensionResults; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link PublicKeyCredentialBuilder} |
||||
* @param <T> the response type |
||||
* @return the {@link PublicKeyCredentialBuilder} |
||||
*/ |
||||
public static <T extends AuthenticatorResponse> PublicKeyCredentialBuilder<T> builder() { |
||||
return new PublicKeyCredentialBuilder<T>(); |
||||
} |
||||
|
||||
/** |
||||
* The {@link PublicKeyCredentialBuilder} |
||||
* |
||||
* @param <R> the response type |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public static final class PublicKeyCredentialBuilder<R extends AuthenticatorResponse> { |
||||
|
||||
private String id; |
||||
|
||||
private PublicKeyCredentialType type; |
||||
|
||||
private Bytes rawId; |
||||
|
||||
private R response; |
||||
|
||||
private AuthenticatorAttachment authenticatorAttachment; |
||||
|
||||
private AuthenticationExtensionsClientOutputs clientExtensionResults; |
||||
|
||||
private PublicKeyCredentialBuilder() { |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getId()} property |
||||
* @param id the id |
||||
* @return the PublicKeyCredentialBuilder |
||||
*/ |
||||
public PublicKeyCredentialBuilder id(String id) { |
||||
this.id = id; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getType()} property. |
||||
* @param type the type |
||||
* @return the PublicKeyCredentialBuilder |
||||
*/ |
||||
public PublicKeyCredentialBuilder type(PublicKeyCredentialType type) { |
||||
this.type = type; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getRawId()} property. |
||||
* @param rawId the raw id |
||||
* @return the PublicKeyCredentialBuilder |
||||
*/ |
||||
public PublicKeyCredentialBuilder rawId(Bytes rawId) { |
||||
this.rawId = rawId; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getResponse()} property. |
||||
* @param response the response |
||||
* @return the PublicKeyCredentialBuilder |
||||
*/ |
||||
public PublicKeyCredentialBuilder response(R response) { |
||||
this.response = response; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getAuthenticatorAttachment()} property. |
||||
* @param authenticatorAttachment the authenticator attachement |
||||
* @return the PublicKeyCredentialBuilder |
||||
*/ |
||||
public PublicKeyCredentialBuilder authenticatorAttachment(AuthenticatorAttachment authenticatorAttachment) { |
||||
this.authenticatorAttachment = authenticatorAttachment; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getClientExtensionResults()} property. |
||||
* @param clientExtensionResults the client extension results |
||||
* @return the PublicKeyCredentialBuilder |
||||
*/ |
||||
public PublicKeyCredentialBuilder clientExtensionResults( |
||||
AuthenticationExtensionsClientOutputs clientExtensionResults) { |
||||
this.clientExtensionResults = clientExtensionResults; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link PublicKeyCredential} |
||||
* @return a new {@link PublicKeyCredential} |
||||
*/ |
||||
public PublicKeyCredential<R> build() { |
||||
return new PublicKeyCredential(this.id, this.type, this.rawId, this.response, this.authenticatorAttachment, |
||||
this.clientExtensionResults); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,332 @@
@@ -0,0 +1,332 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.time.Duration; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.function.Consumer; |
||||
|
||||
/** |
||||
* Represents the <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions">PublicKeyCredentialCreationOptions</a> |
||||
* which is an argument to <a href= |
||||
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">creating</a> |
||||
* a new credential. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class PublicKeyCredentialCreationOptions { |
||||
|
||||
private final PublicKeyCredentialRpEntity rp; |
||||
|
||||
private final PublicKeyCredentialUserEntity user; |
||||
|
||||
private final Bytes challenge; |
||||
|
||||
private final List<PublicKeyCredentialParameters> pubKeyCredParams; |
||||
|
||||
private final Duration timeout; |
||||
|
||||
private final List<PublicKeyCredentialDescriptor> excludeCredentials; |
||||
|
||||
private final AuthenticatorSelectionCriteria authenticatorSelection; |
||||
|
||||
private final AttestationConveyancePreference attestation; |
||||
|
||||
private final AuthenticationExtensionsClientInputs extensions; |
||||
|
||||
private PublicKeyCredentialCreationOptions(PublicKeyCredentialRpEntity rp, PublicKeyCredentialUserEntity user, |
||||
Bytes challenge, List<PublicKeyCredentialParameters> pubKeyCredParams, Duration timeout, |
||||
List<PublicKeyCredentialDescriptor> excludeCredentials, |
||||
AuthenticatorSelectionCriteria authenticatorSelection, AttestationConveyancePreference attestation, |
||||
AuthenticationExtensionsClientInputs extensions) { |
||||
this.rp = rp; |
||||
this.user = user; |
||||
this.challenge = challenge; |
||||
this.pubKeyCredParams = pubKeyCredParams; |
||||
this.timeout = timeout; |
||||
this.excludeCredentials = excludeCredentials; |
||||
this.authenticatorSelection = authenticatorSelection; |
||||
this.attestation = attestation; |
||||
this.extensions = extensions; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-rp">rp</a> |
||||
* property contains data about the Relying Party responsible for the request. |
||||
* @return the relying party |
||||
*/ |
||||
public PublicKeyCredentialRpEntity getRp() { |
||||
return this.rp; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-user">user</a> |
||||
* contains names and an identifier for the user account performing the registration. |
||||
* @return the user |
||||
*/ |
||||
public PublicKeyCredentialUserEntity getUser() { |
||||
return this.user; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge">challenge</a> |
||||
* specifies the challenge that the authenticator signs, along with other data, when |
||||
* producing an attestation object for the newly created credential. |
||||
* @return the challenge |
||||
*/ |
||||
public Bytes getChallenge() { |
||||
return this.challenge; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-pubkeycredparams">publicKeyCredParams</a> |
||||
* params lisst the key types and signature algorithms the Relying Party Supports, |
||||
* ordered from most preferred to least preferred. |
||||
* @return the public key credential parameters |
||||
*/ |
||||
public List<PublicKeyCredentialParameters> getPubKeyCredParams() { |
||||
return this.pubKeyCredParams; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-timeout">timeout</a> |
||||
* property specifies a time, in milliseconds, that the Relying Party is willing to |
||||
* wait for the call to complete. |
||||
* @return the timeout |
||||
*/ |
||||
public Duration getTimeout() { |
||||
return this.timeout; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-excludecredentials">excludeCredentials</a> |
||||
* property is the OPTIONAL member used by the Relying Party to list any existing |
||||
* credentials mapped to this user account (as identified by user.id). |
||||
* @return exclude credentials |
||||
*/ |
||||
public List<PublicKeyCredentialDescriptor> getExcludeCredentials() { |
||||
return this.excludeCredentials; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection">authenticatorSelection</a> |
||||
* property is an OPTIONAL member used by the Relying Party to list any existing |
||||
* credentials mapped to this user account (as identified by user.id). |
||||
* @return the authenticatorSelection |
||||
*/ |
||||
public AuthenticatorSelectionCriteria getAuthenticatorSelection() { |
||||
return this.authenticatorSelection; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-attestation">attestation</a> |
||||
* property is an OPTIONAL member used by the Relying Party to specify a preference |
||||
* regarding attestation conveyance. |
||||
* @return the attestation preference |
||||
*/ |
||||
public AttestationConveyancePreference getAttestation() { |
||||
return this.attestation; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions">extensions</a> |
||||
* property is an OPTIONAL member used by the Relying Party to provide client |
||||
* extension inputs requesting additional processing by the client and authenticator. |
||||
* @return the extensions |
||||
*/ |
||||
public AuthenticationExtensionsClientInputs getExtensions() { |
||||
return this.extensions; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link PublicKeyCredentialCreationOptions} |
||||
* @return a new {@link PublicKeyCredentialCreationOptions} |
||||
*/ |
||||
public static PublicKeyCredentialCreationOptionsBuilder builder() { |
||||
return new PublicKeyCredentialCreationOptionsBuilder(); |
||||
} |
||||
|
||||
/** |
||||
* Used to build {@link PublicKeyCredentialCreationOptions}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public static final class PublicKeyCredentialCreationOptionsBuilder { |
||||
|
||||
private PublicKeyCredentialRpEntity rp; |
||||
|
||||
private PublicKeyCredentialUserEntity user; |
||||
|
||||
private Bytes challenge; |
||||
|
||||
private List<PublicKeyCredentialParameters> pubKeyCredParams = new ArrayList<>(); |
||||
|
||||
private Duration timeout; |
||||
|
||||
private List<PublicKeyCredentialDescriptor> excludeCredentials = new ArrayList<>(); |
||||
|
||||
private AuthenticatorSelectionCriteria authenticatorSelection; |
||||
|
||||
private AttestationConveyancePreference attestation; |
||||
|
||||
private AuthenticationExtensionsClientInputs extensions; |
||||
|
||||
private PublicKeyCredentialCreationOptionsBuilder() { |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getRp()} property. |
||||
* @param rp the relying party |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder rp(PublicKeyCredentialRpEntity rp) { |
||||
this.rp = rp; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getUser()} property. |
||||
* @param user the user entity |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder user(PublicKeyCredentialUserEntity user) { |
||||
this.user = user; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getChallenge()} property. |
||||
* @param challenge the challenge |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder challenge(Bytes challenge) { |
||||
this.challenge = challenge; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getPubKeyCredParams()} property. |
||||
* @param pubKeyCredParams the public key credential parameters |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder pubKeyCredParams( |
||||
PublicKeyCredentialParameters... pubKeyCredParams) { |
||||
return pubKeyCredParams(Arrays.asList(pubKeyCredParams)); |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getPubKeyCredParams()} property. |
||||
* @param pubKeyCredParams the public key credential parameters |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder pubKeyCredParams( |
||||
List<PublicKeyCredentialParameters> pubKeyCredParams) { |
||||
this.pubKeyCredParams = pubKeyCredParams; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getTimeout()} property. |
||||
* @param timeout the timeout |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder timeout(Duration timeout) { |
||||
this.timeout = timeout; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getExcludeCredentials()} property. |
||||
* @param excludeCredentials the excluded credentials. |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder excludeCredentials( |
||||
List<PublicKeyCredentialDescriptor> excludeCredentials) { |
||||
this.excludeCredentials = excludeCredentials; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getAuthenticatorSelection()} property. |
||||
* @param authenticatorSelection the authenticator selection |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder authenticatorSelection( |
||||
AuthenticatorSelectionCriteria authenticatorSelection) { |
||||
this.authenticatorSelection = authenticatorSelection; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getAttestation()} property. |
||||
* @param attestation the attestation |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder attestation(AttestationConveyancePreference attestation) { |
||||
this.attestation = attestation; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getExtensions()} property. |
||||
* @param extensions the extensions |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder extensions(AuthenticationExtensionsClientInputs extensions) { |
||||
this.extensions = extensions; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Allows customizing the builder using the {@link Consumer} that is passed in. |
||||
* @param customizer the {@link Consumer} that can be used to customize the |
||||
* {@link PublicKeyCredentialCreationOptionsBuilder} |
||||
* @return the PublicKeyCredentialCreationOptionsBuilder |
||||
*/ |
||||
public PublicKeyCredentialCreationOptionsBuilder customize( |
||||
Consumer<PublicKeyCredentialCreationOptionsBuilder> customizer) { |
||||
customizer.accept(this); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Builds a new {@link PublicKeyCredentialCreationOptions} |
||||
* @return the new {@link PublicKeyCredentialCreationOptions} |
||||
*/ |
||||
public PublicKeyCredentialCreationOptions build() { |
||||
return new PublicKeyCredentialCreationOptions(this.rp, this.user, this.challenge, this.pubKeyCredParams, |
||||
this.timeout, this.excludeCredentials, this.authenticatorSelection, this.attestation, |
||||
this.extensions); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,154 @@
@@ -0,0 +1,154 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.util.Set; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptor">PublicKeyCredentialDescriptor</a> |
||||
* identifies a specific public key credential. It is used in create() to prevent creating |
||||
* duplicate credentials on the same authenticator, and in get() to determine if and how |
||||
* the credential can currently be reached by the client. It mirrors some fields of the |
||||
* PublicKeyCredential object returned by create() and get(). |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class PublicKeyCredentialDescriptor { |
||||
|
||||
private final PublicKeyCredentialType type; |
||||
|
||||
private final Bytes id; |
||||
|
||||
private final Set<AuthenticatorTransport> transports; |
||||
|
||||
private PublicKeyCredentialDescriptor(PublicKeyCredentialType type, Bytes id, |
||||
Set<AuthenticatorTransport> transports) { |
||||
this.type = type; |
||||
this.id = id; |
||||
this.transports = transports; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-type">type</a> |
||||
* property contains the type of the public key credential the caller is referring to. |
||||
* @return the type |
||||
*/ |
||||
public PublicKeyCredentialType getType() { |
||||
return this.type; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-id">id</a> |
||||
* property contains the credential ID of the public key credential the caller is |
||||
* referring to. |
||||
* @return the id |
||||
*/ |
||||
public Bytes getId() { |
||||
return this.id; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports">transports</a> |
||||
* property is an OPTIONAL member that contains a hint as to how the client might |
||||
* communicate with the managing authenticator of the public key credential the caller |
||||
* is referring to. |
||||
* @return the transports |
||||
*/ |
||||
public Set<AuthenticatorTransport> getTransports() { |
||||
return this.transports; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link PublicKeyCredentialDescriptorBuilder} |
||||
* @return a new {@link PublicKeyCredentialDescriptorBuilder} |
||||
*/ |
||||
public static PublicKeyCredentialDescriptorBuilder builder() { |
||||
return new PublicKeyCredentialDescriptorBuilder(); |
||||
} |
||||
|
||||
/** |
||||
* Used to create {@link PublicKeyCredentialDescriptor} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public static final class PublicKeyCredentialDescriptorBuilder { |
||||
|
||||
private PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY; |
||||
|
||||
private Bytes id; |
||||
|
||||
private Set<AuthenticatorTransport> transports; |
||||
|
||||
private PublicKeyCredentialDescriptorBuilder() { |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getType()} property. |
||||
* @param type the type |
||||
* @return the {@link PublicKeyCredentialDescriptorBuilder} |
||||
*/ |
||||
public PublicKeyCredentialDescriptorBuilder type(PublicKeyCredentialType type) { |
||||
this.type = type; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getId()} property. |
||||
* @param id the id |
||||
* @return the {@link PublicKeyCredentialDescriptorBuilder} |
||||
*/ |
||||
public PublicKeyCredentialDescriptorBuilder id(Bytes id) { |
||||
this.id = id; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getTransports()} property. |
||||
* @param transports the transports |
||||
* @return the {@link PublicKeyCredentialDescriptorBuilder} |
||||
*/ |
||||
public PublicKeyCredentialDescriptorBuilder transports(Set<AuthenticatorTransport> transports) { |
||||
this.transports = transports; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getTransports()} property. |
||||
* @param transports the transports |
||||
* @return the {@link PublicKeyCredentialDescriptorBuilder} |
||||
*/ |
||||
public PublicKeyCredentialDescriptorBuilder transports(AuthenticatorTransport... transports) { |
||||
return transports(Set.of(transports)); |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@link PublicKeyCredentialDescriptor} |
||||
* @return a new {@link PublicKeyCredentialDescriptor} |
||||
*/ |
||||
public PublicKeyCredentialDescriptor build() { |
||||
return new PublicKeyCredentialDescriptor(this.type, this.id, this.transports); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialparameters">PublicKeyCredentialParameters</a> |
||||
* is used to supply additional parameters when creating a new credential. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see PublicKeyCredentialCreationOptions#getPubKeyCredParams() |
||||
*/ |
||||
public final class PublicKeyCredentialParameters { |
||||
|
||||
public static final PublicKeyCredentialParameters EdDSA = new PublicKeyCredentialParameters( |
||||
COSEAlgorithmIdentifier.EdDSA); |
||||
|
||||
public static final PublicKeyCredentialParameters ES256 = new PublicKeyCredentialParameters( |
||||
COSEAlgorithmIdentifier.ES256); |
||||
|
||||
public static final PublicKeyCredentialParameters ES384 = new PublicKeyCredentialParameters( |
||||
COSEAlgorithmIdentifier.ES384); |
||||
|
||||
public static final PublicKeyCredentialParameters ES512 = new PublicKeyCredentialParameters( |
||||
COSEAlgorithmIdentifier.ES512); |
||||
|
||||
public static final PublicKeyCredentialParameters RS256 = new PublicKeyCredentialParameters( |
||||
COSEAlgorithmIdentifier.RS256); |
||||
|
||||
public static final PublicKeyCredentialParameters RS384 = new PublicKeyCredentialParameters( |
||||
COSEAlgorithmIdentifier.RS384); |
||||
|
||||
public static final PublicKeyCredentialParameters RS512 = new PublicKeyCredentialParameters( |
||||
COSEAlgorithmIdentifier.RS512); |
||||
|
||||
public static final PublicKeyCredentialParameters RS1 = new PublicKeyCredentialParameters( |
||||
COSEAlgorithmIdentifier.RS1); |
||||
|
||||
/** |
||||
* This member specifies the type of credential to be created. The value SHOULD be a |
||||
* member of PublicKeyCredentialType but client platforms MUST ignore unknown values, |
||||
* ignoring any PublicKeyCredentialParameters with an unknown type. |
||||
*/ |
||||
private final PublicKeyCredentialType type; |
||||
|
||||
/** |
||||
* This member specifies the cryptographic signature algorithm with which the newly |
||||
* generated credential will be used, and thus also the type of asymmetric key pair to |
||||
* be generated, e.g., RSA or Elliptic Curve. |
||||
*/ |
||||
private final COSEAlgorithmIdentifier alg; |
||||
|
||||
private PublicKeyCredentialParameters(COSEAlgorithmIdentifier alg) { |
||||
this(PublicKeyCredentialType.PUBLIC_KEY, alg); |
||||
} |
||||
|
||||
private PublicKeyCredentialParameters(PublicKeyCredentialType type, COSEAlgorithmIdentifier alg) { |
||||
this.type = type; |
||||
this.alg = alg; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialparameters-type">type</a> |
||||
* property member specifies the type of credential to be created. |
||||
* @return the type |
||||
*/ |
||||
public PublicKeyCredentialType getType() { |
||||
return this.type; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialparameters-alg">alg</a> |
||||
* member specifies the cryptographic signature algorithm with which the newly |
||||
* generated credential will be used, and thus also the type of asymmetric key pair to |
||||
* be generated, e.g., RSA or Elliptic Curve. |
||||
* @return the algorithm |
||||
*/ |
||||
public COSEAlgorithmIdentifier getAlg() { |
||||
return this.alg; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,248 @@
@@ -0,0 +1,248 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import java.time.Duration; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions">PublicKeyCredentialRequestOptions</a> |
||||
* contains the information to create an assertion used for authentication. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class PublicKeyCredentialRequestOptions { |
||||
|
||||
private final Bytes challenge; |
||||
|
||||
private final Duration timeout; |
||||
|
||||
private final String rpId; |
||||
|
||||
private final List<PublicKeyCredentialDescriptor> allowCredentials; |
||||
|
||||
private final UserVerificationRequirement userVerification; |
||||
|
||||
private final AuthenticationExtensionsClientInputs extensions; |
||||
|
||||
private PublicKeyCredentialRequestOptions(Bytes challenge, Duration timeout, String rpId, |
||||
List<PublicKeyCredentialDescriptor> allowCredentials, UserVerificationRequirement userVerification, |
||||
AuthenticationExtensionsClientInputs extensions) { |
||||
Assert.notNull(challenge, "challenge cannot be null"); |
||||
Assert.hasText(rpId, "rpId cannot be empty"); |
||||
this.challenge = challenge; |
||||
this.timeout = timeout; |
||||
this.rpId = rpId; |
||||
this.allowCredentials = allowCredentials; |
||||
this.userVerification = userVerification; |
||||
this.extensions = extensions; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge">challenge</a> |
||||
* property specifies a challenge that the authenticator signs, along with other data, |
||||
* when producing an authentication assertion. |
||||
* @return the challenge |
||||
*/ |
||||
public Bytes getChallenge() { |
||||
return this.challenge; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout">timeout</a> |
||||
* property is an OPTIONAL member specifies a time, in milliseconds, that the Relying |
||||
* Party is willing to wait for the call to complete. |
||||
* @return the timeout |
||||
*/ |
||||
public Duration getTimeout() { |
||||
return this.timeout; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-rpid">rpId</a> |
||||
* is an OPTIONAL member specifies the RP ID claimed by the Relying Party. The client |
||||
* MUST verify that the Relying Party's origin matches the scope of this RP ID. |
||||
* @return the relying party id |
||||
*/ |
||||
public String getRpId() { |
||||
return this.rpId; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials">allowCredentials</a> |
||||
* property is an OPTIONAL member is used by the client to find authenticators |
||||
* eligible for this authentication ceremony. |
||||
* @return the allowCredentials property |
||||
*/ |
||||
public List<PublicKeyCredentialDescriptor> getAllowCredentials() { |
||||
return this.allowCredentials; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification">userVerification</a> |
||||
* property is an OPTIONAL member specifies the Relying Party's requirements regarding |
||||
* user verification for the get() operation. |
||||
* @return the user verification |
||||
*/ |
||||
public UserVerificationRequirement getUserVerification() { |
||||
return this.userVerification; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions">extensions</a> |
||||
* is an OPTIONAL property used by the Relying Party to provide client extension |
||||
* inputs requesting additional processing by the client and authenticator. |
||||
* @return the extensions |
||||
*/ |
||||
public AuthenticationExtensionsClientInputs getExtensions() { |
||||
return this.extensions; |
||||
} |
||||
|
||||
/** |
||||
* Creates a {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
* @return the {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
*/ |
||||
public static PublicKeyCredentialRequestOptionsBuilder builder() { |
||||
return new PublicKeyCredentialRequestOptionsBuilder(); |
||||
} |
||||
|
||||
/** |
||||
* Used to build a {@link PublicKeyCredentialCreationOptions}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public static final class PublicKeyCredentialRequestOptionsBuilder { |
||||
|
||||
private Bytes challenge; |
||||
|
||||
private Duration timeout = Duration.ofMinutes(5); |
||||
|
||||
private String rpId; |
||||
|
||||
private List<PublicKeyCredentialDescriptor> allowCredentials = Collections.emptyList(); |
||||
|
||||
private UserVerificationRequirement userVerification; |
||||
|
||||
private AuthenticationExtensionsClientInputs extensions = new ImmutableAuthenticationExtensionsClientInputs( |
||||
new ArrayList<>()); |
||||
|
||||
private PublicKeyCredentialRequestOptionsBuilder() { |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getChallenge()} property. |
||||
* @param challenge the challenge |
||||
* @return the {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
*/ |
||||
public PublicKeyCredentialRequestOptionsBuilder challenge(Bytes challenge) { |
||||
this.challenge = challenge; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getTimeout()} property. |
||||
* @param timeout the timeout |
||||
* @return the {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
*/ |
||||
public PublicKeyCredentialRequestOptionsBuilder timeout(Duration timeout) { |
||||
Assert.notNull(timeout, "timeout cannot be null"); |
||||
this.timeout = timeout; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getRpId()} property. |
||||
* @param rpId the rpId property |
||||
* @return the {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
*/ |
||||
public PublicKeyCredentialRequestOptionsBuilder rpId(String rpId) { |
||||
this.rpId = rpId; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getAllowCredentials()} property |
||||
* @param allowCredentials the allowed credentials |
||||
* @return the {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
*/ |
||||
public PublicKeyCredentialRequestOptionsBuilder allowCredentials( |
||||
List<PublicKeyCredentialDescriptor> allowCredentials) { |
||||
Assert.notNull(allowCredentials, "allowCredentials cannot be null"); |
||||
this.allowCredentials = allowCredentials; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getUserVerification()} property. |
||||
* @param userVerification the user verification |
||||
* @return the {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
*/ |
||||
public PublicKeyCredentialRequestOptionsBuilder userVerification(UserVerificationRequirement userVerification) { |
||||
this.userVerification = userVerification; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getExtensions()} property |
||||
* @param extensions the extensions |
||||
* @return the {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
*/ |
||||
public PublicKeyCredentialRequestOptionsBuilder extensions(AuthenticationExtensionsClientInputs extensions) { |
||||
this.extensions = extensions; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Allows customizing the {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
* @param customizer the {@link Consumer} used to customize the builder |
||||
* @return the {@link PublicKeyCredentialRequestOptionsBuilder} |
||||
*/ |
||||
public PublicKeyCredentialRequestOptionsBuilder customize( |
||||
Consumer<PublicKeyCredentialRequestOptionsBuilder> customizer) { |
||||
customizer.accept(this); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Builds a new {@link PublicKeyCredentialRequestOptions} |
||||
* @return a new {@link PublicKeyCredentialRequestOptions} |
||||
*/ |
||||
public PublicKeyCredentialRequestOptions build() { |
||||
if (this.challenge == null) { |
||||
this.challenge = Bytes.random(); |
||||
} |
||||
return new PublicKeyCredentialRequestOptions(this.challenge, this.timeout, this.rpId, this.allowCredentials, |
||||
this.userVerification, this.extensions); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrpentity">PublicKeyCredentialRpEntity</a> |
||||
* dictionary is used to supply additional Relying Party attributes when creating a new |
||||
* credential. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class PublicKeyCredentialRpEntity { |
||||
|
||||
private final String name; |
||||
|
||||
private final String id; |
||||
|
||||
private PublicKeyCredentialRpEntity(String name, String id) { |
||||
this.name = name; |
||||
this.id = id; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name">name</a> |
||||
* property is a human-palatable name for the entity. Its function depends on what the |
||||
* PublicKeyCredentialEntity represents for the Relying Party, intended only for |
||||
* display. |
||||
* @return the name |
||||
*/ |
||||
public String getName() { |
||||
return this.name; |
||||
} |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrpentity-id">id</a> |
||||
* property is a unique identifier for the Relying Party entity, which sets the |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#rp-id">RP ID</a>. |
||||
* @return the relying party id |
||||
*/ |
||||
public String getId() { |
||||
return this.id; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link PublicKeyCredentialRpEntityBuilder} |
||||
* @return a new {@link PublicKeyCredentialRpEntityBuilder} |
||||
*/ |
||||
public static PublicKeyCredentialRpEntityBuilder builder() { |
||||
return new PublicKeyCredentialRpEntityBuilder(); |
||||
} |
||||
|
||||
/** |
||||
* Used to create a {@link PublicKeyCredentialRpEntity}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public static final class PublicKeyCredentialRpEntityBuilder { |
||||
|
||||
private String name; |
||||
|
||||
private String id; |
||||
|
||||
private PublicKeyCredentialRpEntityBuilder() { |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getName()} property. |
||||
* @param name the name property |
||||
* @return the {@link PublicKeyCredentialRpEntityBuilder} |
||||
*/ |
||||
public PublicKeyCredentialRpEntityBuilder name(String name) { |
||||
this.name = name; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link #getId()} property. |
||||
* @param id the id |
||||
* @return the {@link PublicKeyCredentialRpEntityBuilder} |
||||
*/ |
||||
public PublicKeyCredentialRpEntityBuilder id(String id) { |
||||
this.id = id; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link PublicKeyCredentialRpEntity}. |
||||
* @return a new {@link PublicKeyCredentialRpEntity}. |
||||
*/ |
||||
public PublicKeyCredentialRpEntity build() { |
||||
return new PublicKeyCredentialRpEntity(this.name, this.id); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#enum-credentialType">PublicKeyCredentialType</a> |
||||
* defines the credential types. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class PublicKeyCredentialType { |
||||
|
||||
/** |
||||
* The only credential type that currently exists. |
||||
*/ |
||||
public static final PublicKeyCredentialType PUBLIC_KEY = new PublicKeyCredentialType("public-key"); |
||||
|
||||
private final String value; |
||||
|
||||
private PublicKeyCredentialType(String value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
/** |
||||
* Gets the value. |
||||
* @return the value |
||||
*/ |
||||
public String getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
public static PublicKeyCredentialType valueOf(String value) { |
||||
if (PUBLIC_KEY.getValue().equals(value)) { |
||||
return PUBLIC_KEY; |
||||
} |
||||
return new PublicKeyCredentialType(value); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest; |
||||
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity">PublicKeyCredentialUserEntity</a> |
||||
* is used to supply additional |
||||
* <a href="https://www.w3.org/TR/webauthn-3/#user-account">user account</a> attributes |
||||
* when creating a new credential. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest) |
||||
*/ |
||||
public interface PublicKeyCredentialUserEntity { |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name">name</a> |
||||
* property is a human-palatable identifier for a user account. |
||||
* @return the name |
||||
*/ |
||||
String getName(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id">id</a> is |
||||
* the user handle of the user account. A user handle is an opaque byte sequence with |
||||
* a maximum size of 64 bytes, and is not meant to be displayed to the user. |
||||
* @return the user handle of the user account |
||||
*/ |
||||
Bytes getId(); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname">displayName</a> |
||||
* is a human-palatable name for the user account, intended only for display. |
||||
* @return the display name |
||||
*/ |
||||
String getDisplayName(); |
||||
|
||||
} |
||||
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement">ResidentKeyRequirement</a> |
||||
* describes the Relying Partys requirements for client-side discoverable credentials. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class ResidentKeyRequirement { |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-discouraged">discouraged</a> |
||||
* requirement indicates that the Relying Party prefers creating a server-side |
||||
* credential, but will accept a client-side discoverable credential. |
||||
*/ |
||||
public static final ResidentKeyRequirement DISCOURAGED = new ResidentKeyRequirement("discouraged"); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred">preferred</a> |
||||
* requirement indicates that the Relying Party strongly prefers creating a |
||||
* client-side discoverable credential, but will accept a server-side credential. |
||||
*/ |
||||
public static final ResidentKeyRequirement PREFERRED = new ResidentKeyRequirement("preferred"); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-required">required</a> |
||||
* value indicates that the Relying Party requires a client-side discoverable |
||||
* credential. |
||||
*/ |
||||
public static final ResidentKeyRequirement REQUIRED = new ResidentKeyRequirement("required"); |
||||
|
||||
private final String value; |
||||
|
||||
private ResidentKeyRequirement(String value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
/** |
||||
* Gets the value. |
||||
* @return the value |
||||
*/ |
||||
public String getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
public static ResidentKeyRequirement valueOf(String value) { |
||||
if (DISCOURAGED.getValue().equals(value)) { |
||||
return DISCOURAGED; |
||||
} |
||||
if (PREFERRED.getValue().equals(value)) { |
||||
return PREFERRED; |
||||
} |
||||
if (REQUIRED.getValue().equals(value)) { |
||||
return REQUIRED; |
||||
} |
||||
return new ResidentKeyRequirement(value); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.api; |
||||
|
||||
/** |
||||
* <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement">UserVerificationRequirement</a> |
||||
* is used by the Relying Party to indicate if user verification is needed. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public final class UserVerificationRequirement { |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-discouraged">discouraged</a> |
||||
* value indicates that the Relying Party does not want user verification employed |
||||
* during the operation (e.g., in the interest of minimizing disruption to the user |
||||
* interaction flow). |
||||
*/ |
||||
public static final UserVerificationRequirement DISCOURAGED = new UserVerificationRequirement("discouraged"); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred">preferred</a> |
||||
* value indicates that the Relying Party prefers user verification for the operation |
||||
* if possible, but will not fail the operation if the response does not have the UV |
||||
* flag set. |
||||
*/ |
||||
public static final UserVerificationRequirement PREFERRED = new UserVerificationRequirement("preferred"); |
||||
|
||||
/** |
||||
* The <a href= |
||||
* "https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-required">required</a> |
||||
* value indicates that the Relying Party requires user verification for the operation |
||||
* and will fail the overall ceremony if the response does not have the UV flag set. |
||||
*/ |
||||
public static final UserVerificationRequirement REQUIRED = new UserVerificationRequirement("required"); |
||||
|
||||
private final String value; |
||||
|
||||
UserVerificationRequirement(String value) { |
||||
this.value = value; |
||||
} |
||||
|
||||
/** |
||||
* Gets the value |
||||
* @return the value |
||||
*/ |
||||
public String getValue() { |
||||
return this.value; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.authentication; |
||||
|
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import jakarta.servlet.http.HttpSession; |
||||
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* A {@link PublicKeyCredentialRequestOptionsRepository} that stores the |
||||
* {@link PublicKeyCredentialRequestOptions} in the |
||||
* {@link jakarta.servlet.http.HttpSession}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public class HttpSessionPublicKeyCredentialRequestOptionsRepository |
||||
implements PublicKeyCredentialRequestOptionsRepository { |
||||
|
||||
static final String DEFAULT_ATTR_NAME = PublicKeyCredentialRequestOptionsRepository.class.getName() |
||||
.concat(".ATTR_NAME"); |
||||
|
||||
private String attrName = DEFAULT_ATTR_NAME; |
||||
|
||||
@Override |
||||
public void save(HttpServletRequest request, HttpServletResponse response, |
||||
PublicKeyCredentialRequestOptions options) { |
||||
HttpSession session = request.getSession(); |
||||
session.setAttribute(this.attrName, options); |
||||
} |
||||
|
||||
@Override |
||||
public PublicKeyCredentialRequestOptions load(HttpServletRequest request) { |
||||
HttpSession session = request.getSession(false); |
||||
if (session == null) { |
||||
return null; |
||||
} |
||||
return (PublicKeyCredentialRequestOptions) session.getAttribute(this.attrName); |
||||
} |
||||
|
||||
public void setAttrName(String attrName) { |
||||
Assert.notNull(attrName, "attrName cannot be null"); |
||||
this.attrName = attrName; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,127 @@
@@ -0,0 +1,127 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.authentication; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import jakarta.servlet.FilterChain; |
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
|
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.HttpMessageConverter; |
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; |
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; |
||||
import org.springframework.http.server.ServletServerHttpResponse; |
||||
import org.springframework.security.core.context.SecurityContext; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions; |
||||
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module; |
||||
import org.springframework.security.web.webauthn.management.ImmutablePublicKeyCredentialRequestOptionsRequest; |
||||
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.filter.OncePerRequestFilter; |
||||
|
||||
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; |
||||
|
||||
/** |
||||
* A {@link jakarta.servlet.Filter} that renders the |
||||
* {@link PublicKeyCredentialRequestOptions} in order to <a href= |
||||
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-get">get</a> |
||||
* a credential. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public class PublicKeyCredentialRequestOptionsFilter extends OncePerRequestFilter { |
||||
|
||||
private RequestMatcher matcher = antMatcher(HttpMethod.POST, "/webauthn/authenticate/options"); |
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder |
||||
.getContextHolderStrategy(); |
||||
|
||||
private final WebAuthnRelyingPartyOperations rpOptions; |
||||
|
||||
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository = new HttpSessionPublicKeyCredentialRequestOptionsRepository(); |
||||
|
||||
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter( |
||||
Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build()); |
||||
|
||||
/** |
||||
* Creates a new instance with the provided {@link WebAuthnRelyingPartyOperations}. |
||||
* @param rpOptions the {@link WebAuthnRelyingPartyOperations} to use. Cannot be null. |
||||
*/ |
||||
public PublicKeyCredentialRequestOptionsFilter(WebAuthnRelyingPartyOperations rpOptions) { |
||||
Assert.notNull(rpOptions, "rpOperations cannot be null"); |
||||
this.rpOptions = rpOptions; |
||||
} |
||||
|
||||
@Override |
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
||||
throws ServletException, IOException { |
||||
if (!this.matcher.matches(request)) { |
||||
filterChain.doFilter(request, response); |
||||
return; |
||||
} |
||||
|
||||
SecurityContext context = this.securityContextHolderStrategy.getContext(); |
||||
ImmutablePublicKeyCredentialRequestOptionsRequest optionsRequest = new ImmutablePublicKeyCredentialRequestOptionsRequest( |
||||
context.getAuthentication()); |
||||
PublicKeyCredentialRequestOptions credentialRequestOptions = this.rpOptions |
||||
.createCredentialRequestOptions(optionsRequest); |
||||
this.requestOptionsRepository.save(request, response, credentialRequestOptions); |
||||
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); |
||||
this.converter.write(credentialRequestOptions, MediaType.APPLICATION_JSON, |
||||
new ServletServerHttpResponse(response)); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link PublicKeyCredentialRequestOptionsRepository} to use. |
||||
* @param requestOptionsRepository the |
||||
* {@link PublicKeyCredentialRequestOptionsRepository} to use. Cannot be null. |
||||
*/ |
||||
public void setRequestOptionsRepository(PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) { |
||||
Assert.notNull(requestOptionsRepository, "requestOptionsRepository cannot be null"); |
||||
this.requestOptionsRepository = requestOptionsRepository; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link HttpMessageConverter} to use. |
||||
* @param converter the {@link HttpMessageConverter} to use. Cannot be null. |
||||
*/ |
||||
public void setConverter(HttpMessageConverter<Object> converter) { |
||||
Assert.notNull(converter, "converter cannot be null"); |
||||
this.converter = converter; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link SecurityContextHolderStrategy} to use. |
||||
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to |
||||
* use. Cannot be null. |
||||
*/ |
||||
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { |
||||
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null"); |
||||
this.securityContextHolderStrategy = securityContextHolderStrategy; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.authentication; |
||||
|
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
|
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions; |
||||
|
||||
/** |
||||
* Saves {@link PublicKeyCredentialRequestOptions} between a request to generate an |
||||
* assertion and the validation of the assertion. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public interface PublicKeyCredentialRequestOptionsRepository { |
||||
|
||||
/** |
||||
* Saves the provided {@link PublicKeyCredentialRequestOptions} or clears an existing |
||||
* {@link PublicKeyCredentialRequestOptions} if {@code options} is null. |
||||
* @param request the {@link HttpServletRequest} |
||||
* @param response the {@link HttpServletResponse} |
||||
* @param options the {@link PublicKeyCredentialRequestOptions} to save or null if an |
||||
* existing {@link PublicKeyCredentialRequestOptions} should be removed. |
||||
*/ |
||||
void save(HttpServletRequest request, HttpServletResponse response, PublicKeyCredentialRequestOptions options); |
||||
|
||||
/** |
||||
* Gets a saved {@link PublicKeyCredentialRequestOptions} if it exists, otherwise |
||||
* null. |
||||
* @param request the {@link HttpServletRequest} |
||||
* @return the {@link PublicKeyCredentialRequestOptions} that was saved, otherwise |
||||
* null. |
||||
*/ |
||||
PublicKeyCredentialRequestOptions load(HttpServletRequest request); |
||||
|
||||
} |
||||
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.authentication; |
||||
|
||||
import java.util.Collection; |
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
import org.springframework.security.core.GrantedAuthority; |
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* A {@link WebAuthnAuthentication} is used to represent successful authentication with |
||||
* WebAuthn. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
* @see WebAuthnAuthenticationRequestToken |
||||
*/ |
||||
public class WebAuthnAuthentication extends AbstractAuthenticationToken { |
||||
|
||||
private final PublicKeyCredentialUserEntity principal; |
||||
|
||||
public WebAuthnAuthentication(PublicKeyCredentialUserEntity principal, |
||||
Collection<? extends GrantedAuthority> authorities) { |
||||
super(authorities); |
||||
this.principal = principal; |
||||
super.setAuthenticated(true); |
||||
} |
||||
|
||||
@Override |
||||
public void setAuthenticated(boolean authenticated) { |
||||
Assert.isTrue(!authenticated, "Cannot set this token to trusted"); |
||||
super.setAuthenticated(authenticated); |
||||
} |
||||
|
||||
@Override |
||||
public Object getCredentials() { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public PublicKeyCredentialUserEntity getPrincipal() { |
||||
return this.principal; |
||||
} |
||||
|
||||
@Override |
||||
public String getName() { |
||||
return this.principal.getName(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,136 @@
@@ -0,0 +1,136 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.authentication; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
|
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.converter.GenericHttpMessageConverter; |
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; |
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; |
||||
import org.springframework.http.server.ServletServerHttpRequest; |
||||
import org.springframework.security.authentication.BadCredentialsException; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; |
||||
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler; |
||||
import org.springframework.security.web.authentication.HttpMessageConverterAuthenticationSuccessHandler; |
||||
import org.springframework.security.web.authentication.HttpStatusEntryPoint; |
||||
import org.springframework.security.web.context.HttpSessionSecurityContextRepository; |
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse; |
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredential; |
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions; |
||||
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module; |
||||
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest; |
||||
import org.springframework.util.Assert; |
||||
|
||||
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; |
||||
|
||||
/** |
||||
* Authenticates {@code PublicKeyCredential<AuthenticatorAssertionResponse>} that is |
||||
* parsed from the body of the {@link HttpServletRequest} using the |
||||
* {@link #setConverter(GenericHttpMessageConverter)}. An example request is provided |
||||
* below: |
||||
* |
||||
* <pre> |
||||
* { |
||||
* "id": "dYF7EGnRFFIXkpXi9XU2wg", |
||||
* "rawId": "dYF7EGnRFFIXkpXi9XU2wg", |
||||
* "response": { |
||||
* "authenticatorData": "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA", |
||||
* "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0", |
||||
* "signature": "MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z", |
||||
* "userHandle": "Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo" |
||||
* }, |
||||
* "clientExtensionResults": {}, |
||||
* "authenticatorAttachment": "platform" |
||||
* } |
||||
* </pre> |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public class WebAuthnAuthenticationFilter extends AbstractAuthenticationProcessingFilter { |
||||
|
||||
private GenericHttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter( |
||||
Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build()); |
||||
|
||||
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository = new HttpSessionPublicKeyCredentialRequestOptionsRepository(); |
||||
|
||||
public WebAuthnAuthenticationFilter() { |
||||
super(antMatcher(HttpMethod.POST, "/login/webauthn")); |
||||
setSecurityContextRepository(new HttpSessionSecurityContextRepository()); |
||||
setAuthenticationFailureHandler( |
||||
new AuthenticationEntryPointFailureHandler(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))); |
||||
setAuthenticationSuccessHandler(new HttpMessageConverterAuthenticationSuccessHandler()); |
||||
} |
||||
|
||||
@Override |
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) |
||||
throws AuthenticationException, IOException, ServletException { |
||||
ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request); |
||||
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(PublicKeyCredential.class, |
||||
AuthenticatorAssertionResponse.class); |
||||
PublicKeyCredential<AuthenticatorAssertionResponse> publicKeyCredential = null; |
||||
try { |
||||
publicKeyCredential = (PublicKeyCredential<AuthenticatorAssertionResponse>) this.converter |
||||
.read(resolvableType.getType(), getClass(), httpRequest); |
||||
} |
||||
catch (Exception ex) { |
||||
throw new BadCredentialsException("Unable to authenticate the PublicKeyCredential", ex); |
||||
} |
||||
PublicKeyCredentialRequestOptions requestOptions = this.requestOptionsRepository.load(request); |
||||
if (requestOptions == null) { |
||||
throw new BadCredentialsException( |
||||
"Unable to authenticate the PublicKeyCredential. No PublicKeyCredentialRequestOptions found."); |
||||
} |
||||
this.requestOptionsRepository.save(request, response, null); |
||||
RelyingPartyAuthenticationRequest authenticationRequest = new RelyingPartyAuthenticationRequest(requestOptions, |
||||
publicKeyCredential); |
||||
WebAuthnAuthenticationRequestToken token = new WebAuthnAuthenticationRequestToken(authenticationRequest); |
||||
return getAuthenticationManager().authenticate(token); |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link GenericHttpMessageConverter} to use for writing |
||||
* {@code PublicKeyCredential<AuthenticatorAssertionResponse>} to the response. The |
||||
* default is @{code MappingJackson2HttpMessageConverter} |
||||
* @param converter the {@link GenericHttpMessageConverter} to use. Cannot be null. |
||||
*/ |
||||
public void setConverter(GenericHttpMessageConverter<Object> converter) { |
||||
Assert.notNull(converter, "converter cannot be null"); |
||||
this.converter = converter; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link PublicKeyCredentialRequestOptionsRepository} to use. The default is |
||||
* {@link HttpSessionPublicKeyCredentialRequestOptionsRepository}. |
||||
* @param requestOptionsRepository the |
||||
* {@link PublicKeyCredentialRequestOptionsRepository} to use. Cannot be null. |
||||
*/ |
||||
public void setRequestOptionsRepository(PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) { |
||||
Assert.notNull(requestOptionsRepository, "requestOptionsRepository cannot be null"); |
||||
this.requestOptionsRepository = requestOptionsRepository; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.authentication; |
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider; |
||||
import org.springframework.security.authentication.BadCredentialsException; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
import org.springframework.security.core.userdetails.UserDetailsService; |
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; |
||||
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest; |
||||
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An {@link AuthenticationProvider} that uses {@link WebAuthnRelyingPartyOperations} for |
||||
* authentication using an {@link WebAuthnAuthenticationRequestToken}. First |
||||
* {@link WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest)} |
||||
* is invoked. The result is a username passed into {@link UserDetailsService}. The |
||||
* {@link UserDetails} is used to create an {@link Authentication}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public class WebAuthnAuthenticationProvider implements AuthenticationProvider { |
||||
|
||||
private final WebAuthnRelyingPartyOperations relyingPartyOperations; |
||||
|
||||
private final UserDetailsService userDetailsService; |
||||
|
||||
/** |
||||
* Creates a new instance. |
||||
* @param relyingPartyOperations the {@link WebAuthnRelyingPartyOperations} to use. |
||||
* Cannot be null. |
||||
* @param userDetailsService the {@link UserDetailsService} to use. Cannot be null. |
||||
*/ |
||||
public WebAuthnAuthenticationProvider(WebAuthnRelyingPartyOperations relyingPartyOperations, |
||||
UserDetailsService userDetailsService) { |
||||
Assert.notNull(relyingPartyOperations, "relyingPartyOperations cannot be null"); |
||||
Assert.notNull(userDetailsService, "userDetailsService cannot be null"); |
||||
this.relyingPartyOperations = relyingPartyOperations; |
||||
this.userDetailsService = userDetailsService; |
||||
} |
||||
|
||||
@Override |
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
||||
WebAuthnAuthenticationRequestToken webAuthnRequest = (WebAuthnAuthenticationRequestToken) authentication; |
||||
try { |
||||
PublicKeyCredentialUserEntity userEntity = this.relyingPartyOperations |
||||
.authenticate(webAuthnRequest.getWebAuthnRequest()); |
||||
String username = userEntity.getName(); |
||||
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); |
||||
return new WebAuthnAuthentication(userEntity, userDetails.getAuthorities()); |
||||
} |
||||
catch (RuntimeException ex) { |
||||
throw new BadCredentialsException(ex.getMessage(), ex); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public boolean supports(Class<?> authentication) { |
||||
return WebAuthnAuthenticationRequestToken.class.isAssignableFrom(authentication); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.authentication; |
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
import org.springframework.security.core.authority.AuthorityUtils; |
||||
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An {@link org.springframework.security.core.Authentication} used in |
||||
* {@link WebAuthnAuthenticationProvider} for authenticating via WebAuthn. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
public class WebAuthnAuthenticationRequestToken extends AbstractAuthenticationToken { |
||||
|
||||
private final RelyingPartyAuthenticationRequest webAuthnRequest; |
||||
|
||||
/** |
||||
* Creates a new instance. |
||||
* @param webAuthnRequest the {@link RelyingPartyAuthenticationRequest} to use for |
||||
* authentication. Cannot be null. |
||||
*/ |
||||
public WebAuthnAuthenticationRequestToken(RelyingPartyAuthenticationRequest webAuthnRequest) { |
||||
super(AuthorityUtils.NO_AUTHORITIES); |
||||
Assert.notNull(webAuthnRequest, "webAuthnRequest cannot be null"); |
||||
this.webAuthnRequest = webAuthnRequest; |
||||
} |
||||
|
||||
/** |
||||
* Gets the {@link RelyingPartyAuthenticationRequest} |
||||
* @return the {@link RelyingPartyAuthenticationRequest} |
||||
*/ |
||||
public RelyingPartyAuthenticationRequest getWebAuthnRequest() { |
||||
return this.webAuthnRequest; |
||||
} |
||||
|
||||
@Override |
||||
public void setAuthenticated(boolean authenticated) { |
||||
Assert.isTrue(!authenticated, "Cannot set this token to trusted"); |
||||
super.setAuthenticated(authenticated); |
||||
} |
||||
|
||||
@Override |
||||
public Object getCredentials() { |
||||
return this.webAuthnRequest.getPublicKey(); |
||||
} |
||||
|
||||
@Override |
||||
public Object getPrincipal() { |
||||
return this.webAuthnRequest.getPublicKey().getResponse().getUserHandle(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link AttestationConveyancePreference} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonSerialize(using = AttestationConveyancePreferenceSerializer.class) |
||||
class AttestationConveyancePreferenceMixin { |
||||
|
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator; |
||||
import com.fasterxml.jackson.databind.SerializerProvider; |
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference; |
||||
|
||||
/** |
||||
* Jackson serializer for {@link AttestationConveyancePreference} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class AttestationConveyancePreferenceSerializer extends StdSerializer<AttestationConveyancePreference> { |
||||
|
||||
AttestationConveyancePreferenceSerializer() { |
||||
super(AttestationConveyancePreference.class); |
||||
} |
||||
|
||||
@Override |
||||
public void serialize(AttestationConveyancePreference preference, JsonGenerator jgen, SerializerProvider provider) |
||||
throws IOException { |
||||
jgen.writeString(preference.getValue()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link AuthenticationExtensionsClientInputs} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonSerialize(using = AuthenticationExtensionsClientInputSerializer.class) |
||||
class AuthenticationExtensionsClientInputMixin { |
||||
|
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator; |
||||
import com.fasterxml.jackson.databind.SerializerProvider; |
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInput; |
||||
|
||||
/** |
||||
* Provides Jackson serialization of {@link AuthenticationExtensionsClientInput}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class AuthenticationExtensionsClientInputSerializer extends StdSerializer<AuthenticationExtensionsClientInput> { |
||||
|
||||
/** |
||||
* Creates a new instance. |
||||
*/ |
||||
AuthenticationExtensionsClientInputSerializer() { |
||||
super(AuthenticationExtensionsClientInput.class); |
||||
} |
||||
|
||||
@Override |
||||
public void serialize(AuthenticationExtensionsClientInput input, JsonGenerator jgen, SerializerProvider provider) |
||||
throws IOException { |
||||
jgen.writeObjectField(input.getExtensionId(), input.getInput()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link AuthenticationExtensionsClientInputs} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonSerialize(using = AuthenticationExtensionsClientInputsSerializer.class) |
||||
class AuthenticationExtensionsClientInputsMixin { |
||||
|
||||
} |
||||
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator; |
||||
import com.fasterxml.jackson.databind.SerializerProvider; |
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInput; |
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs; |
||||
|
||||
/** |
||||
* Provides Jackson serialization of {@link AuthenticationExtensionsClientInputs}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class AuthenticationExtensionsClientInputsSerializer extends StdSerializer<AuthenticationExtensionsClientInputs> { |
||||
|
||||
/** |
||||
* Creates a new instance. |
||||
*/ |
||||
AuthenticationExtensionsClientInputsSerializer() { |
||||
super(AuthenticationExtensionsClientInputs.class); |
||||
} |
||||
|
||||
@Override |
||||
public void serialize(AuthenticationExtensionsClientInputs inputs, JsonGenerator jgen, SerializerProvider provider) |
||||
throws IOException { |
||||
jgen.writeStartObject(); |
||||
for (AuthenticationExtensionsClientInput input : inputs.getInputs()) { |
||||
jgen.writeObject(input); |
||||
} |
||||
jgen.writeEndObject(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import com.fasterxml.jackson.core.JacksonException; |
||||
import com.fasterxml.jackson.core.JsonParser; |
||||
import com.fasterxml.jackson.core.JsonToken; |
||||
import com.fasterxml.jackson.databind.DeserializationContext; |
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer; |
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutput; |
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs; |
||||
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput; |
||||
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientOutputs; |
||||
|
||||
/** |
||||
* Provides Jackson deserialization of {@link AuthenticationExtensionsClientOutputs}. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<AuthenticationExtensionsClientOutputs> { |
||||
|
||||
private static final Log logger = LogFactory.getLog(AuthenticationExtensionsClientOutputsDeserializer.class); |
||||
|
||||
/** |
||||
* Creates a new instance. |
||||
*/ |
||||
AuthenticationExtensionsClientOutputsDeserializer() { |
||||
super(AuthenticationExtensionsClientOutputs.class); |
||||
} |
||||
|
||||
@Override |
||||
public AuthenticationExtensionsClientOutputs deserialize(JsonParser parser, DeserializationContext ctxt) |
||||
throws IOException, JacksonException { |
||||
List<AuthenticationExtensionsClientOutput<?>> outputs = new ArrayList<>(); |
||||
for (String key = parser.nextFieldName(); key != null; key = parser.nextFieldName()) { |
||||
JsonToken startObject = parser.nextValue(); |
||||
if (startObject != JsonToken.START_OBJECT) { |
||||
break; |
||||
} |
||||
if (CredentialPropertiesOutput.EXTENSION_ID.equals(key)) { |
||||
CredentialPropertiesOutput output = parser.readValueAs(CredentialPropertiesOutput.class); |
||||
outputs.add(output); |
||||
} |
||||
else { |
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug("Skipping unknown extension with id " + key); |
||||
} |
||||
parser.nextValue(); |
||||
} |
||||
} |
||||
|
||||
return new ImmutableAuthenticationExtensionsClientOutputs(outputs); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link AuthenticationExtensionsClientOutputs} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonDeserialize(using = AuthenticationExtensionsClientOutputsDeserializer.class) |
||||
class AuthenticationExtensionsClientOutputsMixin { |
||||
|
||||
} |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; |
||||
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link AuthenticatorAssertionResponse} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonDeserialize(builder = AuthenticatorAssertionResponse.AuthenticatorAssertionResponseBuilder.class) |
||||
class AuthenticatorAssertionResponseMixin { |
||||
|
||||
@JsonPOJOBuilder(withPrefix = "") |
||||
abstract class AuthenticatorAssertionResponseBuilderMixin { |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JacksonException; |
||||
import com.fasterxml.jackson.core.JsonParser; |
||||
import com.fasterxml.jackson.databind.DeserializationContext; |
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment; |
||||
|
||||
/** |
||||
* Jackson deserializer for {@link AuthenticatorAttachment} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class AuthenticatorAttachmentDeserializer extends StdDeserializer<AuthenticatorAttachment> { |
||||
|
||||
AuthenticatorAttachmentDeserializer() { |
||||
super(AuthenticatorAttachment.class); |
||||
} |
||||
|
||||
@Override |
||||
public AuthenticatorAttachment deserialize(JsonParser parser, DeserializationContext ctxt) |
||||
throws IOException, JacksonException { |
||||
String type = parser.readValueAs(String.class); |
||||
for (AuthenticatorAttachment publicKeyCredentialType : AuthenticatorAttachment.values()) { |
||||
if (publicKeyCredentialType.getValue().equals(type)) { |
||||
return publicKeyCredentialType; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; |
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link AuthenticatorAttachment} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonDeserialize(using = AuthenticatorAttachmentDeserializer.class) |
||||
@JsonSerialize(using = AuthenticatorAttachmentSerializer.class) |
||||
class AuthenticatorAttachmentMixin { |
||||
|
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator; |
||||
import com.fasterxml.jackson.databind.SerializerProvider; |
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment; |
||||
|
||||
/** |
||||
* Jackson serializer for {@link AuthenticatorAttachment} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class AuthenticatorAttachmentSerializer extends StdSerializer<AuthenticatorAttachment> { |
||||
|
||||
AuthenticatorAttachmentSerializer() { |
||||
super(AuthenticatorAttachment.class); |
||||
} |
||||
|
||||
@Override |
||||
public void serialize(AuthenticatorAttachment attachment, JsonGenerator jgen, SerializerProvider provider) |
||||
throws IOException { |
||||
jgen.writeString(attachment.getValue()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.util.List; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
import com.fasterxml.jackson.annotation.JsonSetter; |
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; |
||||
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse; |
||||
import org.springframework.security.web.webauthn.api.AuthenticatorTransport; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link AuthenticatorAttestationResponse} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonDeserialize(builder = AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder.class) |
||||
class AuthenticatorAttestationResponseMixin { |
||||
|
||||
@JsonPOJOBuilder(withPrefix = "") |
||||
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
abstract class AuthenticatorAttestationResponseBuilderMixin { |
||||
|
||||
@JsonSetter |
||||
abstract AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder transports( |
||||
List<AuthenticatorTransport> transports); |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link AuthenticatorSelectionCriteria} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonInclude(JsonInclude.Include.NON_NULL) |
||||
abstract class AuthenticatorSelectionCriteriaMixin { |
||||
|
||||
} |
||||
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JacksonException; |
||||
import com.fasterxml.jackson.core.JsonParser; |
||||
import com.fasterxml.jackson.databind.DeserializationContext; |
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorTransport; |
||||
|
||||
/** |
||||
* Jackson deserializer for {@link AuthenticatorTransport} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class AuthenticatorTransportDeserializer extends StdDeserializer<AuthenticatorTransport> { |
||||
|
||||
AuthenticatorTransportDeserializer() { |
||||
super(AuthenticatorTransport.class); |
||||
} |
||||
|
||||
@Override |
||||
public AuthenticatorTransport deserialize(JsonParser parser, DeserializationContext ctxt) |
||||
throws IOException, JacksonException { |
||||
String transportValue = parser.readValueAs(String.class); |
||||
for (AuthenticatorTransport transport : AuthenticatorTransport.values()) { |
||||
if (transport.getValue().equals(transportValue)) { |
||||
return transport; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; |
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorTransport; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link AuthenticatorTransport} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonDeserialize(using = AuthenticatorTransportDeserializer.class) |
||||
@JsonSerialize(using = AuthenticatorTransportSerializer.class) |
||||
class AuthenticatorTransportMixin { |
||||
|
||||
} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator; |
||||
import com.fasterxml.jackson.databind.JsonSerializer; |
||||
import com.fasterxml.jackson.databind.SerializerProvider; |
||||
|
||||
import org.springframework.security.web.webauthn.api.AuthenticatorTransport; |
||||
|
||||
/** |
||||
* Jackson serializer for {@link AuthenticatorTransport} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class AuthenticatorTransportSerializer extends JsonSerializer<AuthenticatorTransport> { |
||||
|
||||
@Override |
||||
public void serialize(AuthenticatorTransport transport, JsonGenerator jgen, SerializerProvider provider) |
||||
throws IOException { |
||||
jgen.writeString(transport.getValue()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator; |
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
||||
import org.springframework.security.web.webauthn.api.Bytes; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link Bytes} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonSerialize(using = BytesSerializer.class) |
||||
final class BytesMixin { |
||||
|
||||
@JsonCreator |
||||
static Bytes fromBase64(String value) { |
||||
return Bytes.fromBase64(value); |
||||
} |
||||
|
||||
private BytesMixin() { |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator; |
||||
import com.fasterxml.jackson.databind.SerializerProvider; |
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.Bytes; |
||||
|
||||
/** |
||||
* Jackson serializer for {@link Bytes} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class BytesSerializer extends StdSerializer<Bytes> { |
||||
|
||||
/** |
||||
* Creates a new instance. |
||||
*/ |
||||
BytesSerializer() { |
||||
super(Bytes.class); |
||||
} |
||||
|
||||
@Override |
||||
public void serialize(Bytes bytes, JsonGenerator jgen, SerializerProvider provider) throws IOException { |
||||
jgen.writeString(bytes.toBase64UrlString()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JacksonException; |
||||
import com.fasterxml.jackson.core.JsonParser; |
||||
import com.fasterxml.jackson.databind.DeserializationContext; |
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier; |
||||
|
||||
/** |
||||
* Jackson serializer for {@link COSEAlgorithmIdentifier} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class COSEAlgorithmIdentifierDeserializer extends StdDeserializer<COSEAlgorithmIdentifier> { |
||||
|
||||
COSEAlgorithmIdentifierDeserializer() { |
||||
super(COSEAlgorithmIdentifier.class); |
||||
} |
||||
|
||||
@Override |
||||
public COSEAlgorithmIdentifier deserialize(JsonParser parser, DeserializationContext ctxt) |
||||
throws IOException, JacksonException { |
||||
Long transportValue = parser.readValueAs(Long.class); |
||||
for (COSEAlgorithmIdentifier identifier : COSEAlgorithmIdentifier.values()) { |
||||
if (identifier.getValue() == transportValue.longValue()) { |
||||
return identifier; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; |
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
||||
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link COSEAlgorithmIdentifier} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonSerialize(using = COSEAlgorithmIdentifierSerializer.class) |
||||
@JsonDeserialize(using = COSEAlgorithmIdentifierDeserializer.class) |
||||
abstract class COSEAlgorithmIdentifierMixin { |
||||
|
||||
} |
||||
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator; |
||||
import com.fasterxml.jackson.databind.SerializerProvider; |
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier; |
||||
|
||||
/** |
||||
* Jackson serializer for {@link COSEAlgorithmIdentifier} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class COSEAlgorithmIdentifierSerializer extends StdSerializer<COSEAlgorithmIdentifier> { |
||||
|
||||
COSEAlgorithmIdentifierSerializer() { |
||||
super(COSEAlgorithmIdentifier.class); |
||||
} |
||||
|
||||
@Override |
||||
public void serialize(COSEAlgorithmIdentifier identifier, JsonGenerator jgen, SerializerProvider provider) |
||||
throws IOException { |
||||
jgen.writeNumber(identifier.getValue()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize; |
||||
|
||||
@JsonSerialize(using = CredProtectAuthenticationExtensionsClientInputSerializer.class) |
||||
class CredProtectAuthenticationExtensionsClientInputMixin { |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator; |
||||
import com.fasterxml.jackson.databind.SerializerProvider; |
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
||||
|
||||
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput; |
||||
|
||||
/** |
||||
* Serializes <a href= |
||||
* "https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension">credProtect |
||||
* extension</a>. |
||||
* |
||||
* @author Rob Winch |
||||
*/ |
||||
class CredProtectAuthenticationExtensionsClientInputSerializer |
||||
extends StdSerializer<CredProtectAuthenticationExtensionsClientInput> { |
||||
|
||||
protected CredProtectAuthenticationExtensionsClientInputSerializer() { |
||||
super(CredProtectAuthenticationExtensionsClientInput.class); |
||||
} |
||||
|
||||
@Override |
||||
public void serialize(CredProtectAuthenticationExtensionsClientInput input, JsonGenerator jgen, |
||||
SerializerProvider provider) throws IOException { |
||||
CredProtectAuthenticationExtensionsClientInput.CredProtect credProtect = input.getInput(); |
||||
String policy = toString(credProtect.getCredProtectionPolicy()); |
||||
jgen.writeObjectField("credentialProtectionPolicy", policy); |
||||
jgen.writeObjectField("enforceCredentialProtectionPolicy", credProtect.isEnforceCredentialProtectionPolicy()); |
||||
} |
||||
|
||||
private static String toString(CredProtectAuthenticationExtensionsClientInput.CredProtect.ProtectionPolicy policy) { |
||||
switch (policy) { |
||||
case USER_VERIFICATION_OPTIONAL: |
||||
return "userVerificationOptional"; |
||||
case USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST: |
||||
return "userVerificationOptionalWithCredentialIdList"; |
||||
case USER_VERIFICATION_REQUIRED: |
||||
return "userVerificationRequired"; |
||||
default: |
||||
throw new IllegalArgumentException("Unsupported ProtectionPolicy " + policy); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
||||
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
|
||||
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput; |
||||
|
||||
/** |
||||
* Jackson mixin for {@link CredentialPropertiesOutput} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
abstract class CredentialPropertiesOutputMixin { |
||||
|
||||
CredentialPropertiesOutputMixin(@JsonProperty("rk") boolean rk) { |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* Copyright 2002-2024 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.web.webauthn.jackson; |
||||
|
||||
import java.io.IOException; |
||||
import java.time.Duration; |
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator; |
||||
import com.fasterxml.jackson.databind.SerializerProvider; |
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer; |
||||
|
||||
/** |
||||
* Jackson serializer for {@link Duration} |
||||
* |
||||
* @author Rob Winch |
||||
* @since 6.4 |
||||
*/ |
||||
class DurationSerializer extends StdSerializer<Duration> { |
||||
|
||||
/** |
||||
* Creates an instance. |
||||
*/ |
||||
DurationSerializer() { |
||||
super(Duration.class); |
||||
} |
||||
|
||||
@Override |
||||
public void serialize(Duration duration, JsonGenerator jgen, SerializerProvider provider) throws IOException { |
||||
jgen.writeNumber(duration.toMillis()); |
||||
} |
||||
|
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue