-
Implementation Overview:
I will implement PAT authentication of REST API by using a dedicated Spring Security Filter which parses the bearer token and if present, validates it and create security context.
Details:
PATAuthenticationFilter extends OncePerRequestFilter, extracts the Bearer token, validates it againstPersonalAccessTokenRepository(not expired, not revoked), and populates theSecurityContextwith the token owner's identity and roles. Wired intoSessionAuthSecurityConfigbeforeUsernamePasswordAuthenticationFilterso it runs first without disturbing session-based auth for browser clients.Unit Test Cases:
- Happy path — valid global PAT, SecurityContext populated with the token owner's identity
- Non-existent PAT — resolveToken returns empty, SecurityContext remains null, 403 from downstream
- Revoked PAT — same result as case 2, resolveToken returns empty because findActiveByPrefix excludes revoked tokens at the query level
- Project-scoped PAT, wrong project — PAT is valid but scoped to project A, request targets project B, filter halts the chain and returns 403 directly
- Project-scoped PAT, correct project — same PAT as case 4 but request targets project A, SecurityContext populated, chain continues normally
Please note: In Spring Security, once authentication is complete, everything downstream only cares about if Authentication token can be retrieved from SecurityCOntext as in
Authentication auth = SecurityContextHolder.getContext().getAuthentication();It does not care whether it came from Form login, OAuth2, JWT, API Key, PA, Kerberos or LDAP.
The following is the flow of Sztab spring security filters and where PATAuthenticationFilter in this flow:
Screenshot 2026-03-21 at 3.54.30 PM -
rksuma@Ramakrishnans-MacBook-Pro backend % mvn clean test //... [WARNING] Tests run: 310, Failures: 0, Errors: 0, Skipped: 57 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 11.804 s [INFO] Finished at: 2026-03-21T13:00:49-07:00 [INFO] ------------------------------------------------------------------------ rksuma@Ramakrishnans-MacBook-Pro backend % git add -A; git status On branch feature/SZ-117-PAT-Authentication-for-REST-API Your branch is up to date with 'origin/feature/SZ-117-PAT-Authentication-for-REST-API'. Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: src/main/java/com/sztab/model/PersonalAccessToken.java modified: src/main/java/com/sztab/repository/PersonalAccessTokenRepository.java modified: src/main/java/com/sztab/security/config/SessionAuthSecurityConfig.java new file: src/main/java/com/sztab/security/entity/PATPrincipal.java modified: src/main/java/com/sztab/security/filter/PATAuthenticationFilter.java new file: src/main/resources/db/migration/V16__drop_scopes_column_from_pat_entity.sql modified: src/test/java/com/sztab/security/filter/PATAuthenticationFilterTest.java rksuma@Ramakrishnans-MacBook-Pro backend % git commit [feature/SZ-117-PAT-Authentication-for-REST-API c8d111f] Introduce PATAuthenticationFilter to enable stateless Bearer token authentication for REST endpoints. 7 files changed, 133 insertions(+), 103 deletions(-) create mode 100644 backend/src/main/java/com/sztab/security/entity/PATPrincipal.java create mode 100644 backend/src/main/resources/db/migration/V16__drop_scopes_column_from_pat_entity.sql rksuma@Ramakrishnans-MacBook-Pro backend % git push Enumerating objects: 53, done. Counting objects: 100% (53/53), done. Delta compression using up to 12 threads Compressing objects: 100% (24/24), done. Writing objects: 100% (30/30), 4.87 KiB | 4.87 MiB/s, done. Total 30 (delta 14), reused 0 (delta 0), pack-reused 0 (from 0) remote: remote: Create a pull request for 'feature/SZ-117-PAT-Authentication-for-REST-API' by visiting: remote: https://tigase.dev/sztab/~pulls/new?target=1325:wolnosc&source=1325:feature/SZ-117-PAT-Authentication-for-REST-API remote: To https://tigase.dev/sztab.git bc15db9..c8d111f feature/SZ-117-PAT-Authentication-for-REST-API -> feature/SZ-117-PAT-Authentication-for-REST-API rksuma@Ramakrishnans-MacBook-Pro backend % git checkout wolnosc Switched to branch 'wolnosc' Your branch is up to date with 'origin/wolnosc'. rksuma@Ramakrishnans-MacBook-Pro backend % git pull origin wolnosc From https://tigase.dev/sztab * branch wolnosc -> FETCH_HEAD Already up to date. rksuma@Ramakrishnans-MacBook-Pro backend % git merge --ff-only feature/SZ-117-PAT-Authentication-for-REST-API Updating 256c6b2..c8d111f Fast-forward backend/src/main/java/com/sztab/model/PersonalAccessToken.java | 8 ++- backend/src/main/java/com/sztab/repository/PersonalAccessTokenRepository.java | 27 +++++---- backend/src/main/java/com/sztab/security/config/SessionAuthSecurityConfig.java | 15 +++++ backend/src/main/java/com/sztab/security/entity/PATPrincipal.java | 17 ++++++ backend/src/main/java/com/sztab/security/filter/PATAuthenticationFilter.java | 74 ++++++++++++++++++++++++ backend/src/main/resources/db/migration/V16__drop_scopes_column_from_pat_entity.sql | 3 + backend/src/test/java/com/sztab/security/filter/PATAuthenticationFilterTest.java | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 350 insertions(+), 12 deletions(-) create mode 100644 backend/src/main/java/com/sztab/security/entity/PATPrincipal.java create mode 100644 backend/src/main/java/com/sztab/security/filter/PATAuthenticationFilter.java create mode 100644 backend/src/main/resources/db/migration/V16__drop_scopes_column_from_pat_entity.sql create mode 100644 backend/src/test/java/com/sztab/security/filter/PATAuthenticationFilterTest.java rksuma@Ramakrishnans-MacBook-Pro backend % git push origin wolnosc Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) To https://tigase.dev/sztab.git 256c6b2..c8d111f wolnosc -> wolnosc rksuma@Ramakrishnans-MacBook-Pro backend % git branch -d feature/SZ-117-PAT-Authentication-for-REST-API Deleted branch feature/SZ-117-PAT-Authentication-for-REST-API (was c8d111f). rksuma@Ramakrishnans-MacBook-Pro backend % git push origin --delete feature/SZ-117-PAT-Authentication-for-REST-API remote: remote: Create a pull request for 'feature/SZ-117-PAT-Authentication-for-REST-API' by visiting: remote: https://tigase.dev/sztab/~pulls/new?target=1325:wolnosc&source=1325:feature/SZ-117-PAT-Authentication-for-REST-API remote: To https://tigase.dev/sztab.git - [deleted] feature/SZ-117-PAT-Authentication-for-REST-API rksuma@Ramakrishnans-MacBook-Pro backend %
| Type |
New Feature
|
| Priority |
Normal
|
| Assignee | |
| Version |
none
|
| Sprints |
n/a
|
| Customer |
n/a
|
Problem
I woke up to this problem today when discussing the Bot testing approach over email with Artur. PATs only work for git operations (via Caddy
forward_auth). REST API consumers — CI/CD pipelines, third-party integrations, authorized bots — have no way to authenticate programmatically without simulating browser session login, which is fragile and stateful.Expected Behavior
Any REST API call with
Authorization: Bearer <token>should be validated againstPersonalAccessTokenRepositoryand granted access with the token owner's identity and roles, identical to a session-authenticated user.Current Behavior
Spring Security ignores the
Authorization: Bearerheader on REST endpoints. Only session cookies (JSESSIONID) are recognized. PATs are validated by Caddy only for/git/*routes.Fix
Implement a
PATAuthenticationFilterin Spring Security:Authorization: Bearer <token>from the request headerPersonalAccessTokenRepositorySecurityContextwith the token owner's identity and rolesSessionAuthSecurityConfigbeforeUsernamePasswordAuthenticationFilterEstimate
2-3h
Worklog
PATAuthenticationFilterextendingOncePerRequestFilterSessionAuthSecurityConfigNotes
PersonalAccessTokenRepositoryandPATHashServicealready exist — filter is the missing piece