Skip to content

Integrate authentication via UserAuth (player accounts + login)#45

Merged
dmccoystephenson merged 3 commits into
mainfrom
feature/userauth-integration
Jun 7, 2026
Merged

Integrate authentication via UserAuth (player accounts + login)#45
dmccoystephenson merged 3 commits into
mainfrom
feature/userauth-integration

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

Implements #44 — adds real player authentication to Barony by integrating the standalone UserAuth service, replacing the previous anonymous username-only session flow. This makes Barony the first real-world consumer of UserAuth.

Architecture: browser → web-client (proxy) → backend → UserAuth. The backend owns the UserAuth integration so credentials/JWTs never need direct browser↔UserAuth CORS. The backend validates the bearer token against UserAuth GET /session/validate on every authenticated request — the issue's recommended correctness-first option, which honours logout/revocation immediately.

Modules touched

Both (backend + web-client), plus docker-compose.yml and docs.

Backend

  • UserAuthClient (new) — proxies UserAuth /register, /login, /session/validate, /logout; base URL via userauth.url (default http://localhost:9998).
  • AuthController — rewritten to /api/auth/register, /api/auth/login, /api/auth/logout (proxy to UserAuth; login returns the JWT, logout revokes it).
  • GameController/api/session/* now authenticate via Authorization: Bearer; missing/invalid/expired/revoked tokens → 401. Game state is keyed by the authenticated username.
  • SessionService — refreshes a session's access time on reuse (removed the now-dead getSession lookup path) so an active player's state isn't reclaimed mid-play.

Web client

  • BackendService/WebController — proxy register/login/logout and forward the bearer token on game requests.
  • login.html — adds a password field + register link; stores the JWT. register.html (new). game.html — token-based headers; logout revokes server-side.

docker-compose

Adds userauth + userauth-db (Postgres) alongside Barony, wired via JWT_SECRET, USERAUTH_PATH (sibling checkout build context), ALLOWED_ORIGINS, and USERAUTH_URL for the backend.

Acceptance criteria

  • Register → log in → play authenticated session → log out via the web client
  • Backend rejects missing/invalid/expired/revoked tokens (401)
  • UserAuth + Postgres come up via docker-compose with Barony; CORS allows the web client
  • Flow documented (README API + Quick Start + auth flow, PLAYER_GUIDE, CHANGELOG, DOCS)

Test plan

  • backend: ./mvnw test157 passing (new: UserAuthClientTest, AuthControllerTest, GameControllerAuthTest, SessionServiceTest)
  • web-client: ./mvnw test4 passing (first test source for this module: BackendServiceTest)
  • Live end-to-end against a running UserAuth instance (not run in CI — UserAuth is an external service; mocked at the client boundary instead)

Notes

  • This is a feature-sized diff that necessarily spans both modules + compose + docs; it modifies docker-compose.yml, so per the dev-loop's do-not-auto-merge policy it is left for manual review rather than auto-merged.
  • UserAuth's README currently carries an "early development" caveat alongside its documented /register//login//session/validate//logout contract; this integration targets that documented contract. Any rough edges found while wiring it up will be filed back to the UserAuth repo per the issue.

Closes #44

🤖 Generated with Claude Code

BuildTools and others added 2 commits June 7, 2026 15:11
Replace the anonymous username-only session flow with real authentication
delegated to the standalone UserAuth service (register/login/logout + JWT).

Backend:
- Add UserAuthClient proxying UserAuth /register, /login, /session/validate, /logout
- Rewrite AuthController for register/login/logout (proxy to UserAuth)
- Authenticate /api/session/* via Authorization: Bearer, validating the token on
  every request so missing/invalid/expired/revoked tokens are refused (401)
- Key game state by the authenticated username; refresh session access time on reuse
- Add userauth.url config (USERAUTH_URL env)

Web client:
- Proxy register/login/logout and forward the bearer token on game requests
- Add password field + register link to login screen, add register screen
- Store the JWT client-side; logout revokes it server-side

docker-compose: add userauth + its Postgres (JWT_SECRET, USERAUTH_PATH, ALLOWED_ORIGINS)
Docs: README API + Quick Start + auth flow, PLAYER_GUIDE account section, CHANGELOG, DOCS
Tests: UserAuthClientTest, AuthControllerTest, GameControllerAuthTest, SessionServiceTest;
first web-client tests (BackendServiceTest)

Closes #44

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oxies

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@dmccoystephenson dmccoystephenson left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review rubric (adversarial — evidence-grounded; CI green on head 78b11bb as the external anchor):

Universal:

  • Scope: PASS — all 17 touched paths serve #44. The one judgment call: GameController was rewritten for Bearer auth and I extracted a shared validateDecision() helper (previously inline-duplicated in decision/sessionDecision); de-dup confined to the file already being changed, not unrelated churn.
  • Tests-new: PASS — every new public method is exercised: UserAuthClient (register/login/validate/logout), AuthController (register/login/logout incl. 400s), GameController auth gating (401 no-token, 401 invalid/revoked, 200 valid), SessionService.getOrCreateSession, and BackendService (register/login/logout/getSessionState/sessionTick/sessionCommand) after the follow-up test commit.
  • Tests-fix: PASS — the SessionService lastAccessed-refresh fix (regression from removing getSession) is covered by getOrCreateSessionRefreshesLastAccessedOnReuse, which fails without the refresh.
  • Sibling structure: PASS — UserAuthClient mirrors BackendService (constructor-injected RestTemplate); register.html mirrors login.html; tests follow the <Class>Test convention.
  • Sibling renames: PASS — sessionIdtoken renamed together across game.html (checkSession/getSessionHeaders/logout) and BackendService session methods; X-Session-IdAuthorization: Bearer swapped on both the backend endpoints and the web-client callers.
  • Docs: PASS — README (API + Quick Start + auth flow), PLAYER_GUIDE (account section), CHANGELOG, DOCS updated; MVP.md grep for auth/session/X-Session-Id returned nothing, so no drift there.
  • Issue resolution: PASS — register/login/logout + per-request token validation + per-user state all implemented; not a partial close.
  • CI: PASS — Backend (157 tests) and Web Client (6 tests) jobs both green.

Repo-specific:

  • Both-modules: PASS — separate CI jobs per module both pass; verified locally too.
  • API-contract: PASS — backend endpoint changes (/api/session/* → Bearer; new /api/auth/register|logout) reflected in BackendService/WebController and the README API section.
  • Changelog: PASS — Unreleased > Authentication entry added.
  • Constructor-injection: PASS — UserAuthClient + AuthController/GameController/WebController use constructor injection (@RequiredArgsConstructor); no field @Autowired introduced (the two rewritten controllers previously used field injection).
  • Override-correct: PASS (n/a) — no new @Override.

Flagged for human reviewer (judgment, not blocking): live end-to-end against a running UserAuth was not run in CI (external service; mocked at the client boundary). docker-compose is modified, so this PR is intentionally left for manual review rather than auto-merged.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Integrates real player authentication into Barony by proxying the external UserAuth service through the backend and switching per-player game APIs from anonymous sessions to Authorization: Bearer <token>.

Changes:

  • Added backend-side UserAuth integration (register/login/logout + per-request token validation) and migrated /api/session/* to be per-player and token-authenticated.
  • Updated the web-client to support register/login/logout UI + bearer token forwarding, and added initial web-client tests.
  • Extended docker-compose.yml and docs to run UserAuth (+ Postgres) alongside Barony and document the auth flow.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
web-client/src/test/java/com/barony/webclient/service/BackendServiceTest.java Adds first web-client tests for auth proxying + bearer forwarding.
web-client/src/main/resources/templates/register.html New registration page posting credentials to /api/auth/register.
web-client/src/main/resources/templates/login.html Adds password-based login and stores JWT client-side.
web-client/src/main/resources/templates/game.html Switches game requests from X-Session-Id to bearer token; adds logout revocation call.
web-client/src/main/java/com/barony/webclient/service/BackendService.java Adds register/logout methods and bearer-token session API calls.
web-client/src/main/java/com/barony/webclient/controller/WebController.java Adds register page + auth proxy endpoints; forwards bearer token on session endpoints.
README.md Documents UserAuth dependency, docker-compose startup, endpoints, and auth flow examples.
PLAYER_GUIDE.md Updates player instructions for registration/login/logout with UserAuth.
DOCS.md Notes authentication flow documentation coverage.
docker-compose.yml Adds userauth + userauth-db services and wires backend to UserAuth.
CHANGELOG.md Records UserAuth integration in Unreleased section.
backend/src/test/java/com/barony/backend/service/UserAuthClientTest.java Tests for UserAuth client proxy behavior.
backend/src/test/java/com/barony/backend/service/SessionServiceTest.java Tests session keying by username + last-access refresh.
backend/src/test/java/com/barony/backend/controller/GameControllerAuthTest.java MVC tests for 401 behavior and per-user scoping via mocked validation.
backend/src/test/java/com/barony/backend/controller/AuthControllerTest.java MVC tests for register/login/logout proxy behavior.
backend/src/main/resources/application.properties Adds userauth.url configuration via USERAUTH_URL.
backend/src/main/java/com/barony/backend/service/UserAuthClient.java New RestTemplate-based UserAuth client (register/login/validate/logout).
backend/src/main/java/com/barony/backend/service/SessionService.java Removes sessionId lookup and refreshes last-access on username reuse.
backend/src/main/java/com/barony/backend/controller/GameController.java Migrates /api/session/* to authenticate via bearer token + UserAuth validation.
backend/src/main/java/com/barony/backend/controller/AuthController.java Replaces sessionId login with UserAuth proxy register/login/logout.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +124 to 129
// Authenticated, per-user proxy endpoints (forward the bearer token to the backend)
@GetMapping("/api/session/state")
@ResponseBody
public GameState getSessionState(@RequestHeader("X-Session-Id") String sessionId) {
return backendService.getSessionState(sessionId);
public GameState getSessionState(@RequestHeader(value = "Authorization", required = false) String authorization) {
return backendService.getSessionState(token(authorization));
}
Comment on lines 131 to 135
@PostMapping("/api/session/tick")
@ResponseBody
public GameState sessionTick(@RequestHeader("X-Session-Id") String sessionId) {
return backendService.sessionTick(sessionId);
public GameState sessionTick(@RequestHeader(value = "Authorization", required = false) String authorization) {
return backendService.sessionTick(token(authorization));
}
Comment on lines 137 to 143
@PostMapping("/api/session/command")
@ResponseBody
public GameState sessionCommand(
@RequestHeader("X-Session-Id") String sessionId,
@RequestHeader(value = "Authorization", required = false) String authorization,
@RequestBody Command command) {
return backendService.sessionCommand(sessionId, command);
return backendService.sessionCommand(token(authorization), command);
}
Comment on lines 145 to 149
@PostMapping("/api/session/reset")
@ResponseBody
public GameState sessionReset(@RequestHeader("X-Session-Id") String sessionId) {
return backendService.sessionReset(sessionId);
public GameState sessionReset(@RequestHeader(value = "Authorization", required = false) String authorization) {
return backendService.sessionReset(token(authorization));
}
Comment on lines +165 to +169
@GetMapping("/api/session/ruler-stats")
@ResponseBody
public RulerStats getSessionRulerStats(@RequestHeader("X-Session-Id") String sessionId) {
return backendService.sessionRulerStats(sessionId);
public RulerStats getSessionRulerStats(@RequestHeader(value = "Authorization", required = false) String authorization) {
return backendService.sessionRulerStats(token(authorization));
}
Comment on lines 141 to 145
synchronized (session.getGameState()) {
gameService.setGameState(session.getGameState());

try {
gameService.changePolicy(decision.getCategory(), decision.getChoice());
return gameService.getState();
Comment on lines 159 to 162
synchronized (session.getGameState()) {
gameService.setGameState(session.getGameState());
return gameService.getRulerStats();
}
Comment on lines +43 to +47
@PostMapping("/logout")
public Map<String, String> logout(@RequestHeader(value = "Authorization", required = false) String authorization) {
userAuthClient.logout(bearerToken(authorization));
return Map.of("message", "logged out");
}
Comment on lines +75 to +79
// Store the UserAuth-issued JWT and username
localStorage.setItem('token', result.data.token);
localStorage.setItem('username', username);
if (result.data.expiresAt) {
localStorage.setItem('tokenExpiresAt', result.data.expiresAt);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The bearer-token-in-localStorage approach was taken to match the integration contract described in #44 ("store the returned JWT, send it as Authorization: Bearer <token>"). A move to an HttpOnly cookie (or a CSP, which would first require refactoring the inline scripts/handlers out of the templates) is a larger change that was kept out of this PR to bound its scope; it has been filed as a follow-up in #46.

— drafted by Claude on behalf of Daniel Stephenson

Comment on lines +44 to +47
/**
* Proxy UserAuth {@code POST /register}. Returns the created user's public fields.
* Propagates UserAuth's status (e.g. 400 validation, 409 username taken).
*/
…logout/atomicity

- web-client: per-player session proxy endpoints now pass the backend's status
  through (401/4xx/5xx) instead of collapsing to 500, so the browser's re-login
  flow on a revoked/expired token works; factored the try/catch into a proxy() helper
- backend GameController: synchronize the load-operate-read sequence on the shared
  GameService monitor (not the per-session state object) to prevent cross-user
  interleaving of the singleton's mutable game state
- backend SessionService: make getOrCreateSession atomic (synchronized) so concurrent
  first requests for a username can't create split sessions
- backend AuthController: logout now requires a bearer token (400 if absent) rather
  than reporting success while revoking nothing
- UserAuthClient: correct register() javadoc (4xx propagated; 5xx/connection → 503)
- tests: WebControllerTest (backend 401→401 passthrough, unreachable→503),
  AuthController logout-without-token → 400

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Review comments addressed — commit 8743645

Thanks for the thorough review. All 15 comments were triaged; 14 are fixed in code and 1 is deferred to a follow-up with rationale. Both modules are green (backend 158 tests, web-client 8).

Proxy status passthrough — web-client /api/session/* (5 comments: getSessionState, sessionTick, sessionCommand, sessionReset, getSessionRulerStats)
Correct and important: a backend 401 on a revoked/expired token was being collapsed to 500, so the game page's status === 401 re-login handling never fired. The five endpoints now pass the backend status through (the shared try/catch was factored into a proxy() helper, which decision/sessionDecision/login/logout now use too). Covered by a new WebControllerTest (401 → 401, unreachable → 503).

Cross-user interleaving — backend GameController (6 comments)
Correct. The load-operate-read sequence was synchronizing on the per-session state object while mutating the shared singleton GameService. It now synchronizes on the gameService monitor across the full sequence, so a concurrent request for another user can't swap the shared state mid-operation. A comment was added explaining the monitor choice.

SessionService.getOrCreateSession atomicity (1 comment)
Correct. The scan-then-create was a non-atomic check-then-act; it is now synchronized so concurrent first requests for the same username cannot create split sessions.

AuthController logout with no token (1 comment)
Correct. Logout now returns 400 when no bearer token is present rather than reporting success while revoking nothing. Covered by a new test.

UserAuthClient.register javadoc (1 comment)
Correct. The javadoc now states that 4xx is propagated verbatim while 5xx/connection failures surface as 503, matching the implementation.

JWT in localStorage / XSS (1 comment) — deferred
Valid concern. The client-side bearer-token approach matches the contract specified in #44; an HttpOnly-cookie flow (or a CSP, which needs the inline scripts refactored first) is a larger change kept out of this PR to bound scope. Filed as a follow-up: #46.


Drafted by Claude on behalf of Daniel Stephenson.

@dmccoystephenson dmccoystephenson merged commit 894fcb2 into main Jun 7, 2026
2 checks passed
@dmccoystephenson dmccoystephenson deleted the feature/userauth-integration branch June 7, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integrate authentication via UserAuth (player accounts + login)

2 participants