Quellcode durchsuchen

Enable Null checking in spring-security-webauthn via JSpecify

Closes gh-17839
Rob Winch vor 1 Woche
Ursprung
Commit
0a991a91ce
46 geänderte Dateien mit 422 neuen und 153 gelöschten Zeilen
  1. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/aot/PublicKeyCredentialUserEntityRuntimeHints.java
  2. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/aot/UserCredentialRuntimeHints.java
  3. 23 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/aot/package-info.java
  4. 17 10
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/AuthenticatorAssertionResponse.java
  5. 8 4
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/AuthenticatorAttestationResponse.java
  6. 13 11
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/AuthenticatorSelectionCriteria.java
  7. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/Bytes.java
  8. 5 3
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/CredentialRecord.java
  9. 20 13
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/ImmutableCredentialRecord.java
  10. 6 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/ImmutablePublicKeyCose.java
  11. 8 4
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/ImmutablePublicKeyCredentialUserEntity.java
  12. 20 9
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredential.java
  13. 19 12
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialCreationOptions.java
  14. 10 8
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialDescriptor.java
  15. 12 10
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialRequestOptions.java
  16. 8 2
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialRpEntity.java
  17. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialUserEntity.java
  18. 23 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/api/package-info.java
  19. 3 2
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/HttpSessionPublicKeyCredentialRequestOptionsRepository.java
  20. 3 2
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/PublicKeyCredentialRequestOptionsRepository.java
  21. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java
  22. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationRequestToken.java
  23. 23 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/package-info.java
  24. 2 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/AuthenticatorAttachmentDeserializer.java
  25. 2 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/AuthenticatorTransportDeserializer.java
  26. 2 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/COSEAlgorithmIdentifierDeserializer.java
  27. 2 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/PublicKeyCredentialCreationOptionsMixin.java
  28. 2 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/PublicKeyCredentialRequestOptionsMixin.java
  29. 23 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/package-info.java
  30. 5 3
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/ImmutablePublicKeyCredentialRequestOptionsRequest.java
  31. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/ImmutableRelyingPartyRegistrationRequest.java
  32. 4 2
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepository.java
  33. 11 6
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java
  34. 4 2
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/MapPublicKeyCredentialUserEntityRepository.java
  35. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/MapUserCredentialRepository.java
  36. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/PublicKeyCredentialRequestOptionsRequest.java
  37. 4 2
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/PublicKeyCredentialUserEntityRepository.java
  38. 2 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/RelyingPartyRegistrationRequest.java
  39. 3 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/UserCredentialRepository.java
  40. 2 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/WebAuthnRelyingPartyOperations.java
  41. 47 24
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/Webauthn4JRelyingPartyOperations.java
  42. 23 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/management/package-info.java
  43. 5 2
      webauthn/src/main/java/org/springframework/security/web/webauthn/registration/HttpSessionPublicKeyCredentialCreationOptionsRepository.java
  44. 3 2
      webauthn/src/main/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsRepository.java
  45. 5 4
      webauthn/src/main/java/org/springframework/security/web/webauthn/registration/WebAuthnRegistrationFilter.java
  46. 23 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/registration/package-info.java

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/aot/PublicKeyCredentialUserEntityRuntimeHints.java

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

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/aot/UserCredentialRuntimeHints.java

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

+ 23 - 0
webauthn/src/main/java/org/springframework/security/web/webauthn/aot/package-info.java

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

+ 17 - 10
webauthn/src/main/java/org/springframework/security/web/webauthn/api/AuthenticatorAssertionResponse.java

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

+ 8 - 4
webauthn/src/main/java/org/springframework/security/web/webauthn/api/AuthenticatorAttestationResponse.java

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

+ 13 - 11
webauthn/src/main/java/org/springframework/security/web/webauthn/api/AuthenticatorSelectionCriteria.java

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

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/api/Bytes.java

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

+ 5 - 3
webauthn/src/main/java/org/springframework/security/web/webauthn/api/CredentialRecord.java

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

+ 20 - 13
webauthn/src/main/java/org/springframework/security/web/webauthn/api/ImmutableCredentialRecord.java

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

