diff --git a/src/main/java/com/ddf/vodsystem/controllers/UserController.java b/src/main/java/com/ddf/vodsystem/controllers/UserController.java index a8bd579..c0e4241 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/UserController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/UserController.java @@ -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. *
- * 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
+ * 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.
+ *
+ * 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) {
diff --git a/src/main/java/com/ddf/vodsystem/services/UserService.java b/src/main/java/com/ddf/vodsystem/services/UserService.java
index ff91495..801f775 100644
--- a/src/main/java/com/ddf/vodsystem/services/UserService.java
+++ b/src/main/java/com/ddf/vodsystem/services/UserService.java
@@ -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.
+ *
+ * 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);
}