Enable Null checking in spring-security-webauthn via JSpecify

Closes gh-17839
This commit is contained in:
Rob Winch 2025-09-03 12:04:09 -05:00
parent 3dbcf266e9
commit 0a991a91ce
No known key found for this signature in database
46 changed files with 421 additions and 152 deletions

View File

@ -16,6 +16,8 @@
package org.springframework.security.web.webauthn.aot;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.jdbc.core.JdbcOperations;
@ -33,7 +35,7 @@ import org.springframework.security.web.webauthn.management.PublicKeyCredentialU
class PublicKeyCredentialUserEntityRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
hints.resources().registerPattern("org/springframework/security/user-entities-schema.sql");
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.web.webauthn.aot;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.jdbc.core.JdbcOperations;
@ -33,7 +35,7 @@ import org.springframework.security.web.webauthn.management.UserCredentialReposi
class UserCredentialRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
hints.resources()
.registerPattern("org/springframework/security/user-credentials-schema.sql")
.registerPattern("org/springframework/security/user-credentials-schema-postgres.sql");

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* WebAuthn AOT support.
*/
@NullMarked
package org.springframework.security.web.webauthn.aot;
import org.jspecify.annotations.NullMarked;

View File

@ -18,6 +18,10 @@ package org.springframework.security.web.webauthn.api;
import java.io.Serial;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse">AuthenticatorAssertionResponse</a>
@ -47,12 +51,12 @@ public final class AuthenticatorAssertionResponse extends AuthenticatorResponse
private final Bytes signature;
private final Bytes userHandle;
private final @Nullable Bytes userHandle;
private final Bytes attestationObject;
private final @Nullable Bytes attestationObject;
private AuthenticatorAssertionResponse(Bytes clientDataJSON, Bytes authenticatorData, Bytes signature,
Bytes userHandle, Bytes attestationObject) {
@Nullable Bytes userHandle, @Nullable Bytes attestationObject) {
super(clientDataJSON);
this.authenticatorData = authenticatorData;
this.signature = signature;
@ -101,7 +105,7 @@ public final class AuthenticatorAssertionResponse extends AuthenticatorResponse
* 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() {
public @Nullable Bytes getUserHandle() {
return this.userHandle;
}
@ -113,7 +117,7 @@ public final class AuthenticatorAssertionResponse extends AuthenticatorResponse
* object</a>, if the authenticator supports attestation in assertions.
* @return the {@code attestationObject}
*/
public Bytes getAttestationObject() {
public @Nullable Bytes getAttestationObject() {
return this.attestationObject;
}
@ -133,15 +137,15 @@ public final class AuthenticatorAssertionResponse extends AuthenticatorResponse
*/
public static final class AuthenticatorAssertionResponseBuilder {
private Bytes authenticatorData;
private @Nullable Bytes authenticatorData;
private Bytes signature;
private @Nullable Bytes signature;
private Bytes userHandle;
private @Nullable Bytes userHandle;
private Bytes attestationObject;
private @Nullable Bytes attestationObject;
private Bytes clientDataJSON;
private @Nullable Bytes clientDataJSON;
private AuthenticatorAssertionResponseBuilder() {
}
@ -201,6 +205,9 @@ public final class AuthenticatorAssertionResponse extends AuthenticatorResponse
* @return the {@link AuthenticatorAssertionResponse}
*/
public AuthenticatorAssertionResponse build() {
Assert.notNull(this.clientDataJSON, "clientDataJSON cannot be null");
Assert.notNull(this.authenticatorData, "authenticatorData cannot be null");
Assert.notNull(this.signature, "signature cannot be null");
return new AuthenticatorAssertionResponse(this.clientDataJSON, this.authenticatorData, this.signature,
this.userHandle, this.attestationObject);
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.web.webauthn.api;
import java.util.Arrays;
import java.util.List;
import org.jspecify.annotations.Nullable;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse">AuthenticatorAttestationResponse</a>
@ -36,10 +38,10 @@ public final class AuthenticatorAttestationResponse extends AuthenticatorRespons
private final Bytes attestationObject;
private final List<AuthenticatorTransport> transports;
private final @Nullable List<AuthenticatorTransport> transports;
private AuthenticatorAttestationResponse(Bytes clientDataJSON, Bytes attestationObject,
List<AuthenticatorTransport> transports) {
@Nullable List<AuthenticatorTransport> transports) {
super(clientDataJSON);
this.attestationObject = attestationObject;
this.transports = transports;
@ -63,7 +65,7 @@ public final class AuthenticatorAttestationResponse extends AuthenticatorRespons
* "https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-transports-slot">transports</a>
* @return the transports
*/
public List<AuthenticatorTransport> getTransports() {
public @Nullable List<AuthenticatorTransport> getTransports() {
return this.transports;
}
@ -83,10 +85,12 @@ public final class AuthenticatorAttestationResponse extends AuthenticatorRespons
*/
public static final class AuthenticatorAttestationResponseBuilder {
@SuppressWarnings("NullAway.Init")
private Bytes attestationObject;
private List<AuthenticatorTransport> transports;
private @Nullable List<AuthenticatorTransport> transports;
@SuppressWarnings("NullAway.Init")
private Bytes clientDataJSON;
private AuthenticatorAttestationResponseBuilder() {

View File

@ -16,6 +16,8 @@
package org.springframework.security.web.webauthn.api;
import org.jspecify.annotations.Nullable;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-authenticatorselectioncriteria">AuthenticatorAttachment</a>
@ -33,11 +35,11 @@ package org.springframework.security.web.webauthn.api;
*/
public final class AuthenticatorSelectionCriteria {
private final AuthenticatorAttachment authenticatorAttachment;
private final @Nullable AuthenticatorAttachment authenticatorAttachment;
private final ResidentKeyRequirement residentKey;
private final @Nullable ResidentKeyRequirement residentKey;
private final UserVerificationRequirement userVerification;
private final @Nullable UserVerificationRequirement userVerification;
// NOTE: There is no requireResidentKey property because it is only for backward
// compatibility with WebAuthn Level 1
@ -48,8 +50,8 @@ public final class AuthenticatorSelectionCriteria {
* @param residentKey the resident key requirement
* @param userVerification the user verification
*/
private AuthenticatorSelectionCriteria(AuthenticatorAttachment authenticatorAttachment,
ResidentKeyRequirement residentKey, UserVerificationRequirement userVerification) {
private AuthenticatorSelectionCriteria(@Nullable AuthenticatorAttachment authenticatorAttachment,
@Nullable ResidentKeyRequirement residentKey, @Nullable UserVerificationRequirement userVerification) {
this.authenticatorAttachment = authenticatorAttachment;
this.residentKey = residentKey;
this.userVerification = userVerification;
@ -67,7 +69,7 @@ public final class AuthenticatorSelectionCriteria {
* Authenticator Attachment Modality</a>).
* @return the authenticator attachment
*/
public AuthenticatorAttachment getAuthenticatorAttachment() {
public @Nullable AuthenticatorAttachment getAuthenticatorAttachment() {
return this.authenticatorAttachment;
}
@ -81,7 +83,7 @@ public final class AuthenticatorSelectionCriteria {
* discoverable credential</a>.
* @return the resident key requirement
*/
public ResidentKeyRequirement getResidentKey() {
public @Nullable ResidentKeyRequirement getResidentKey() {
return this.residentKey;
}
@ -96,7 +98,7 @@ public final class AuthenticatorSelectionCriteria {
* operation.
* @return the user verification requirement
*/
public UserVerificationRequirement getUserVerification() {
public @Nullable UserVerificationRequirement getUserVerification() {
return this.userVerification;
}
@ -116,11 +118,11 @@ public final class AuthenticatorSelectionCriteria {
*/
public static final class AuthenticatorSelectionCriteriaBuilder {
private AuthenticatorAttachment authenticatorAttachment;
private @Nullable AuthenticatorAttachment authenticatorAttachment;
private ResidentKeyRequirement residentKey;
private @Nullable ResidentKeyRequirement residentKey;
private UserVerificationRequirement userVerification;
private @Nullable UserVerificationRequirement userVerification;
private AuthenticatorSelectionCriteriaBuilder() {
}

View File

@ -22,6 +22,8 @@ import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@ -100,7 +102,7 @@ public final class Bytes implements Serializable {
* @param base64UrlString the base64 url string
* @return the {@link Bytes}
*/
public static Bytes fromBase64(String base64UrlString) {
public static Bytes fromBase64(@Nullable String base64UrlString) {
byte[] bytes = DECODER.decode(base64UrlString);
return new Bytes(bytes);
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.web.webauthn.api;
import java.time.Instant;
import java.util.Set;
import org.jspecify.annotations.Nullable;
/**
* Represents a <a href="https://www.w3.org/TR/webauthn-3/#credential-record">Credential
* Record</a> that is stored by the Relying Party
@ -35,7 +37,7 @@ public interface CredentialRecord {
* "https://www.w3.org/TR/webauthn-3/#abstract-opdef-credential-record-type">credential.type</a>
* @return
*/
PublicKeyCredentialType getCredentialType();
@Nullable PublicKeyCredentialType getCredentialType();
/**
* The <a href=
@ -104,7 +106,7 @@ public interface CredentialRecord {
* source was registered.
* @return the attestationObject
*/
Bytes getAttestationObject();
@Nullable Bytes getAttestationObject();
/**
* The <a href=
@ -113,7 +115,7 @@ public interface CredentialRecord {
* source was registered.
* @return
*/
Bytes getAttestationClientDataJSON();
@Nullable Bytes getAttestationClientDataJSON();
/**
* A human-readable label for this {@link CredentialRecord} assigned by the user.

View File

@ -19,6 +19,8 @@ package org.springframework.security.web.webauthn.api;
import java.time.Instant;
import java.util.Set;
import org.jspecify.annotations.Nullable;
/**
* An immutable {@link CredentialRecord}.
*
@ -27,7 +29,7 @@ import java.util.Set;
*/
public final class ImmutableCredentialRecord implements CredentialRecord {
private final PublicKeyCredentialType credentialType;
private final @Nullable PublicKeyCredentialType credentialType;
private final Bytes credentialId;
@ -45,9 +47,9 @@ public final class ImmutableCredentialRecord implements CredentialRecord {
private final boolean backupState;
private final Bytes attestationObject;
private final @Nullable Bytes attestationObject;
private final Bytes attestationClientDataJSON;
private final @Nullable Bytes attestationClientDataJSON;
private final Instant created;
@ -55,10 +57,10 @@ public final class ImmutableCredentialRecord implements CredentialRecord {
private final String label;
private ImmutableCredentialRecord(PublicKeyCredentialType credentialType, Bytes credentialId,
private ImmutableCredentialRecord(@Nullable 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) {
@Nullable Bytes attestationObject, @Nullable Bytes attestationClientDataJSON, Instant created, Instant lastUsed, String label) {
this.credentialType = credentialType;
this.credentialId = credentialId;
this.userEntityUserId = userEntityUserId;
@ -76,7 +78,7 @@ public final class ImmutableCredentialRecord implements CredentialRecord {
}
@Override
public PublicKeyCredentialType getCredentialType() {
public @Nullable PublicKeyCredentialType getCredentialType() {
return this.credentialType;
}
@ -121,12 +123,12 @@ public final class ImmutableCredentialRecord implements CredentialRecord {
}
@Override
public Bytes getAttestationObject() {
public @Nullable Bytes getAttestationObject() {
return this.attestationObject;
}
@Override
public Bytes getAttestationClientDataJSON() {
public @Nullable Bytes getAttestationClientDataJSON() {
return this.attestationClientDataJSON;
}
@ -155,32 +157,37 @@ public final class ImmutableCredentialRecord implements CredentialRecord {
public static final class ImmutableCredentialRecordBuilder {
private PublicKeyCredentialType credentialType;
private @Nullable PublicKeyCredentialType credentialType;
@SuppressWarnings("NullAway.Init")
private Bytes credentialId;
@SuppressWarnings("NullAway.Init")
private Bytes userEntityUserId;
@SuppressWarnings("NullAway.Init")
private PublicKeyCose publicKey;
private long signatureCount;
private boolean uvInitialized;
@SuppressWarnings("NullAway.Init")
private Set<AuthenticatorTransport> transports;
private boolean backupEligible;
private boolean backupState;
private Bytes attestationObject;
private @Nullable Bytes attestationObject;
private Bytes attestationClientDataJSON;
private @Nullable Bytes attestationClientDataJSON;
private Instant created = Instant.now();
private Instant lastUsed = this.created;
@SuppressWarnings("NullAway.Init")
private String label;
private ImmutableCredentialRecordBuilder() {
@ -248,12 +255,12 @@ public final class ImmutableCredentialRecord implements CredentialRecord {
return this;
}
public ImmutableCredentialRecordBuilder attestationObject(Bytes attestationObject) {
public ImmutableCredentialRecordBuilder attestationObject(@Nullable Bytes attestationObject) {
this.attestationObject = attestationObject;
return this;
}
public ImmutableCredentialRecordBuilder attestationClientDataJSON(Bytes attestationClientDataJSON) {
public ImmutableCredentialRecordBuilder attestationClientDataJSON(@Nullable Bytes attestationClientDataJSON) {
this.attestationClientDataJSON = attestationClientDataJSON;
return this;
}

View File

@ -19,6 +19,10 @@ package org.springframework.security.web.webauthn.api;
import java.util.Arrays;
import java.util.Base64;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* An immutable {@link PublicKeyCose}
*
@ -33,7 +37,8 @@ public class ImmutablePublicKeyCose implements PublicKeyCose {
* Creates a new instance.
* @param bytes the raw bytes of the public key.
*/
public ImmutablePublicKeyCose(byte[] bytes) {
public ImmutablePublicKeyCose(byte @Nullable [] bytes) {
Assert.notNull(bytes, "bytes cannot be null");
this.bytes = Arrays.copyOf(bytes, bytes.length);
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.web.webauthn.api;
import java.io.Serial;
import org.jspecify.annotations.Nullable;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialuserentity">PublicKeyCredentialUserEntity</a>
@ -102,9 +104,9 @@ public final class ImmutablePublicKeyCredentialUserEntity implements PublicKeyCr
* fits within 64 bytes. See 6.4.1 String Truncation about truncation and other
* considerations.
*/
private final String displayName;
private final @Nullable String displayName;
private ImmutablePublicKeyCredentialUserEntity(String name, Bytes id, String displayName) {
private ImmutablePublicKeyCredentialUserEntity(String name, Bytes id, @Nullable String displayName) {
this.name = name;
this.id = id;
this.displayName = displayName;
@ -121,7 +123,7 @@ public final class ImmutablePublicKeyCredentialUserEntity implements PublicKeyCr
}
@Override
public String getDisplayName() {
public @Nullable String getDisplayName() {
return this.displayName;
}
@ -141,11 +143,13 @@ public final class ImmutablePublicKeyCredentialUserEntity implements PublicKeyCr
*/
public static final class PublicKeyCredentialUserEntityBuilder {
@SuppressWarnings("NullAway.Init")
private String name;
@SuppressWarnings("NullAway.Init")
private Bytes id;
private String displayName;
private @Nullable String displayName;
private PublicKeyCredentialUserEntityBuilder() {
}

View File

@ -19,6 +19,10 @@ package org.springframework.security.web.webauthn.api;
import java.io.Serial;
import java.io.Serializable;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* <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
@ -40,13 +44,13 @@ public final class PublicKeyCredential<R extends AuthenticatorResponse> implemen
private final R response;
private final AuthenticatorAttachment authenticatorAttachment;
private final @Nullable AuthenticatorAttachment authenticatorAttachment;
private final AuthenticationExtensionsClientOutputs clientExtensionResults;
private final @Nullable AuthenticationExtensionsClientOutputs clientExtensionResults;
private PublicKeyCredential(String id, PublicKeyCredentialType type, Bytes rawId, R response,
AuthenticatorAttachment authenticatorAttachment,
AuthenticationExtensionsClientOutputs clientExtensionResults) {
@Nullable AuthenticatorAttachment authenticatorAttachment,
@Nullable AuthenticationExtensionsClientOutputs clientExtensionResults) {
this.id = id;
this.type = type;
this.rawId = rawId;
@ -107,7 +111,7 @@ public final class PublicKeyCredential<R extends AuthenticatorResponse> implemen
* navigator.credentials.get() methods successfully complete.
* @return the authenticator attachment
*/
public AuthenticatorAttachment getAuthenticatorAttachment() {
public @Nullable AuthenticatorAttachment getAuthenticatorAttachment() {
return this.authenticatorAttachment;
}
@ -117,7 +121,7 @@ public final class PublicKeyCredential<R extends AuthenticatorResponse> implemen
* is a mapping of extension identifier to client extension output.
* @return the extension results
*/
public AuthenticationExtensionsClientOutputs getClientExtensionResults() {
public @Nullable AuthenticationExtensionsClientOutputs getClientExtensionResults() {
return this.clientExtensionResults;
}
@ -139,17 +143,20 @@ public final class PublicKeyCredential<R extends AuthenticatorResponse> implemen
*/
public static final class PublicKeyCredentialBuilder<R extends AuthenticatorResponse> {
@SuppressWarnings("NullAway.Init")
private String id;
private PublicKeyCredentialType type;
private @Nullable PublicKeyCredentialType type;
@SuppressWarnings("NullAway.Init")
private Bytes rawId;
@SuppressWarnings("NullAway.Init")
private R response;
private AuthenticatorAttachment authenticatorAttachment;
private @Nullable AuthenticatorAttachment authenticatorAttachment;
private AuthenticationExtensionsClientOutputs clientExtensionResults;
private @Nullable AuthenticationExtensionsClientOutputs clientExtensionResults;
private PublicKeyCredentialBuilder() {
}
@ -220,6 +227,10 @@ public final class PublicKeyCredential<R extends AuthenticatorResponse> implemen
* @return a new {@link PublicKeyCredential}
*/
public PublicKeyCredential<R> build() {
Assert.notNull(this.id, "id cannot be null");
Assert.notNull(this.type, "type cannot be null");
Assert.notNull(this.rawId, "rawId cannot be null");
Assert.notNull(this.response, "response cannot be null");
return new PublicKeyCredential(this.id, this.type, this.rawId, this.response, this.authenticatorAttachment,
this.clientExtensionResults);
}

View File

@ -22,6 +22,8 @@ import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
/**
* Represents the <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions">PublicKeyCredentialCreationOptions</a>
@ -42,21 +44,22 @@ public final class PublicKeyCredentialCreationOptions {
private final List<PublicKeyCredentialParameters> pubKeyCredParams;
private final Duration timeout;
private final @Nullable Duration timeout;
private final List<PublicKeyCredentialDescriptor> excludeCredentials;
private final AuthenticatorSelectionCriteria authenticatorSelection;
private final AttestationConveyancePreference attestation;
private final @Nullable AttestationConveyancePreference attestation;
private final AuthenticationExtensionsClientInputs extensions;
private final @Nullable AuthenticationExtensionsClientInputs extensions;
private PublicKeyCredentialCreationOptions(PublicKeyCredentialRpEntity rp, PublicKeyCredentialUserEntity user,
Bytes challenge, List<PublicKeyCredentialParameters> pubKeyCredParams, Duration timeout,
Bytes challenge, List<PublicKeyCredentialParameters> pubKeyCredParams, @Nullable Duration timeout,
List<PublicKeyCredentialDescriptor> excludeCredentials,
AuthenticatorSelectionCriteria authenticatorSelection, AttestationConveyancePreference attestation,
AuthenticationExtensionsClientInputs extensions) {
AuthenticatorSelectionCriteria authenticatorSelection,
@Nullable AttestationConveyancePreference attestation,
@Nullable AuthenticationExtensionsClientInputs extensions) {
this.rp = rp;
this.user = user;
this.challenge = challenge;
@ -117,7 +120,7 @@ public final class PublicKeyCredentialCreationOptions {
* wait for the call to complete.
* @return the timeout
*/
public Duration getTimeout() {
public @Nullable Duration getTimeout() {
return this.timeout;
}
@ -150,7 +153,7 @@ public final class PublicKeyCredentialCreationOptions {
* regarding attestation conveyance.
* @return the attestation preference
*/
public AttestationConveyancePreference getAttestation() {
public @Nullable AttestationConveyancePreference getAttestation() {
return this.attestation;
}
@ -161,7 +164,7 @@ public final class PublicKeyCredentialCreationOptions {
* extension inputs requesting additional processing by the client and authenticator.
* @return the extensions
*/
public AuthenticationExtensionsClientInputs getExtensions() {
public @Nullable AuthenticationExtensionsClientInputs getExtensions() {
return this.extensions;
}
@ -181,23 +184,27 @@ public final class PublicKeyCredentialCreationOptions {
*/
public static final class PublicKeyCredentialCreationOptionsBuilder {
@SuppressWarnings("NullAway.Init")
private PublicKeyCredentialRpEntity rp;
@SuppressWarnings("NullAway.Init")
private PublicKeyCredentialUserEntity user;
@SuppressWarnings("NullAway.Init")
private Bytes challenge;
private List<PublicKeyCredentialParameters> pubKeyCredParams = new ArrayList<>();
private Duration timeout;
private @Nullable Duration timeout;
private List<PublicKeyCredentialDescriptor> excludeCredentials = new ArrayList<>();
@SuppressWarnings("NullAway.Init")
private AuthenticatorSelectionCriteria authenticatorSelection;
private AttestationConveyancePreference attestation;
private @Nullable AttestationConveyancePreference attestation;
private AuthenticationExtensionsClientInputs extensions;
private @Nullable AuthenticationExtensionsClientInputs extensions;
private PublicKeyCredentialCreationOptionsBuilder() {
}

View File

@ -20,6 +20,8 @@ import java.io.Serial;
import java.io.Serializable;
import java.util.Set;
import org.jspecify.annotations.Nullable;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptor">PublicKeyCredentialDescriptor</a>
@ -38,12 +40,12 @@ public final class PublicKeyCredentialDescriptor implements Serializable {
private final PublicKeyCredentialType type;
private final Bytes id;
private final @Nullable Bytes id;
private final Set<AuthenticatorTransport> transports;
private final @Nullable Set<AuthenticatorTransport> transports;
private PublicKeyCredentialDescriptor(PublicKeyCredentialType type, Bytes id,
Set<AuthenticatorTransport> transports) {
private PublicKeyCredentialDescriptor(PublicKeyCredentialType type, @Nullable Bytes id,
@Nullable Set<AuthenticatorTransport> transports) {
this.type = type;
this.id = id;
this.transports = transports;
@ -66,7 +68,7 @@ public final class PublicKeyCredentialDescriptor implements Serializable {
* referring to.
* @return the id
*/
public Bytes getId() {
public @Nullable Bytes getId() {
return this.id;
}
@ -78,7 +80,7 @@ public final class PublicKeyCredentialDescriptor implements Serializable {
* is referring to.
* @return the transports
*/
public Set<AuthenticatorTransport> getTransports() {
public @Nullable Set<AuthenticatorTransport> getTransports() {
return this.transports;
}
@ -100,9 +102,9 @@ public final class PublicKeyCredentialDescriptor implements Serializable {
private PublicKeyCredentialType type = PublicKeyCredentialType.PUBLIC_KEY;
private Bytes id;
private @Nullable Bytes id;
private Set<AuthenticatorTransport> transports;
private @Nullable Set<AuthenticatorTransport> transports;
private PublicKeyCredentialDescriptorBuilder() {
}

View File

@ -24,6 +24,8 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@ -43,17 +45,17 @@ public final class PublicKeyCredentialRequestOptions implements Serializable {
private final Duration timeout;
private final String rpId;
private final @Nullable String rpId;
private final List<PublicKeyCredentialDescriptor> allowCredentials;
private final UserVerificationRequirement userVerification;
private final @Nullable UserVerificationRequirement userVerification;
private final AuthenticationExtensionsClientInputs extensions;
private PublicKeyCredentialRequestOptions(Bytes challenge, Duration timeout, String rpId,
List<PublicKeyCredentialDescriptor> allowCredentials, UserVerificationRequirement userVerification,
AuthenticationExtensionsClientInputs extensions) {
private PublicKeyCredentialRequestOptions(Bytes challenge, Duration timeout, @Nullable String rpId,
List<PublicKeyCredentialDescriptor> allowCredentials,
@Nullable UserVerificationRequirement userVerification, AuthenticationExtensionsClientInputs extensions) {
Assert.notNull(challenge, "challenge cannot be null");
Assert.hasText(rpId, "rpId cannot be empty");
this.challenge = challenge;
@ -93,7 +95,7 @@ public final class PublicKeyCredentialRequestOptions implements Serializable {
* MUST verify that the Relying Party's origin matches the scope of this RP ID.
* @return the relying party id
*/
public String getRpId() {
public @Nullable String getRpId() {
return this.rpId;
}
@ -115,7 +117,7 @@ public final class PublicKeyCredentialRequestOptions implements Serializable {
* user verification for the get() operation.
* @return the user verification
*/
public UserVerificationRequirement getUserVerification() {
public @Nullable UserVerificationRequirement getUserVerification() {
return this.userVerification;
}
@ -146,15 +148,15 @@ public final class PublicKeyCredentialRequestOptions implements Serializable {
*/
public static final class PublicKeyCredentialRequestOptionsBuilder {
private Bytes challenge;
private @Nullable Bytes challenge;
private Duration timeout = Duration.ofMinutes(5);
private String rpId;
private @Nullable String rpId;
private List<PublicKeyCredentialDescriptor> allowCredentials = Collections.emptyList();
private UserVerificationRequirement userVerification;
private @Nullable UserVerificationRequirement userVerification;
private AuthenticationExtensionsClientInputs extensions = new ImmutableAuthenticationExtensionsClientInputs(
new ArrayList<>());

View File

@ -16,6 +16,10 @@
package org.springframework.security.web.webauthn.api;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* The <a href=
* "https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrpentity">PublicKeyCredentialRpEntity</a>
@ -75,9 +79,9 @@ public final class PublicKeyCredentialRpEntity {
*/
public static final class PublicKeyCredentialRpEntityBuilder {
private String name;
private @Nullable String name;
private String id;
private @Nullable String id;
private PublicKeyCredentialRpEntityBuilder() {
}
@ -107,6 +111,8 @@ public final class PublicKeyCredentialRpEntity {
* @return a new {@link PublicKeyCredentialRpEntity}.
*/
public PublicKeyCredentialRpEntity build() {
Assert.notNull(this.name, "name cannot be null");
Assert.notNull(this.id, "id cannot be null");
return new PublicKeyCredentialRpEntity(this.name, this.id);
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.web.webauthn.api;
import java.io.Serializable;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
@ -57,6 +59,6 @@ public interface PublicKeyCredentialUserEntity extends Serializable {
* is a human-palatable name for the user account, intended only for display.
* @return the display name
*/
String getDisplayName();
@Nullable String getDisplayName();
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* WebAuthn APIs.
*/
@NullMarked
package org.springframework.security.web.webauthn.api;
import org.jspecify.annotations.NullMarked;

View File

@ -19,6 +19,7 @@ package org.springframework.security.web.webauthn.authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.util.Assert;
@ -41,13 +42,13 @@ public class HttpSessionPublicKeyCredentialRequestOptionsRepository
@Override
public void save(HttpServletRequest request, HttpServletResponse response,
PublicKeyCredentialRequestOptions options) {
@Nullable PublicKeyCredentialRequestOptions options) {
HttpSession session = request.getSession();
session.setAttribute(this.attrName, options);
}
@Override
public PublicKeyCredentialRequestOptions load(HttpServletRequest request) {
public @Nullable PublicKeyCredentialRequestOptions load(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;

View File

@ -18,6 +18,7 @@ package org.springframework.security.web.webauthn.authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
@ -38,7 +39,7 @@ public interface PublicKeyCredentialRequestOptionsRepository {
* @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);
void save(HttpServletRequest request, HttpServletResponse response, @Nullable PublicKeyCredentialRequestOptions options);
/**
* Gets a saved {@link PublicKeyCredentialRequestOptions} if it exists, otherwise
@ -47,6 +48,6 @@ public interface PublicKeyCredentialRequestOptionsRepository {
* @return the {@link PublicKeyCredentialRequestOptions} that was saved, otherwise
* null.
*/
PublicKeyCredentialRequestOptions load(HttpServletRequest request);
@Nullable PublicKeyCredentialRequestOptions load(HttpServletRequest request);
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.web.webauthn.authentication;
import java.io.Serial;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
@ -53,7 +55,7 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken {
}
@Override
public Object getCredentials() {
public @Nullable Object getCredentials() {
return null;
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.web.webauthn.authentication;
import java.io.Serial;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest;
@ -68,7 +70,7 @@ public class WebAuthnAuthenticationRequestToken extends AbstractAuthenticationTo
}
@Override
public Object getPrincipal() {
public @Nullable Object getPrincipal() {
return this.webAuthnRequest.getPublicKey().getResponse().getUserHandle();
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* WebAuthn Authentication support.
*/
@NullMarked
package org.springframework.security.web.webauthn.authentication;
import org.jspecify.annotations.NullMarked;

View File

@ -22,6 +22,7 @@ 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.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.AuthenticatorAttachment;
@ -39,7 +40,7 @@ class AuthenticatorAttachmentDeserializer extends StdDeserializer<AuthenticatorA
}
@Override
public AuthenticatorAttachment deserialize(JsonParser parser, DeserializationContext ctxt)
public @Nullable AuthenticatorAttachment deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JacksonException {
String type = parser.readValueAs(String.class);
for (AuthenticatorAttachment publicKeyCredentialType : AuthenticatorAttachment.values()) {

View File

@ -22,6 +22,7 @@ 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.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.AuthenticatorTransport;
@ -39,7 +40,7 @@ class AuthenticatorTransportDeserializer extends StdDeserializer<AuthenticatorTr
}
@Override
public AuthenticatorTransport deserialize(JsonParser parser, DeserializationContext ctxt)
public @Nullable AuthenticatorTransport deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JacksonException {
String transportValue = parser.readValueAs(String.class);
for (AuthenticatorTransport transport : AuthenticatorTransport.values()) {

View File

@ -22,6 +22,7 @@ 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.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier;
@ -39,7 +40,7 @@ class COSEAlgorithmIdentifierDeserializer extends StdDeserializer<COSEAlgorithmI
}
@Override
public COSEAlgorithmIdentifier deserialize(JsonParser parser, DeserializationContext ctxt)
public @Nullable COSEAlgorithmIdentifier deserialize(JsonParser parser, DeserializationContext ctxt)
throws IOException, JacksonException {
Long transportValue = parser.readValueAs(Long.class);
for (COSEAlgorithmIdentifier identifier : COSEAlgorithmIdentifier.values()) {

View File

@ -20,6 +20,7 @@ import java.time.Duration;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
@ -33,6 +34,6 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreation
abstract class PublicKeyCredentialCreationOptionsMixin {
@JsonSerialize(using = DurationSerializer.class)
private Duration timeout;
private @Nullable Duration timeout;
}

View File

@ -20,6 +20,7 @@ import java.time.Duration;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
@ -33,6 +34,6 @@ import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestO
class PublicKeyCredentialRequestOptionsMixin {
@JsonSerialize(using = DurationSerializer.class)
private final Duration timeout = null;
private final @Nullable Duration timeout = null;
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* WebAuthn Jackson Support.
*/
@NullMarked
package org.springframework.security.web.webauthn.jackson;
import org.jspecify.annotations.NullMarked;

View File

@ -16,18 +16,20 @@
package org.springframework.security.web.webauthn.management;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
public class ImmutablePublicKeyCredentialRequestOptionsRequest implements PublicKeyCredentialRequestOptionsRequest {
private final Authentication authentication;
private final @Nullable Authentication authentication;
public ImmutablePublicKeyCredentialRequestOptionsRequest(Authentication authentication) {
public ImmutablePublicKeyCredentialRequestOptionsRequest(@Nullable Authentication authentication) {
this.authentication = authentication;
}
@Override
public Authentication getAuthentication() {
public @Nullable Authentication getAuthentication() {
return this.authentication;
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.web.webauthn.management;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.util.Assert;
@ -40,7 +42,7 @@ public class ImmutableRelyingPartyRegistrationRequest implements RelyingPartyReg
* @param publicKey this is submitted by the client and if validated stored.
*/
public ImmutableRelyingPartyRegistrationRequest(PublicKeyCredentialCreationOptions options,
RelyingPartyPublicKey publicKey) {
@Nullable RelyingPartyPublicKey publicKey) {
Assert.notNull(options, "options cannot be null");
Assert.notNull(publicKey, "publicKey cannot be null");
this.options = options;

View File

@ -23,6 +23,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementSetter;
@ -105,7 +107,7 @@ public final class JdbcPublicKeyCredentialUserEntityRepository implements Public
}
@Override
public PublicKeyCredentialUserEntity findById(Bytes id) {
public @Nullable PublicKeyCredentialUserEntity findById(Bytes id) {
Assert.notNull(id, "id cannot be null");
List<PublicKeyCredentialUserEntity> result = this.jdbcOperations.query(FIND_USER_BY_ID_SQL,
this.userEntityRowMapper, id.toBase64UrlString());
@ -113,7 +115,7 @@ public final class JdbcPublicKeyCredentialUserEntityRepository implements Public
}
@Override
public PublicKeyCredentialUserEntity findByUsername(String username) {
public @Nullable PublicKeyCredentialUserEntity findByUsername(String username) {
Assert.hasText(username, "name cannot be null or empty");
List<PublicKeyCredentialUserEntity> result = this.jdbcOperations.query(FIND_USER_BY_NAME_SQL,
this.userEntityRowMapper, username);

View File

@ -28,6 +28,8 @@ import java.util.List;
import java.util.Set;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementSetter;
@ -171,7 +173,7 @@ public final class JdbcUserCredentialRepository implements UserCredentialReposit
}
@Override
public CredentialRecord findByCredentialId(Bytes credentialId) {
public @Nullable CredentialRecord findByCredentialId(Bytes credentialId) {
Assert.notNull(credentialId, "credentialId cannot be null");
List<CredentialRecord> result = this.jdbcOperations.query(FIND_CREDENTIAL_RECORD_BY_ID_SQL,
this.credentialRecordRowMapper, credentialId.toBase64UrlString());
@ -238,7 +240,7 @@ public final class JdbcUserCredentialRepository implements UserCredentialReposit
return parameters;
}
private Timestamp fromInstant(Instant instant) {
private @Nullable Timestamp fromInstant(Instant instant) {
if (instant == null) {
return null;
}
@ -249,13 +251,13 @@ public final class JdbcUserCredentialRepository implements UserCredentialReposit
private interface SetBytes {
void setBytes(PreparedStatement ps, int index, byte[] bytes) throws SQLException;
void setBytes(PreparedStatement ps, int index, byte @Nullable [] bytes) throws SQLException;
}
private interface GetBytes {
byte[] getBytes(ResultSet rs, String columnName) throws SQLException;
byte @Nullable [] getBytes(ResultSet rs, String columnName) throws SQLException;
}
@ -269,7 +271,8 @@ public final class JdbcUserCredentialRepository implements UserCredentialReposit
}
@Override
protected void doSetValue(PreparedStatement ps, int parameterPosition, Object argValue) throws SQLException {
protected void doSetValue(PreparedStatement ps, int parameterPosition, @Nullable Object argValue)
throws SQLException {
if (argValue instanceof SqlParameterValue paramValue) {
if (paramValue.getSqlType() == Types.BLOB) {
if (paramValue.getValue() != null) {
@ -327,6 +330,8 @@ public final class JdbcUserCredentialRepository implements UserCredentialReposit
for (String transport : transports) {
authenticatorTransports.add(AuthenticatorTransport.valueOf(transport));
}
Assert.notNull(lastUsed, "last_used cannot be null");
Assert.notNull(created, "created cannot be null");
return ImmutableCredentialRecord.builder()
.credentialId(credentialId)
.userEntityUserId(userEntityUserId)
@ -345,7 +350,7 @@ public final class JdbcUserCredentialRepository implements UserCredentialReposit
.build();
}
private Instant fromTimestamp(Timestamp timestamp) {
private @Nullable Instant fromTimestamp(Timestamp timestamp) {
if (timestamp == null) {
return null;
}

View File

@ -19,6 +19,8 @@ package org.springframework.security.web.webauthn.management;
import java.util.HashMap;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
import org.springframework.util.Assert;
@ -36,13 +38,13 @@ public class MapPublicKeyCredentialUserEntityRepository implements PublicKeyCred
private final Map<Bytes, PublicKeyCredentialUserEntity> idToUserEntity = new HashMap<>();
@Override
public PublicKeyCredentialUserEntity findById(Bytes id) {
public @Nullable PublicKeyCredentialUserEntity findById(Bytes id) {
Assert.notNull(id, "id cannot be null");
return this.idToUserEntity.get(id);
}
@Override
public PublicKeyCredentialUserEntity findByUsername(String username) {
public @Nullable PublicKeyCredentialUserEntity findByUsername(String username) {
Assert.notNull(username, "username cannot be null");
return this.usernameToUserEntity.get(username);
}

View File

@ -24,6 +24,8 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.util.Assert;
@ -62,7 +64,7 @@ public class MapUserCredentialRepository implements UserCredentialRepository {
}
@Override
public CredentialRecord findByCredentialId(Bytes credentialId) {
public @Nullable CredentialRecord findByCredentialId(Bytes credentialId) {
Assert.notNull(credentialId, "credentialId cannot be null");
return this.credentialIdToUserCredential.get(credentialId);
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.web.webauthn.management;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
public interface PublicKeyCredentialRequestOptionsRequest {
@ -24,6 +26,6 @@ public interface PublicKeyCredentialRequestOptionsRequest {
* The current {@link Authentication}. Possibly null or an anonymous.
* @return the current {@link Authentication}
*/
Authentication getAuthentication();
@Nullable Authentication getAuthentication();
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.web.webauthn.management;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
@ -33,14 +35,14 @@ public interface PublicKeyCredentialUserEntityRepository {
* @param id the id to lookup the username by
* @return the username or null if not found.
*/
PublicKeyCredentialUserEntity findById(Bytes id);
@Nullable 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);
@Nullable PublicKeyCredentialUserEntity findByUsername(String username);
/**
* Saves the {@link PublicKeyCredentialUserEntity} to the associated username.

View File

@ -16,6 +16,8 @@
package org.springframework.security.web.webauthn.management;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
/**

View File

@ -18,6 +18,8 @@ package org.springframework.security.web.webauthn.management;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.Bytes;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
@ -47,7 +49,7 @@ public interface UserCredentialRepository {
* @param credentialId {@link CredentialRecord#getCredentialId()}
* @return the {@link CredentialRecord} or null if not found.
*/
CredentialRecord findByCredentialId(Bytes credentialId);
@Nullable CredentialRecord findByCredentialId(Bytes credentialId);
/**
* Finds all {@link CredentialRecord} instances for a specific user.

View File

@ -16,6 +16,8 @@
package org.springframework.security.web.webauthn.management;
import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.webauthn.api.CredentialRecord;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;

View File

@ -43,8 +43,11 @@ 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.AuthenticationExtensionAuthenticatorOutput;
import com.webauthn4j.data.extension.authenticator.RegistrationExtensionAuthenticatorOutput;
import com.webauthn4j.server.ServerProperty;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
@ -260,24 +263,28 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
transports);
RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, pubKeyCredParams,
userVerificationRequired, userPresenceRequired);
RegistrationData registrationData = this.webAuthnManager.validate(webauthn4jRegistrationRequest,
RegistrationData wa4jRegistrationData = this.webAuthnManager.validate(webauthn4jRegistrationRequest,
registrationParameters);
AuthenticatorData<RegistrationExtensionAuthenticatorOutput> authData = registrationData.getAttestationObject()
AttestationObject wa4jAttestationObject = wa4jRegistrationData.getAttestationObject();
Assert.notNull(wa4jAttestationObject, "attestationObject cannot be null");
AuthenticatorData<RegistrationExtensionAuthenticatorOutput> wa4jAuthData = wa4jAttestationObject
.getAuthenticatorData();
CborConverter cborConverter = this.objectConverter.getCborConverter();
COSEKey coseKey = authData.getAttestedCredentialData().getCOSEKey();
AttestedCredentialData wa4jCredData = wa4jAuthData.getAttestedCredentialData();
Assert.notNull(wa4jCredData, "attestedCredentialData cannot be null");
COSEKey coseKey = wa4jCredData.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())
.signatureCount(wa4jAuthData.getSignCount())
.uvInitialized(wa4jAuthData.isFlagUV())
.transports(convertTransports(wa4jRegistrationData.getTransports()))
.backupEligible(wa4jAuthData.isFlagBE())
.backupState(wa4jAuthData.isFlagBS())
.label(publicKey.getLabel())
.attestationClientDataJSON(credential.getResponse().getClientDataJSON())
.attestationObject(credential.getResponse().getAttestationObject())
@ -286,7 +293,7 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
return userCredential;
}
private static Set<String> convertTransportsToString(AuthenticatorAttestationResponse response) {
private static @Nullable Set<String> convertTransportsToString(AuthenticatorAttestationResponse response) {
if (response.getTransports() == null) {
return null;
}
@ -320,7 +327,7 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
}
private static Set<AuthenticatorTransport> convertTransports(
Set<com.webauthn4j.data.AuthenticatorTransport> transports) {
@Nullable Set<com.webauthn4j.data.AuthenticatorTransport> transports) {
if (transports == null) {
return Collections.emptySet();
}
@ -344,7 +351,8 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
.build();
}
private List<CredentialRecord> findCredentialRecords(Authentication authentication) {
@NullUnmarked
private List<CredentialRecord> findCredentialRecords(@Nullable Authentication authentication) {
if (!this.trustResolver.isAuthenticated(authentication)) {
return Collections.emptyList();
}
@ -361,22 +369,30 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
AuthenticatorAssertionResponse assertionResponse = request.getPublicKey().getResponse();
Bytes keyId = request.getPublicKey().getRawId();
CredentialRecord credentialRecord = this.userCredentials.findByCredentialId(keyId);
if (credentialRecord == null) {
throw new IllegalArgumentException("Unable to find CredentialRecord with id " + keyId);
}
CborConverter cborConverter = this.objectConverter.getCborConverter();
AttestationObject attestationObject = cborConverter
.readValue(credentialRecord.getAttestationObject().getBytes(), AttestationObject.class);
Bytes attestationObject = credentialRecord.getAttestationObject();
Assert.notNull(attestationObject, "attestationObject cannot be null");
AttestationObject wa4jAttestationObject = cborConverter
.readValue(attestationObject.getBytes(), AttestationObject.class);
Assert.notNull(wa4jAttestationObject, "attestationObject cannot be null");
AuthenticatorData<RegistrationExtensionAuthenticatorOutput> wa4jAuthData = wa4jAttestationObject.getAuthenticatorData();
AttestedCredentialData wa4jCredData = wa4jAuthData.getAttestedCredentialData();
Assert.notNull(wa4jCredData, "attestedCredentialData cannot be null");
AttestedCredentialData data = new AttestedCredentialData(wa4jCredData.getAaguid(),
keyId.getBytes(), wa4jCredData.getCOSEKey());
AuthenticatorData<RegistrationExtensionAuthenticatorOutput> authData = attestationObject.getAuthenticatorData();
AttestedCredentialData data = new AttestedCredentialData(authData.getAttestedCredentialData().getAaguid(),
keyId.getBytes(), authData.getAttestedCredentialData().getCOSEKey());
Authenticator authenticator = new AuthenticatorImpl(data, attestationObject.getAttestationStatement(),
Authenticator authenticator = new AuthenticatorImpl(data, wa4jAttestationObject.getAttestationStatement(),
credentialRecord.getSignatureCount());
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,
String rpId = requestOptions.getRpId();
Assert.notNull(rpId, "rpId cannot be null");
ServerProperty serverProperty = new ServerProperty(origins, rpId, challenge,
tokenBindingId);
boolean userVerificationRequired = request.getRequestOptions()
.getUserVerification() == UserVerificationRequirement.REQUIRED;
@ -387,17 +403,24 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
AuthenticationParameters authenticationParameters = new AuthenticationParameters(serverProperty, authenticator,
userVerificationRequired);
AuthenticationData authenticationData = this.webAuthnManager.validate(authenticationRequest,
AuthenticationData wa4jAuthenticationData = this.webAuthnManager.validate(authenticationRequest,
authenticationParameters);
long updatedSignCount = authenticationData.getAuthenticatorData().getSignCount();
AuthenticatorData<AuthenticationExtensionAuthenticatorOutput> wa4jValidatedAuthData = wa4jAuthenticationData.getAuthenticatorData();
Assert.notNull(wa4jValidatedAuthData, "authenticatorData cannot be null");
long updatedSignCount = wa4jValidatedAuthData.getSignCount();
ImmutableCredentialRecord updatedRecord = ImmutableCredentialRecord.fromCredentialRecord(credentialRecord)
.lastUsed(Instant.now())
.signatureCount(updatedSignCount)
.build();
this.userCredentials.save(updatedRecord);
return this.userEntities.findById(credentialRecord.getUserEntityUserId());
PublicKeyCredentialUserEntity userEntity = this.userEntities.findById(
credentialRecord.getUserEntityUserId());
if (userEntity == null) {
throw new IllegalArgumentException("Unable to find UserEntity with id " + credentialRecord.getUserEntityUserId() + " for " + request);
}
return userEntity;
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Management of the WebAuthn APIs.
*/
@NullMarked
package org.springframework.security.web.webauthn.management;
import org.jspecify.annotations.NullMarked;

View File

@ -19,8 +19,10 @@ package org.springframework.security.web.webauthn.registration;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
import org.springframework.util.Assert;
public class HttpSessionPublicKeyCredentialCreationOptionsRepository
@ -32,11 +34,12 @@ public class HttpSessionPublicKeyCredentialCreationOptionsRepository
@Override
public void save(HttpServletRequest request, HttpServletResponse response,
PublicKeyCredentialCreationOptions options) {
@Nullable PublicKeyCredentialCreationOptions options) {
request.getSession().setAttribute(this.attrName, options);
}
public PublicKeyCredentialCreationOptions load(HttpServletRequest request) {
public @Nullable PublicKeyCredentialCreationOptions load(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return null;

View File

@ -18,6 +18,7 @@ package org.springframework.security.web.webauthn.registration;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
@ -38,7 +39,7 @@ public interface PublicKeyCredentialCreationOptionsRepository {
* @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);
void save(HttpServletRequest request, HttpServletResponse response, @Nullable PublicKeyCredentialCreationOptions options);
/**
* Gets a saved {@link PublicKeyCredentialCreationOptions} if it exists, otherwise
@ -47,6 +48,6 @@ public interface PublicKeyCredentialCreationOptionsRepository {
* @return the {@link PublicKeyCredentialCreationOptions} that was saved, otherwise
* null.
*/
PublicKeyCredentialCreationOptions load(HttpServletRequest request);
@Nullable PublicKeyCredentialCreationOptions load(HttpServletRequest request);
}

View File

@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
@ -190,7 +191,7 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
this.converter.write(registrationResponse, MediaType.APPLICATION_JSON, outputMessage);
}
private WebAuthnRegistrationRequest readRegistrationRequest(HttpServletRequest request) {
private @Nullable WebAuthnRegistrationRequest readRegistrationRequest(HttpServletRequest request) {
HttpInputMessage inputMessage = new ServletServerHttpRequest(request);
try {
return (WebAuthnRegistrationRequest) this.converter.read(WebAuthnRegistrationRequest.class, inputMessage);
@ -201,7 +202,7 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
}
}
private void removeCredential(HttpServletRequest request, HttpServletResponse response, String id)
private void removeCredential(HttpServletRequest request, HttpServletResponse response, @Nullable String id)
throws IOException {
this.userCredentials.delete(Bytes.fromBase64(id));
response.setStatus(HttpStatus.NO_CONTENT.value());
@ -209,9 +210,9 @@ public class WebAuthnRegistrationFilter extends OncePerRequestFilter {
static class WebAuthnRegistrationRequest {
private RelyingPartyPublicKey publicKey;
private @Nullable RelyingPartyPublicKey publicKey;
RelyingPartyPublicKey getPublicKey() {
@Nullable RelyingPartyPublicKey getPublicKey() {
return this.publicKey;
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* WebAuthn Registration support.
*/
@NullMarked
package org.springframework.security.web.webauthn.registration;
import org.jspecify.annotations.NullMarked;