lobo 930e08e6dd
Build, Test & Push Auth Service / quality-check (push) Successful in 30s
Build, Test & Push Auth Service / docker (push) Successful in 1m23s
feat(login): cookie session with rotating refresh token
The login token moves out of localStorage into an HttpOnly cookie
(15 min), so a script on the page can no longer read it. A random
refresh token (30 days, sliding) renews it; only its SHA-256 is
stored. Each refresh replaces the token, and a replaced token that
comes back revokes every token of that login.

Unsafe requests need an X-TBP-Client or Authorization header, and
CORS allows only the exact frontend origin. A missing or expired
token now answers 401 instead of 403, so the frontend can refresh.
2026-09-25 13:45:04 +02:00
2026-03-31 19:54:02 +02:00
2026-03-31 18:57:06 +02:00
2026-03-31 18:57:06 +02:00
2026-03-31 18:57:06 +02:00
2026-03-31 18:57:06 +02:00
2026-04-28 15:43:27 +02:00
2026-03-31 18:57:06 +02:00

To Be Played – Auth Service

Identity service of To Be Played, a Steam backlog tracker. It handles registration, login, user settings and the public profile, and it signs the JWTs that the Core service trusts.

To Be Played is split into three repositories:

Repository Role
ToBePlayed-Auth (this one) Users, login, settings, signs JWTs
ToBePlayed-Core Steam library, backlog, wishlist, stats; verifies JWTs
ToBePlayed-Frontend React single-page app

Features

  • Registration with an invitation code, login with email and password (BCrypt).
  • Brute-force protection in memory: 5 failed logins per email or 20 per IP, and 10 failed registrations per IP, block for 15 minutes with 429 TOO_MANY_ATTEMPTS and Retry-After. Unknown emails cost the same BCrypt time as a wrong password, and every failed login gets the same error.
  • JWT signed with EdDSA (Ed25519). Auth holds the private key, Core only the public key, so Core can verify tokens but never create them. Tokens carry a kid header, and GET /.well-known/jwks.json publishes the public key as a JWK, so Core finds the right key after a rotation.
  • Session in two HttpOnly cookies, so no script on the page can read a token. The access token lives 15 minutes in tbp_access (sent to Auth and Core). The refresh token is random, lives 30 days from its last use in tbp_refresh (sent to /api/auth only) and is stored only as a SHA-256 hash. Each refresh replaces it; a replaced token that comes back revokes all tokens of that login. Logout revokes them too.
  • CSRF guard: POST, PUT and DELETE need an X-TBP-Client or Authorization header, and CORS allows only the frontend origin. A Bearer header still works for API clients and tests.
  • User settings: display name (unique, case-insensitive), Steam ID, language, dark mode.
  • Appearance settings synced across devices: accent color, corner style, wide layout, background and dashboard banner from the user's own game art. Only whitelisted presets and Steam app ids are stored, never raw CSS or URLs.
  • Opt-in public profile at /u/<display name>. Auth answers with a short-lived profile ticket (EdDSA JWT, 10 minutes, typ=public-profile) that Core accepts for the public library data only. The ticket is not a login: both services reject every token with a typ claim on normal endpoints.

API

Method Path Auth
POST /api/registration/new public
POST /api/auth/login public, sets both cookies
POST /api/auth/refresh refresh cookie, replaces both cookies
POST /api/auth/logout refresh cookie, clears both cookies
GET /api/users/me access token
PUT /api/users/settings access token, sets a new access cookie
PUT /api/users/settings/appearance access token
GET /api/public/profiles/{displayName} public
GET /api/stats/public public

Endpoints use Spring's API versioning with a query parameter: ?version=1.0 (default 1.0).

Tech stack

  • Java 25, Spring Boot 4.1 (Web MVC, Security, Data JPA, Validation, Cache with Caffeine), virtual threads
  • PostgreSQL 17, Flyway migrations
  • JJWT with Ed25519 keys
  • Spring Boot Actuator + Micrometer (Prometheus)
  • Tests: JUnit 5, Testcontainers (PostgreSQL), ArchUnit
  • CI: Forgejo Actions, multi-stage Docker image

Architecture

Package by feature (modules/login, modules/registration, modules/user, ...), and inside each feature three layers:

Layer Package Rule
API api REST controllers, request/response records, ApiMapper. Calls services, never repositories.
Domain model, service, exception Plain Java models and business logic. No dependency on API or persistence.
Persistence persistence JPA entities, Spring Data repositories, EntityMapper. Entities never reach the API layer.

ArchUnit enforces these rules in src/test/java/auth_service/architecture/ArchitectureTest.java, so a layer violation fails the build. Test base classes (AbstractUnitTest, AbstractWebMvcTest, AbstractPersistenceTest, AbstractIntegrationTest) live in auth_service.shared.test; all integration tests share one PostgreSQL container.

Run locally

Needs Java 25 and Docker.

./mvnw spring-boot:run -Dspring-boot.run.profiles=dev

Spring Boot's Docker Compose support starts PostgreSQL from compose.yaml, Flyway applies the migrations, and the service listens on http://localhost:8080. Health: /actuator/health, metrics: /actuator/prometheus.

The cookies are host-only unless APP_AUTH_COOKIE_DOMAIN is set; production sets it to the parent domain, so Core gets the access cookie too. Browsers accept Secure cookies on http://localhost, so local development needs no cookie settings. Open the frontend on localhost (not 127.0.0.1), so the browser sends the cookies to every local port.

Configuration comes from environment variables:

Variable Purpose
JWT_PRIVATE_KEY, JWT_PUBLIC_KEY Ed25519 keypair, Base64 of the DER bytes (PKCS#8 / X.509)
JWT_KID Key id written into the token header
AUTH_INVITATION_CODE Code required for registration
DB_PASSWORD Local PostgreSQL password

Generate a keypair:

openssl genpkey -algorithm ed25519 -out private.pem
openssl pkey -in private.pem -outform DER | base64 -w0   # JWT_PRIVATE_KEY
openssl pkey -in private.pem -pubout -outform DER | base64 -w0   # JWT_PUBLIC_KEY

Tests

./mvnw clean verify

Runs unit, WebMvc, persistence and integration tests plus the ArchUnit rules. Needs Docker for Testcontainers.

Roadmap

  • Account deletion and data export.

License

All rights reserved. The source is public for reading only.

S
Description
No description provided
Readme
1.3 MiB
0 Stars 3 Watchers 0 Forks
Languages
Java 99.8%
Dockerfile 0.2%