update docstrings
This commit is contained in:
@@ -56,15 +56,16 @@ public class UserController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a user from a Google ID token and issues a session JWT.
|
||||
* Authenticates a user from a Google ID token and starts a session.
|
||||
* <p>
|
||||
* The JWT is set as an HttpOnly {@code token} cookie (scoped by request security) and also
|
||||
* returned in the response body.
|
||||
* Sets two HttpOnly cookies: {@code accessToken} (the JWT, path {@code /}) and {@code refreshToken}
|
||||
* (path {@code /api/v1/users}). Neither token is returned in the response body.
|
||||
*
|
||||
* @param token the payload carrying the Google ID token to verify
|
||||
* @param request the incoming request, used to decide cookie {@code Secure}/{@code SameSite} flags
|
||||
* @param response the response the session cookie is written to
|
||||
* @return {@code 200 OK} wrapping the issued JWT as a {@link Token}
|
||||
* @param request the incoming request, used to decide the cookies' {@code Secure} flag
|
||||
* @param response the response the cookies are written to
|
||||
* @return {@code 200 OK} with no data
|
||||
* @throws NotAuthenticated if the Google ID token is rejected
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<APIResponse<Void>> login(@RequestBody Token token,
|
||||
@@ -78,6 +79,15 @@ public class UserController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges a refresh token for a new refresh token and JWT, replacing both cookies.
|
||||
*
|
||||
* @param token the payload carrying the current refresh token
|
||||
* @param response the response the new cookies are written to
|
||||
* @param request the incoming request, used to decide the cookies' {@code Secure} flag
|
||||
* @return {@code 200 OK} with no data
|
||||
* @throws NotAuthenticated if the refresh token is rejected
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
public ResponseEntity<APIResponse<Void>> refresh(
|
||||
@RequestBody Token token,
|
||||
@@ -92,8 +102,10 @@ public class UserController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user out by clearing the session cookie.
|
||||
* Logs the user out by revoking the session tied to the {@code refreshToken} cookie.
|
||||
* The auth cookies are not cleared.
|
||||
*
|
||||
* @param refreshToken the refresh token from the {@code refreshToken} cookie, if present
|
||||
* @return {@code 200 OK} confirming logout
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@@ -106,6 +118,11 @@ public class UserController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the {@code accessToken} cookie ({@code SameSite=Lax}, path {@code /}) and the
|
||||
* {@code refreshToken} cookie ({@code SameSite=Strict}, path {@code /api/v1/users}), both HttpOnly
|
||||
* and {@code Secure} when the request is.
|
||||
*/
|
||||
private void addCookies(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
|
||||
@@ -19,6 +19,13 @@ import java.time.Instant;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Issues, rotates and revokes opaque refresh tokens.
|
||||
* <p>
|
||||
* Each login starts a {@link TokenFamily}; every refresh token issued for that session belongs to it.
|
||||
* Only the SHA-256 hash of a token is stored. Presenting an already-used token after the grace
|
||||
* period is treated as theft and revokes the whole family.
|
||||
*/
|
||||
@Service
|
||||
public class TokenService {
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
@@ -33,6 +40,12 @@ public class TokenService {
|
||||
private final Duration refreshTokenExpiration;
|
||||
private final Duration refreshGracePeriod;
|
||||
|
||||
/**
|
||||
* @param tokenFamilyRepository stores token families (one per login session)
|
||||
* @param refreshTokenRepository stores hashed refresh tokens
|
||||
* @param props supplies token family lifetime, refresh token lifetime and reuse grace period
|
||||
* @param clock time source for all timestamps and expiry checks
|
||||
*/
|
||||
public TokenService (
|
||||
TokenFamilyRepository tokenFamilyRepository,
|
||||
RefreshTokenRepository refreshTokenRepository,
|
||||
@@ -48,6 +61,12 @@ public class TokenService {
|
||||
this.refreshGracePeriod = props.expiration().gracePeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new session for a user by creating a token family and its first refresh token.
|
||||
*
|
||||
* @param user the user the session belongs to
|
||||
* @return the raw refresh token as a 64-character hex string; only its hash is persisted
|
||||
*/
|
||||
@Transactional
|
||||
public String createSession(User user) {
|
||||
Instant now = Instant.now(clock);
|
||||
@@ -68,6 +87,19 @@ public class TokenService {
|
||||
return HEX.formatHex(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchanges a refresh token for a new one in the same family.
|
||||
* <p>
|
||||
* The presented token is marked used and a new token is issued. Presenting a used token again
|
||||
* within the grace period returns another new token without extending the grace period. Presenting
|
||||
* it after the grace period revokes the family; this revocation is committed even though the
|
||||
* method throws.
|
||||
*
|
||||
* @param hexToken the raw refresh token as a hex string
|
||||
* @return the owning user's ID and the new raw refresh token
|
||||
* @throws NotAuthenticated if the token is missing, malformed, unknown, expired, belongs to a revoked
|
||||
* or expired family, or is reused after the grace period
|
||||
*/
|
||||
@Transactional(noRollbackFor = NotAuthenticated.class)
|
||||
public Rotation rotate(String hexToken) {
|
||||
if (hexToken == null || hexToken.isBlank()) {
|
||||
@@ -124,6 +156,13 @@ public class TokenService {
|
||||
return new Rotation(tokenFamily.getUser().getId(), HEX.formatHex(newRawToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes a refresh token and its whole token family, ending the session. Does nothing if the
|
||||
* token is unknown, and leaves existing revocation timestamps unchanged.
|
||||
*
|
||||
* @param hexToken the raw refresh token as a hex string
|
||||
* @throws NotAuthenticated if the token is not valid hex
|
||||
*/
|
||||
public void revoke(String hexToken) {
|
||||
Instant now = Instant.now(clock);
|
||||
|
||||
@@ -144,6 +183,12 @@ public class TokenService {
|
||||
if (refreshToken.getRevokedAt() == null) refreshToken.setRevokedAt(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a successful {@link #rotate(String)}.
|
||||
*
|
||||
* @param userId ID of the user who owns the token family
|
||||
* @param newRefreshToken the newly issued raw refresh token as a hex string
|
||||
*/
|
||||
public record Rotation(Long userId, String newRefreshToken) {}
|
||||
|
||||
private static byte[] sha256(byte[] in) {
|
||||
|
||||
@@ -61,11 +61,14 @@ public class UserService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a Google ID token, creating or updating the corresponding user, and issues a session JWT.
|
||||
* Verifies a Google ID token, creates or updates the matching user, and starts a new session.
|
||||
* <p>
|
||||
* A new user gets a username equal to their email, the default role and a random stream key.
|
||||
* Email, name and profile picture are refreshed from Google on every login.
|
||||
*
|
||||
* @param idToken the Google ID token to verify
|
||||
* @return a signed JWT for the authenticated user
|
||||
* @throws NotAuthenticated if the token is invalid or its subject is missing
|
||||
* @return a new refresh token and a signed JWT access token for the user
|
||||
* @throws NotAuthenticated if the ID token is invalid, has no subject, or its email is unverified
|
||||
*/
|
||||
@Transactional
|
||||
public TokenPackage login(String idToken) {
|
||||
@@ -78,6 +81,13 @@ public class UserService {
|
||||
return new TokenPackage(refreshToken, jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates a refresh token and issues a new JWT access token for its owner.
|
||||
*
|
||||
* @param refreshToken the raw refresh token as a hex string
|
||||
* @return the new refresh token and a signed JWT access token
|
||||
* @throws NotAuthenticated if the refresh token is rejected; see {@link TokenService#rotate(String)}
|
||||
*/
|
||||
public TokenPackage refresh(String refreshToken) {
|
||||
TokenService.Rotation rotation = tokenService.rotate(refreshToken);
|
||||
String jwt = jwtService.generateToken(rotation.userId());
|
||||
@@ -85,6 +95,13 @@ public class UserService {
|
||||
return new TokenPackage(rotation.newRefreshToken(), jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends the session tied to a refresh token by revoking its token family.
|
||||
* Already-issued JWT access tokens stay valid until they expire.
|
||||
*
|
||||
* @param refreshToken the raw refresh token as a hex string
|
||||
* @throws NotAuthenticated if the token is not valid hex
|
||||
*/
|
||||
public void logout(String refreshToken) {
|
||||
tokenService.revoke(refreshToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user