feat(backend): JWT Authentication & RBAC with Refresh Token Rotation (#23) - #33
Merged
meshackyaro merged 1 commit intoJul 24, 2026
Conversation
…kman-labs#23) Implements Spring Security JWT auth with role-based authorization and secure refresh-token rotation, including revocation and reuse detection. Auth model: - New UserAccount principal (email + bcrypt hash + Role), separate from the Client/SkilledWorker domain entities so existing flows are untouched. - Access token: short-lived HS256 JWT (sub/email/role claims) so per-request authorization needs no DB hit. Stateless SecurityConfig + JWT filter. - Refresh token: opaque 256-bit secret, stored only as a SHA-256 hash. Rotation & security properties: - Every refresh rotates: the presented token is revoked and a successor is issued in the same family (login-session lineage). - Reuse detection: replaying an already-revoked token revokes the whole family, killing the session. The family revoke runs in a REQUIRES_NEW transaction so the signalling throw can't roll it back. - Concurrency-safe: revoke-old is an atomic conditional UPDATE, so two simultaneous refreshes cannot both mint a successor; the loser is reuse. - Self-registration can never create an ADMIN (privilege-escalation guard). Also: - RBAC via URL rules + @EnableMethodSecurity/@PreAuthorize; /api/v1/admin/** requires ADMIN. - Consistent {error, success} error contract across 400/401/403/409 and filter-level 401/403 handlers. - OpenAPI/Swagger UI (springdoc) + bean validation on request DTOs. - Tests: unit (JwtService, RefreshTokenService incl. reuse & race-loss) and @SpringBootTest integration (register/login/refresh/logout, RBAC 403/200, reuse -> 401, concurrent-refresh). Full suite: 43 passing via `./mvnw verify`. - Docs: backend-api/docs/AUTHENTICATION.md + README API-reference section. New deps: spring-boot-starter-validation, springdoc-openapi-starter-webmvc-ui. Closes workman-labs#23.
meshackyaro
approved these changes
Jul 24, 2026
meshackyaro
left a comment
Contributor
There was a problem hiding this comment.
Security is an integral part of this SaaS project and I can see that it was handled properly.
Well done @jayteemoney. Thanks for your contribution
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #23.
What this adds
Spring Security JWT authentication with role-based authorization and secure refresh-token rotation (revocation + reuse detection) for
backend-api. Full design notes:backend-api/docs/AUTHENTICATION.md.Flow:
register→login→refresh(rotate) →logout, plus/meand an ADMIN-gated demo endpoint.Key design decisions
UserAccountprincipal (email + bcrypt hash +Role), separate from theClient/SkilledWorkerdomain entities, so the existing (messy) domain login/registration is left untouched. The legacyJwtUtils/ClientServiceImpl.loginstub is intentionally not modified — migrating it onto this flow is a documented follow-up.sub/email/roleclaims) → per-request authorization needs no DB hit. Refresh token = opaque 256-bit secret, stored only as a SHA-256 hash (a DB leak exposes no usable tokens).REQUIRES_NEWtransaction. Detection happens inrotate(), which then throws to signal the caller — and that throw would otherwise roll back the revocation. This was a real bug the integration test caught before it could ship.UPDATE … WHERE revoked=false, so two simultaneous refreshes can't both mint a successor; the loser is treated as reuse.@EnableMethodSecurity/@PreAuthorize(/api/v1/admin/** → hasRole('ADMIN')).{ error, success:false }across 400/401/403/409, including filter-level 401/403 via custom entry-point / access-denied handlers.Acceptance criteria
/swagger-ui.html; uniform{error, success}responses.actions/setup-javacache: mavenintest.yml(the CI already runs a Postgres service the integration tests use).Verification (run locally against Postgres 16)
./mvnw test— 43 passed, 0 failed./mvnw verify— BUILD SUCCESS (jar built + repackaged)Tests: unit (
JwtServiceTest,RefreshTokenServiceTest) run without a DB;AuthIntegrationTest(@SpringBootTest+ MockMvc) runs against the CI Postgres service.New dependencies
spring-boot-starter-validation—@Validon request DTOsspringdoc-openapi-starter-webmvc-ui:2.6.0— OpenAPI 3 + Swagger UINotes for reviewers
user_accounts,refresh_tokens) are created by Hibernate (ddl-auto=update).JWT_SECRETships with a dev-only placeholder default; it must be overridden in real environments (documented).client/skilledWorkerroutes remain permitAll, so current behaviour and tests are unaffected.🤖 Generated with Claude Code