+ 6 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/api/ImmutablePublicKeyCose.java

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

+ 8 - 4
webauthn/src/main/java/org/springframework/security/web/webauthn/api/ImmutablePublicKeyCredentialUserEntity.java

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

+ 20 - 9
webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredential.java

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

+ 19 - 12
webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialCreationOptions.java

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

+ 10 - 8
webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialDescriptor.java

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

+ 12 - 10
webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialRequestOptions.java

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

+ 8 - 2
webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialRpEntity.java

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

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/api/PublicKeyCredentialUserEntity.java

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

+ 23 - 0
webauthn/src/main/java/org/springframework/security/web/webauthn/api/package-info.java

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

+ 3 - 2
webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/HttpSessionPublicKeyCredentialRequestOptionsRepository.java

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

+ 3 - 2
webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/PublicKeyCredentialRequestOptionsRepository.java

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

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java

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

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationRequestToken.java

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

+ 23 - 0
webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/package-info.java

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

+ 2 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/AuthenticatorAttachmentDeserializer.java

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

+ 2 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/AuthenticatorTransportDeserializer.java

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

+ 2 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/COSEAlgorithmIdentifierDeserializer.java

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

+ 2 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/PublicKeyCredentialCreationOptionsMixin.java

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

+ 2 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/PublicKeyCredentialRequestOptionsMixin.java

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

+ 23 - 0
webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/package-info.java

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

+ 5 - 3
webauthn/src/main/java/org/springframework/security/web/webauthn/management/ImmutablePublicKeyCredentialRequestOptionsRequest.java

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

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/management/ImmutableRelyingPartyRegistrationRequest.java

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

+ 4 - 2
webauthn/src/main/java/org/springframework/security/web/webauthn/management/JdbcPublicKeyCredentialUserEntityRepository.java

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

+ 11 - 6
webauthn/src/main/java/org/springframework/security/web/webauthn/management/JdbcUserCredentialRepository.java

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

+ 4 - 2
webauthn/src/main/java/org/springframework/security/web/webauthn/management/MapPublicKeyCredentialUserEntityRepository.java

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

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/management/MapUserCredentialRepository.java

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

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/management/PublicKeyCredentialRequestOptionsRequest.java

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

+ 4 - 2
webauthn/src/main/java/org/springframework/security/web/webauthn/management/PublicKeyCredentialUserEntityRepository.java

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

+ 2 - 0
webauthn/src/main/java/org/springframework/security/web/webauthn/management/RelyingPartyRegistrationRequest.java

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

+ 3 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/management/UserCredentialRepository.java

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

+ 2 - 0
webauthn/src/main/java/org/springframework/security/web/webauthn/management/WebAuthnRelyingPartyOperations.java

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

+ 47 - 24
webauthn/src/main/java/org/springframework/security/web/webauthn/management/Webauthn4JRelyingPartyOperations.java

@@ -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);
-
-		AuthenticatorData<RegistrationExtensionAuthenticatorOutput> authData = attestationObject.getAuthenticatorData();
-		AttestedCredentialData data = new AttestedCredentialData(authData.getAttestedCredentialData().getAaguid(),
-				keyId.getBytes(), authData.getAttestedCredentialData().getCOSEKey());
-
-		Authenticator authenticator = new AuthenticatorImpl(data, attestationObject.getAttestationStatement(),
+		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());
+
+		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;
 	}
 
 }

+ 23 - 0
webauthn/src/main/java/org/springframework/security/web/webauthn/management/package-info.java

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

+ 5 - 2
webauthn/src/main/java/org/springframework/security/web/webauthn/registration/HttpSessionPublicKeyCredentialCreationOptionsRepository.java

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

+ 3 - 2
webauthn/src/main/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsRepository.java

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

+ 5 - 4
webauthn/src/main/java/org/springframework/security/web/webauthn/registration/WebAuthnRegistrationFilter.java

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

+ 23 - 0
webauthn/src/main/java/org/springframework/security/web/webauthn/registration/package-info.java

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