Repository navigation
feat(auth): Refresh Token Rotation, Clean Architecture & Enterprise Hardening (#54) - #55
Conversation
…ardening (#54) - Security & Dual-Token Lifecycle: - Implement short-lived JWT Access Tokens (15-min TTL) and cryptographically secure random Refresh Tokens (32 bytes). - Add RefreshToken model and CreateRefreshToken migration with unique index on SHA-256 token hash. - Add POST /auth/refresh with token rotation: issues a new token pair and revokes used token. - Implement Compromised Token Reuse Detection: replaying an already-revoked refresh token revokes all active sessions for that user ID and logs critical alert. - Update POST /auth/login to return dual-token pair while preserving backward compatibility. - Update POST /auth/logout to revoke both access token JTI and refresh token. - Domain & Clean Architecture: - Add StudentRepository protocol and DatabaseStudentRepository implementation. - Add RefreshTokenRepository protocol and DatabaseRefreshTokenRepository implementation. - Refactor StudentService and TokenService to use repository abstractions. - Concurrency & Environment Hardening: - Explicitly configure PostgreSQL and MySQL database connection pools (maxConnectionsPerEventLoop: 8, connectionPoolTimeout: 10s). - Enforce in-memory SQLite database isolation for test environments. - Enterprise Standard Error Envelope: - Add UnifiedErrorMiddleware formatting all 4xx/5xx API responses into predictable { "error": { "code", "message", "timestamp" } } payloads. - Testing & Verification: - Add integration tests for login token pair, refresh rotation, reuse detection, invalid refresh tokens, and unified error envelopes. - All 125 integration tests passing.
…correct error specs (#54) - Implement atomic consumeIfActive with row-level locking (FOR UPDATE) in RefreshTokenRepository - Complete Clean Architecture decoupling by adding PasswordResetRepository and RevokedTokenRepository - Decouple AuthController, GraphQLResolver, and TokenService from direct Fluent ORM calls - Add secondary database indexes on refresh_tokens user_id and expires_at - Add lifecycle cleanup routine for expired/revoked refresh tokens - Handle DecodingError as 400 Bad Request with BAD_REQUEST in UnifiedErrorMiddleware - Update README to accurately describe Unified API Error Envelope and login token backward compatibility - Expand test suite to 133 tests including atomic consume and malformed JSON tests
…uests - Introduce RequestLoggingMiddleware capturing method, path, client IP, User-Agent, status code, and latency in milliseconds - Configure app.logger.logLevel to read LOG_LEVEL env var, defaulting to .info in development - Add LOG_LEVEL=info to .env and .env.example
…ith RETURNING - Replaced SELECT-then-UPDATE with atomic UPDATE-first statement matching token_hash, is_revoked=false, and expires_at > now - Uses RETURNING to return consumed row in a single atomic database statement - Handles zero-row updates with follow-up SELECT to distinguish not-found, already-revoked (replay), or expired - Added flexible RefreshTokenRow decoding for PostgreSQL, SQLite, and MySQL dialect compatibility - MySQL fallback uses row-level write lock under transaction
|
Assessment: Not production-safe yet Blocking findings The implementation is not actually atomic on every configured database. The code uses SELECT ... FOR UPDATE followed by a separate read and a separate model save. This is safe only when the driver supports row locks and all operations remain on the same transaction connection. SQLite does not provide the same FOR UPDATE semantics, yet the test suite explicitly defaults to in-memory SQLite. Either implement an atomic conditional update (is_revoked = false and expires_at > now) or add database-specific write-lock handling and a production-like PostgreSQL/MySQL concurrency test. The expired-token mutation is rolled back. consumeIfActive sets isRevoked = true for an expired token and returns .expired, but rotateRefreshToken then throws inside the transaction. The transaction therefore rolls back the revocation, so expired tokens remain unrevoked indefinitely. This is not an immediate token-replay vulnerability because expiry is checked on every use, but it defeats the lifecycle invariant and causes repeated writes/queries for the same expired token. Either commit the expiry revocation separately or treat expiry as a successful transactional state transition before returning the 401 response. Recommended transaction shape SQL One returned row: the request won; issue the new pair. SQL Additional database concerns Replace the read-then-save consume flow with an atomic conditional update. |
…sume (#54) - Replace try? with try await on atomic UPDATE ... RETURNING, SELECT, and expired UPDATE queries to prevent suppressing database outages or decoding errors as missing tokens or false reuse detections - Throw DecodingError.dataCorrupted on malformed ISO-8601 token expiration values instead of falling back to Date()
…mic transaction (#54) - Move session family revocation on reuse detection inside the atomic transaction so revocation is committed without a race window - Commit expired-token revocation within the transaction before returning 401 Unauthorized - Model rotation domain states via RotationOutcome enum to avoid exception-driven rollback of legitimate mutations - Add integration test asserting that expired refresh tokens are rejected and their revocation is persisted in the database
…nd PostgreSQL concurrency test (#54) - MySQL path: execute conditional UPDATE ... WHERE is_revoked = false AND expires_at > now followed by ROW_COUNT() affected-row inspection - User row locking: acquire exclusive FOR UPDATE lock on student record across token creation, reuse revocation, and session cleanup to serialize token minting against compromise invalidation - Concurrency test: add PostgreSQL integration test harness and testPostgresConcurrentRefreshRequests to validate row-locking semantics on production database engines
…ntract, and ensure robust CI concurrency testing (#54) - Wrap logout access-token revocation and session-family revocation inside req.db.transaction with automatic enclosing transaction support in revokeAllSessions - Reject non-SQL drivers in consumeIfActive with an explicit Abort error to guarantee atomic single-use semantics - Replace raw C socket reachability with portable SQLPostgresConfiguration probe, align database name with CI (student_db), and enforce non-skipping in CI environments - Add MySQL concurrency test harness and testMySQLConcurrentRefreshRequests
|
I checked the current code in the PR branch, not the comment’s resolved state, and there is no remaining review feedback from that comment that is still unaddressed. What the reviewer asked for, and whether the code now satisfies it: Reuse detection and session-family revocation in the same transaction Addressed in Sources/StudentAppBackend/Services/TokenService.swift Addressed in Sources/StudentAppBackend/Repositories/RefreshTokenRepository.swift Addressed in the same repo file and token service flow Addressed in Tests/StudentAppBackendTests/StudentAppBackendTests.swift Addressed in Sources/StudentAppBackend/Migrations/CreateRefreshToken.swift Unresolved reviewer feedback: none |
Summary
Closes #54
Implements enterprise-grade architectural hardening of
StudentAppBackend:refresh_tokens. CallingPOST /auth/refreshrotates the token pair and revokes the used token.StudentRepositoryandRefreshTokenRepositoryprotocols with Fluent database implementations to decouple persistence from controllers and GraphQL resolvers.maxConnectionsPerEventLoop: 8,connectionPoolTimeout: .seconds(10)) and enforces in-memory SQLite isolation for test environments.UnifiedErrorMiddlewareformatting all 4xx/5xx API responses into predictable{ "error": { "code": "...", "message": "...", "timestamp": "..." } }contracts while preserving domain error identifiers (EMAIL_ALREADY_EXISTS, etc.).Key Changes
RefreshToken.swift: Fluent model tracking token hash, user reference with cascade deletion, expiry, and revocation flag.CreateRefreshToken.swift: Migration definingrefresh_tokensschema with unique index ontoken_hash.StudentRepository.swift: Protocol andDatabaseStudentRepositoryimplementation.RefreshTokenRepository.swift: Protocol andDatabaseRefreshTokenRepositoryimplementation.TokenService.swift: Conforms toTokenServiceProtocolwith SHA-256 digest hashing, token pair generation, rotation with reuse detection, and session revocation.StudentService.swift: Refactored to operate throughStudentRepository.AuthController.swift: AddedPOST /auth/refresh, enhancedPOST /auth/loginto return dual tokens (with backward compatibility), and updatedPOST /auth/logoutto revoke refresh tokens.UnifiedErrorMiddleware.swift: Enterprise JSON error response pipeline.configure.swift: RegisteredCreateRefreshTokenmigration, connection pool tuning, and test environment isolation.Verification