diff --git a/pk-auth-core/src/main/java/com/codeheadsystems/pkauth/api/Transport.java b/pk-auth-core/src/main/java/com/codeheadsystems/pkauth/api/Transport.java index f9b0daa..2b75605 100644 --- a/pk-auth-core/src/main/java/com/codeheadsystems/pkauth/api/Transport.java +++ b/pk-auth-core/src/main/java/com/codeheadsystems/pkauth/api/Transport.java @@ -3,6 +3,7 @@ import java.util.Objects; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** * WebAuthn {@code AuthenticatorTransport} values, per the Level 3 spec §5.8.4. Each constant knows @@ -33,7 +34,7 @@ public String wireName() { * Optional#empty()} when the string is null or not a recognized value (e.g. when an authenticator * advertises a future transport this enum does not yet enumerate). */ - public static Optional fromWire(String s) { + public static Optional fromWire(@Nullable String s) { if (s == null) { return Optional.empty(); } diff --git a/pk-auth-magic-link/src/main/java/com/codeheadsystems/pkauth/magiclink/JavaMailEmailSender.java b/pk-auth-magic-link/src/main/java/com/codeheadsystems/pkauth/magiclink/JavaMailEmailSender.java index 4604cf0..7b41897 100644 --- a/pk-auth-magic-link/src/main/java/com/codeheadsystems/pkauth/magiclink/JavaMailEmailSender.java +++ b/pk-auth-magic-link/src/main/java/com/codeheadsystems/pkauth/magiclink/JavaMailEmailSender.java @@ -45,8 +45,8 @@ public void send(String to, String subject, String body) { + " to=" + to + " subjectLength=" - + (subject == null ? 0 : subject.length()) + + subject.length() + " bodyLength=" - + (body == null ? 0 : body.length())); + + body.length()); } } diff --git a/pk-auth-magic-link/src/main/java/com/codeheadsystems/pkauth/magiclink/LoggingEmailSender.java b/pk-auth-magic-link/src/main/java/com/codeheadsystems/pkauth/magiclink/LoggingEmailSender.java index f2e03f3..36aa338 100644 --- a/pk-auth-magic-link/src/main/java/com/codeheadsystems/pkauth/magiclink/LoggingEmailSender.java +++ b/pk-auth-magic-link/src/main/java/com/codeheadsystems/pkauth/magiclink/LoggingEmailSender.java @@ -23,10 +23,6 @@ public LoggingEmailSender() {} @Override public void send(String to, String subject, String body) { // Do not log the body — it contains the plaintext magic-link token. - LOG.info( - "email.dev to={} subject={} bodyLength={}", - to, - subject, - (body == null ? 0 : body.length())); + LOG.info("email.dev to={} subject={} bodyLength={}", to, subject, body.length()); } } diff --git a/pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthCeremonyController.java b/pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthCeremonyController.java index 70930a7..3fc09b0 100644 --- a/pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthCeremonyController.java +++ b/pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthCeremonyController.java @@ -23,6 +23,7 @@ import io.micronaut.scheduling.annotation.ExecuteOn; import java.net.InetSocketAddress; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -89,10 +90,7 @@ private static HttpResponse> toResponse(CeremonyResponse wir .body(wire.body()); } - private static String clientIp(HttpRequest request) { - if (request == null) { - return null; - } + private static @Nullable String clientIp(HttpRequest request) { InetSocketAddress addr = request.getRemoteAddress(); if (addr == null || addr.getAddress() == null) { return null; diff --git a/pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthJwtAuthenticationFilter.java b/pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthJwtAuthenticationFilter.java index 36d361c..139cc5e 100644 --- a/pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthJwtAuthenticationFilter.java +++ b/pk-auth-micronaut/src/main/java/com/codeheadsystems/pkauth/micronaut/PkAuthJwtAuthenticationFilter.java @@ -9,6 +9,7 @@ import io.micronaut.http.annotation.Filter; import io.micronaut.http.filter.HttpServerFilter; import io.micronaut.http.filter.ServerFilterChain; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; /** @@ -54,7 +55,7 @@ public Publisher> doFilter( } /** Extracts the authenticated user handle from a request, or null if none was attached. */ - public static UserHandle attachedUserHandle(HttpRequest request) { + public static @Nullable UserHandle attachedUserHandle(HttpRequest request) { return request.getAttribute(ATTR_USER_HANDLE, UserHandle.class).orElse(null); } diff --git a/pk-auth-otp/src/main/java/com/codeheadsystems/pkauth/otp/LoggingSmsSender.java b/pk-auth-otp/src/main/java/com/codeheadsystems/pkauth/otp/LoggingSmsSender.java index e5ee4f4..07a6c63 100644 --- a/pk-auth-otp/src/main/java/com/codeheadsystems/pkauth/otp/LoggingSmsSender.java +++ b/pk-auth-otp/src/main/java/com/codeheadsystems/pkauth/otp/LoggingSmsSender.java @@ -23,9 +23,6 @@ public LoggingSmsSender() {} @Override public void send(String phoneE164, String body) { // Do not log the message body — it contains the plaintext OTP code. - LOG.info( - "sms.dev phone={} messageLength={}", - OtpService.maskPhone(phoneE164), - (body == null ? 0 : body.length())); + LOG.info("sms.dev phone={} messageLength={}", OtpService.maskPhone(phoneE164), body.length()); } } diff --git a/pk-auth-otp/src/main/java/com/codeheadsystems/pkauth/otp/TwilioSmsSender.java b/pk-auth-otp/src/main/java/com/codeheadsystems/pkauth/otp/TwilioSmsSender.java index 38c576c..22bcb9c 100644 --- a/pk-auth-otp/src/main/java/com/codeheadsystems/pkauth/otp/TwilioSmsSender.java +++ b/pk-auth-otp/src/main/java/com/codeheadsystems/pkauth/otp/TwilioSmsSender.java @@ -44,6 +44,6 @@ public void send(String phoneE164, String body) { + ") phone=" + phoneE164 + " bodyLength=" - + (body == null ? 0 : body.length())); + + body.length()); } } diff --git a/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiBackupCodeRepository.java b/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiBackupCodeRepository.java index 4322c2a..4130303 100644 --- a/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiBackupCodeRepository.java +++ b/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiBackupCodeRepository.java @@ -18,6 +18,7 @@ import org.jdbi.v3.core.JdbiException; import org.jdbi.v3.core.mapper.RowMapper; import org.jdbi.v3.core.statement.Update; +import org.jspecify.annotations.Nullable; /** * {@link BackupCodeRepository} backed by the {@code backup_codes} table (Flyway V3). @@ -45,7 +46,7 @@ public void save(StoredBackupCode code) { } private static void insertBackupCode(Handle h, StoredBackupCode code) { - Update update = + try (Update update = h.createUpdate( "INSERT INTO backup_codes (code_id, user_handle, hashed_code, consumed," + " consumed_at, created_at)" @@ -54,20 +55,22 @@ private static void insertBackupCode(Handle h, StoredBackupCode code) { .bind("uh", code.userHandle().value()) .bind("hash", code.hashedCode()) .bind("consumed", code.consumed()) - .bind("createdAt", OffsetDateTime.ofInstant(code.createdAt(), ZoneOffset.UTC)); - // consumed_at is TIMESTAMPTZ; force Types.TIMESTAMP_WITH_TIMEZONE on the null branch so PG - // does not reject the untyped-null (Types.VARCHAR) default. - bindNullable( - update, - "consumedAt", - code.consumedAt() == null - ? null - : OffsetDateTime.ofInstant(code.consumedAt(), ZoneOffset.UTC), - Types.TIMESTAMP_WITH_TIMEZONE); - update.execute(); + .bind("createdAt", OffsetDateTime.ofInstant(code.createdAt(), ZoneOffset.UTC))) { + // consumed_at is TIMESTAMPTZ; force Types.TIMESTAMP_WITH_TIMEZONE on the null branch so PG + // does not reject the untyped-null (Types.VARCHAR) default. + bindNullable( + update, + "consumedAt", + code.consumedAt() == null + ? null + : OffsetDateTime.ofInstant(code.consumedAt(), ZoneOffset.UTC), + Types.TIMESTAMP_WITH_TIMEZONE); + update.execute(); + } } - private static void bindNullable(Update update, String name, Object value, int sqlType) { + private static void bindNullable( + Update update, String name, @Nullable Object value, int sqlType) { if (value == null) { update.bindNull(name, sqlType); } else { diff --git a/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiCredentialRepository.java b/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiCredentialRepository.java index e12bf28..41684cd 100644 --- a/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiCredentialRepository.java +++ b/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiCredentialRepository.java @@ -27,6 +27,7 @@ import org.jdbi.v3.core.JdbiException; import org.jdbi.v3.core.mapper.RowMapper; import org.jdbi.v3.core.statement.Update; +import org.jspecify.annotations.Nullable; /** * {@link CredentialRepository} backed by the Phase 5 {@code credentials} table. @@ -210,7 +211,8 @@ public int deleteByUserHandle(UserHandle userHandle) { * untyped-null default (Types.VARCHAR), which strict typing rejects against UUID and TIMESTAMPTZ * columns. See bug report for the original {@code aaguid} failure mode. */ - private static void bindNullable(Update update, String name, Object value, int sqlType) { + private static void bindNullable( + Update update, String name, @Nullable Object value, int sqlType) { if (value == null) { update.bindNull(name, sqlType); } else { diff --git a/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiRefreshTokenRepository.java b/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiRefreshTokenRepository.java index 46e6d7a..000cb18 100644 --- a/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiRefreshTokenRepository.java +++ b/pk-auth-persistence-jdbi/src/main/java/com/codeheadsystems/pkauth/persistence/jdbi/JdbiRefreshTokenRepository.java @@ -21,6 +21,7 @@ import org.jdbi.v3.core.JdbiException; import org.jdbi.v3.core.mapper.RowMapper; import org.jdbi.v3.core.statement.Update; +import org.jspecify.annotations.Nullable; /** * {@link RefreshTokenRepository} backed by the {@code refresh_tokens} table (Flyway V9; the {@code @@ -176,7 +177,7 @@ public int deleteExpiredBefore(Instant cutoff) { // -- Internals -------------------------------------------------------------------------- private static void insert(Handle h, RefreshTokenRecord r) { - Update update = + try (Update update = h.createUpdate( "INSERT INTO refresh_tokens" + " (refresh_id, token_hash, user_handle, audience, device_id, family_id," @@ -194,24 +195,26 @@ private static void insert(Handle h, RefreshTokenRecord r) { .bind("iat", OffsetDateTime.ofInstant(r.issuedAt(), ZoneOffset.UTC)) .bind("exp", OffsetDateTime.ofInstant(r.expiresAt(), ZoneOffset.UTC)) .bind("reason", r.revokedReason().map(Enum::name).orElse(null)) - .bind("amr", joinAmr(r.amr())); - // used_at and revoked_at are TIMESTAMPTZ; JDBI's untyped-null default (Types.VARCHAR) is - // rejected by Postgres against a TIMESTAMPTZ column. Force Types.TIMESTAMP_WITH_TIMEZONE on - // the null branch. - bindNullable( - update, - "uat", - r.usedAt().map(t -> OffsetDateTime.ofInstant(t, ZoneOffset.UTC)).orElse(null), - Types.TIMESTAMP_WITH_TIMEZONE); - bindNullable( - update, - "rat", - r.revokedAt().map(t -> OffsetDateTime.ofInstant(t, ZoneOffset.UTC)).orElse(null), - Types.TIMESTAMP_WITH_TIMEZONE); - update.execute(); + .bind("amr", joinAmr(r.amr()))) { + // used_at and revoked_at are TIMESTAMPTZ; JDBI's untyped-null default (Types.VARCHAR) is + // rejected by Postgres against a TIMESTAMPTZ column. Force Types.TIMESTAMP_WITH_TIMEZONE on + // the null branch. + bindNullable( + update, + "uat", + r.usedAt().map(t -> OffsetDateTime.ofInstant(t, ZoneOffset.UTC)).orElse(null), + Types.TIMESTAMP_WITH_TIMEZONE); + bindNullable( + update, + "rat", + r.revokedAt().map(t -> OffsetDateTime.ofInstant(t, ZoneOffset.UTC)).orElse(null), + Types.TIMESTAMP_WITH_TIMEZONE); + update.execute(); + } } - private static void bindNullable(Update update, String name, Object value, int sqlType) { + private static void bindNullable( + Update update, String name, @Nullable Object value, int sqlType) { if (value == null) { update.bindNull(name, sqlType); } else { diff --git a/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/admin/PkAuthAdminController.java b/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/admin/PkAuthAdminController.java index 1b3a162..60c6ffb 100644 --- a/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/admin/PkAuthAdminController.java +++ b/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/admin/PkAuthAdminController.java @@ -13,6 +13,7 @@ import com.codeheadsystems.pkauth.api.CredentialId; import com.codeheadsystems.pkauth.api.UserHandle; import com.codeheadsystems.pkauth.spring.security.PkAuthJwtAuthenticationToken; +import org.jspecify.annotations.Nullable; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; @@ -62,7 +63,8 @@ public ResponseEntity listCredentials() { @PatchMapping("/credentials/{credentialId}") public ResponseEntity rename( - @PathVariable("credentialId") String credentialId, @RequestBody RenameCredential body) { + @PathVariable("credentialId") String credentialId, + @RequestBody @Nullable RenameCredential body) { UserHandle user = currentUser(); CredentialId id = CredentialId.fromB64Url(credentialId); return PkAuthAdminResultMapper.toResponse( @@ -95,7 +97,8 @@ public ResponseEntity remainingBackupCodes() { // -- Email ----------------------------------------------------------------------------------- @PostMapping("/email/start-verification") - public ResponseEntity startEmailVerification(@RequestBody StartEmailVerification body) { + public ResponseEntity startEmailVerification( + @RequestBody @Nullable StartEmailVerification body) { UserHandle user = currentUser(); return PkAuthAdminResultMapper.toResponse( adminService.startEmailVerification(user, user, body == null ? "" : body.email())); @@ -103,7 +106,8 @@ public ResponseEntity startEmailVerification(@RequestBody StartEmailVeri /** Unauthenticated per brief §6.9 ("token identifies the user"). */ @PostMapping("/email/complete-verification") - public ResponseEntity finishEmailVerification(@RequestBody FinishEmailVerification body) { + public ResponseEntity finishEmailVerification( + @RequestBody @Nullable FinishEmailVerification body) { return PkAuthAdminResultMapper.toResponseEntity( AdminResponseMapper.toResponse( adminService.finishEmailVerification(body == null ? "" : body.token()), @@ -113,14 +117,16 @@ public ResponseEntity finishEmailVerification(@RequestBody FinishEmailVe // -- Phone ----------------------------------------------------------------------------------- @PostMapping("/phone/start-verification") - public ResponseEntity startPhoneVerification(@RequestBody StartPhoneVerification body) { + public ResponseEntity startPhoneVerification( + @RequestBody @Nullable StartPhoneVerification body) { UserHandle user = currentUser(); return PkAuthAdminResultMapper.toResponse( adminService.startPhoneVerification(user, user, body == null ? "" : body.phone())); } @PostMapping("/phone/complete-verification") - public ResponseEntity finishPhoneVerification(@RequestBody FinishPhoneVerification body) { + public ResponseEntity finishPhoneVerification( + @RequestBody @Nullable FinishPhoneVerification body) { UserHandle user = currentUser(); return PkAuthAdminResultMapper.toResponse( adminService.finishPhoneVerification( diff --git a/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/security/PkAuthJwtAuthenticationToken.java b/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/security/PkAuthJwtAuthenticationToken.java index 2f8d1c2..b410200 100644 --- a/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/security/PkAuthJwtAuthenticationToken.java +++ b/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/security/PkAuthJwtAuthenticationToken.java @@ -4,6 +4,7 @@ import com.codeheadsystems.pkauth.api.UserHandle; import com.codeheadsystems.pkauth.jwt.JwtClaims; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -23,7 +24,7 @@ public final class PkAuthJwtAuthenticationToken extends AbstractAuthenticationTo private final UserHandle principal; private final JwtClaims claims; - private String token; + private @Nullable String token; public PkAuthJwtAuthenticationToken(UserHandle principal, JwtClaims claims, String token) { super(authorities(claims)); @@ -40,7 +41,7 @@ private static List authorities(JwtClaims claims) { } @Override - public Object getCredentials() { + public @Nullable Object getCredentials() { return token; } @@ -60,7 +61,7 @@ public JwtClaims getClaims() { * — applications that need the raw bearer for outbound calls must capture it before the security * context post-processing pass. */ - public String getToken() { + public @Nullable String getToken() { return token; } diff --git a/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/web/PkAuthCeremonyController.java b/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/web/PkAuthCeremonyController.java index 7cbeac1..26eaea4 100644 --- a/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/web/PkAuthCeremonyController.java +++ b/pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/web/PkAuthCeremonyController.java @@ -11,6 +11,7 @@ import com.codeheadsystems.pkauth.jwt.CeremonyOrchestrator; import com.codeheadsystems.pkauth.spi.CeremonyRateLimitedException; import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -79,7 +80,7 @@ private static ResponseEntity toResponseEntity(CeremonyResponse wire) { return ResponseEntity.status(wire.status()).body(wire.body()); } - private static String clientIp(HttpServletRequest request) { - return request == null ? null : request.getRemoteAddr(); + private static @Nullable String clientIp(HttpServletRequest request) { + return request.getRemoteAddr(); } } diff --git a/pk-auth-testkit/src/main/java/com/codeheadsystems/pkauth/testkit/ChallengeStoreScenarios.java b/pk-auth-testkit/src/main/java/com/codeheadsystems/pkauth/testkit/ChallengeStoreScenarios.java index 2134292..ff1f291 100644 --- a/pk-auth-testkit/src/main/java/com/codeheadsystems/pkauth/testkit/ChallengeStoreScenarios.java +++ b/pk-auth-testkit/src/main/java/com/codeheadsystems/pkauth/testkit/ChallengeStoreScenarios.java @@ -61,9 +61,8 @@ public void concurrentTakeOnceYieldsExactlyOneWinner() throws Exception { int threads = 8; CountDownLatch ready = new CountDownLatch(threads); CountDownLatch fire = new CountDownLatch(1); - ExecutorService pool = Executors.newFixedThreadPool(threads); AtomicInteger winners = new AtomicInteger(); - try { + try (ExecutorService pool = Executors.newFixedThreadPool(threads)) { List> futures = new java.util.ArrayList<>(); for (int i = 0; i < threads; i++) { futures.add( @@ -89,8 +88,6 @@ public void concurrentTakeOnceYieldsExactlyOneWinner() throws Exception { } assertThat(winners.get()).as("exactly one thread consumes the challenge").isEqualTo(1); assertThat(store.takeOnce(id)).as("nothing left after the race").isEmpty(); - } finally { - pool.shutdownNow(); } } diff --git a/pk-auth-testkit/src/main/java/com/codeheadsystems/pkauth/testkit/RefreshTokenScenarios.java b/pk-auth-testkit/src/main/java/com/codeheadsystems/pkauth/testkit/RefreshTokenScenarios.java index fa5baee..ddcf6cb 100644 --- a/pk-auth-testkit/src/main/java/com/codeheadsystems/pkauth/testkit/RefreshTokenScenarios.java +++ b/pk-auth-testkit/src/main/java/com/codeheadsystems/pkauth/testkit/RefreshTokenScenarios.java @@ -199,9 +199,8 @@ public void concurrentRotationExactlyOneSucceedsFamilyRevoked() throws Exception int threads = 8; CountDownLatch ready = new CountDownLatch(threads); CountDownLatch fire = new CountDownLatch(1); - ExecutorService pool = Executors.newFixedThreadPool(threads); List> futures = new ArrayList<>(); - try { + try (ExecutorService pool = Executors.newFixedThreadPool(threads)) { for (int i = 0; i < threads; i++) { futures.add( pool.submit( @@ -238,8 +237,6 @@ public void concurrentRotationExactlyOneSucceedsFamilyRevoked() throws Exception assertThat(family) .allSatisfy( r -> assertThat(r.revokedAt()).as("row %s revoked", r.refreshId()).isPresent()); - } finally { - pool.shutdownNow(); } }