Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Transport> fromWire(String s) {
public static Optional<Transport> fromWire(@Nullable String s) {
if (s == null) {
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -89,10 +90,7 @@ private static HttpResponse<Map<String, Object>> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -54,7 +55,7 @@ public Publisher<io.micronaut.http.MutableHttpResponse<?>> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@
@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());

Check warning on line 26 in pk-auth-otp/src/main/java/com/codeheadsystems/pkauth/otp/LoggingSmsSender.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Invoke method(s) only conditionally.

See more on https://sonarcloud.io/project/issues?id=codeheadsystems_pk-auth&issues=AZ63M9zXwmy3pcsx89JW&open=AZ63M9zXwmy3pcsx89JW&pullRequest=54
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ public void send(String phoneE164, String body) {
+ ") phone="
+ phoneE164
+ " bodyLength="
+ (body == null ? 0 : body.length()));
+ body.length());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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)"
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,"
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -62,7 +63,8 @@ public ResponseEntity<Object> listCredentials() {

@PatchMapping("/credentials/{credentialId}")
public ResponseEntity<Object> 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(
Expand Down Expand Up @@ -95,15 +97,17 @@ public ResponseEntity<Object> remainingBackupCodes() {
// -- Email -----------------------------------------------------------------------------------

@PostMapping("/email/start-verification")
public ResponseEntity<Object> startEmailVerification(@RequestBody StartEmailVerification body) {
public ResponseEntity<Object> startEmailVerification(
@RequestBody @Nullable StartEmailVerification body) {
UserHandle user = currentUser();
return PkAuthAdminResultMapper.toResponse(
adminService.startEmailVerification(user, user, body == null ? "" : body.email()));
}

/** Unauthenticated per brief §6.9 ("token identifies the user"). */
@PostMapping("/email/complete-verification")
public ResponseEntity<Object> finishEmailVerification(@RequestBody FinishEmailVerification body) {
public ResponseEntity<Object> finishEmailVerification(
@RequestBody @Nullable FinishEmailVerification body) {
return PkAuthAdminResultMapper.toResponseEntity(
AdminResponseMapper.toResponse(
adminService.finishEmailVerification(body == null ? "" : body.token()),
Expand All @@ -113,14 +117,16 @@ public ResponseEntity<Object> finishEmailVerification(@RequestBody FinishEmailVe
// -- Phone -----------------------------------------------------------------------------------

@PostMapping("/phone/start-verification")
public ResponseEntity<Object> startPhoneVerification(@RequestBody StartPhoneVerification body) {
public ResponseEntity<Object> 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<Object> finishPhoneVerification(@RequestBody FinishPhoneVerification body) {
public ResponseEntity<Object> finishPhoneVerification(
@RequestBody @Nullable FinishPhoneVerification body) {
UserHandle user = currentUser();
return PkAuthAdminResultMapper.toResponse(
adminService.finishPhoneVerification(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,7 +24,7 @@

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));
Expand All @@ -40,7 +41,7 @@
}

@Override
public Object getCredentials() {
public @Nullable Object getCredentials() {
return token;
}

Expand All @@ -60,7 +61,7 @@
* — 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() {

Check warning on line 64 in pk-auth-spring-boot-starter/src/main/java/com/codeheadsystems/pkauth/spring/security/PkAuthJwtAuthenticationToken.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Update this method so that its implementation is not identical to "getCredentials" on line 44.

See more on https://sonarcloud.io/project/issues?id=codeheadsystems_pk-auth&issues=AZ63M928wmy3pcsx89JX&open=AZ63M928wmy3pcsx89JX&pullRequest=54
return token;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -79,7 +80,7 @@ private static ResponseEntity<Object> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Future<?>> futures = new java.util.ArrayList<>();
for (int i = 0; i < threads; i++) {
futures.add(
Expand All @@ -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();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Future<RotateResult>> futures = new ArrayList<>();
try {
try (ExecutorService pool = Executors.newFixedThreadPool(threads)) {
for (int i = 0; i < threads; i++) {
futures.add(
pool.submit(
Expand Down Expand Up @@ -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();
}
}

Expand Down
Loading