1
0
mirror of synced 2026-08-07 02:38:47 +00:00

Add Passkeys Support

Closes gh-13305
This commit is contained in:
Rob Winch
2024-10-17 21:40:51 -05:00
parent f280aa390b
commit b0e8730d70
157 changed files with 19203 additions and 5 deletions
@@ -0,0 +1,115 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication;
import java.io.IOException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationSuccessHandler} that writes a JSON response with the redirect
* URL and an authenticated status similar to:
*
* <code>
* {
* "redirectUrl": "/user/profile",
* "authenticated": true
* }
* </code>
*
* @author Rob Winch
* @since 6.4
*/
public final class HttpMessageConverterAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
JsonMapper.builder().addModule(new WebauthnJackson2Module()).build());
private RequestCache requestCache = new HttpSessionRequestCache();
/**
* Sets the {@link GenericHttpMessageConverter} to write to the response. The default
* is {@link MappingJackson2HttpMessageConverter}.
* @param converter the {@link GenericHttpMessageConverter} to use. Cannot be null.
*/
public void setConverter(HttpMessageConverter<Object> converter) {
Assert.notNull(converter, "converter cannot be null");
this.converter = converter;
}
/**
* Sets the {@link RequestCache} to use. The default is
* {@link HttpSessionRequestCache}.
* @param requestCache the {@link RequestCache} to use. Cannot be null
*/
public void setRequestCache(RequestCache requestCache) {
Assert.notNull(requestCache, "requestCache cannot be null");
this.requestCache = requestCache;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
final SavedRequest savedRequest = this.requestCache.getRequest(request, response);
final String redirectUrl = (savedRequest != null) ? savedRequest.getRedirectUrl()
: request.getContextPath() + "/";
this.requestCache.removeRequest(request, response);
this.converter.write(new AuthenticationSuccess(redirectUrl), MediaType.APPLICATION_JSON,
new ServletServerHttpResponse(response));
}
/**
* A response object used to write the JSON response for successful authentication.
*
* NOTE: We should be careful about writing {@link Authentication} or
* {@link Authentication#getPrincipal()} to the response since it contains
* credentials.
*/
public static final class AuthenticationSuccess {
private final String redirectUrl;
private AuthenticationSuccess(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
public String getRedirectUrl() {
return this.redirectUrl;
}
public boolean isAuthenticated() {
return true;
}
}
}
@@ -67,6 +67,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
private boolean saml2LoginEnabled;
private boolean passkeysEnabled;
private boolean oneTimeTokenEnabled;
private String authenticationUrl;
@@ -85,6 +87,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = (request) -> Collections.emptyMap();
private Function<HttpServletRequest, Map<String, String>> resolveHeaders = (request) -> Collections.emptyMap();
public DefaultLoginPageGeneratingFilter() {
}
@@ -117,6 +121,17 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
this.resolveHiddenInputs = resolveHiddenInputs;
}
/**
* Sets a Function used to resolve a Map of the HTTP headers where the key is the name
* of the header and the value is the value of the header. Typically, this is used to
* resolve the CSRF token.
* @param resolveHeaders the function to resolve the headers
*/
public void setResolveHeaders(Function<HttpServletRequest, Map<String, String>> resolveHeaders) {
Assert.notNull(resolveHeaders, "resolveHeaders cannot be null");
this.resolveHeaders = resolveHeaders;
}
public boolean isEnabled() {
return this.formLoginEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled;
}
@@ -153,6 +168,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
this.saml2LoginEnabled = saml2LoginEnabled;
}
public void setPasskeysEnabled(boolean passkeysEnabled) {
this.passkeysEnabled = passkeysEnabled;
}
public void setAuthenticationUrl(String authenticationUrl) {
this.authenticationUrl = authenticationUrl;
}
@@ -207,14 +226,46 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
return HtmlTemplates.fromTemplate(LOGIN_PAGE_TEMPLATE)
.withRawHtml("contextPath", contextPath)
.withRawHtml("javaScript", renderJavaScript(request, contextPath))
.withRawHtml("formLogin", renderFormLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
.withRawHtml("oneTimeTokenLogin",
renderOneTimeTokenLogin(request, loginError, logoutSuccess, contextPath, errorMsg))
.withRawHtml("oauth2Login", renderOAuth2Login(loginError, logoutSuccess, errorMsg, contextPath))
.withRawHtml("saml2Login", renderSaml2Login(loginError, logoutSuccess, errorMsg, contextPath))
.withRawHtml("passkeyLogin", renderPasskeyLogin())
.render();
}
private String renderJavaScript(HttpServletRequest request, String contextPath) {
if (this.passkeysEnabled) {
return HtmlTemplates.fromTemplate(PASSKEY_SCRIPT_TEMPLATE)
.withValue("loginPageUrl", this.loginPageUrl)
.withValue("contextPath", contextPath)
.withRawHtml("csrfHeaders", renderHeaders(request))
.render();
}
return "";
}
private String renderPasskeyLogin() {
if (this.passkeysEnabled) {
return PASSKEY_FORM_TEMPLATE;
}
return "";
}
private String renderHeaders(HttpServletRequest request) {
StringBuffer javascriptHeadersEntries = new StringBuffer();
Map<String, String> headers = this.resolveHeaders.apply(request);
for (Map.Entry<String, String> header : headers.entrySet()) {
javascriptHeadersEntries.append(HtmlTemplates.fromTemplate(CSRF_HEADERS)
.withValue("headerName", header.getKey())
.withValue("headerValue", header.getValue())
.render());
}
return javascriptHeadersEntries.toString();
}
private String renderFormLogin(HttpServletRequest request, boolean loginError, boolean logoutSuccess,
String contextPath, String errorMsg) {
if (!this.formLoginEnabled) {
@@ -235,6 +286,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
.withValue("passwordParameter", this.passwordParameter)
.withRawHtml("rememberMeInput", renderRememberMe(this.rememberMeParameter))
.withRawHtml("hiddenInputs", hiddenInputs)
.withValue("autocomplete", this.passkeysEnabled ? "autocomplete=\"password webauthn\"" : "")
.render();
}
@@ -383,6 +435,26 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
return uri.equals(request.getContextPath() + url);
}
private static final String CSRF_HEADERS = """
{"{{headerName}}" : "{{headerValue}}"}""";
private static final String PASSKEY_SCRIPT_TEMPLATE = """
<script type="text/javascript" src="{{contextPath}}/login/webauthn.js"></script>
<script type="text/javascript">
<!--
document.addEventListener("DOMContentLoaded",() => setupLogin({{csrfHeaders}}, "{{contextPath}}", document.getElementById('passkey-signin')));
//-->
</script>
""";
private static final String PASSKEY_FORM_TEMPLATE = """
<div class="login-form">
<h2>Login with Passkeys</h2>
<button id="passkey-signin" type="submit" class="primary">Sign in with a passkey</button>
</form>
""";
private static final String LOGIN_PAGE_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
@@ -392,12 +464,12 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="{{contextPath}}/default-ui.css" rel="stylesheet" />
<link href="{{contextPath}}/default-ui.css" rel="stylesheet" />{{javaScript}}
</head>
<body>
<div class="content">
{{formLogin}}
{{oneTimeTokenLogin}}
{{oneTimeTokenLogin}}{{passkeyLogin}}
{{oauth2Login}}
{{saml2Login}}
</div>
@@ -414,7 +486,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
</p>
<p>
<label for="password" class="screenreader">Password</label>
<input type="password" id="password" name="{{passwordParameter}}" placeholder="Password" required>
<input type="password" id="password" name="{{passwordParameter}}" placeholder="Password" {{autocomplete}}required>
</p>
{{rememberMeInput}}
{{hiddenInputs}}
@@ -0,0 +1,103 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-relying-party">WebAuthn Relying
* Parties</a> may use <a href=
* "https://www.w3.org/TR/webauthn-3/#enumdef-attestationconveyancepreference">AttestationConveyancePreference</a>
* to specify their preference regarding attestation conveyance during credential
* generation.
*
* @author Rob Winch
* @since 6.4
*/
public final class AttestationConveyancePreference {
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-none">none</a>
* preference indicates that the Relying Party is not interested in
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>
* <a href="https://www.w3.org/TR/webauthn-3/#attestation">attestation</a>.
*/
public static final AttestationConveyancePreference NONE = new AttestationConveyancePreference("none");
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-indirect">indirect</a>
* preference indicates that the Relying Party wants to receive a verifiable
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-statement">attestation
* statement</a>, but allows the client to decide how to obtain such an attestation
* statement.
*/
public static final AttestationConveyancePreference INDIRECT = new AttestationConveyancePreference("indirect");
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-direct">direct</a>
* preference indicates that the Relying Party wants to receive the
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-statement">attestation
* statement</a> as generated by the
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>.
*/
public static final AttestationConveyancePreference DIRECT = new AttestationConveyancePreference("direct");
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-attestationconveyancepreference-enterprise">enterprise</a>
* preference indicates that the Relying Party wants to receive an
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-statement">attestation
* statement</a> that may include uniquely identifying information.
*/
public static final AttestationConveyancePreference ENTERPRISE = new AttestationConveyancePreference("enterprise");
private final String value;
AttestationConveyancePreference(String value) {
this.value = value;
}
/**
* Gets the String value of the preference.
* @return the String value of the preference.
*/
public String getValue() {
return this.value;
}
/**
* Gets an instance of {@link AttestationConveyancePreference}
* @param value the {@link #getValue()}
* @return an {@link AttestationConveyancePreference}
*/
public static AttestationConveyancePreference valueOf(String value) {
switch (value) {
case "none":
return NONE;
case "indirect":
return INDIRECT;
case "direct":
return DIRECT;
case "enterprise":
return ENTERPRISE;
default:
return new AttestationConveyancePreference(value);
}
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* A <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client extension
* input</a> entry in the {@link AuthenticationExtensionsClientInputs}.
*
* @param <T>
* @author Rob Winch
* @since 6.4
* @see ImmutableAuthenticationExtensionsClientInput
*/
public interface AuthenticationExtensionsClientInput<T> {
/**
* Gets the <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension
* identifier</a>.
* @return the
* <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension
* identifier</a>.
*/
String getExtensionId();
/**
* Gets the <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client
* extension</a>.
* @return the
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client
* extension</a>.
*/
T getInput();
}
@@ -0,0 +1,42 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.util.List;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#iface-authentication-extensions-client-inputs">AuthenticationExtensionsClientInputs</a>
* is a dictionary containing the
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-input">client extension
* input</a> values for zero or more
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-extensions">WebAuthn
* Extensions</a>.
*
* @author Rob Winch
* @since 6.4
* @see PublicKeyCredentialCreationOptions#getExtensions()
*/
public interface AuthenticationExtensionsClientInputs {
/**
* Gets all of the {@link AuthenticationExtensionsClientInput}.
* @return a non-null {@link List} of {@link AuthenticationExtensionsClientInput}.
*/
List<AuthenticationExtensionsClientInput> getInputs();
}
@@ -0,0 +1,47 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* A <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client extension
* output</a> entry in {@link AuthenticationExtensionsClientOutputs}.
*
* @param <T>
* @see AuthenticationExtensionsClientOutputs#getOutputs()
* @see CredentialPropertiesOutput
*/
public interface AuthenticationExtensionsClientOutput<T> {
/**
* Gets the <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension
* identifier</a>.
* @return the
* <a href="https://www.w3.org/TR/webauthn-3/#extension-identifier">extension
* identifier</a>.
*/
String getExtensionId();
/**
* The <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client
* extension output</a>.
* @return the
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client
* extension output</a>.
*/
T getOutput();
}
@@ -0,0 +1,42 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.util.List;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-authenticationextensionsclientoutputs">AuthenticationExtensionsClientOutputs</a>
* is a dictionary containing the
* <a href="https://www.w3.org/TR/webauthn-3/#client-extension-output">client extension
* output</a> values for zero or more
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-extensions">WebAuthn
* Extensions</a>.
*
* @author Rob Winch
* @since 6.4
* @see PublicKeyCredential#getClientExtensionResults()
*/
public interface AuthenticationExtensionsClientOutputs {
/**
* Gets all of the {@link AuthenticationExtensionsClientOutput}.
* @return a non-null {@link List} of {@link AuthenticationExtensionsClientOutput}.
*/
List<AuthenticationExtensionsClientOutput<?>> getOutputs();
}
@@ -0,0 +1,205 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse">AuthenticatorAssertionResponse</a>
* interface represents an
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>'s response
* to a clients request for generation of a new
* <a href="https://www.w3.org/TR/webauthn-3/#authentication-assertion">authentication
* assertion</a> given the
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-relying-party">WebAuthn Relying
* Party</a>'s challenge and OPTIONAL list of credentials it is aware of. This response
* contains a cryptographic signature proving possession of the
* <a href="https://www.w3.org/TR/webauthn-3/#credential-private-key">credential private
* key</a>, and optionally evidence of
* <a href="https://www.w3.org/TR/webauthn-3/#user-consent">user consent</a> to a specific
* transaction.
*
* @author Rob Winch
* @since 6.4
* @see PublicKeyCredential#getResponse()
*/
public final class AuthenticatorAssertionResponse extends AuthenticatorResponse {
private final Bytes authenticatorData;
private final Bytes signature;
private final Bytes userHandle;
private final Bytes attestationObject;
private AuthenticatorAssertionResponse(Bytes clientDataJSON, Bytes authenticatorData, Bytes signature,
Bytes userHandle, Bytes attestationObject) {
super(clientDataJSON);
this.authenticatorData = authenticatorData;
this.signature = signature;
this.userHandle = userHandle;
this.attestationObject = attestationObject;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-authenticatordata">authenticatorData</a>
* contains the
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator-data">authenticator
* data</a> returned by the authenticator. See
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data">6.1
* Authenticator Data.</a>.
* @return the {@code authenticatorData}
*/
public Bytes getAuthenticatorData() {
return this.authenticatorData;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-signature">signature</a>
* contains the raw signature returned from the authenticator. See
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-op-get-assertion">6.3.3 The
* authenticatorGetAssertion Operation</a>.
* @return the {@code signature}
*/
public Bytes getSignature() {
return this.signature;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorassertionresponse-userhandle">userHandle</a>
* is the <a href="https://www.w3.org/TR/webauthn-3/#user-handle">user handle</a>
* which is returned from the authenticator, or null if the authenticator did not
* return a user handle. See
* <a href="https://www.w3.org/TR/webauthn-3/#sctn-op-get-assertion">6.3.3 The
* authenticatorGetAssertion Operation</a>. The authenticator MUST always return a
* user handle if the <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials">allowCredentials</a>
* option used in the
* <a href="https://www.w3.org/TR/webauthn-3/#authentication-ceremony">authentication
* ceremony</a> is empty, and MAY return one otherwise.
* @return the <a href="https://www.w3.org/TR/webauthn-3/#user-handle">user handle</a>
*/
public Bytes getUserHandle() {
return this.userHandle;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-attestationobject">attestationObject</a>
* is an OPTIONAL attribute contains an
* <a href="https://www.w3.org/TR/webauthn-3/#attestation-object">attestation
* object</a>, if the authenticator supports attestation in assertions.
* @return the {@code attestationObject}
*/
public Bytes getAttestationObject() {
return this.attestationObject;
}
/**
* Creates a new {@link AuthenticatorAssertionResponseBuilder}
* @return the {@link AuthenticatorAssertionResponseBuilder}
*/
public static AuthenticatorAssertionResponseBuilder builder() {
return new AuthenticatorAssertionResponseBuilder();
}
/**
* Builds a {@link AuthenticatorAssertionResponse}.
*
* @author Rob Winch
* @since 6.4
*/
public static final class AuthenticatorAssertionResponseBuilder {
private Bytes authenticatorData;
private Bytes signature;
private Bytes userHandle;
private Bytes attestationObject;
private Bytes clientDataJSON;
private AuthenticatorAssertionResponseBuilder() {
}
/**
* Set the {@link #getAuthenticatorData()} property
* @param authenticatorData the authenticator data.
* @return the {@link AuthenticatorAssertionResponseBuilder}
*/
public AuthenticatorAssertionResponseBuilder authenticatorData(Bytes authenticatorData) {
this.authenticatorData = authenticatorData;
return this;
}
/**
* Set the {@link #getSignature()} property
* @param signature the signature
* @return the {@link AuthenticatorAssertionResponseBuilder}
*/
public AuthenticatorAssertionResponseBuilder signature(Bytes signature) {
this.signature = signature;
return this;
}
/**
* Set the {@link #getUserHandle()} property
* @param userHandle the user handle
* @return the {@link AuthenticatorAssertionResponseBuilder}
*/
public AuthenticatorAssertionResponseBuilder userHandle(Bytes userHandle) {
this.userHandle = userHandle;
return this;
}
/**
* Set the {@link #attestationObject} property
* @param attestationObject the attestation object
* @return the {@link AuthenticatorAssertionResponseBuilder}
*/
public AuthenticatorAssertionResponseBuilder attestationObject(Bytes attestationObject) {
this.attestationObject = attestationObject;
return this;
}
/**
* Set the {@link #getClientDataJSON()} property
* @param clientDataJSON the client data JSON
* @return the {@link AuthenticatorAssertionResponseBuilder}
*/
public AuthenticatorAssertionResponseBuilder clientDataJSON(Bytes clientDataJSON) {
this.clientDataJSON = clientDataJSON;
return this;
}
/**
* Builds the {@link AuthenticatorAssertionResponse}
* @return the {@link AuthenticatorAssertionResponse}
*/
public AuthenticatorAssertionResponse build() {
return new AuthenticatorAssertionResponse(this.clientDataJSON, this.authenticatorData, this.signature,
this.userHandle, this.attestationObject);
}
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#enumdef-authenticatorattachment">AuthenticatorAttachment</a>.
*
* @author Rob Winch
* @since 6.4
*/
public final class AuthenticatorAttachment {
/**
* Indicates <a href=
* "https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#cross-platform-attachment">cross-platform
* attachment</a>.
*
* <p>
* Authenticators of this class are removable from, and can "roam" among, client
* platforms.
*/
public static final AuthenticatorAttachment CROSS_PLATFORM = new AuthenticatorAttachment("cross-platform");
/**
* Indicates <a href=
* "https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#platform-attachment">platform
* attachment</a>.
*
* <p>
* Usually, authenticators of this class are not removable from the platform.
*/
public static final AuthenticatorAttachment PLATFORM = new AuthenticatorAttachment("platform");
private final String value;
AuthenticatorAttachment(String value) {
this.value = value;
}
/**
* Gets the value.
* @return the value.
*/
public String getValue() {
return this.value;
}
@Override
public String toString() {
return "AuthenticatorAttachment [" + this.value + "]";
}
/**
* Gets an instance of {@link AuthenticatorAttachment} based upon the value passed in.
* @param value the value to obtain the {@link AuthenticatorAttachment}
* @return the {@link AuthenticatorAttachment}
*/
public static AuthenticatorAttachment valueOf(String value) {
switch (value) {
case "cross-platform":
return CROSS_PLATFORM;
case "platform":
return PLATFORM;
default:
return new AuthenticatorAttachment(value);
}
}
public static AuthenticatorAttachment[] values() {
return new AuthenticatorAttachment[] { CROSS_PLATFORM, PLATFORM };
}
}
@@ -0,0 +1,144 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.util.Arrays;
import java.util.List;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse">AuthenticatorAttestationResponse</a>
* represents the
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticator</a>'s response
* to a clients request for the creation of a new
* <a href="https://www.w3.org/TR/webauthn-3/#public-key-credential">public key
* credential</a>.
*
* @author Rob Winch
* @since 6.4
* @see PublicKeyCredential#getResponse()
*/
public final class AuthenticatorAttestationResponse extends AuthenticatorResponse {
private final Bytes attestationObject;
private final List<AuthenticatorTransport> transports;
private AuthenticatorAttestationResponse(Bytes clientDataJSON, Bytes attestationObject,
List<AuthenticatorTransport> transports) {
super(clientDataJSON);
this.attestationObject = attestationObject;
this.transports = transports;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-attestationobject">attestationObject</a>
* attribute contains an attestation object, which is opaque to, and cryptographically
* protected against tampering by, the client.
* @return the attestationObject
*/
public Bytes getAttestationObject() {
return this.attestationObject;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-gettransports">transports</a>
* returns the <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-transports-slot">transports</a>
* @return the transports
*/
public List<AuthenticatorTransport> getTransports() {
return this.transports;
}
/**
* Creates a new instance.
* @return the {@link AuthenticatorAttestationResponseBuilder}
*/
public static AuthenticatorAttestationResponseBuilder builder() {
return new AuthenticatorAttestationResponseBuilder();
}
/**
* Builds {@link AuthenticatorAssertionResponse}.
*
* @author Rob Winch
* @since 6.4
*/
public static final class AuthenticatorAttestationResponseBuilder {
private Bytes attestationObject;
private List<AuthenticatorTransport> transports;
private Bytes clientDataJSON;
private AuthenticatorAttestationResponseBuilder() {
}
/**
* Sets the {@link #getAttestationObject()} property.
* @param attestationObject the attestation object.
* @return the {@link AuthenticatorAttestationResponseBuilder}
*/
public AuthenticatorAttestationResponseBuilder attestationObject(Bytes attestationObject) {
this.attestationObject = attestationObject;
return this;
}
/**
* Sets the {@link #getTransports()} property.
* @param transports the transports
* @return the {@link AuthenticatorAttestationResponseBuilder}
*/
public AuthenticatorAttestationResponseBuilder transports(AuthenticatorTransport... transports) {
return transports(Arrays.asList(transports));
}
/**
* Sets the {@link #getTransports()} property.
* @param transports the transports
* @return the {@link AuthenticatorAttestationResponseBuilder}
*/
public AuthenticatorAttestationResponseBuilder transports(List<AuthenticatorTransport> transports) {
this.transports = transports;
return this;
}
/**
* Sets the {@link #getClientDataJSON()} property.
* @param clientDataJSON the client data JSON.
* @return the {@link AuthenticatorAttestationResponseBuilder}
*/
public AuthenticatorAttestationResponseBuilder clientDataJSON(Bytes clientDataJSON) {
this.clientDataJSON = clientDataJSON;
return this;
}
/**
* Builds a {@link AuthenticatorAssertionResponse}.
* @return the {@link AuthenticatorAttestationResponseBuilder}
*/
public AuthenticatorAttestationResponse build() {
return new AuthenticatorAttestationResponse(this.clientDataJSON, this.attestationObject, this.transports);
}
}
}
@@ -0,0 +1,53 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#iface-authenticatorresponse">AuthenticatorResponse</a>
* represents <a href="https://www.w3.org/TR/webauthn-3/#authenticator">Authenticators</a>
* respond to <a href="https://www.w3.org/TR/webauthn-3/#relying-party">Relying Party</a>
* requests.
*
* @author Rob Winch
* @since 6.4
*/
public abstract class AuthenticatorResponse {
private final Bytes clientDataJSON;
/**
* Creates a new instance
* @param clientDataJSON the {@link #getClientDataJSON()}
*/
AuthenticatorResponse(Bytes clientDataJSON) {
this.clientDataJSON = clientDataJSON;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorresponse-clientdatajson">clientDataJSON</a>
* contains a JSON-compatible serialization of the client data, the hash of which is
* passed to the authenticator by the client in its call to either create() or get()
* (i.e., the client data itself is not sent to the authenticator).
* @return the client data JSON
*/
public Bytes getClientDataJSON() {
return this.clientDataJSON;
}
}
@@ -0,0 +1,170 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria">AuthenticatorAttachment</a>
* can be used by
* <a href="https://www.w3.org/TR/webauthn-3/#webauthn-relying-party">WebAuthn Relying
* Parties</a> to specify their requirements regarding authenticator attributes.
*
* There is no <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-requireresidentkey">requireResidentKey</a>
* property because it is only for backwards compatability with WebAuthn Level 1.
*
* @author Rob Winch
* @since 6.4
* @see PublicKeyCredentialCreationOptions#getAuthenticatorSelection()
*/
public final class AuthenticatorSelectionCriteria {
private final AuthenticatorAttachment authenticatorAttachment;
private final ResidentKeyRequirement residentKey;
private final UserVerificationRequirement userVerification;
// NOTE: There is no requireResidentKey property because it is only for backward
// compatability with WebAuthn Level 1
/**
* Creates a new instance
* @param authenticatorAttachment the authenticator attachment
* @param residentKey the resident key requirement
* @param userVerification the user verification
*/
private AuthenticatorSelectionCriteria(AuthenticatorAttachment authenticatorAttachment,
ResidentKeyRequirement residentKey, UserVerificationRequirement userVerification) {
this.authenticatorAttachment = authenticatorAttachment;
this.residentKey = residentKey;
this.userVerification = userVerification;
}
/**
* If <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-authenticatorattachment">
* authenticatorAttachment</a> is present, eligible
* <a href="https://www.w3.org/TR/webauthn-3/#authenticator">authenticators</a> are
* filtered to be only those authenticators attached with the specified
* <a href="https://www.w3.org/TR/webauthn-3/#enum-attachment">authenticator
* attachment modality</a> (see also <a href=
* "https://www.w3.org/TR/webauthn-3/#sctn-authenticator-attachment-modality">6.2.1
* Authenticator Attachment Modality</a>).
* @return the authenticator attachment
*/
public AuthenticatorAttachment getAuthenticatorAttachment() {
return this.authenticatorAttachment;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-residentkey">residentKey</a>
* specifies the extent to which the
* <a href="https://www.w3.org/TR/webauthn-3/#relying-party">Relying Party</a> desires
* to create a <a href=
* "https://www.w3.org/TR/webauthn-3/#client-side-discoverable-credential">client-side
* discoverable credential</a>.
* @return the residenty key requirement
*/
public ResidentKeyRequirement getResidentKey() {
return this.residentKey;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorselectioncriteria-userverification">userVerification</a>
* specifies the <a href="https://www.w3.org/TR/webauthn-3/#relying-party">Relying
* Party</a>'s requirements regarding
* <a href="https://www.w3.org/TR/webauthn-3/#user-verification">user verification</a>
* for the <a href=
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">create()</a>
* operation.
* @return the user verification requirement
*/
public UserVerificationRequirement getUserVerification() {
return this.userVerification;
}
/**
* Creates a new {@link AuthenticatorSelectionCriteriaBuilder}
* @return a new {@link AuthenticatorSelectionCriteriaBuilder}
*/
public static AuthenticatorSelectionCriteriaBuilder builder() {
return new AuthenticatorSelectionCriteriaBuilder();
}
/**
* Creates a {@link AuthenticatorSelectionCriteria}
*
* @author Rob Winch
* @since 6.4
*/
public static final class AuthenticatorSelectionCriteriaBuilder {
private AuthenticatorAttachment authenticatorAttachment;
private ResidentKeyRequirement residentKey;
private UserVerificationRequirement userVerification;
private AuthenticatorSelectionCriteriaBuilder() {
}
/**
* Sets the {@link #getAuthenticatorAttachment()} property.
* @param authenticatorAttachment the authenticator attachment
* @return the {@link AuthenticatorSelectionCriteriaBuilder}
*/
public AuthenticatorSelectionCriteriaBuilder authenticatorAttachment(
AuthenticatorAttachment authenticatorAttachment) {
this.authenticatorAttachment = authenticatorAttachment;
return this;
}
/**
* Sets the {@link #getResidentKey()} property.
* @param residentKey the resident key
* @return the {@link AuthenticatorSelectionCriteriaBuilder}
*/
public AuthenticatorSelectionCriteriaBuilder residentKey(ResidentKeyRequirement residentKey) {
this.residentKey = residentKey;
return this;
}
/**
* Sets the {@link #getUserVerification()} property.
* @param userVerification the user verification requirement
* @return the {@link AuthenticatorSelectionCriteriaBuilder}
*/
public AuthenticatorSelectionCriteriaBuilder userVerification(UserVerificationRequirement userVerification) {
this.userVerification = userVerification;
return this;
}
/**
* Builds a {@link AuthenticatorSelectionCriteria}
* @return a new {@link AuthenticatorSelectionCriteria}
*/
public AuthenticatorSelectionCriteria build() {
return new AuthenticatorSelectionCriteria(this.authenticatorAttachment, this.residentKey,
this.userVerification);
}
}
}
@@ -0,0 +1,118 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#enumdef-authenticatortransport">AuthenticatorTransport</a>
* defines hints as to how clients might communicate with a particular authenticator in
* order to obtain an assertion for a specific credential.
*
* @author Rob Winch
* @since 6.4
*/
public final class AuthenticatorTransport {
/**
* <a href="https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-usb">usbc</a>
* indicates the respective authenticator can be contacted over removable USB.
*/
public static final AuthenticatorTransport USB = new AuthenticatorTransport("usb");
/**
* <a href="https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-nfc">nfc</a>
* indicates the respective authenticator can be contacted over Near Field
* Communication (NFC).
*/
public static final AuthenticatorTransport NFC = new AuthenticatorTransport("nfc");
/**
* <a href="https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-ble">ble</a>
* Indicates the respective authenticator can be contacted over Bluetooth Smart
* (Bluetooth Low Energy / BLE).
*/
public static final AuthenticatorTransport BLE = new AuthenticatorTransport("ble");
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-smart-card">smart-card</a>
* indicates the respective authenticator can be contacted over ISO/IEC 7816 smart
* card with contacts.
*/
public static final AuthenticatorTransport SMART_CARD = new AuthenticatorTransport("smart-card");
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-hybrid">hybrid</a>
* indicates the respective authenticator can be contacted using a combination of
* (often separate) data-transport and proximity mechanisms. This supports, for
* example, authentication on a desktop computer using a smartphone.
*/
public static final AuthenticatorTransport HYBRID = new AuthenticatorTransport("hybrid");
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatortransport-internal">internal</a>
* indicates the respective authenticator is contacted using a client device-specific
* transport, i.e., it is a platform authenticator. These authenticators are not
* removable from the client device.
*/
public static final AuthenticatorTransport INTERNAL = new AuthenticatorTransport("internal");
private final String value;
AuthenticatorTransport(String value) {
this.value = value;
}
/**
* Get's the value.
* @return the value.
*/
public String getValue() {
return this.value;
}
/**
* Gets an instance of {@link AuthenticatorTransport}.
* @param value the value of the {@link AuthenticatorTransport}
* @return the {@link AuthenticatorTransport}
*/
public static AuthenticatorTransport valueOf(String value) {
switch (value) {
case "usb":
return USB;
case "nfc":
return NFC;
case "ble":
return BLE;
case "smart-card":
return SMART_CARD;
case "hybrid":
return HYBRID;
case "internal":
return INTERNAL;
default:
return new AuthenticatorTransport(value);
}
}
public static AuthenticatorTransport[] values() {
return new AuthenticatorTransport[] { USB, NFC, BLE, HYBRID, INTERNAL };
}
}
@@ -0,0 +1,103 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import org.springframework.util.Assert;
/**
* An object representation of byte[].
*
* @author Rob Winch
* @since 6.4
*/
public final class Bytes {
private static final SecureRandom RANDOM = new SecureRandom();
private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding();
private static final Base64.Decoder DECODER = Base64.getUrlDecoder();
private final byte[] bytes;
/**
* Creates a new instance
* @param bytes the raw base64UrlString that will be encoded.
*/
public Bytes(byte[] bytes) {
Assert.notNull(bytes, "bytes cannot be null");
this.bytes = bytes;
}
/**
* Gets the raw bytes.
* @return the bytes
*/
public byte[] getBytes() {
return Arrays.copyOf(this.bytes, this.bytes.length);
}
/**
* Gets the bytes as Base64 URL encoded String.
* @return
*/
public String toBase64UrlString() {
return ENCODER.encodeToString(getBytes());
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Bytes that) {
return that.toBase64UrlString().equals(toBase64UrlString());
}
return false;
}
@Override
public int hashCode() {
return toBase64UrlString().hashCode();
}
public String toString() {
return "Bytes[" + toBase64UrlString() + "]";
}
/**
* Creates a secure random {@link Bytes} with random bytes and sufficient entropy.
* @return a new secure random generated {@link Bytes}
*/
public static Bytes random() {
byte[] bytes = new byte[32];
RANDOM.nextBytes(bytes);
return new Bytes(bytes);
}
/**
* Creates a new instance from a base64 url string.
* @param base64UrlString the base64 url string
* @return the {@link Bytes}
*/
public static Bytes fromBase64(String base64UrlString) {
byte[] bytes = DECODER.decode(base64UrlString);
return new Bytes(bytes);
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier">COSEAlgorithmIdentifier</a> is
* used to identify a cryptographic algorithm.
*
* @author Rob Winch
* @since 6.4
* @see PublicKeyCredentialParameters#getAlg()
*/
public final class COSEAlgorithmIdentifier {
public static final COSEAlgorithmIdentifier EdDSA = new COSEAlgorithmIdentifier(-8);
public static final COSEAlgorithmIdentifier ES256 = new COSEAlgorithmIdentifier(-7);
public static final COSEAlgorithmIdentifier ES384 = new COSEAlgorithmIdentifier(-35);
public static final COSEAlgorithmIdentifier ES512 = new COSEAlgorithmIdentifier(-36);
public static final COSEAlgorithmIdentifier RS256 = new COSEAlgorithmIdentifier(-257);
public static final COSEAlgorithmIdentifier RS384 = new COSEAlgorithmIdentifier(-258);
public static final COSEAlgorithmIdentifier RS512 = new COSEAlgorithmIdentifier(-259);
public static final COSEAlgorithmIdentifier RS1 = new COSEAlgorithmIdentifier(-65535);
private final long value;
private COSEAlgorithmIdentifier(long value) {
this.value = value;
}
public long getValue() {
return this.value;
}
@Override
public String toString() {
return String.valueOf(this.value);
}
public static COSEAlgorithmIdentifier[] values() {
return new COSEAlgorithmIdentifier[] { EdDSA, ES256, ES384, ES512, RS256, RS384, RS512, RS1 };
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* Implements <a href=
* "https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension">
* Credential Protection (credProtect)</a>.
*
* @author Rob Winch
* @since 6.4
*/
public class CredProtectAuthenticationExtensionsClientInput
implements AuthenticationExtensionsClientInput<CredProtectAuthenticationExtensionsClientInput.CredProtect> {
private final CredProtect input;
public CredProtectAuthenticationExtensionsClientInput(CredProtect input) {
this.input = input;
}
@Override
public String getExtensionId() {
return "credProtect";
}
@Override
public CredProtect getInput() {
return this.input;
}
public static class CredProtect {
private final ProtectionPolicy credProtectionPolicy;
private final boolean enforceCredentialProtectionPolicy;
public CredProtect(ProtectionPolicy credProtectionPolicy, boolean enforceCredentialProtectionPolicy) {
this.enforceCredentialProtectionPolicy = enforceCredentialProtectionPolicy;
this.credProtectionPolicy = credProtectionPolicy;
}
public boolean isEnforceCredentialProtectionPolicy() {
return this.enforceCredentialProtectionPolicy;
}
public ProtectionPolicy getCredProtectionPolicy() {
return this.credProtectionPolicy;
}
public enum ProtectionPolicy {
USER_VERIFICATION_OPTIONAL, USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST, USER_VERIFICATION_REQUIRED
}
}
}
@@ -0,0 +1,83 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-credentialpropertiesoutput">CredentialPropertiesOutput</a>
* is the Client extension output.
*
* @author Rob Winch
* @since 6.4
*/
public class CredentialPropertiesOutput
implements AuthenticationExtensionsClientOutput<CredentialPropertiesOutput.ExtensionOutput> {
/**
* The extension id.
*/
public static final String EXTENSION_ID = "credProps";
private final ExtensionOutput output;
/**
* Creates a new instance.
* @param rk is the resident key is discoverable
*/
public CredentialPropertiesOutput(boolean rk) {
this.output = new ExtensionOutput(rk);
}
@Override
public String getExtensionId() {
return EXTENSION_ID;
}
@Override
public ExtensionOutput getOutput() {
return this.output;
}
/**
* The output for {@link CredentialPropertiesOutput}
*
* @author Rob Winch
* @since 6.4
* @see #getOutput()
*/
public static final class ExtensionOutput {
private final boolean rk;
private ExtensionOutput(boolean rk) {
this.rk = rk;
}
/**
* This OPTIONAL property, known abstractly as the resident key credential
* property (i.e., client-side discoverable credential property), is a Boolean
* value indicating whether the PublicKeyCredential returned as a result of a
* registration ceremony is a client-side discoverable credential.
* @return is resident key credential property
*/
public boolean isRk() {
return this.rk;
}
}
}
@@ -0,0 +1,136 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.time.Instant;
import java.util.Set;
/**
* Represents a <a href="https://www.w3.org/TR/webauthn-3/#credential-record">Credential
* Record</a> that is stored by the Relying Party
* <a href="https://www.w3.org/TR/webauthn-3/#reg-ceremony-store-credential-record">after
* successful registration</a>.
*
* @author Rob Winch
* @since 6.4
*/
public interface CredentialRecord {
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-type">credential.type</a>
* @return
*/
PublicKeyCredentialType getCredentialType();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-id">credential.id</a>.
* @return
*/
Bytes getCredentialId();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-publickey">publicKey</a>
* @return
*/
PublicKeyCose getPublicKey();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-signcount">authData.signCount</a>
* @return
*/
long getSignatureCount();
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-uvinitialized">uvInitialized</a>
* is the value of the UV (user verified) flag in authData and indicates whether any
* credential from this public key credential source has had the UV flag set.
* @return
*/
boolean isUvInitialized();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-transports">transpots</a>
* is the value returned from {@code response.getTransports()}.
* @return
*/
Set<AuthenticatorTransport> getTransports();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-backupeligible">backupElgible</a>
* flag is the same as the BE flag in authData.
* @return
*/
boolean isBackupEligible();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-backupstate">backupState</a>
* flag is the same as the BS flag in authData.
* @return
*/
boolean isBackupState();
/**
* A reference to the associated {@link PublicKeyCredentialUserEntity#getId()}
* @return
*/
Bytes getUserEntityUserId();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-attestationobject">attestationObject</a>
* is the value of the attestationObject attribute when the public key credential
* source was registered.
* @return the attestationObject
*/
Bytes getAttestationObject();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-attestationclientdatajson">attestationClientDataJSON</a>
* is the value of the attestationObject attribute when the public key credential
* source was registered.
* @return
*/
Bytes getAttestationClientDataJSON();
/**
* A human-readable label for this {@link CredentialRecord} assigned by the user.
* @return a label
*/
String getLabel();
/**
* The last time this {@link CredentialRecord} was used.
* @return the last time this {@link CredentialRecord} was used.
*/
Instant getLastUsed();
/**
* When this {@link CredentialRecord} was created.
* @return When this {@link CredentialRecord} was created.
*/
Instant getCreated();
}
@@ -0,0 +1,59 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* An immutable {@link AuthenticationExtensionsClientInput}.
*
* @param <T> the input type
* @author Rob Winch
* @since 6.4
* @see AuthenticationExtensionsClientInputs
*/
public class ImmutableAuthenticationExtensionsClientInput<T> implements AuthenticationExtensionsClientInput<T> {
/**
* https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
*/
public static final AuthenticationExtensionsClientInput<Boolean> credProps = new ImmutableAuthenticationExtensionsClientInput<>(
"credProps", true);
private final String extensionId;
private final T input;
/**
* Creates a new instance
* @param extensionId the extension id.
* @param input the input.
*/
public ImmutableAuthenticationExtensionsClientInput(String extensionId, T input) {
this.extensionId = extensionId;
this.input = input;
}
@Override
public String getExtensionId() {
return this.extensionId;
}
@Override
public T getInput() {
return this.input;
}
}
@@ -0,0 +1,45 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.util.Arrays;
import java.util.List;
/**
* An immutable implementation of {@link AuthenticationExtensionsClientInputs}.
*
* @author Rob Winch
* @since 6.4
*/
public class ImmutableAuthenticationExtensionsClientInputs implements AuthenticationExtensionsClientInputs {
private final List<AuthenticationExtensionsClientInput> inputs;
public ImmutableAuthenticationExtensionsClientInputs(List<AuthenticationExtensionsClientInput> inputs) {
this.inputs = inputs;
}
public ImmutableAuthenticationExtensionsClientInputs(AuthenticationExtensionsClientInput... inputs) {
this(Arrays.asList(inputs));
}
@Override
public List<AuthenticationExtensionsClientInput> getInputs() {
return this.inputs;
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.util.Arrays;
import java.util.List;
/**
* An immutable implementation of {@link AuthenticationExtensionsClientOutputs}.
*
* @author Rob Winch
*/
public class ImmutableAuthenticationExtensionsClientOutputs implements AuthenticationExtensionsClientOutputs {
private final List<AuthenticationExtensionsClientOutput<?>> outputs;
public ImmutableAuthenticationExtensionsClientOutputs(List<AuthenticationExtensionsClientOutput<?>> outputs) {
this.outputs = outputs;
}
public ImmutableAuthenticationExtensionsClientOutputs(AuthenticationExtensionsClientOutput<?>... outputs) {
this(Arrays.asList(outputs));
}
@Override
public List<AuthenticationExtensionsClientOutput<?>> getOutputs() {
return this.outputs;
}
}
@@ -0,0 +1,285 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.time.Instant;
import java.util.Set;
/**
* An immutable {@link CredentialRecord}.
*
* @author Rob Winch
* @since 6.4
*/
public final class ImmutableCredentialRecord implements CredentialRecord {
private final PublicKeyCredentialType credentialType;
private final Bytes credentialId;
private final Bytes userEntityUserId;
private final PublicKeyCose publicKey;
private final long signatureCount;
private final boolean uvInitialized;
private final Set<AuthenticatorTransport> transports;
private final boolean backupEligible;
private final boolean backupState;
private final Bytes attestationObject;
private final Bytes attestationClientDataJSON;
private final Instant created;
private final Instant lastUsed;
private final String label;
private ImmutableCredentialRecord(PublicKeyCredentialType credentialType, Bytes credentialId,
Bytes userEntityUserId, PublicKeyCose publicKey, long signatureCount, boolean uvInitialized,
Set<AuthenticatorTransport> transports, boolean backupEligible, boolean backupState,
Bytes attestationObject, Bytes attestationClientDataJSON, Instant created, Instant lastUsed, String label) {
this.credentialType = credentialType;
this.credentialId = credentialId;
this.userEntityUserId = userEntityUserId;
this.publicKey = publicKey;
this.signatureCount = signatureCount;
this.uvInitialized = uvInitialized;
this.transports = transports;
this.backupEligible = backupEligible;
this.backupState = backupState;
this.attestationObject = attestationObject;
this.attestationClientDataJSON = attestationClientDataJSON;
this.created = created;
this.lastUsed = lastUsed;
this.label = label;
}
@Override
public PublicKeyCredentialType getCredentialType() {
return this.credentialType;
}
@Override
public Bytes getCredentialId() {
return this.credentialId;
}
@Override
public Bytes getUserEntityUserId() {
return this.userEntityUserId;
}
@Override
public PublicKeyCose getPublicKey() {
return this.publicKey;
}
@Override
public long getSignatureCount() {
return this.signatureCount;
}
@Override
public boolean isUvInitialized() {
return this.uvInitialized;
}
@Override
public Set<AuthenticatorTransport> getTransports() {
return this.transports;
}
@Override
public boolean isBackupEligible() {
return this.backupEligible;
}
@Override
public boolean isBackupState() {
return this.backupState;
}
@Override
public Bytes getAttestationObject() {
return this.attestationObject;
}
@Override
public Bytes getAttestationClientDataJSON() {
return this.attestationClientDataJSON;
}
@Override
public Instant getCreated() {
return this.created;
}
@Override
public Instant getLastUsed() {
return this.lastUsed;
}
@Override
public String getLabel() {
return this.label;
}
public static ImmutableCredentialRecordBuilder builder() {
return new ImmutableCredentialRecordBuilder();
}
public static ImmutableCredentialRecordBuilder fromCredentialRecord(CredentialRecord credentialRecord) {
return new ImmutableCredentialRecordBuilder(credentialRecord);
}
public static final class ImmutableCredentialRecordBuilder {
private PublicKeyCredentialType credentialType;
private Bytes credentialId;
private Bytes userEntityUserId;
private PublicKeyCose publicKey;
private long signatureCount;
private boolean uvInitialized;
private Set<AuthenticatorTransport> transports;
private boolean backupEligible;
private boolean backupState;
private Bytes attestationObject;
private Bytes attestationClientDataJSON;
private Instant created = Instant.now();
private Instant lastUsed = this.created;
private String label;
private ImmutableCredentialRecordBuilder() {
}
private ImmutableCredentialRecordBuilder(CredentialRecord other) {
this.credentialType = other.getCredentialType();
this.credentialId = other.getCredentialId();
this.userEntityUserId = other.getUserEntityUserId();
this.publicKey = other.getPublicKey();
this.signatureCount = other.getSignatureCount();
this.uvInitialized = other.isUvInitialized();
this.transports = other.getTransports();
this.backupEligible = other.isBackupEligible();
this.backupState = other.isBackupState();
this.attestationObject = other.getAttestationObject();
this.attestationClientDataJSON = other.getAttestationClientDataJSON();
this.created = other.getCreated();
this.lastUsed = other.getLastUsed();
this.label = other.getLabel();
}
public ImmutableCredentialRecordBuilder credentialType(PublicKeyCredentialType credentialType) {
this.credentialType = credentialType;
return this;
}
public ImmutableCredentialRecordBuilder credentialId(Bytes credentialId) {
this.credentialId = credentialId;
return this;
}
public ImmutableCredentialRecordBuilder userEntityUserId(Bytes userEntityUserId) {
this.userEntityUserId = userEntityUserId;
return this;
}
public ImmutableCredentialRecordBuilder publicKey(PublicKeyCose publicKey) {
this.publicKey = publicKey;
return this;
}
public ImmutableCredentialRecordBuilder signatureCount(long signatureCount) {
this.signatureCount = signatureCount;
return this;
}
public ImmutableCredentialRecordBuilder uvInitialized(boolean uvInitialized) {
this.uvInitialized = uvInitialized;
return this;
}
public ImmutableCredentialRecordBuilder transports(Set<AuthenticatorTransport> transports) {
this.transports = transports;
return this;
}
public ImmutableCredentialRecordBuilder backupEligible(boolean backupEligible) {
this.backupEligible = backupEligible;
return this;
}
public ImmutableCredentialRecordBuilder backupState(boolean backupState) {
this.backupState = backupState;
return this;
}
public ImmutableCredentialRecordBuilder attestationObject(Bytes attestationObject) {
this.attestationObject = attestationObject;
return this;
}
public ImmutableCredentialRecordBuilder attestationClientDataJSON(Bytes attestationClientDataJSON) {
this.attestationClientDataJSON = attestationClientDataJSON;
return this;
}
public ImmutableCredentialRecordBuilder created(Instant created) {
this.created = created;
return this;
}
public ImmutableCredentialRecordBuilder lastUsed(Instant lastUsed) {
this.lastUsed = lastUsed;
return this;
}
public ImmutableCredentialRecordBuilder label(String label) {
this.label = label;
return this;
}
public ImmutableCredentialRecord build() {
return new ImmutableCredentialRecord(this.credentialType, this.credentialId, this.userEntityUserId,
this.publicKey, this.signatureCount, this.uvInitialized, this.transports, this.backupEligible,
this.backupState, this.attestationObject, this.attestationClientDataJSON, this.created,
this.lastUsed, this.label);
}
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.util.Arrays;
import java.util.Base64;
/**
* An immutable {@link PublicKeyCose}
*
* @author Rob Winch
* @since 6.4
*/
public class ImmutablePublicKeyCose implements PublicKeyCose {
private final byte[] bytes;
/**
* Creates a new instance.
* @param bytes the raw bytes of the public key.
*/
public ImmutablePublicKeyCose(byte[] bytes) {
this.bytes = Arrays.copyOf(bytes, bytes.length);
}
@Override
public byte[] getBytes() {
return Arrays.copyOf(this.bytes, this.bytes.length);
}
/**
* Creates a new instance form a Base64 URL encoded String
* @param base64EncodedString the base64EncodedString encoded String
* @return
*/
public static ImmutablePublicKeyCose fromBase64(String base64EncodedString) {
byte[] decode = Base64.getUrlDecoder().decode(base64EncodedString);
return new ImmutablePublicKeyCose(decode);
}
}
@@ -0,0 +1,188 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity">PublicKeyCredentialUserEntity</a>
* is used to supply additional
* <a href="https://www.w3.org/TR/webauthn-3/#user-account">user account</a> attributes
* when creating a new credential.
*
* @author Rob Winch
* @since 6.4
*/
public final class ImmutablePublicKeyCredentialUserEntity implements PublicKeyCredentialUserEntity {
/**
* When inherited by PublicKeyCredentialUserEntity, it is a human-palatable identifier
* for a user account. It is intended only for display, i.e., aiding the user in
* determining the difference between user accounts with similar displayNames. For
* example, "alexm", "alex.mueller@example.com" or "+14255551234".
*
* The Relying Party MAY let the user choose this value. The Relying Party SHOULD
* perform enforcement, as prescribed in Section 3.4.3 of [RFC8265] for the
* UsernameCasePreserved Profile of the PRECIS IdentifierClass [RFC8264], when setting
* name's value, or displaying the value to the user.
*
* This string MAY contain language and direction metadata. Relying Parties SHOULD
* consider providing this information. See § 6.4.2 Language and Direction Encoding
* about how this metadata is encoded.
*
* Clients SHOULD perform enforcement, as prescribed in Section 3.4.3 of [RFC8265] for
* the UsernameCasePreserved Profile of the PRECIS IdentifierClass [RFC8264], on
* name's value prior to displaying the value to the user or including the value as a
* parameter of the authenticatorMakeCredential operation.
*/
private final String name;
/**
* The user handle of the user account entity. A user handle is an opaque byte
* sequence with a maximum size of 64 bytes, and is not meant to be displayed to the
* user.
*
* To ensure secure operation, authentication and authorization decisions MUST be made
* on the basis of this id member, not the displayName nor name members. See Section
* 6.1 of [RFC8266].
*
* The user handle MUST NOT contain personally identifying information about the user,
* such as a username or e-mail address; see § 14.6.1 User Handle Contents for
* details. The user handle MUST NOT be empty, though it MAY be null.
*
* Note: the user handle ought not be a constant value across different accounts, even
* for non-discoverable credentials, because some authenticators always create
* discoverable credentials. Thus a constant user handle would prevent a user from
* using such an authenticator with more than one account at the Relying Party.
*/
private final Bytes id;
/**
* A human-palatable name for the user account, intended only for display. For
* example, "Alex Müller" or "田中倫". The Relying Party SHOULD let the user choose this,
* and SHOULD NOT restrict the choice more than necessary.
*
* Relying Parties SHOULD perform enforcement, as prescribed in Section 2.3 of
* [RFC8266] for the Nickname Profile of the PRECIS FreeformClass [RFC8264], when
* setting displayName's value, or displaying the value to the user.
*
* This string MAY contain language and direction metadata. Relying Parties SHOULD
* consider providing this information. See § 6.4.2 Language and Direction Encoding
* about how this metadata is encoded.
*
* Clients SHOULD perform enforcement, as prescribed in Section 2.3 of [RFC8266] for
* the Nickname Profile of the PRECIS FreeformClass [RFC8264], on displayName's value
* prior to displaying the value to the user or including the value as a parameter of
* the authenticatorMakeCredential operation.
*
* When clients, client platforms, or authenticators display a displayName's value,
* they should always use UI elements to provide a clear boundary around the displayed
* value, and not allow overflow into other elements [css-overflow-3].
*
* Authenticators MUST accept and store a 64-byte minimum length for a displayName
* members value. Authenticators MAY truncate a displayName members value so that it
* fits within 64 bytes. See § 6.4.1 String Truncation about truncation and other
* considerations.
*/
private final String displayName;
private ImmutablePublicKeyCredentialUserEntity(String name, Bytes id, String displayName) {
this.name = name;
this.id = id;
this.displayName = displayName;
}
@Override
public String getName() {
return this.name;
}
@Override
public Bytes getId() {
return this.id;
}
@Override
public String getDisplayName() {
return this.displayName;
}
/**
* Create a new {@link PublicKeyCredentialUserEntityBuilder}
* @return a new {@link PublicKeyCredentialUserEntityBuilder}
*/
public static PublicKeyCredentialUserEntityBuilder builder() {
return new PublicKeyCredentialUserEntityBuilder();
}
/**
* Used to build {@link PublicKeyCredentialUserEntity}.
*
* @author Rob Winch
* @since 6.4
*/
public static final class PublicKeyCredentialUserEntityBuilder {
private String name;
private Bytes id;
private String displayName;
private PublicKeyCredentialUserEntityBuilder() {
}
/**
* Sets the {@link #getName()} property.
* @param name the name
* @return the {@link PublicKeyCredentialUserEntityBuilder}
*/
public PublicKeyCredentialUserEntityBuilder name(String name) {
this.name = name;
return this;
}
/**
* Sets the {@link #getId()} property.
* @param id the id
* @return the {@link PublicKeyCredentialUserEntityBuilder}
*/
public PublicKeyCredentialUserEntityBuilder id(Bytes id) {
this.id = id;
return this;
}
/**
* Sets the {@link #getDisplayName()} property.
* @param displayName the display name
* @return the {@link PublicKeyCredentialUserEntityBuilder}
*/
public PublicKeyCredentialUserEntityBuilder displayName(String displayName) {
this.displayName = displayName;
return this;
}
/**
* Builds a new {@link PublicKeyCredentialUserEntity}
* @return a new {@link PublicKeyCredentialUserEntity}
*/
public PublicKeyCredentialUserEntity build() {
return new ImmutablePublicKeyCredentialUserEntity(this.name, this.id, this.displayName);
}
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* A <a href="https://www.w3.org/TR/webauthn-3/#sctn-encoded-credPubKey-examples">COSE
* encoded public key</a>.
*
* @author Rob Winch
* @since 6.4
*/
public interface PublicKeyCose {
/**
* The byes of a COSE encoded public key.
* @return the bytes of a COSE encoded public key.
*/
byte[] getBytes();
}
@@ -0,0 +1,223 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* <a href="https://www.w3.org/TR/webauthn-3/#iface-pkcredential">PublicKeyCredential</a>
* contains the attributes that are returned to the caller when a new credential is
* created, or a new assertion is requested.
*
* @author Rob Winch
* @since 6.4
*/
public final class PublicKeyCredential<R extends AuthenticatorResponse> {
private final String id;
private final PublicKeyCredentialType type;
private final Bytes rawId;
private final R response;
private final AuthenticatorAttachment authenticatorAttachment;
private final AuthenticationExtensionsClientOutputs clientExtensionResults;
private PublicKeyCredential(String id, PublicKeyCredentialType type, Bytes rawId, R response,
AuthenticatorAttachment authenticatorAttachment,
AuthenticationExtensionsClientOutputs clientExtensionResults) {
this.id = id;
this.type = type;
this.rawId = rawId;
this.response = response;
this.authenticatorAttachment = authenticatorAttachment;
this.clientExtensionResults = clientExtensionResults;
}
/**
* The
* <a href="https://www.w3.org/TR/credential-management-1/#dom-credential-id">id</a>
* attribute is inherited from Credential, though PublicKeyCredential overrides
* Credential's getter, instead returning the base64url encoding of the data contained
* in the objects [[identifier]] internal slot.
*/
public String getId() {
return this.id;
}
/**
* The <a href=
* "https://www.w3.org/TR/credential-management-1/#dom-credential-type">type</a>
* attribute returns the value of the objects interface object's [[type]] slot, which
* specifies the credential type represented by this object.
* @return the credential type
*/
public PublicKeyCredentialType getType() {
return this.type;
}
/**
* The
* <a href="https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid">rawId</a>
* returns the raw identifier.
* @return the raw id
*/
public Bytes getRawId() {
return this.rawId;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-response">response</a>
* to the client's request to either create a public key credential, or generate an
* authentication assertion.
* @return the response
*/
public R getResponse() {
return this.response;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-authenticatorattachment">authenticatorAttachment</a>
* reports the <a href=
* "https://www.w3.org/TR/webauthn-3/#authenticator-attachment-modality">authenticator
* attachment modality</a> in effect at the time the navigator.credentials.create() or
* navigator.credentials.get() methods successfully complete.
* @return the authenticator attachment
*/
public AuthenticatorAttachment getAuthenticatorAttachment() {
return this.authenticatorAttachment;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-getclientextensionresults">clientExtensionsResults</a>
* is a mapping of extension identifier to client extension output.
* @return the extension results
*/
public AuthenticationExtensionsClientOutputs getClientExtensionResults() {
return this.clientExtensionResults;
}
/**
* Creates a new {@link PublicKeyCredentialBuilder}
* @param <T> the response type
* @return the {@link PublicKeyCredentialBuilder}
*/
public static <T extends AuthenticatorResponse> PublicKeyCredentialBuilder<T> builder() {
return new PublicKeyCredentialBuilder<T>();
}
/**
* The {@link PublicKeyCredentialBuilder}
*
* @param <R> the response type
* @author Rob Winch
* @since 6.4
*/
public static final class PublicKeyCredentialBuilder<R extends AuthenticatorResponse> {
private String id;
private PublicKeyCredentialType type;
private Bytes rawId;
private R response;
private AuthenticatorAttachment authenticatorAttachment;
private AuthenticationExtensionsClientOutputs clientExtensionResults;
private PublicKeyCredentialBuilder() {
}
/**
* Sets the {@link #getId()} property
* @param id the id
* @return the PublicKeyCredentialBuilder
*/
public PublicKeyCredentialBuilder id(String id) {
this.id = id;
return this;
}
/**
* Sets the {@link #getType()} property.
* @param type the type
* @return the PublicKeyCredentialBuilder
*/
public PublicKeyCredentialBuilder type(PublicKeyCredentialType type) {
this.type = type;
return this;
}
/**
* Sets the {@link #getRawId()} property.
* @param rawId the raw id
* @return the PublicKeyCredentialBuilder
*/
public PublicKeyCredentialBuilder rawId(Bytes rawId) {
this.rawId = rawId;
return this;
}
/**
* Sets the {@link #getResponse()} property.
* @param response the response
* @return the PublicKeyCredentialBuilder
*/
public PublicKeyCredentialBuilder response(R response) {
this.response = response;
return this;
}
/**
* Sets the {@link #getAuthenticatorAttachment()} property.
* @param authenticatorAttachment the authenticator attachement
* @return the PublicKeyCredentialBuilder
*/
public PublicKeyCredentialBuilder authenticatorAttachment(AuthenticatorAttachment authenticatorAttachment) {
this.authenticatorAttachment = authenticatorAttachment;
return this;
}
/**
* Sets the {@link #getClientExtensionResults()} property.
* @param clientExtensionResults the client extension results
* @return the PublicKeyCredentialBuilder
*/
public PublicKeyCredentialBuilder clientExtensionResults(
AuthenticationExtensionsClientOutputs clientExtensionResults) {
this.clientExtensionResults = clientExtensionResults;
return this;
}
/**
* Creates a new {@link PublicKeyCredential}
* @return a new {@link PublicKeyCredential}
*/
public PublicKeyCredential<R> build() {
return new PublicKeyCredential(this.id, this.type, this.rawId, this.response, this.authenticatorAttachment,
this.clientExtensionResults);
}
}
}
@@ -0,0 +1,332 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
/**
* Represents the <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions">PublicKeyCredentialCreationOptions</a>
* which is an argument to <a href=
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">creating</a>
* a new credential.
*
* @author Rob Winch
* @since 6.4
*/
public final class PublicKeyCredentialCreationOptions {
private final PublicKeyCredentialRpEntity rp;
private final PublicKeyCredentialUserEntity user;
private final Bytes challenge;
private final List<PublicKeyCredentialParameters> pubKeyCredParams;
private final Duration timeout;
private final List<PublicKeyCredentialDescriptor> excludeCredentials;
private final AuthenticatorSelectionCriteria authenticatorSelection;
private final AttestationConveyancePreference attestation;
private final AuthenticationExtensionsClientInputs extensions;
private PublicKeyCredentialCreationOptions(PublicKeyCredentialRpEntity rp, PublicKeyCredentialUserEntity user,
Bytes challenge, List<PublicKeyCredentialParameters> pubKeyCredParams, Duration timeout,
List<PublicKeyCredentialDescriptor> excludeCredentials,
AuthenticatorSelectionCriteria authenticatorSelection, AttestationConveyancePreference attestation,
AuthenticationExtensionsClientInputs extensions) {
this.rp = rp;
this.user = user;
this.challenge = challenge;
this.pubKeyCredParams = pubKeyCredParams;
this.timeout = timeout;
this.excludeCredentials = excludeCredentials;
this.authenticatorSelection = authenticatorSelection;
this.attestation = attestation;
this.extensions = extensions;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-rp">rp</a>
* property contains data about the Relying Party responsible for the request.
* @return the relying party
*/
public PublicKeyCredentialRpEntity getRp() {
return this.rp;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-user">user</a>
* contains names and an identifier for the user account performing the registration.
* @return the user
*/
public PublicKeyCredentialUserEntity getUser() {
return this.user;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-challenge">challenge</a>
* specifies the challenge that the authenticator signs, along with other data, when
* producing an attestation object for the newly created credential.
* @return the challenge
*/
public Bytes getChallenge() {
return this.challenge;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-pubkeycredparams">publicKeyCredParams</a>
* params lisst the key types and signature algorithms the Relying Party Supports,
* ordered from most preferred to least preferred.
* @return the public key credential parameters
*/
public List<PublicKeyCredentialParameters> getPubKeyCredParams() {
return this.pubKeyCredParams;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-timeout">timeout</a>
* property specifies a time, in milliseconds, that the Relying Party is willing to
* wait for the call to complete.
* @return the timeout
*/
public Duration getTimeout() {
return this.timeout;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-excludecredentials">excludeCredentials</a>
* property is the OPTIONAL member used by the Relying Party to list any existing
* credentials mapped to this user account (as identified by user.id).
* @return exclude credentials
*/
public List<PublicKeyCredentialDescriptor> getExcludeCredentials() {
return this.excludeCredentials;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-authenticatorselection">authenticatorSelection</a>
* property is an OPTIONAL member used by the Relying Party to list any existing
* credentials mapped to this user account (as identified by user.id).
* @return the authenticatorSelection
*/
public AuthenticatorSelectionCriteria getAuthenticatorSelection() {
return this.authenticatorSelection;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-attestation">attestation</a>
* property is an OPTIONAL member used by the Relying Party to specify a preference
* regarding attestation conveyance.
* @return the attestation preference
*/
public AttestationConveyancePreference getAttestation() {
return this.attestation;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions">extensions</a>
* property is an OPTIONAL member used by the Relying Party to provide client
* extension inputs requesting additional processing by the client and authenticator.
* @return the extensions
*/
public AuthenticationExtensionsClientInputs getExtensions() {
return this.extensions;
}
/**
* Creates a new {@link PublicKeyCredentialCreationOptions}
* @return a new {@link PublicKeyCredentialCreationOptions}
*/
public static PublicKeyCredentialCreationOptionsBuilder builder() {
return new PublicKeyCredentialCreationOptionsBuilder();
}
/**
* Used to build {@link PublicKeyCredentialCreationOptions}.
*
* @author Rob Winch
* @since 6.4
*/
public static final class PublicKeyCredentialCreationOptionsBuilder {
private PublicKeyCredentialRpEntity rp;
private PublicKeyCredentialUserEntity user;
private Bytes challenge;
private List<PublicKeyCredentialParameters> pubKeyCredParams = new ArrayList<>();
private Duration timeout;
private List<PublicKeyCredentialDescriptor> excludeCredentials = new ArrayList<>();
private AuthenticatorSelectionCriteria authenticatorSelection;
private AttestationConveyancePreference attestation;
private AuthenticationExtensionsClientInputs extensions;
private PublicKeyCredentialCreationOptionsBuilder() {
}
/**
* Sets the {@link #getRp()} property.
* @param rp the relying party
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder rp(PublicKeyCredentialRpEntity rp) {
this.rp = rp;
return this;
}
/**
* Sets the {@link #getUser()} property.
* @param user the user entity
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder user(PublicKeyCredentialUserEntity user) {
this.user = user;
return this;
}
/**
* Sets the {@link #getChallenge()} property.
* @param challenge the challenge
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder challenge(Bytes challenge) {
this.challenge = challenge;
return this;
}
/**
* Sets the {@link #getPubKeyCredParams()} property.
* @param pubKeyCredParams the public key credential parameters
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder pubKeyCredParams(
PublicKeyCredentialParameters... pubKeyCredParams) {
return pubKeyCredParams(Arrays.asList(pubKeyCredParams));
}
/**
* Sets the {@link #getPubKeyCredParams()} property.
* @param pubKeyCredParams the public key credential parameters
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder pubKeyCredParams(
List<PublicKeyCredentialParameters> pubKeyCredParams) {
this.pubKeyCredParams = pubKeyCredParams;
return this;
}
/**
* Sets the {@link #getTimeout()} property.
* @param timeout the timeout
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder timeout(Duration timeout) {
this.timeout = timeout;
return this;
}
/**
* Sets the {@link #getExcludeCredentials()} property.
* @param excludeCredentials the excluded credentials.
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder excludeCredentials(
List<PublicKeyCredentialDescriptor> excludeCredentials) {
this.excludeCredentials = excludeCredentials;
return this;
}
/**
* Sets the {@link #getAuthenticatorSelection()} property.
* @param authenticatorSelection the authenticator selection
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder authenticatorSelection(
AuthenticatorSelectionCriteria authenticatorSelection) {
this.authenticatorSelection = authenticatorSelection;
return this;
}
/**
* Sets the {@link #getAttestation()} property.
* @param attestation the attestation
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder attestation(AttestationConveyancePreference attestation) {
this.attestation = attestation;
return this;
}
/**
* Sets the {@link #getExtensions()} property.
* @param extensions the extensions
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder extensions(AuthenticationExtensionsClientInputs extensions) {
this.extensions = extensions;
return this;
}
/**
* Allows customizing the builder using the {@link Consumer} that is passed in.
* @param customizer the {@link Consumer} that can be used to customize the
* {@link PublicKeyCredentialCreationOptionsBuilder}
* @return the PublicKeyCredentialCreationOptionsBuilder
*/
public PublicKeyCredentialCreationOptionsBuilder customize(
Consumer<PublicKeyCredentialCreationOptionsBuilder> customizer) {
customizer.accept(this);
return this;
}
/**
* Builds a new {@link PublicKeyCredentialCreationOptions}
* @return the new {@link PublicKeyCredentialCreationOptions}
*/
public PublicKeyCredentialCreationOptions build() {
return new PublicKeyCredentialCreationOptions(this.rp, this.user, this.challenge, this.pubKeyCredParams,
this.timeout, this.excludeCredentials, this.authenticatorSelection, this.attestation,
this.extensions);
}
}
}
@@ -0,0 +1,154 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.util.Set;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptor">PublicKeyCredentialDescriptor</a>
* identifies a specific public key credential. It is used in create() to prevent creating
* duplicate credentials on the same authenticator, and in get() to determine if and how
* the credential can currently be reached by the client. It mirrors some fields of the
* PublicKeyCredential object returned by create() and get().
*
* @author Rob Winch
* @since 6.4
*/
public final class PublicKeyCredentialDescriptor {
private final PublicKeyCredentialType type;
private final Bytes id;
private final Set<AuthenticatorTransport> transports;
private PublicKeyCredentialDescriptor(PublicKeyCredentialType type, Bytes id,
Set<AuthenticatorTransport> transports) {
this.type = type;
this.id = id;
this.transports = transports;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-type">type</a>
* property contains the type of the public key credential the caller is referring to.
* @return the type
*/
public PublicKeyCredentialType getType() {
return this.type;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-id">id</a>
* property contains the credential ID of the public key credential the caller is
* referring to.
* @return the id
*/
public Bytes getId() {
return this.id;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialdescriptor-transports">transports</a>
* property is an OPTIONAL member that contains a hint as to how the client might
* communicate with the managing authenticator of the public key credential the caller
* is referring to.
* @return the transports
*/
public Set<AuthenticatorTransport> getTransports() {
return this.transports;
}
/**
* Creates a new {@link PublicKeyCredentialDescriptorBuilder}
* @return a new {@link PublicKeyCredentialDescriptorBuilder}
*/
public static PublicKeyCredentialDescriptorBuilder builder() {
return new PublicKeyCredentialDescriptorBuilder();
}
/**
* Used to create {@link PublicKeyCredentialDescriptor}
*
* @author Rob Winch
* @since 6.4
*/
public static final class PublicKeyCredentialDescriptorBuilder {
private PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY;
private Bytes id;
private Set<AuthenticatorTransport> transports;
private PublicKeyCredentialDescriptorBuilder() {
}
/**
* Sets the {@link #getType()} property.
* @param type the type
* @return the {@link PublicKeyCredentialDescriptorBuilder}
*/
public PublicKeyCredentialDescriptorBuilder type(PublicKeyCredentialType type) {
this.type = type;
return this;
}
/**
* Sets the {@link #getId()} property.
* @param id the id
* @return the {@link PublicKeyCredentialDescriptorBuilder}
*/
public PublicKeyCredentialDescriptorBuilder id(Bytes id) {
this.id = id;
return this;
}
/**
* Sets the {@link #getTransports()} property.
* @param transports the transports
* @return the {@link PublicKeyCredentialDescriptorBuilder}
*/
public PublicKeyCredentialDescriptorBuilder transports(Set<AuthenticatorTransport> transports) {
this.transports = transports;
return this;
}
/**
* Sets the {@link #getTransports()} property.
* @param transports the transports
* @return the {@link PublicKeyCredentialDescriptorBuilder}
*/
public PublicKeyCredentialDescriptorBuilder transports(AuthenticatorTransport... transports) {
return transports(Set.of(transports));
}
/**
* Create a new {@link PublicKeyCredentialDescriptor}
* @return a new {@link PublicKeyCredentialDescriptor}
*/
public PublicKeyCredentialDescriptor build() {
return new PublicKeyCredentialDescriptor(this.type, this.id, this.transports);
}
}
}
@@ -0,0 +1,99 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialparameters">PublicKeyCredentialParameters</a>
* is used to supply additional parameters when creating a new credential.
*
* @author Rob Winch
* @since 6.4
* @see PublicKeyCredentialCreationOptions#getPubKeyCredParams()
*/
public final class PublicKeyCredentialParameters {
public static final PublicKeyCredentialParameters EdDSA = new PublicKeyCredentialParameters(
COSEAlgorithmIdentifier.EdDSA);
public static final PublicKeyCredentialParameters ES256 = new PublicKeyCredentialParameters(
COSEAlgorithmIdentifier.ES256);
public static final PublicKeyCredentialParameters ES384 = new PublicKeyCredentialParameters(
COSEAlgorithmIdentifier.ES384);
public static final PublicKeyCredentialParameters ES512 = new PublicKeyCredentialParameters(
COSEAlgorithmIdentifier.ES512);
public static final PublicKeyCredentialParameters RS256 = new PublicKeyCredentialParameters(
COSEAlgorithmIdentifier.RS256);
public static final PublicKeyCredentialParameters RS384 = new PublicKeyCredentialParameters(
COSEAlgorithmIdentifier.RS384);
public static final PublicKeyCredentialParameters RS512 = new PublicKeyCredentialParameters(
COSEAlgorithmIdentifier.RS512);
public static final PublicKeyCredentialParameters RS1 = new PublicKeyCredentialParameters(
COSEAlgorithmIdentifier.RS1);
/**
* This member specifies the type of credential to be created. The value SHOULD be a
* member of PublicKeyCredentialType but client platforms MUST ignore unknown values,
* ignoring any PublicKeyCredentialParameters with an unknown type.
*/
private final PublicKeyCredentialType type;
/**
* This member specifies the cryptographic signature algorithm with which the newly
* generated credential will be used, and thus also the type of asymmetric key pair to
* be generated, e.g., RSA or Elliptic Curve.
*/
private final COSEAlgorithmIdentifier alg;
private PublicKeyCredentialParameters(COSEAlgorithmIdentifier alg) {
this(PublicKeyCredentialType.PUBLIC_KEY, alg);
}
private PublicKeyCredentialParameters(PublicKeyCredentialType type, COSEAlgorithmIdentifier alg) {
this.type = type;
this.alg = alg;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialparameters-type">type</a>
* property member specifies the type of credential to be created.
* @return the type
*/
public PublicKeyCredentialType getType() {
return this.type;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialparameters-alg">alg</a>
* member specifies the cryptographic signature algorithm with which the newly
* generated credential will be used, and thus also the type of asymmetric key pair to
* be generated, e.g., RSA or Elliptic Curve.
* @return the algorithm
*/
public COSEAlgorithmIdentifier getAlg() {
return this.alg;
}
}
@@ -0,0 +1,248 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.util.Assert;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions">PublicKeyCredentialRequestOptions</a>
* contains the information to create an assertion used for authentication.
*
* @author Rob Winch
* @since 6.4
*/
public final class PublicKeyCredentialRequestOptions {
private final Bytes challenge;
private final Duration timeout;
private final String rpId;
private final List<PublicKeyCredentialDescriptor> allowCredentials;
private final UserVerificationRequirement userVerification;
private final AuthenticationExtensionsClientInputs extensions;
private PublicKeyCredentialRequestOptions(Bytes challenge, Duration timeout, String rpId,
List<PublicKeyCredentialDescriptor> allowCredentials, UserVerificationRequirement userVerification,
AuthenticationExtensionsClientInputs extensions) {
Assert.notNull(challenge, "challenge cannot be null");
Assert.hasText(rpId, "rpId cannot be empty");
this.challenge = challenge;
this.timeout = timeout;
this.rpId = rpId;
this.allowCredentials = allowCredentials;
this.userVerification = userVerification;
this.extensions = extensions;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-challenge">challenge</a>
* property specifies a challenge that the authenticator signs, along with other data,
* when producing an authentication assertion.
* @return the challenge
*/
public Bytes getChallenge() {
return this.challenge;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-timeout">timeout</a>
* property is an OPTIONAL member specifies a time, in milliseconds, that the Relying
* Party is willing to wait for the call to complete.
* @return the timeout
*/
public Duration getTimeout() {
return this.timeout;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-rpid">rpId</a>
* is an OPTIONAL member specifies the RP ID claimed by the Relying Party. The client
* MUST verify that the Relying Party's origin matches the scope of this RP ID.
* @return the relying party id
*/
public String getRpId() {
return this.rpId;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-allowcredentials">allowCredentials</a>
* property is an OPTIONAL member is used by the client to find authenticators
* eligible for this authentication ceremony.
* @return the allowCredentials property
*/
public List<PublicKeyCredentialDescriptor> getAllowCredentials() {
return this.allowCredentials;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-userverification">userVerification</a>
* property is an OPTIONAL member specifies the Relying Party's requirements regarding
* user verification for the get() operation.
* @return the user verification
*/
public UserVerificationRequirement getUserVerification() {
return this.userVerification;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-extensions">extensions</a>
* is an OPTIONAL property used by the Relying Party to provide client extension
* inputs requesting additional processing by the client and authenticator.
* @return the extensions
*/
public AuthenticationExtensionsClientInputs getExtensions() {
return this.extensions;
}
/**
* Creates a {@link PublicKeyCredentialRequestOptionsBuilder}
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
*/
public static PublicKeyCredentialRequestOptionsBuilder builder() {
return new PublicKeyCredentialRequestOptionsBuilder();
}
/**
* Used to build a {@link PublicKeyCredentialCreationOptions}.
*
* @author Rob Winch
* @since 6.4
*/
public static final class PublicKeyCredentialRequestOptionsBuilder {
private Bytes challenge;
private Duration timeout = Duration.ofMinutes(5);
private String rpId;
private List<PublicKeyCredentialDescriptor> allowCredentials = Collections.emptyList();
private UserVerificationRequirement userVerification;
private AuthenticationExtensionsClientInputs extensions = new ImmutableAuthenticationExtensionsClientInputs(
new ArrayList<>());
private PublicKeyCredentialRequestOptionsBuilder() {
}
/**
* Sets the {@link #getChallenge()} property.
* @param challenge the challenge
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
*/
public PublicKeyCredentialRequestOptionsBuilder challenge(Bytes challenge) {
this.challenge = challenge;
return this;
}
/**
* Sets the {@link #getTimeout()} property.
* @param timeout the timeout
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
*/
public PublicKeyCredentialRequestOptionsBuilder timeout(Duration timeout) {
Assert.notNull(timeout, "timeout cannot be null");
this.timeout = timeout;
return this;
}
/**
* Sets the {@link #getRpId()} property.
* @param rpId the rpId property
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
*/
public PublicKeyCredentialRequestOptionsBuilder rpId(String rpId) {
this.rpId = rpId;
return this;
}
/**
* Sets the {@link #getAllowCredentials()} property
* @param allowCredentials the allowed credentials
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
*/
public PublicKeyCredentialRequestOptionsBuilder allowCredentials(
List<PublicKeyCredentialDescriptor> allowCredentials) {
Assert.notNull(allowCredentials, "allowCredentials cannot be null");
this.allowCredentials = allowCredentials;
return this;
}
/**
* Sets the {@link #getUserVerification()} property.
* @param userVerification the user verification
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
*/
public PublicKeyCredentialRequestOptionsBuilder userVerification(UserVerificationRequirement userVerification) {
this.userVerification = userVerification;
return this;
}
/**
* Sets the {@link #getExtensions()} property
* @param extensions the extensions
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
*/
public PublicKeyCredentialRequestOptionsBuilder extensions(AuthenticationExtensionsClientInputs extensions) {
this.extensions = extensions;
return this;
}
/**
* Allows customizing the {@link PublicKeyCredentialRequestOptionsBuilder}
* @param customizer the {@link Consumer} used to customize the builder
* @return the {@link PublicKeyCredentialRequestOptionsBuilder}
*/
public PublicKeyCredentialRequestOptionsBuilder customize(
Consumer<PublicKeyCredentialRequestOptionsBuilder> customizer) {
customizer.accept(this);
return this;
}
/**
* Builds a new {@link PublicKeyCredentialRequestOptions}
* @return a new {@link PublicKeyCredentialRequestOptions}
*/
public PublicKeyCredentialRequestOptions build() {
if (this.challenge == null) {
this.challenge = Bytes.random();
}
return new PublicKeyCredentialRequestOptions(this.challenge, this.timeout, this.rpId, this.allowCredentials,
this.userVerification, this.extensions);
}
}
}
@@ -0,0 +1,115 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrpentity">PublicKeyCredentialRpEntity</a>
* dictionary is used to supply additional Relying Party attributes when creating a new
* credential.
*
* @author Rob Winch
* @since 6.4
*/
public final class PublicKeyCredentialRpEntity {
private final String name;
private final String id;
private PublicKeyCredentialRpEntity(String name, String id) {
this.name = name;
this.id = id;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name">name</a>
* property is a human-palatable name for the entity. Its function depends on what the
* PublicKeyCredentialEntity represents for the Relying Party, intended only for
* display.
* @return the name
*/
public String getName() {
return this.name;
}
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrpentity-id">id</a>
* property is a unique identifier for the Relying Party entity, which sets the
* <a href="https://www.w3.org/TR/webauthn-3/#rp-id">RP ID</a>.
* @return the relying party id
*/
public String getId() {
return this.id;
}
/**
* Creates a new {@link PublicKeyCredentialRpEntityBuilder}
* @return a new {@link PublicKeyCredentialRpEntityBuilder}
*/
public static PublicKeyCredentialRpEntityBuilder builder() {
return new PublicKeyCredentialRpEntityBuilder();
}
/**
* Used to create a {@link PublicKeyCredentialRpEntity}.
*
* @author Rob Winch
* @since 6.4
*/
public static final class PublicKeyCredentialRpEntityBuilder {
private String name;
private String id;
private PublicKeyCredentialRpEntityBuilder() {
}
/**
* Sets the {@link #getName()} property.
* @param name the name property
* @return the {@link PublicKeyCredentialRpEntityBuilder}
*/
public PublicKeyCredentialRpEntityBuilder name(String name) {
this.name = name;
return this;
}
/**
* Sets the {@link #getId()} property.
* @param id the id
* @return the {@link PublicKeyCredentialRpEntityBuilder}
*/
public PublicKeyCredentialRpEntityBuilder id(String id) {
this.id = id;
return this;
}
/**
* Creates a new {@link PublicKeyCredentialRpEntity}.
* @return a new {@link PublicKeyCredentialRpEntity}.
*/
public PublicKeyCredentialRpEntity build() {
return new PublicKeyCredentialRpEntity(this.name, this.id);
}
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#enum-credentialType">PublicKeyCredentialType</a>
* defines the credential types.
*
* @author Rob Winch
* @since 6.4
*/
public final class PublicKeyCredentialType {
/**
* The only credential type that currently exists.
*/
public static final PublicKeyCredentialType PUBLIC_KEY = new PublicKeyCredentialType("public-key");
private final String value;
private PublicKeyCredentialType(String value) {
this.value = value;
}
/**
* Gets the value.
* @return the value
*/
public String getValue() {
return this.value;
}
public static PublicKeyCredentialType valueOf(String value) {
if (PUBLIC_KEY.getValue().equals(value)) {
return PUBLIC_KEY;
}
return new PublicKeyCredentialType(value);
}
}
@@ -0,0 +1,60 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity">PublicKeyCredentialUserEntity</a>
* is used to supply additional
* <a href="https://www.w3.org/TR/webauthn-3/#user-account">user account</a> attributes
* when creating a new credential.
*
* @author Rob Winch
* @since 6.4
* @see WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest)
*/
public interface PublicKeyCredentialUserEntity {
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialentity-name">name</a>
* property is a human-palatable identifier for a user account.
* @return the name
*/
String getName();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-id">id</a> is
* the user handle of the user account. A user handle is an opaque byte sequence with
* a maximum size of 64 bytes, and is not meant to be displayed to the user.
* @return the user handle of the user account
*/
Bytes getId();
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialuserentity-displayname">displayName</a>
* is a human-palatable name for the user account, intended only for display.
* @return the display name
*/
String getDisplayName();
}
@@ -0,0 +1,80 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#enumdef-residentkeyrequirement">ResidentKeyRequirement</a>
* describes the Relying Partys requirements for client-side discoverable credentials.
*
* @author Rob Winch
* @since 6.4
*/
public final class ResidentKeyRequirement {
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-discouraged">discouraged</a>
* requirement indicates that the Relying Party prefers creating a server-side
* credential, but will accept a client-side discoverable credential.
*/
public static final ResidentKeyRequirement DISCOURAGED = new ResidentKeyRequirement("discouraged");
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-preferred">preferred</a>
* requirement indicates that the Relying Party strongly prefers creating a
* client-side discoverable credential, but will accept a server-side credential.
*/
public static final ResidentKeyRequirement PREFERRED = new ResidentKeyRequirement("preferred");
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-residentkeyrequirement-required">required</a>
* value indicates that the Relying Party requires a client-side discoverable
* credential.
*/
public static final ResidentKeyRequirement REQUIRED = new ResidentKeyRequirement("required");
private final String value;
private ResidentKeyRequirement(String value) {
this.value = value;
}
/**
* Gets the value.
* @return the value
*/
public String getValue() {
return this.value;
}
public static ResidentKeyRequirement valueOf(String value) {
if (DISCOURAGED.getValue().equals(value)) {
return DISCOURAGED;
}
if (PREFERRED.getValue().equals(value)) {
return PREFERRED;
}
if (REQUIRED.getValue().equals(value)) {
return REQUIRED;
}
return new ResidentKeyRequirement(value);
}
}
@@ -0,0 +1,69 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.api;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement">UserVerificationRequirement</a>
* is used by the Relying Party to indicate if user verification is needed.
*
* @author Rob Winch
* @since 6.4
*/
public final class UserVerificationRequirement {
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-discouraged">discouraged</a>
* value indicates that the Relying Party does not want user verification employed
* during the operation (e.g., in the interest of minimizing disruption to the user
* interaction flow).
*/
public static final UserVerificationRequirement DISCOURAGED = new UserVerificationRequirement("discouraged");
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-preferred">preferred</a>
* value indicates that the Relying Party prefers user verification for the operation
* if possible, but will not fail the operation if the response does not have the UV
* flag set.
*/
public static final UserVerificationRequirement PREFERRED = new UserVerificationRequirement("preferred");
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dom-userverificationrequirement-required">required</a>
* value indicates that the Relying Party requires user verification for the operation
* and will fail the overall ceremony if the response does not have the UV flag set.
*/
public static final UserVerificationRequirement REQUIRED = new UserVerificationRequirement("required");
private final String value;
UserVerificationRequirement(String value) {
this.value = value;
}
/**
* Gets the value
* @return the value
*/
public String getValue() {
return this.value;
}
}
@@ -0,0 +1,63 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.util.Assert;
/**
* A {@link PublicKeyCredentialRequestOptionsRepository} that stores the
* {@link PublicKeyCredentialRequestOptions} in the
* {@link jakarta.servlet.http.HttpSession}.
*
* @author Rob Winch
* @since 6.4
*/
public class HttpSessionPublicKeyCredentialRequestOptionsRepository
implements PublicKeyCredentialRequestOptionsRepository {
static final String DEFAULT_ATTR_NAME = PublicKeyCredentialRequestOptionsRepository.class.getName()
.concat(".ATTR_NAME");
private String attrName = DEFAULT_ATTR_NAME;
@Override
public void save(HttpServletRequest request, HttpServletResponse response,
PublicKeyCredentialRequestOptions options) {
HttpSession session = request.getSession();
session.setAttribute(this.attrName, options);
}
@Override
public PublicKeyCredentialRequestOptions load(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return (PublicKeyCredentialRequestOptions) session.getAttribute(this.attrName);
}
public void setAttrName(String attrName) {
Assert.notNull(attrName, "attrName cannot be null");
this.attrName = attrName;
}
}
@@ -0,0 +1,127 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.authentication;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module;
import org.springframework.security.web.webauthn.management.ImmutablePublicKeyCredentialRequestOptionsRequest;
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
/**
* A {@link jakarta.servlet.Filter} that renders the
* {@link PublicKeyCredentialRequestOptions} in order to <a href=
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-get">get</a>
* a credential.
*
* @author Rob Winch
* @since 6.4
*/
public class PublicKeyCredentialRequestOptionsFilter extends OncePerRequestFilter {
private RequestMatcher matcher = antMatcher(HttpMethod.POST, "/webauthn/authenticate/options");
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private final WebAuthnRelyingPartyOperations rpOptions;
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository = new HttpSessionPublicKeyCredentialRequestOptionsRepository();
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build());
/**
* Creates a new instance with the provided {@link WebAuthnRelyingPartyOperations}.
* @param rpOptions the {@link WebAuthnRelyingPartyOperations} to use. Cannot be null.
*/
public PublicKeyCredentialRequestOptionsFilter(WebAuthnRelyingPartyOperations rpOptions) {
Assert.notNull(rpOptions, "rpOperations cannot be null");
this.rpOptions = rpOptions;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.matcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
SecurityContext context = this.securityContextHolderStrategy.getContext();
ImmutablePublicKeyCredentialRequestOptionsRequest optionsRequest = new ImmutablePublicKeyCredentialRequestOptionsRequest(
context.getAuthentication());
PublicKeyCredentialRequestOptions credentialRequestOptions = this.rpOptions
.createCredentialRequestOptions(optionsRequest);
this.requestOptionsRepository.save(request, response, credentialRequestOptions);
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
this.converter.write(credentialRequestOptions, MediaType.APPLICATION_JSON,
new ServletServerHttpResponse(response));
}
/**
* Sets the {@link PublicKeyCredentialRequestOptionsRepository} to use.
* @param requestOptionsRepository the
* {@link PublicKeyCredentialRequestOptionsRepository} to use. Cannot be null.
*/
public void setRequestOptionsRepository(PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) {
Assert.notNull(requestOptionsRepository, "requestOptionsRepository cannot be null");
this.requestOptionsRepository = requestOptionsRepository;
}
/**
* Sets the {@link HttpMessageConverter} to use.
* @param converter the {@link HttpMessageConverter} to use. Cannot be null.
*/
public void setConverter(HttpMessageConverter<Object> converter) {
Assert.notNull(converter, "converter cannot be null");
this.converter = converter;
}
/**
* Sets the {@link SecurityContextHolderStrategy} to use.
* @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
* use. Cannot be null.
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
/**
* Saves {@link PublicKeyCredentialRequestOptions} between a request to generate an
* assertion and the validation of the assertion.
*
* @author Rob Winch
* @since 6.4
*/
public interface PublicKeyCredentialRequestOptionsRepository {
/**
* Saves the provided {@link PublicKeyCredentialRequestOptions} or clears an existing
* {@link PublicKeyCredentialRequestOptions} if {@code options} is null.
* @param request the {@link HttpServletRequest}
* @param response the {@link HttpServletResponse}
* @param options the {@link PublicKeyCredentialRequestOptions} to save or null if an
* existing {@link PublicKeyCredentialRequestOptions} should be removed.
*/
void save(HttpServletRequest request, HttpServletResponse response, PublicKeyCredentialRequestOptions options);
/**
* Gets a saved {@link PublicKeyCredentialRequestOptions} if it exists, otherwise
* null.
* @param request the {@link HttpServletRequest}
* @return the {@link PublicKeyCredentialRequestOptions} that was saved, otherwise
* null.
*/
PublicKeyCredentialRequestOptions load(HttpServletRequest request);
}
@@ -0,0 +1,66 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.authentication;
import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.util.Assert;
/**
* A {@link WebAuthnAuthentication} is used to represent successful authentication with
* WebAuthn.
*
* @author Rob Winch
* @since 6.4
* @see WebAuthnAuthenticationRequestToken
*/
public class WebAuthnAuthentication extends AbstractAuthenticationToken {
private final PublicKeyCredentialUserEntity principal;
public WebAuthnAuthentication(PublicKeyCredentialUserEntity principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public void setAuthenticated(boolean authenticated) {
Assert.isTrue(!authenticated, "Cannot set this token to trusted");
super.setAuthenticated(authenticated);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public PublicKeyCredentialUserEntity getPrincipal() {
return this.principal;
}
@Override
public String getName() {
return this.principal.getName();
}
}
@@ -0,0 +1,136 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.authentication;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler;
import org.springframework.security.web.authentication.HttpMessageConverterAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module;
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
import org.springframework.util.Assert;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
/**
* Authenticates {@code PublicKeyCredential<AuthenticatorAssertionResponse>} that is
* parsed from the body of the {@link HttpServletRequest} using the
* {@link #setConverter(GenericHttpMessageConverter)}. An example request is provided
* below:
*
* <pre>
* {
* "id": "dYF7EGnRFFIXkpXi9XU2wg",
* "rawId": "dYF7EGnRFFIXkpXi9XU2wg",
* "response": {
* "authenticatorData": "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
* "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
* "signature": "MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z",
* "userHandle": "Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo"
* },
* "clientExtensionResults": {},
* "authenticatorAttachment": "platform"
* }
* </pre>
*
* @author Rob Winch
* @since 6.4
*/
public class WebAuthnAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private GenericHttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build());
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository = new HttpSessionPublicKeyCredentialRequestOptionsRepository();
public WebAuthnAuthenticationFilter() {
super(antMatcher(HttpMethod.POST, "/login/webauthn"));
setSecurityContextRepository(new HttpSessionSecurityContextRepository());
setAuthenticationFailureHandler(
new AuthenticationEntryPointFailureHandler(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)));
setAuthenticationSuccessHandler(new HttpMessageConverterAuthenticationSuccessHandler());
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request);
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(PublicKeyCredential.class,
AuthenticatorAssertionResponse.class);
PublicKeyCredential<AuthenticatorAssertionResponse> publicKeyCredential = null;
try {
publicKeyCredential = (PublicKeyCredential<AuthenticatorAssertionResponse>) this.converter
.read(resolvableType.getType(), getClass(), httpRequest);
}
catch (Exception ex) {
throw new BadCredentialsException("Unable to authenticate the PublicKeyCredential", ex);
}
PublicKeyCredentialRequestOptions requestOptions = this.requestOptionsRepository.load(request);
if (requestOptions == null) {
throw new BadCredentialsException(
"Unable to authenticate the PublicKeyCredential. No PublicKeyCredentialRequestOptions found.");
}
this.requestOptionsRepository.save(request, response, null);
RelyingPartyAuthenticationRequest authenticationRequest = new RelyingPartyAuthenticationRequest(requestOptions,
publicKeyCredential);
WebAuthnAuthenticationRequestToken token = new WebAuthnAuthenticationRequestToken(authenticationRequest);
return getAuthenticationManager().authenticate(token);
}
/**
* Sets the {@link GenericHttpMessageConverter} to use for writing
* {@code PublicKeyCredential<AuthenticatorAssertionResponse>} to the response. The
* default is @{code MappingJackson2HttpMessageConverter}
* @param converter the {@link GenericHttpMessageConverter} to use. Cannot be null.
*/
public void setConverter(GenericHttpMessageConverter<Object> converter) {
Assert.notNull(converter, "converter cannot be null");
this.converter = converter;
}
/**
* Sets the {@link PublicKeyCredentialRequestOptionsRepository} to use. The default is
* {@link HttpSessionPublicKeyCredentialRequestOptionsRepository}.
* @param requestOptionsRepository the
* {@link PublicKeyCredentialRequestOptionsRepository} to use. Cannot be null.
*/
public void setRequestOptionsRepository(PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) {
Assert.notNull(requestOptionsRepository, "requestOptionsRepository cannot be null");
this.requestOptionsRepository = requestOptionsRepository;
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.authentication;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} that uses {@link WebAuthnRelyingPartyOperations} for
* authentication using an {@link WebAuthnAuthenticationRequestToken}. First
* {@link WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest)}
* is invoked. The result is a username passed into {@link UserDetailsService}. The
* {@link UserDetails} is used to create an {@link Authentication}.
*
* @author Rob Winch
* @since 6.4
*/
public class WebAuthnAuthenticationProvider implements AuthenticationProvider {
private final WebAuthnRelyingPartyOperations relyingPartyOperations;
private final UserDetailsService userDetailsService;
/**
* Creates a new instance.
* @param relyingPartyOperations the {@link WebAuthnRelyingPartyOperations} to use.
* Cannot be null.
* @param userDetailsService the {@link UserDetailsService} to use. Cannot be null.
*/
public WebAuthnAuthenticationProvider(WebAuthnRelyingPartyOperations relyingPartyOperations,
UserDetailsService userDetailsService) {
Assert.notNull(relyingPartyOperations, "relyingPartyOperations cannot be null");
Assert.notNull(userDetailsService, "userDetailsService cannot be null");
this.relyingPartyOperations = relyingPartyOperations;
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
WebAuthnAuthenticationRequestToken webAuthnRequest = (WebAuthnAuthenticationRequestToken) authentication;
try {
PublicKeyCredentialUserEntity userEntity = this.relyingPartyOperations
.authenticate(webAuthnRequest.getWebAuthnRequest());
String username = userEntity.getName();
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
return new WebAuthnAuthentication(userEntity, userDetails.getAuthorities());
}
catch (RuntimeException ex) {
throw new BadCredentialsException(ex.getMessage(), ex);
}
}
@Override
public boolean supports(Class<?> authentication) {
return WebAuthnAuthenticationRequestToken.class.isAssignableFrom(authentication);
}
}
@@ -0,0 +1,70 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.authentication;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
import org.springframework.util.Assert;
/**
* An {@link org.springframework.security.core.Authentication} used in
* {@link WebAuthnAuthenticationProvider} for authenticating via WebAuthn.
*
* @author Rob Winch
* @since 6.4
*/
public class WebAuthnAuthenticationRequestToken extends AbstractAuthenticationToken {
private final RelyingPartyAuthenticationRequest webAuthnRequest;
/**
* Creates a new instance.
* @param webAuthnRequest the {@link RelyingPartyAuthenticationRequest} to use for
* authentication. Cannot be null.
*/
public WebAuthnAuthenticationRequestToken(RelyingPartyAuthenticationRequest webAuthnRequest) {
super(AuthorityUtils.NO_AUTHORITIES);
Assert.notNull(webAuthnRequest, "webAuthnRequest cannot be null");
this.webAuthnRequest = webAuthnRequest;
}
/**
* Gets the {@link RelyingPartyAuthenticationRequest}
* @return the {@link RelyingPartyAuthenticationRequest}
*/
public RelyingPartyAuthenticationRequest getWebAuthnRequest() {
return this.webAuthnRequest;
}
@Override
public void setAuthenticated(boolean authenticated) {
Assert.isTrue(!authenticated, "Cannot set this token to trusted");
super.setAuthenticated(authenticated);
}
@Override
public Object getCredentials() {
return this.webAuthnRequest.getPublicKey();
}
@Override
public Object getPrincipal() {
return this.webAuthnRequest.getPublicKey().getResponse().getUserHandle();
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference;
/**
* Jackson mixin for {@link AttestationConveyancePreference}
*
* @author Rob Winch
* @since 6.4
*/
@JsonSerialize(using = AttestationConveyancePreferenceSerializer.class)
class AttestationConveyancePreferenceMixin {
}
@@ -0,0 +1,45 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference;
/**
* Jackson serializer for {@link AttestationConveyancePreference}
*
* @author Rob Winch
* @since 6.4
*/
class AttestationConveyancePreferenceSerializer extends StdSerializer<AttestationConveyancePreference> {
AttestationConveyancePreferenceSerializer() {
super(AttestationConveyancePreference.class);
}
@Override
public void serialize(AttestationConveyancePreference preference, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeString(preference.getValue());
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs;
/**
* Jackson mixin for {@link AuthenticationExtensionsClientInputs}
*
* @author Rob Winch
* @since 6.4
*/
@JsonSerialize(using = AuthenticationExtensionsClientInputSerializer.class)
class AuthenticationExtensionsClientInputMixin {
}
@@ -0,0 +1,48 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInput;
/**
* Provides Jackson serialization of {@link AuthenticationExtensionsClientInput}.
*
* @author Rob Winch
* @since 6.4
*/
class AuthenticationExtensionsClientInputSerializer extends StdSerializer<AuthenticationExtensionsClientInput> {
/**
* Creates a new instance.
*/
AuthenticationExtensionsClientInputSerializer() {
super(AuthenticationExtensionsClientInput.class);
}
@Override
public void serialize(AuthenticationExtensionsClientInput input, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeObjectField(input.getExtensionId(), input.getInput());
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs;
/**
* Jackson mixin for {@link AuthenticationExtensionsClientInputs}
*
* @author Rob Winch
* @since 6.4
*/
@JsonSerialize(using = AuthenticationExtensionsClientInputsSerializer.class)
class AuthenticationExtensionsClientInputsMixin {
}
@@ -0,0 +1,53 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInput;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs;
/**
* Provides Jackson serialization of {@link AuthenticationExtensionsClientInputs}.
*
* @author Rob Winch
* @since 6.4
*/
class AuthenticationExtensionsClientInputsSerializer extends StdSerializer<AuthenticationExtensionsClientInputs> {
/**
* Creates a new instance.
*/
AuthenticationExtensionsClientInputsSerializer() {
super(AuthenticationExtensionsClientInputs.class);
}
@Override
public void serialize(AuthenticationExtensionsClientInputs inputs, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeStartObject();
for (AuthenticationExtensionsClientInput input : inputs.getInputs()) {
jgen.writeObject(input);
}
jgen.writeEndObject();
}
}
@@ -0,0 +1,77 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutput;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput;
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientOutputs;
/**
* Provides Jackson deserialization of {@link AuthenticationExtensionsClientOutputs}.
*
* @author Rob Winch
* @since 6.4
*/
class AuthenticationExtensionsClientOutputsDeserializer extends StdDeserializer<AuthenticationExtensionsClientOutputs> {
private static final Log logger = LogFactory.getLog(AuthenticationExtensionsClientOutputsDeserializer.class);
/**
* Creates a new instance.
*/
AuthenticationExtensionsClientOutputsDeserializer() {
super(AuthenticationExtensionsClientOutputs.class);
}
@Override
public AuthenticationExtensionsClientOutputs deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JacksonException {
List<AuthenticationExtensionsClientOutput<?>> outputs = new ArrayList<>();
for (String key = parser.nextFieldName(); key != null; key = parser.nextFieldName()) {
JsonToken startObject = parser.nextValue();
if (startObject != JsonToken.START_OBJECT) {
break;
}
if (CredentialPropertiesOutput.EXTENSION_ID.equals(key)) {
CredentialPropertiesOutput output = parser.readValueAs(CredentialPropertiesOutput.class);
outputs.add(output);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping unknown extension with id " + key);
}
parser.nextValue();
}
}
return new ImmutableAuthenticationExtensionsClientOutputs(outputs);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
/**
* Jackson mixin for {@link AuthenticationExtensionsClientOutputs}
*
* @author Rob Winch
* @since 6.4
*/
@JsonDeserialize(using = AuthenticationExtensionsClientOutputsDeserializer.class)
class AuthenticationExtensionsClientOutputsMixin {
}
@@ -0,0 +1,38 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
/**
* Jackson mixin for {@link AuthenticatorAssertionResponse}
*
* @author Rob Winch
* @since 6.4
*/
@JsonDeserialize(builder = AuthenticatorAssertionResponse.AuthenticatorAssertionResponseBuilder.class)
class AuthenticatorAssertionResponseMixin {
@JsonPOJOBuilder(withPrefix = "")
abstract class AuthenticatorAssertionResponseBuilderMixin {
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
/**
* Jackson deserializer for {@link AuthenticatorAttachment}
*
* @author Rob Winch
* @since 6.4
*/
class AuthenticatorAttachmentDeserializer extends StdDeserializer<AuthenticatorAttachment> {
AuthenticatorAttachmentDeserializer() {
super(AuthenticatorAttachment.class);
}
@Override
public AuthenticatorAttachment deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JacksonException {
String type = parser.readValueAs(String.class);
for (AuthenticatorAttachment publicKeyCredentialType : AuthenticatorAttachment.values()) {
if (publicKeyCredentialType.getValue().equals(type)) {
return publicKeyCredentialType;
}
}
return null;
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
/**
* Jackson mixin for {@link AuthenticatorAttachment}
*
* @author Rob Winch
* @since 6.4
*/
@JsonDeserialize(using = AuthenticatorAttachmentDeserializer.class)
@JsonSerialize(using = AuthenticatorAttachmentSerializer.class)
class AuthenticatorAttachmentMixin {
}
@@ -0,0 +1,45 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
/**
* Jackson serializer for {@link AuthenticatorAttachment}
*
* @author Rob Winch
* @since 6.4
*/
class AuthenticatorAttachmentSerializer extends StdSerializer<AuthenticatorAttachment> {
AuthenticatorAttachmentSerializer() {
super(AuthenticatorAttachment.class);
}
@Override
public void serialize(AuthenticatorAttachment attachment, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeString(attachment.getValue());
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
/**
* Jackson mixin for {@link AuthenticatorAttestationResponse}
*
* @author Rob Winch
* @since 6.4
*/
@JsonDeserialize(builder = AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder.class)
class AuthenticatorAttestationResponseMixin {
@JsonPOJOBuilder(withPrefix = "")
@JsonIgnoreProperties(ignoreUnknown = true)
abstract class AuthenticatorAttestationResponseBuilderMixin {
@JsonSetter
abstract AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder transports(
List<AuthenticatorTransport> transports);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria;
/**
* Jackson mixin for {@link AuthenticatorSelectionCriteria}
*
* @author Rob Winch
* @since 6.4
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
abstract class AuthenticatorSelectionCriteriaMixin {
}
@@ -0,0 +1,52 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
/**
* Jackson deserializer for {@link AuthenticatorTransport}
*
* @author Rob Winch
* @since 6.4
*/
class AuthenticatorTransportDeserializer extends StdDeserializer<AuthenticatorTransport> {
AuthenticatorTransportDeserializer() {
super(AuthenticatorTransport.class);
}
@Override
public AuthenticatorTransport deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JacksonException {
String transportValue = parser.readValueAs(String.class);
for (AuthenticatorTransport transport : AuthenticatorTransport.values()) {
if (transport.getValue().equals(transportValue)) {
return transport;
}
}
return null;
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
/**
* Jackson mixin for {@link AuthenticatorTransport}
*
* @author Rob Winch
* @since 6.4
*/
@JsonDeserialize(using = AuthenticatorTransportDeserializer.class)
@JsonSerialize(using = AuthenticatorTransportSerializer.class)
class AuthenticatorTransportMixin {
}
@@ -0,0 +1,41 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
/**
* Jackson serializer for {@link AuthenticatorTransport}
*
* @author Rob Winch
* @since 6.4
*/
class AuthenticatorTransportSerializer extends JsonSerializer<AuthenticatorTransport> {
@Override
public void serialize(AuthenticatorTransport transport, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeString(transport.getValue());
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.Bytes;
/**
* Jackson mixin for {@link Bytes}
*
* @author Rob Winch
* @since 6.4
*/
@JsonSerialize(using = BytesSerializer.class)
final class BytesMixin {
@JsonCreator
static Bytes fromBase64(String value) {
return Bytes.fromBase64(value);
}
private BytesMixin() {
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.security.web.webauthn.api.Bytes;
/**
* Jackson serializer for {@link Bytes}
*
* @author Rob Winch
* @since 6.4
*/
class BytesSerializer extends StdSerializer<Bytes> {
/**
* Creates a new instance.
*/
BytesSerializer() {
super(Bytes.class);
}
@Override
public void serialize(Bytes bytes, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(bytes.toBase64UrlString());
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
/**
* Jackson serializer for {@link COSEAlgorithmIdentifier}
*
* @author Rob Winch
* @since 6.4
*/
class COSEAlgorithmIdentifierDeserializer extends StdDeserializer<COSEAlgorithmIdentifier> {
COSEAlgorithmIdentifierDeserializer() {
super(COSEAlgorithmIdentifier.class);
}
@Override
public COSEAlgorithmIdentifier deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JacksonException {
Long transportValue = parser.readValueAs(Long.class);
for (COSEAlgorithmIdentifier identifier : COSEAlgorithmIdentifier.values()) {
if (identifier.getValue() == transportValue.longValue()) {
return identifier;
}
}
return null;
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
/**
* Jackson mixin for {@link COSEAlgorithmIdentifier}
*
* @author Rob Winch
* @since 6.4
*/
@JsonSerialize(using = COSEAlgorithmIdentifierSerializer.class)
@JsonDeserialize(using = COSEAlgorithmIdentifierDeserializer.class)
abstract class COSEAlgorithmIdentifierMixin {
}
@@ -0,0 +1,45 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
/**
* Jackson serializer for {@link COSEAlgorithmIdentifier}
*
* @author Rob Winch
* @since 6.4
*/
class COSEAlgorithmIdentifierSerializer extends StdSerializer<COSEAlgorithmIdentifier> {
COSEAlgorithmIdentifierSerializer() {
super(COSEAlgorithmIdentifier.class);
}
@Override
public void serialize(COSEAlgorithmIdentifier identifier, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeNumber(identifier.getValue());
}
}
@@ -0,0 +1,24 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(using = CredProtectAuthenticationExtensionsClientInputSerializer.class)
class CredProtectAuthenticationExtensionsClientInputMixin {
}
@@ -0,0 +1,63 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
/**
* Serializes <a href=
* "https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#sctn-credProtect-extension">credProtect
* extension</a>.
*
* @author Rob Winch
*/
class CredProtectAuthenticationExtensionsClientInputSerializer
extends StdSerializer<CredProtectAuthenticationExtensionsClientInput> {
protected CredProtectAuthenticationExtensionsClientInputSerializer() {
super(CredProtectAuthenticationExtensionsClientInput.class);
}
@Override
public void serialize(CredProtectAuthenticationExtensionsClientInput input, JsonGenerator jgen,
SerializerProvider provider) throws IOException {
CredProtectAuthenticationExtensionsClientInput.CredProtect credProtect = input.getInput();
String policy = toString(credProtect.getCredProtectionPolicy());
jgen.writeObjectField("credentialProtectionPolicy", policy);
jgen.writeObjectField("enforceCredentialProtectionPolicy", credProtect.isEnforceCredentialProtectionPolicy());
}
private static String toString(CredProtectAuthenticationExtensionsClientInput.CredProtect.ProtectionPolicy policy) {
switch (policy) {
case USER_VERIFICATION_OPTIONAL:
return "userVerificationOptional";
case USER_VERIFICATION_OPTIONAL_WITH_CREDENTIAL_ID_LIST:
return "userVerificationOptionalWithCredentialIdList";
case USER_VERIFICATION_REQUIRED:
return "userVerificationRequired";
default:
throw new IllegalArgumentException("Unsupported ProtectionPolicy " + policy);
}
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput;
/**
* Jackson mixin for {@link CredentialPropertiesOutput}
*
* @author Rob Winch
* @since 6.4
*/
@JsonIgnoreProperties(ignoreUnknown = true)
abstract class CredentialPropertiesOutputMixin {
CredentialPropertiesOutputMixin(@JsonProperty("rk") boolean rk) {
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import java.io.IOException;
import java.time.Duration;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
/**
* Jackson serializer for {@link Duration}
*
* @author Rob Winch
* @since 6.4
*/
class DurationSerializer extends StdSerializer<Duration> {
/**
* Creates an instance.
*/
DurationSerializer() {
super(Duration.class);
}
@Override
public void serialize(Duration duration, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeNumber(duration.toMillis());
}
}
@@ -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 java.time.Duration;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
/**
* Jackson mixin for {@link PublicKeyCredentialCreationOptions}
*
* @author Rob Winch
* @since 6.4
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
abstract class PublicKeyCredentialCreationOptionsMixin {
@JsonSerialize(using = DurationSerializer.class)
private Duration timeout;
}
@@ -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.PublicKeyCredential;
/**
* Jackson mixin for {@link PublicKeyCredential}
*
* @author Rob Winch
* @since 6.4
*/
@JsonDeserialize(builder = PublicKeyCredential.PublicKeyCredentialBuilder.class)
class PublicKeyCredentialMixin {
@JsonPOJOBuilder(withPrefix = "")
static class PublicKeyCredentialBuilderMixin {
}
}
@@ -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 java.time.Duration;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
/**
* Jackson mixin for {@link PublicKeyCredentialRequestOptions}
*
* @author Rob Winch
* @since 6.4
*/
@JsonInclude(content = JsonInclude.Include.NON_NULL)
class PublicKeyCredentialRequestOptionsMixin {
@JsonSerialize(using = DurationSerializer.class)
private final Duration timeout = null;
}
@@ -0,0 +1,50 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.PublicKeyCredentialType;
/**
* Jackson deserializer for {@link PublicKeyCredentialType}
*
* @author Rob Winch
* @since 6.4
*/
class PublicKeyCredentialTypeDeserializer extends StdDeserializer<PublicKeyCredentialType> {
/**
* Creates a new instance.
*/
PublicKeyCredentialTypeDeserializer() {
super(PublicKeyCredentialType.class);
}
@Override
public PublicKeyCredentialType deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JacksonException {
String type = parser.readValueAs(String.class);
return PublicKeyCredentialType.valueOf(type);
}
}
@@ -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.PublicKeyCredentialType;
/**
* Jackson mixin for {@link PublicKeyCredentialType}
*
* @author Rob Winch
* @since 6.4
*/
@JsonSerialize(using = PublicKeyCredentialTypeSerializer.class)
@JsonDeserialize(using = PublicKeyCredentialTypeDeserializer.class)
abstract class PublicKeyCredentialTypeMixin {
}
@@ -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.PublicKeyCredentialType;
/**
* Jackson serializer for {@link PublicKeyCredentialType}
*
* @author Rob Winch
* @since 6.4
*/
class PublicKeyCredentialTypeSerializer extends StdSerializer<PublicKeyCredentialType> {
/**
* Creates a new instance.
*/
PublicKeyCredentialTypeSerializer() {
super(PublicKeyCredentialType.class);
}
@Override
public void serialize(PublicKeyCredentialType type, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeString(type.getValue());
}
}
@@ -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.annotation.JsonProperty;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
import org.springframework.security.web.webauthn.management.RelyingPartyPublicKey;
/**
* Jackson mixin for {@link RelyingPartyPublicKey}
*
* @author Rob Winch
* @since 6.4
*/
abstract class RelyingPartyPublicKeyMixin {
RelyingPartyPublicKeyMixin(
@JsonProperty("credential") PublicKeyCredential<AuthenticatorAttestationResponse> credential,
@JsonProperty("label") String label) {
}
}
@@ -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.ResidentKeyRequirement;
/**
* Jackson mixin for {@link ResidentKeyRequirement}
*
* @author Rob Winch
* @since 6.4
*/
@JsonSerialize(using = ResidentKeyRequirementSerializer.class)
abstract class ResidentKeyRequirementMixin {
}
@@ -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.ResidentKeyRequirement;
/**
* Jackson serializer for {@link ResidentKeyRequirement}
*
* @author Rob Winch
* @since 6.4
*/
class ResidentKeyRequirementSerializer extends StdSerializer<ResidentKeyRequirement> {
/**
* Creates a new instance.
*/
ResidentKeyRequirementSerializer() {
super(ResidentKeyRequirement.class);
}
@Override
public void serialize(ResidentKeyRequirement requirement, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeString(requirement.getValue());
}
}
@@ -0,0 +1,32 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.jackson;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.web.webauthn.api.UserVerificationRequirement;
/**
* Jackson mixin for {@link UserVerificationRequirement}
*
* @author Rob Winch
* @since 6.4
*/
@JsonSerialize(using = UserVerificationRequirementSerializer.class)
abstract class UserVerificationRequirementMixin {
}
@@ -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.UserVerificationRequirement;
/**
* Jackson serializer for {@link UserVerificationRequirement}
*
* @author Rob Winch
* @since 6.4
*/
class UserVerificationRequirementSerializer extends StdSerializer<UserVerificationRequirement> {
/**
* Creates a new instance.
*/
UserVerificationRequirementSerializer() {
super(UserVerificationRequirement.class);
}
@Override
public void serialize(UserVerificationRequirement requirement, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeString(requirement.getValue());
}
}
@@ -0,0 +1,96 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.core.Version;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInput;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientInputs;
import org.springframework.security.web.webauthn.api.AuthenticationExtensionsClientOutputs;
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput;
import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput;
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
import org.springframework.security.web.webauthn.api.ResidentKeyRequirement;
import org.springframework.security.web.webauthn.api.UserVerificationRequirement;
import org.springframework.security.web.webauthn.management.RelyingPartyPublicKey;
/**
* Adds Jackson support for Spring Security WebAuthn. It is automatically registered when
* using Jackson's SPI support.
*
* @author Rob Winch
* @since 6.4
*/
public class WebauthnJackson2Module extends SimpleModule {
/**
* Creates a new instance.
*/
public WebauthnJackson2Module() {
super(WebauthnJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
}
@Override
public void setupModule(SetupContext context) {
context.setMixInAnnotations(Bytes.class, BytesMixin.class);
context.setMixInAnnotations(AttestationConveyancePreference.class, AttestationConveyancePreferenceMixin.class);
context.setMixInAnnotations(AuthenticationExtensionsClientInput.class,
AuthenticationExtensionsClientInputMixin.class);
context.setMixInAnnotations(AuthenticationExtensionsClientInputs.class,
AuthenticationExtensionsClientInputsMixin.class);
context.setMixInAnnotations(AuthenticationExtensionsClientOutputs.class,
AuthenticationExtensionsClientOutputsMixin.class);
context.setMixInAnnotations(AuthenticatorAssertionResponse.AuthenticatorAssertionResponseBuilder.class,
AuthenticatorAssertionResponseMixin.AuthenticatorAssertionResponseBuilderMixin.class);
context.setMixInAnnotations(AuthenticatorAssertionResponse.class, AuthenticatorAssertionResponseMixin.class);
context.setMixInAnnotations(AuthenticatorAttachment.class, AuthenticatorAttachmentMixin.class);
context.setMixInAnnotations(AuthenticatorAttestationResponse.class,
AuthenticatorAttestationResponseMixin.class);
context.setMixInAnnotations(AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder.class,
AuthenticatorAttestationResponseMixin.AuthenticatorAttestationResponseBuilderMixin.class);
context.setMixInAnnotations(AuthenticatorSelectionCriteria.class, AuthenticatorSelectionCriteriaMixin.class);
context.setMixInAnnotations(AuthenticatorTransport.class, AuthenticatorTransportMixin.class);
context.setMixInAnnotations(COSEAlgorithmIdentifier.class, COSEAlgorithmIdentifierMixin.class);
context.setMixInAnnotations(CredentialPropertiesOutput.class, CredentialPropertiesOutputMixin.class);
context.setMixInAnnotations(CredProtectAuthenticationExtensionsClientInput.class,
CredProtectAuthenticationExtensionsClientInputMixin.class);
context.setMixInAnnotations(PublicKeyCredential.PublicKeyCredentialBuilder.class,
PublicKeyCredentialMixin.PublicKeyCredentialBuilderMixin.class);
context.setMixInAnnotations(PublicKeyCredential.class, PublicKeyCredentialMixin.class);
context.setMixInAnnotations(PublicKeyCredentialCreationOptions.class,
PublicKeyCredentialCreationOptionsMixin.class);
context.setMixInAnnotations(PublicKeyCredentialRequestOptions.class,
PublicKeyCredentialRequestOptionsMixin.class);
context.setMixInAnnotations(PublicKeyCredentialType.class, PublicKeyCredentialTypeMixin.class);
context.setMixInAnnotations(RelyingPartyPublicKey.class, RelyingPartyPublicKeyMixin.class);
context.setMixInAnnotations(ResidentKeyRequirement.class, ResidentKeyRequirementMixin.class);
context.setMixInAnnotations(UserVerificationRequirement.class, UserVerificationRequirementMixin.class);
}
}
@@ -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.management;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* An immutable implementation of {@link PublicKeyCredentialCreationOptionsRequest}.
*
* @author Rob Winch
* @since 6.4
*/
public class ImmutablePublicKeyCredentialCreationOptionsRequest implements PublicKeyCredentialCreationOptionsRequest {
private final Authentication authentication;
public ImmutablePublicKeyCredentialCreationOptionsRequest(Authentication authentication) {
Assert.notNull(authentication, "authentication cannot be null");
this.authentication = authentication;
}
@Override
public Authentication getAuthentication() {
return this.authentication;
}
}
@@ -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.management;
import org.springframework.security.core.Authentication;
public class ImmutablePublicKeyCredentialRequestOptionsRequest implements PublicKeyCredentialRequestOptionsRequest {
private final Authentication authentication;
public ImmutablePublicKeyCredentialRequestOptionsRequest(Authentication authentication) {
this.authentication = authentication;
}
@Override
public Authentication getAuthentication() {
return this.authentication;
}
}
@@ -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.management;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.util.Assert;
/**
* Contains the information necessary to register a new Credential.
*
* @author Rob Winch
* @since 6.4
* @see Webauthn4JRelyingPartyOperations#registerCredential(RelyingPartyRegistrationRequest)
*/
public class ImmutableRelyingPartyRegistrationRequest implements RelyingPartyRegistrationRequest {
private final PublicKeyCredentialCreationOptions options;
private final RelyingPartyPublicKey publicKey;
/**
* Creates a new instance.
* @param options the {@link PublicKeyCredentialCreationOptions} that were saved when
* {@link WebAuthnRelyingPartyOperations#createCredentialRequestOptions(PublicKeyCredentialRequestOptionsRequest)}
* was called.
* @param publicKey this is submitted by the client and if validated stored.
*/
public ImmutableRelyingPartyRegistrationRequest(PublicKeyCredentialCreationOptions options,
RelyingPartyPublicKey publicKey) {
Assert.notNull(options, "options cannot be null");
Assert.notNull(publicKey, "publicKey cannot be null");
this.options = options;
this.publicKey = publicKey;
}
@Override
public PublicKeyCredentialCreationOptions getCreationOptions() {
return this.options;
}
@Override
public RelyingPartyPublicKey getPublicKey() {
return this.publicKey;
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.management;
import java.util.HashMap;
import java.util.Map;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.util.Assert;
/**
* A {@link Map} based implementation of {@link PublicKeyCredentialUserEntityRepository}.
*
* @author Rob Winch
* @since 6.4
*/
public class MapPublicKeyCredentialUserEntityRepository implements PublicKeyCredentialUserEntityRepository {
private final Map<String, PublicKeyCredentialUserEntity> usernameToUserEntity = new HashMap<>();
private final Map<Bytes, PublicKeyCredentialUserEntity> idToUserEntity = new HashMap<>();
@Override
public PublicKeyCredentialUserEntity findById(Bytes id) {
Assert.notNull(id, "id cannot be null");
return this.idToUserEntity.get(id);
}
@Override
public PublicKeyCredentialUserEntity findByUsername(String username) {
Assert.notNull(username, "username cannot be null");
return this.usernameToUserEntity.get(username);
}
@Override
public void save(PublicKeyCredentialUserEntity userEntity) {
if (userEntity == null) {
throw new IllegalArgumentException("userEntity cannot be null");
}
this.usernameToUserEntity.put(userEntity.getName(), userEntity);
this.idToUserEntity.put(userEntity.getId(), userEntity);
}
@Override
public void delete(Bytes id) {
PublicKeyCredentialUserEntity existing = this.idToUserEntity.remove(id);
if (existing != null) {
this.usernameToUserEntity.remove(existing.getName());
}
}
}
@@ -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.management;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.util.Assert;
/**
* A {@link Map} based implementation of {@link UserCredentialRepository}.
*
* @author Rob Winch
* @since 6.4
*/
public class MapUserCredentialRepository implements UserCredentialRepository {
private final Map<Bytes, CredentialRecord> credentialIdToUserCredential = new HashMap<>();
private final Map<Bytes, Set<Bytes>> userEntityIdToUserCredentialIds = new HashMap<>();
@Override
public void delete(Bytes credentialId) {
Assert.notNull(credentialId, "credentialId cannot be null");
CredentialRecord credentialRecord = this.credentialIdToUserCredential.remove(credentialId);
if (credentialRecord != null) {
Set<Bytes> credentialIds = this.userEntityIdToUserCredentialIds.get(credentialRecord.getUserEntityUserId());
if (credentialIds != null) {
credentialIds.remove(credentialId);
}
}
}
@Override
public void save(CredentialRecord credentialRecord) {
Assert.notNull(credentialRecord, "credentialRecord cannot be null");
this.credentialIdToUserCredential.put(credentialRecord.getCredentialId(), credentialRecord);
this.userEntityIdToUserCredentialIds
.computeIfAbsent(credentialRecord.getUserEntityUserId(), (id) -> new HashSet<>())
.add(credentialRecord.getCredentialId());
}
@Override
public CredentialRecord findByCredentialId(Bytes credentialId) {
Assert.notNull(credentialId, "credentialId cannot be null");
return this.credentialIdToUserCredential.get(credentialId);
}
@Override
public List<CredentialRecord> findByUserId(Bytes userId) {
Assert.notNull(userId, "userId cannot be null");
Set<Bytes> credentialIds = this.userEntityIdToUserCredentialIds.getOrDefault(userId, Collections.emptySet());
return credentialIds.stream().map(this::findByCredentialId).collect(Collectors.toUnmodifiableList());
}
}
@@ -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.management;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
/**
* A request to create a new {@link PublicKeyCredentialCreationOptions}.
*
* @author Rob Winch
* @since 6.4
* @see WebAuthnRelyingPartyOperations#createPublicKeyCredentialCreationOptions(PublicKeyCredentialCreationOptionsRequest)
*/
public interface PublicKeyCredentialCreationOptionsRequest {
/**
* The current {@link Authentication}. It must be authenticated to associate the
* credential to a user.
* @return the {@link Authentication} to use.
*/
Authentication getAuthentication();
}
@@ -0,0 +1,29 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.management;
import org.springframework.security.core.Authentication;
public interface PublicKeyCredentialRequestOptionsRequest {
/**
* The current {@link Authentication}. Possibly null or an anonymous.
* @return the current {@link Authentication}
*/
Authentication getAuthentication();
}
@@ -0,0 +1,54 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.management;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
/**
* A repository for managing {@link PublicKeyCredentialUserEntity} instances.
*
* @author Rob Winch
* @since 6.4
*/
public interface PublicKeyCredentialUserEntityRepository {
/**
* Finds the {@link PublicKeyCredentialUserEntity} by
* {@link PublicKeyCredentialUserEntity#getId()}
* @param id the id to lookup the username by
* @return the username or null if not found.
*/
PublicKeyCredentialUserEntity findById(Bytes id);
/**
* Finds the {@link PublicKeyCredentialUserEntity} by the username.
* @param username the username to lookup the {@link PublicKeyCredentialUserEntity}
* @return the {@link PublicKeyCredentialUserEntity} or null if not found.
*/
PublicKeyCredentialUserEntity findByUsername(String username);
/**
* Saves the {@link PublicKeyCredentialUserEntity} to the associated username.
* @param userEntity the {@link PublicKeyCredentialUserEntity} to associate to the
* provided username. If null, any existing entry is deleted.
*/
void save(PublicKeyCredentialUserEntity userEntity);
void delete(Bytes id);
}
@@ -0,0 +1,67 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.management;
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.util.Assert;
/**
* The data object used to provide the information necessary to authenticate a user with
* WebAuthn.
*
* @author Rob Winch
* @since 6.4
* @see WebAuthnRelyingPartyOperations#authenticate(RelyingPartyAuthenticationRequest)
*/
public class RelyingPartyAuthenticationRequest {
private final PublicKeyCredentialRequestOptions requestOptions;
private final PublicKeyCredential<AuthenticatorAssertionResponse> publicKey;
/**
* Creates a new instance.
* @param requestOptions the {@link PublicKeyCredentialRequestOptions}
* @param publicKey the {@link PublicKeyCredential}
*/
public RelyingPartyAuthenticationRequest(PublicKeyCredentialRequestOptions requestOptions,
PublicKeyCredential<AuthenticatorAssertionResponse> publicKey) {
Assert.notNull(requestOptions, "requestOptions cannot be null");
Assert.notNull(publicKey, "publicKey cannot be null");
this.requestOptions = requestOptions;
this.publicKey = publicKey;
}
/**
* Ges the request options.
* @return the request options.
*/
public PublicKeyCredentialRequestOptions getRequestOptions() {
return this.requestOptions;
}
/**
* Gets the public key.
* @return the public key.
*/
public PublicKeyCredential<AuthenticatorAssertionResponse> getPublicKey() {
return this.publicKey;
}
}
@@ -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.management;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
import org.springframework.util.Assert;
/**
* Submitted by a client to request registration of a new credential.
*
* @author Rob Winch
* @since 6.4
*/
public class RelyingPartyPublicKey {
private final PublicKeyCredential<AuthenticatorAttestationResponse> credential;
private final String label;
/**
* Creates a new instance.
* @param credential the credential
* @param label a human readable label that will be associated to the credential
*/
public RelyingPartyPublicKey(PublicKeyCredential<AuthenticatorAttestationResponse> credential, String label) {
Assert.notNull(credential, "credential cannot be null");
Assert.notNull(label, "label cannot be null");
this.credential = credential;
this.label = label;
}
public PublicKeyCredential<AuthenticatorAttestationResponse> getCredential() {
return this.credential;
}
public String getLabel() {
return this.label;
}
}
@@ -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.management;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
/**
* @author Rob Winch
* @since 6.4
* @see WebAuthnRelyingPartyOperations#registerCredential(RelyingPartyRegistrationRequest)
*/
public interface RelyingPartyRegistrationRequest {
PublicKeyCredentialCreationOptions getCreationOptions();
RelyingPartyPublicKey getPublicKey();
}
@@ -0,0 +1,62 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.management;
import java.util.List;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
/**
* A repository for managing {@link CredentialRecord}s associated to a user.
*
* @author Rob Winch
* @since 6.4
*/
public interface UserCredentialRepository {
/**
* Deletes an entry by credential id
* @param credentialId {@link CredentialRecord#getCredentialId()}
*/
void delete(Bytes credentialId);
/**
* Saves a {@link CredentialRecord}
* @param credentialRecord the {@link CredentialRecord} to save.
*/
void save(CredentialRecord credentialRecord);
/**
* Finds an entry by credential id.
* @param credentialId {@link CredentialRecord#getCredentialId()}
* @return the {@link CredentialRecord} or null if not found.
*/
CredentialRecord findByCredentialId(Bytes credentialId);
/**
* Finds all {@link CredentialRecord} instances for a specific user.
* @param userId the {@link PublicKeyCredentialUserEntity#getId()} to search for a
* user.
* @return all {@link CredentialRecord} instances for a specific user or empty if no
* results found. Never null.
* @see PublicKeyCredentialUserEntityRepository
*/
List<CredentialRecord> findByUserId(Bytes userId);
}
@@ -0,0 +1,72 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.management;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
/**
* An API for <a href="https://www.w3.org/TR/webauthn-3/#sctn-rp-operations">WebAuthn
* Relying Party Operations</a>
*
* @author Rob Winch
* @since 6.4
*/
public interface WebAuthnRelyingPartyOperations {
/**
* Creates the {@link PublicKeyCredentialCreationOptions} used to register new
* credentials.
* @param request the {@link PublicKeyCredentialCreationOptionsRequest} to create the
* {@link PublicKeyCredentialCreationOptions}
* @return the {@link PublicKeyCredentialCreationOptions} for the
* {@link Authentication} passed in. Cannot be null.
*/
PublicKeyCredentialCreationOptions createPublicKeyCredentialCreationOptions(
PublicKeyCredentialCreationOptionsRequest request);
/**
* If {@link RelyingPartyRegistrationRequest} is valid, will <a href=
* "https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential">register</a>
* and return a new {@link CredentialRecord}.
* @param relyingPartyRegistrationRequest the {@link RelyingPartyRegistrationRequest}
* to process.
* @return a new {@link CredentialRecord}
* @throws RuntimeException if the {@link RelyingPartyRegistrationRequest} is not
* valid.
*/
CredentialRecord registerCredential(RelyingPartyRegistrationRequest relyingPartyRegistrationRequest);
/**
* Creates the {@link PublicKeyCredentialRequestOptions} used to authenticate a user.
* @param request the {@link PublicKeyCredentialRequestOptionsRequest}.
* @return the {@link PublicKeyCredentialRequestOptions} used to authenticate a user.
*/
PublicKeyCredentialRequestOptions createCredentialRequestOptions(PublicKeyCredentialRequestOptionsRequest request);
/**
* Authenticates the {@link RelyingPartyAuthenticationRequest} passed in
* @param request the {@link RelyingPartyAuthenticationRequest}
* @return the principal name (e.g. username) if authentication was successful
* @throws RuntimeException if authentication fails
*/
PublicKeyCredentialUserEntity authenticate(RelyingPartyAuthenticationRequest request);
}
@@ -0,0 +1,383 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.management;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import com.webauthn4j.WebAuthnManager;
import com.webauthn4j.authenticator.Authenticator;
import com.webauthn4j.authenticator.AuthenticatorImpl;
import com.webauthn4j.converter.util.CborConverter;
import com.webauthn4j.converter.util.ObjectConverter;
import com.webauthn4j.data.AuthenticationData;
import com.webauthn4j.data.AuthenticationParameters;
import com.webauthn4j.data.RegistrationData;
import com.webauthn4j.data.RegistrationParameters;
import com.webauthn4j.data.RegistrationRequest;
import com.webauthn4j.data.attestation.AttestationObject;
import com.webauthn4j.data.attestation.authenticator.AttestedCredentialData;
import com.webauthn4j.data.attestation.authenticator.AuthenticatorData;
import com.webauthn4j.data.attestation.authenticator.COSEKey;
import com.webauthn4j.data.client.Origin;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import com.webauthn4j.data.extension.authenticator.RegistrationExtensionAuthenticatorOutput;
import com.webauthn4j.server.ServerProperty;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.webauthn.api.AttestationConveyancePreference;
import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientInput;
import org.springframework.security.web.webauthn.api.ImmutableAuthenticationExtensionsClientInputs;
import org.springframework.security.web.webauthn.api.ImmutableCredentialRecord;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCose;
import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredential;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions.PublicKeyCredentialCreationOptionsBuilder;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialDescriptor;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialParameters;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions.PublicKeyCredentialRequestOptionsBuilder;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialType;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.api.ResidentKeyRequirement;
import org.springframework.security.web.webauthn.api.UserVerificationRequirement;
import org.springframework.util.Assert;
/**
* A <a href="https://webauthn4j.github.io/webauthn4j/en/">WebAuthn4j</a> implementation
* of {@link WebAuthnRelyingPartyOperations}.
*
* @author Rob Winch
* @since 6.4
*/
public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOperations {
private final PublicKeyCredentialUserEntityRepository userEntities;
private final UserCredentialRepository userCredentials;
private final Set<String> allowedOrigins;
private final PublicKeyCredentialRpEntity rp;
private final ObjectConverter objectConverter = new ObjectConverter();
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private WebAuthnManager webAuthnManager = WebAuthnManager.createNonStrictWebAuthnManager();
private Consumer<PublicKeyCredentialCreationOptionsBuilder> customizeCreationOptions = (options) -> {
};
private Consumer<PublicKeyCredentialRequestOptionsBuilder> customizeRequestOptions = (options) -> {
};
/**
* Creates a new instance.
* @param userEntities the {@link PublicKeyCredentialUserEntityRepository} to use.
* @param userCredentials the {@link UserCredentialRepository} to use.
* @param rpEntity the {@link PublicKeyCredentialRpEntity} to use.
* @param allowedOrigins the allowed origins.
*/
public Webauthn4JRelyingPartyOperations(PublicKeyCredentialUserEntityRepository userEntities,
UserCredentialRepository userCredentials, PublicKeyCredentialRpEntity rpEntity,
Set<String> allowedOrigins) {
Assert.notNull(userEntities, "userEntities cannot be null");
Assert.notNull(userCredentials, "userCredentials cannot be null");
Assert.notNull(rpEntity, "rpEntity cannot be null");
Assert.notNull(allowedOrigins, "allowedOrigins cannot be null");
this.userEntities = userEntities;
this.userCredentials = userCredentials;
this.rp = rpEntity;
this.allowedOrigins = allowedOrigins;
}
/**
* Sets the {@link WebAuthnManager} to use. The default is
* {@link WebAuthnManager#createNonStrictWebAuthnManager()}
* @param webAuthnManager the {@link WebAuthnManager}.
*/
public void setWebAuthnManager(WebAuthnManager webAuthnManager) {
Assert.notNull(webAuthnManager, "webAuthnManager cannot be null");
this.webAuthnManager = webAuthnManager;
}
/**
* Sets a {@link Consumer} used to customize the
* {@link PublicKeyCredentialCreationOptionsBuilder} for
* {@link #createPublicKeyCredentialCreationOptions(PublicKeyCredentialCreationOptionsRequest)}.
* The default values are always populated, but can be overridden with this property.
* @param customizeCreationOptions the {@link Consumer} to customize the
* {@link PublicKeyCredentialCreationOptionsBuilder}
*/
public void setCustomizeCreationOptions(
Consumer<PublicKeyCredentialCreationOptionsBuilder> customizeCreationOptions) {
Assert.notNull(customizeCreationOptions, "customizeCreationOptions must not be null");
this.customizeCreationOptions = customizeCreationOptions;
}
/**
* Sets a {@link Consumer} used to customize the
* {@link PublicKeyCredentialRequestOptionsBuilder} for
* {@link #createCredentialRequestOptions(PublicKeyCredentialRequestOptionsRequest)}.The
* default values are always populated, but can be overridden with this property.
* @param customizeRequestOptions the {@link Consumer} to customize the
* {@link PublicKeyCredentialRequestOptionsBuilder}
*/
public void setCustomizeRequestOptions(Consumer<PublicKeyCredentialRequestOptionsBuilder> customizeRequestOptions) {
Assert.notNull(customizeRequestOptions, "customizeRequestOptions cannot be null");
this.customizeRequestOptions = customizeRequestOptions;
}
@Override
public PublicKeyCredentialCreationOptions createPublicKeyCredentialCreationOptions(
PublicKeyCredentialCreationOptionsRequest request) {
if (request == null) {
throw new IllegalArgumentException("request cannot be null");
}
Authentication authentication = request.getAuthentication();
if (!this.trustResolver.isAuthenticated(authentication)) {
throw new IllegalArgumentException("Authentication must be authenticated");
}
AuthenticatorSelectionCriteria authenticatorSelection = AuthenticatorSelectionCriteria.builder()
.userVerification(UserVerificationRequirement.PREFERRED)
.residentKey(ResidentKeyRequirement.REQUIRED)
.build();
ImmutableAuthenticationExtensionsClientInputs clientInputs = new ImmutableAuthenticationExtensionsClientInputs(
ImmutableAuthenticationExtensionsClientInput.credProps);
PublicKeyCredentialUserEntity userEntity = findUserEntityOrCreateAndSave(authentication.getName());
List<CredentialRecord> credentialRecords = this.userCredentials.findByUserId(userEntity.getId());
PublicKeyCredentialCreationOptions options = PublicKeyCredentialCreationOptions.builder()
.attestation(AttestationConveyancePreference.DIRECT)
.pubKeyCredParams(PublicKeyCredentialParameters.EdDSA, PublicKeyCredentialParameters.ES256,
PublicKeyCredentialParameters.RS256)
.authenticatorSelection(authenticatorSelection)
.challenge(Bytes.random())
.extensions(clientInputs)
.timeout(Duration.ofMinutes(5))
.user(userEntity)
.rp(this.rp)
.excludeCredentials(credentialDescriptors(credentialRecords))
.customize(this.customizeCreationOptions)
.build();
return options;
}
private static List<PublicKeyCredentialDescriptor> credentialDescriptors(List<CredentialRecord> credentialRecords) {
List result = new ArrayList();
for (CredentialRecord credentialRecord : credentialRecords) {
Bytes id = Bytes.fromBase64(credentialRecord.getCredentialId().toBase64UrlString());
PublicKeyCredentialDescriptor credentialDescriptor = PublicKeyCredentialDescriptor.builder()
.id(id)
.transports(credentialRecord.getTransports())
.build();
result.add(credentialDescriptor);
}
return result;
}
private PublicKeyCredentialUserEntity findUserEntityOrCreateAndSave(String username) {
final PublicKeyCredentialUserEntity foundUserEntity = this.userEntities.findByUsername(username);
if (foundUserEntity != null) {
return foundUserEntity;
}
PublicKeyCredentialUserEntity userEntity = ImmutablePublicKeyCredentialUserEntity.builder()
.displayName(username)
.id(Bytes.random())
.name(username)
.build();
this.userEntities.save(userEntity);
return userEntity;
}
@Override
public CredentialRecord registerCredential(RelyingPartyRegistrationRequest rpRegistrationRequest) {
Assert.notNull(rpRegistrationRequest, "rpRegistrationRequest cannot be null");
Bytes credentialId = rpRegistrationRequest.getPublicKey().getCredential().getRawId();
CredentialRecord existingCredential = this.userCredentials.findByCredentialId(credentialId);
if (existingCredential != null) {
throw new IllegalArgumentException("Credential with id " + credentialId + " already exists");
}
PublicKeyCredentialCreationOptions creationOptions = rpRegistrationRequest.getCreationOptions();
String rpId = creationOptions.getRp().getId();
RelyingPartyPublicKey publicKey = rpRegistrationRequest.getPublicKey();
PublicKeyCredential<AuthenticatorAttestationResponse> credential = publicKey.getCredential();
AuthenticatorAttestationResponse response = credential.getResponse();
// Server properties
Set<Origin> origins = toOrigins();
byte[] base64Challenge = creationOptions.getChallenge().getBytes();
byte[] attestationObject = response.getAttestationObject().getBytes();
byte[] clientDataJSON = response.getClientDataJSON().getBytes();
Challenge challenge = new DefaultChallenge(base64Challenge);
byte[] tokenBindingId = null /* set tokenBindingId */; // FIXME:
// https://www.w3.org/TR/webauthn-1/#dom-collectedclientdata-tokenbinding
ServerProperty serverProperty = new ServerProperty(origins, rpId, challenge, tokenBindingId);
boolean userVerificationRequired = creationOptions.getAuthenticatorSelection()
.getUserVerification() == UserVerificationRequirement.REQUIRED;
// requireUserPresence The constant Boolean value true
// https://www.w3.org/TR/webauthn-3/#sctn-op-make-cred
boolean userPresenceRequired = true;
List<com.webauthn4j.data.PublicKeyCredentialParameters> pubKeyCredParams = convertCredentialParamsToWebauthn4j(
creationOptions.getPubKeyCredParams());
RegistrationRequest webauthn4jRegistrationRequest = new RegistrationRequest(attestationObject, clientDataJSON);
RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, pubKeyCredParams,
userVerificationRequired, userPresenceRequired);
RegistrationData registrationData = this.webAuthnManager.validate(webauthn4jRegistrationRequest,
registrationParameters);
AuthenticatorData<RegistrationExtensionAuthenticatorOutput> authData = registrationData.getAttestationObject()
.getAuthenticatorData();
CborConverter cborConverter = this.objectConverter.getCborConverter();
COSEKey coseKey = authData.getAttestedCredentialData().getCOSEKey();
byte[] rawCoseKey = cborConverter.writeValueAsBytes(coseKey);
ImmutableCredentialRecord userCredential = ImmutableCredentialRecord.builder()
.userEntityUserId(creationOptions.getUser().getId())
.credentialType(credential.getType())
.credentialId(credential.getRawId())
.publicKey(new ImmutablePublicKeyCose(rawCoseKey))
.signatureCount(authData.getSignCount())
.uvInitialized(authData.isFlagUV())
.transports(convertTransports(registrationData.getTransports()))
.backupEligible(authData.isFlagBE())
.backupState(authData.isFlagBS())
.label(publicKey.getLabel())
.attestationClientDataJSON(credential.getResponse().getClientDataJSON())
.attestationObject(credential.getResponse().getAttestationObject())
.build();
this.userCredentials.save(userCredential);
return userCredential;
}
private List<com.webauthn4j.data.PublicKeyCredentialParameters> convertCredentialParamsToWebauthn4j(
List<PublicKeyCredentialParameters> parameters) {
return parameters.stream().map(this::convertParamToWebauthn4j).collect(Collectors.toUnmodifiableList());
}
private com.webauthn4j.data.PublicKeyCredentialParameters convertParamToWebauthn4j(
PublicKeyCredentialParameters parameter) {
if (parameter.getType() != PublicKeyCredentialType.PUBLIC_KEY) {
throw new IllegalArgumentException(
"Cannot convert unknown credential type " + parameter.getType() + " to webauthn4j");
}
long algValue = parameter.getAlg().getValue();
com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier alg = com.webauthn4j.data.attestation.statement.COSEAlgorithmIdentifier
.create(algValue);
return new com.webauthn4j.data.PublicKeyCredentialParameters(
com.webauthn4j.data.PublicKeyCredentialType.PUBLIC_KEY, alg);
}
private Set<Origin> toOrigins() {
return this.allowedOrigins.stream().map(Origin::new).collect(Collectors.toSet());
}
private static Set<AuthenticatorTransport> convertTransports(
Set<com.webauthn4j.data.AuthenticatorTransport> transports) {
if (transports == null) {
return Collections.emptySet();
}
return transports.stream()
.map((t) -> AuthenticatorTransport.valueOf(t.getValue()))
.collect(Collectors.toUnmodifiableSet());
}
@Override
public PublicKeyCredentialRequestOptions createCredentialRequestOptions(
PublicKeyCredentialRequestOptionsRequest request) {
Authentication authentication = request.getAuthentication();
// FIXME: do not load credentialRecords if anonymous
PublicKeyCredentialUserEntity userEntity = findUserEntityOrCreateAndSave(authentication.getName());
List<CredentialRecord> credentialRecords = this.userCredentials.findByUserId(userEntity.getId());
return PublicKeyCredentialRequestOptions.builder()
.allowCredentials(credentialDescriptors(credentialRecords))
.challenge(Bytes.random())
.rpId(this.rp.getId())
.timeout(Duration.ofMinutes(5))
.userVerification(UserVerificationRequirement.PREFERRED)
.customize(this.customizeRequestOptions)
.build();
}
@Override
public PublicKeyCredentialUserEntity authenticate(RelyingPartyAuthenticationRequest request) {
PublicKeyCredentialRequestOptions requestOptions = request.getRequestOptions();
AuthenticatorAssertionResponse assertionResponse = request.getPublicKey().getResponse();
Bytes keyId = request.getPublicKey().getRawId();
CredentialRecord credentialRecord = this.userCredentials.findByCredentialId(keyId);
CborConverter cborConverter = this.objectConverter.getCborConverter();
AttestationObject attestationObject = cborConverter
.readValue(credentialRecord.getAttestationObject().getBytes(), AttestationObject.class);
AuthenticatorData<RegistrationExtensionAuthenticatorOutput> authData = attestationObject.getAuthenticatorData();
AttestedCredentialData data = new AttestedCredentialData(authData.getAttestedCredentialData().getAaguid(),
keyId.getBytes(), authData.getAttestedCredentialData().getCOSEKey());
Authenticator authenticator = new AuthenticatorImpl(data, attestationObject.getAttestationStatement(),
credentialRecord.getSignatureCount());
if (authenticator == null) {
throw new IllegalStateException("No authenticator found");
}
Set<Origin> origins = toOrigins();
Challenge challenge = new DefaultChallenge(requestOptions.getChallenge().getBytes());
// FIXME: should populate this
byte[] tokenBindingId = null /* set tokenBindingId */;
ServerProperty serverProperty = new ServerProperty(origins, requestOptions.getRpId(), challenge,
tokenBindingId);
boolean userVerificationRequired = request.getRequestOptions()
.getUserVerification() == UserVerificationRequirement.REQUIRED;
com.webauthn4j.data.AuthenticationRequest authenticationRequest = new com.webauthn4j.data.AuthenticationRequest(
request.getPublicKey().getId().getBytes(), assertionResponse.getAuthenticatorData().getBytes(),
assertionResponse.getClientDataJSON().getBytes(), assertionResponse.getSignature().getBytes());
AuthenticationParameters authenticationParameters = new AuthenticationParameters(serverProperty, authenticator,
userVerificationRequired);
AuthenticationData authenticationData = this.webAuthnManager.validate(authenticationRequest,
authenticationParameters);
long updatedSignCount = authenticationData.getAuthenticatorData().getSignCount();
ImmutableCredentialRecord updatedRecord = ImmutableCredentialRecord.fromCredentialRecord(credentialRecord)
.lastUsed(Instant.now())
.signatureCount(updatedSignCount)
.build();
this.userCredentials.save(updatedRecord);
return this.userEntities.findById(credentialRecord.getUserEntityUserId());
}
}
@@ -0,0 +1,217 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.registration;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
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.CredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A {@link jakarta.servlet.Filter} that renders a default WebAuthn registration page.
*
* @author Rob Winch
* @author Daniel Garnier-Moiroux
* @since 6.4
*/
public class DefaultWebAuthnRegistrationPageGeneratingFilter extends OncePerRequestFilter {
private RequestMatcher matcher = AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/webauthn/register");
private final PublicKeyCredentialUserEntityRepository userEntities;
private final UserCredentialRepository userCredentials;
/**
* Creates a new instance.
* @param userEntities the {@link PublicKeyCredentialUserEntity}
* @param userCredentials
*/
public DefaultWebAuthnRegistrationPageGeneratingFilter(PublicKeyCredentialUserEntityRepository userEntities,
UserCredentialRepository userCredentials) {
Assert.notNull(userEntities, "userEntities cannot be null");
Assert.notNull(userCredentials, "userCredentials cannot be null");
this.userEntities = userEntities;
this.userCredentials = userCredentials;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.matcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.setStatus(HttpServletResponse.SC_OK);
String processedTemplate = HtmlTemplates.fromTemplate(HTML_TEMPLATE)
.withValue("contextPath", request.getContextPath())
.withRawHtml("csrfHeaders", renderCsrfHeader(csrfToken))
.withRawHtml("passkeys", passkeyRows(request.getRemoteUser(), request.getContextPath(), csrfToken))
.render();
response.getWriter().write(processedTemplate);
}
private String passkeyRows(String username, String contextPath, CsrfToken csrfToken) {
PublicKeyCredentialUserEntity userEntity = this.userEntities.findByUsername(username);
List<CredentialRecord> credentials = (userEntity != null)
? this.userCredentials.findByUserId(userEntity.getId()) : Collections.emptyList();
if (credentials.isEmpty()) {
return """
<tr><td colspan="5">No Passkeys</td></tr>
""";
}
return credentials.stream()
.map((credentialRecord) -> renderPasskeyRow(credentialRecord, contextPath, csrfToken))
.collect(Collectors.joining("\n"));
}
private String renderPasskeyRow(CredentialRecord credential, String contextPath, CsrfToken csrfToken) {
return HtmlTemplates.fromTemplate(PASSKEY_ROW_TEMPLATE)
.withValue("label", credential.getLabel())
.withValue("created", formatInstant(credential.getCreated()))
.withValue("lastUsed", formatInstant(credential.getLastUsed()))
.withValue("signatureCount", credential.getSignatureCount())
.withValue("credentialId", credential.getCredentialId().toBase64UrlString())
.withValue("csrfParameterName", csrfToken.getParameterName())
.withValue("csrfToken", csrfToken.getToken())
.withValue("contextPath", contextPath)
.render();
}
private static String formatInstant(Instant created) {
return ZonedDateTime.ofInstant(created, ZoneId.of("UTC"))
.truncatedTo(ChronoUnit.SECONDS)
.format(DateTimeFormatter.ISO_INSTANT);
}
private String renderCsrfHeader(CsrfToken csrfToken) {
return HtmlTemplates.fromTemplate(CSRF_HEADERS)
.withValue("headerName", csrfToken.getHeaderName())
.withValue("headerValue", csrfToken.getToken())
.render();
}
private static final String HTML_TEMPLATE = """
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>WebAuthn Registration</title>
<link href="{{contextPath}}/default-ui.css" rel="stylesheet" />
<script type="text/javascript" src="{{contextPath}}/login/webauthn.js"></script>
<script type="text/javascript">
<!--
const ui = {
getRegisterButton: function() {
return document.getElementById('register')
},
getSuccess: function() {
return document.getElementById('success')
},
getError: function() {
return document.getElementById('error')
},
getLabelInput: function() {
return document.getElementById('label')
},
getDeleteForms: function() {
return Array.from(document.getElementsByClassName("delete-form"))
},
}
document.addEventListener("DOMContentLoaded",() => setupRegistration({{csrfHeaders}}, "{{contextPath}}", ui));
//-->
</script>
</head>
<body>
<div class="content">
<h2 class="center">WebAuthn Registration</h2>
<form class="default-form" method="post" action="#" onclick="return false">
<div id="success" class="alert alert-success" role="alert">Success!</div>
<div id="error" class="alert alert-danger" role="alert"></div>
<p>
<label for="label" class="screenreader">Passkey Label</label>
<input type="text" id="label" name="label" placeholder="Passkey Label" required autofocus>
</p>
<button id="register" class="primary" type="submit">Register</button>
</form>
<table class="table table-striped">
<thead>
<tr class="table-header">
<th>Label</th>
<th>Created</th>
<th>Last Used</th>
<th>Signature Count</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{{passkeys}}
</tbody>
</table>
</div>
</body>
</html>
""";
private static final String PASSKEY_ROW_TEMPLATE = """
<tr class="v-middle">
<td>{{label}}</td>
<td>{{created}}</td>
<td>{{lastUsed}}</td>
<td class="center">{{signatureCount}}</td>
<td>
<form class="delete-form no-margin" method="post" action="{{contextPath}}/webauthn/register/{{credentialId}}">
<input type="hidden" name="method" value="delete">
<input type="hidden" name="{{csrfParameterName}}" value="{{csrfToken}}">
<button class="primary small" type="submit">Delete</button>
</form>
</td>
</tr>
""";
private static final String CSRF_HEADERS = """
{"{{headerName}}" : "{{headerValue}}"}""";
}
@@ -0,0 +1,110 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.registration;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.HtmlUtils;
/**
* Render HTML templates using string substitution. Intended for internal use. Variables
* can be templated using double curly-braces: {@code {{name}}}.
*
* @author Daniel Garnier-Moiroux
* @since 6.4
* @see org.springframework.security.web.authentication.ui.HtmlTemplates
*/
final class HtmlTemplates {
private HtmlTemplates() {
}
static HtmlTemplates.Builder fromTemplate(String template) {
return new HtmlTemplates.Builder(template);
}
static final class Builder {
private final String template;
private final Map<String, String> values = new HashMap<>();
private Builder(String template) {
this.template = template;
}
/**
* HTML-escape, and inject value {@code value} in every {@code {{key}}}
* placeholder.
* @param key the placeholder name
* @param value the value to inject
* @return this instance for further templating
*/
HtmlTemplates.Builder withValue(String key, Object value) {
Assert.notNull(value, "value cannot be null");
this.values.put(key, HtmlUtils.htmlEscape(value.toString()));
return this;
}
/**
* Inject value {@code value} in every {@code {{key}}} placeholder without
* HTML-escaping. Useful for injecting "sub-templates".
* @param key the placeholder name
* @param value the value to inject
* @return this instance for further templating
*/
HtmlTemplates.Builder withRawHtml(String key, String value) {
if (!value.isEmpty() && value.charAt(value.length() - 1) == '\n') {
value = value.substring(0, value.length() - 1);
}
this.values.put(key, value);
return this;
}
/**
* Render the template. All placeholders MUST have a corresponding value. If a
* placeholder does not have a corresponding value, throws
* {@link IllegalStateException}.
* @return the rendered template
*/
String render() {
String template = this.template;
for (String key : this.values.keySet()) {
String pattern = Pattern.quote("{{" + key + "}}");
template = template.replaceAll(pattern, this.values.get(key));
}
String unusedPlaceholders = Pattern.compile("\\{\\{([a-zA-Z0-9]+)}}")
.matcher(template)
.results()
.map((result) -> result.group(1))
.collect(Collectors.joining(", "));
if (StringUtils.hasLength(unusedPlaceholders)) {
throw new IllegalStateException("Unused placeholders in template: [%s]".formatted(unusedPlaceholders));
}
return template;
}
}
}
@@ -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.registration;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.util.Assert;
public class HttpSessionPublicKeyCredentialCreationOptionsRepository
implements PublicKeyCredentialCreationOptionsRepository {
static final String DEFAULT_ATTR_NAME = PublicKeyCredentialCreationOptions.class.getName().concat("ATTR_NAME");
private String attrName = DEFAULT_ATTR_NAME;
@Override
public void save(HttpServletRequest request, HttpServletResponse response,
PublicKeyCredentialCreationOptions options) {
request.getSession().setAttribute(this.attrName, options);
}
public PublicKeyCredentialCreationOptions load(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return (PublicKeyCredentialCreationOptions) session.getAttribute(this.attrName);
}
public void setAttrName(String attrName) {
Assert.notNull(attrName, "attrName cannot be null");
this.attrName = attrName;
}
}
@@ -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.
*/
package org.springframework.security.web.webauthn.registration;
import java.io.IOException;
import java.util.function.Supplier;
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.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
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.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module;
import org.springframework.security.web.webauthn.management.ImmutablePublicKeyCredentialCreationOptionsRequest;
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 PublicKeyCredentialCreationOptions} for <a href=
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">creating</a>
* a new credential.
*/
public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilter {
private PublicKeyCredentialCreationOptionsRepository repository = new HttpSessionPublicKeyCredentialCreationOptionsRepository();
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private RequestMatcher matcher = antMatcher(HttpMethod.POST, "/webauthn/register/options");
private AuthorizationManager<HttpServletRequest> authorization = AuthenticatedAuthorizationManager.authenticated();
private final WebAuthnRelyingPartyOperations rpOperations;
private final HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build());
/**
* Creates a new instance.
* @param rpOperations the {@link WebAuthnRelyingPartyOperations} to use. Cannot be
* null.
*/
public PublicKeyCredentialCreationOptionsFilter(WebAuthnRelyingPartyOperations rpOperations) {
Assert.notNull(rpOperations, "rpOperations cannot be null");
this.rpOperations = rpOperations;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.matcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
Supplier<SecurityContext> context = this.securityContextHolderStrategy.getDeferredContext();
Supplier<Authentication> authentication = () -> context.get().getAuthentication();
AuthorizationDecision decision = this.authorization.check(authentication, request);
if (!decision.isGranted()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
PublicKeyCredentialCreationOptions options = this.rpOperations.createPublicKeyCredentialCreationOptions(
new ImmutablePublicKeyCredentialCreationOptionsRequest(authentication.get()));
this.repository.save(request, response, options);
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
this.converter.write(options, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
}
}
@@ -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.registration;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
/**
* Saves {@link PublicKeyCredentialCreationOptions} between a request to generate an
* assertion and the validation of the assertion.
*
* @author Rob Winch
* @since 6.4
*/
public interface PublicKeyCredentialCreationOptionsRepository {
/**
* Saves the provided {@link PublicKeyCredentialCreationOptions} or clears an existing
* {@link PublicKeyCredentialCreationOptions} if {@code options} is null.
* @param request the {@link HttpServletRequest}
* @param response the {@link HttpServletResponse}
* @param options the {@link PublicKeyCredentialCreationOptions} to save or null if an
* existing {@link PublicKeyCredentialCreationOptions} should be removed.
*/
void save(HttpServletRequest request, HttpServletResponse response, PublicKeyCredentialCreationOptions options);
/**
* Gets a saved {@link PublicKeyCredentialCreationOptions} if it exists, otherwise
* null.
* @param request the {@link HttpServletRequest}
* @return the {@link PublicKeyCredentialCreationOptions} that was saved, otherwise
* null.
*/
PublicKeyCredentialCreationOptions load(HttpServletRequest request);
}
@@ -0,0 +1,211 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.webauthn.registration;
import java.io.IOException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module;
import org.springframework.security.web.webauthn.management.ImmutableRelyingPartyRegistrationRequest;
import org.springframework.security.web.webauthn.management.RelyingPartyPublicKey;
import org.springframework.security.web.webauthn.management.UserCredentialRepository;
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;
/**
* Authenticates {@code PublicKeyCredential<AuthenticatorAssertionResponse>} that is
* parsed from the body of the {@link HttpServletRequest} using the
* {@link #setConverter(HttpMessageConverter)}. An example request is provided below:
*
* <pre>
* {
* "publicKey": {
* "credential": {
* "id": "dYF7EGnRFFIXkpXi9XU2wg",
* "rawId": "dYF7EGnRFFIXkpXi9XU2wg",
* "response": {
* "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YViUy9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNhdAAAAALraVWanqkAfvZZFYZpVEg0AEHWBexBp0RRSF5KV4vV1NsKlAQIDJiABIVggQjmrekPGzyqtoKK9HPUH-8Z2FLpoqkklFpFPQVICQ3IiWCD6I9Jvmor685fOZOyGXqUd87tXfvJk8rxj9OhuZvUALA",
* "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSl9RTi10SFJYRWVKYjlNcUNrWmFPLUdOVmlibXpGVGVWMk43Z0ptQUdrQSIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
* "transports": [
* "internal",
* "hybrid"
* ]
* },
* "type": "public-key",
* "clientExtensionResults": {},
* "authenticatorAttachment": "platform"
* },
* "label": "1password"
* }
* </pre>
*
* @author Rob Winch
* @since 6.4
*/
public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
static final String DEFAULT_REGISTER_CREDENTIAL_URL = "/webauthn/register";
private static final Log logger = LogFactory.getLog(WebAuthnRegistrationFilter.class);
private final WebAuthnRelyingPartyOperations rpOptions;
private final UserCredentialRepository userCredentials;
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
JsonMapper.builder().addModule(new WebauthnJackson2Module()).build());
private PublicKeyCredentialCreationOptionsRepository creationOptionsRepository = new HttpSessionPublicKeyCredentialCreationOptionsRepository();
private RequestMatcher registerCredentialMatcher = antMatcher(HttpMethod.POST, DEFAULT_REGISTER_CREDENTIAL_URL);
private RequestMatcher removeCredentialMatcher = antMatcher(HttpMethod.DELETE, "/webauthn/register/{id}");
public WebAuthnRegistrationFilter(UserCredentialRepository userCredentials,
WebAuthnRelyingPartyOperations rpOptions) {
Assert.notNull(userCredentials, "userCredentials must not be null");
Assert.notNull(rpOptions, "rpOptions must not be null");
this.userCredentials = userCredentials;
this.rpOptions = rpOptions;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (this.registerCredentialMatcher.matches(request)) {
registerCredential(request, response);
return;
}
RequestMatcher.MatchResult removeMatchResult = this.removeCredentialMatcher.matcher(request);
if (removeMatchResult.isMatch()) {
String id = removeMatchResult.getVariables().get("id");
removeCredential(request, response, id);
return;
}
filterChain.doFilter(request, response);
}
/**
* Set the {@link HttpMessageConverter} to read the
* {@link WebAuthnRegistrationRequest} and write the response. The default is
* {@link MappingJackson2HttpMessageConverter}.
* @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 PublicKeyCredentialCreationOptionsRepository} to use. The default
* is {@link HttpSessionPublicKeyCredentialCreationOptionsRepository}.
* @param creationOptionsRepository the
* {@link PublicKeyCredentialCreationOptionsRepository} to use. Cannot be null.
*/
public void setCreationOptionsRepository(PublicKeyCredentialCreationOptionsRepository creationOptionsRepository) {
Assert.notNull(creationOptionsRepository, "creationOptionsRepository cannot be null");
this.creationOptionsRepository = creationOptionsRepository;
}
private void registerCredential(HttpServletRequest request, HttpServletResponse response) throws IOException {
WebAuthnRegistrationRequest registrationRequest = readRegistrationRequest(request);
if (registrationRequest == null) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
return;
}
PublicKeyCredentialCreationOptions options = this.creationOptionsRepository.load(request);
if (options == null) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
return;
}
this.creationOptionsRepository.save(request, response, null);
CredentialRecord credentialRecord = this.rpOptions.registerCredential(
new ImmutableRelyingPartyRegistrationRequest(options, registrationRequest.getPublicKey()));
SuccessfulUserRegistrationResponse registrationResponse = new SuccessfulUserRegistrationResponse(
credentialRecord);
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
this.converter.write(registrationResponse, MediaType.APPLICATION_JSON, outputMessage);
}
private WebAuthnRegistrationRequest readRegistrationRequest(HttpServletRequest request) {
HttpInputMessage inputMessage = new ServletServerHttpRequest(request);
try {
return (WebAuthnRegistrationRequest) this.converter.read(WebAuthnRegistrationRequest.class, inputMessage);
}
catch (Exception ex) {
logger.debug("Unable to parse WebAuthnRegistrationRequest", ex);
return null;
}
}
private void removeCredential(HttpServletRequest request, HttpServletResponse response, String id)
throws IOException {
this.userCredentials.delete(Bytes.fromBase64(id));
response.setStatus(HttpStatus.NO_CONTENT.value());
}
static class WebAuthnRegistrationRequest {
private RelyingPartyPublicKey publicKey;
RelyingPartyPublicKey getPublicKey() {
return this.publicKey;
}
void setPublicKey(RelyingPartyPublicKey publicKey) {
this.publicKey = publicKey;
}
}
public static class SuccessfulUserRegistrationResponse {
private final CredentialRecord credentialRecord;
SuccessfulUserRegistrationResponse(CredentialRecord credentialRecord) {
this.credentialRecord = credentialRecord;
}
public boolean isSuccess() {
return true;
}
}
}

Some files were not shown because too many files have changed in this diff Show More