Public Access
docs(readme): match readme to current code
Readme had drifted from the code. Update the repo list, endpoints, JWKS and cookie rules, env vars, CI and dev data.
This commit is contained in:
1 parent
c362e7047e
commit
c124d0e5d2
1 file changed
+31
-21
@@ -1,15 +1,17 @@
|
||||
# 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.
|
||||
Identity service of **To Be Played**, a backlog tracker for your Steam and PlayStation libraries, plus games added
|
||||
by hand or imported from GOG Galaxy, Playnite, Grouvee and HowLongToBeat files. 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:
|
||||
To Be Played is split into four repositories:
|
||||
|
||||
| Repository | Role |
|
||||
|---|---|
|
||||
| **ToBePlayed-Auth** (this one) | Users, login, settings, platform messages (hints, mail, news), signs JWTs |
|
||||
| ToBePlayed-Core | Steam library, backlog, wishlist, stats; verifies JWTs |
|
||||
| ToBePlayed-Frontend | React single-page app |
|
||||
| ToBePlayed-Deployment | Docker Compose stack, env template, ROADMAP.md (history), plans |
|
||||
|
||||
## Features
|
||||
|
||||
@@ -25,14 +27,16 @@ To Be Played is split into three repositories:
|
||||
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.
|
||||
public key as a JWK. Auth publishes only the current key; Core finds it by `kid`. A rotation (new `JWT_KID`) ends
|
||||
every access token signed with the old key at once. The frontend renews them with the refresh cookie.
|
||||
- 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), language, dark mode.
|
||||
- User settings: display name (unique, case-insensitive), language, dark mode, and the chart form per stat group
|
||||
(`stat_charts`, V31): bars, pie, donut, line or list. `StatCharts` holds the allowed forms per group.
|
||||
- Steam account linked through **"Sign in through Steam"** (Steam OpenID 2.0, no API key), never typed in. Steam
|
||||
proves the SteamID64, and Auth asks Steam to confirm the answer (`check_authentication`) before it stores the ID
|
||||
as verified. A one-time state plus a binding cookie tie the answer to the browser that started the link, so a
|
||||
@@ -174,21 +178,21 @@ To Be Played is split into three repositories:
|
||||
| `PUT` | `/api/users/settings/favorite-platform` (`{platform}`, free text up to 32, e.g. `PS5`; `null` clears it; `204`) | access token |
|
||||
| `PUT` | `/api/users/settings/profile-tiles` (`{tiles: [{id, size}]}`, e.g. `SUMMARY`, `STATUS`, `PLAYING`, `FINISHED`, `STATS_<GROUP>`; `204`) | access token |
|
||||
| `GET` | `/api/public/profiles/{displayName}` | public |
|
||||
| `PUT` | `/api/users/settings/stat-chart` (`{group, chart}`; `chart` null = back to the default; `204`; `400 VALIDATION_FAILED` for an unknown group or form) | access token |
|
||||
| `PUT` | `/api/users/settings/owner-lists` (`{enabled}`; `204`; opt-in to be shown by name in owner and user lists, only together with the public profile) | access token |
|
||||
| `GET` | `/api/stats/users?key=TOTAL_USERS\|STEAM_LINKED&page=` (`{listed: [{displayName}], listedTotal, privateCount, includesYou, page, totalPages}`; never an id; not cached) | access token |
|
||||
| `GET` | `/api/stats/public` | public |
|
||||
| `GET` | `/.well-known/jwks.json` (the current public key as a JWK, `Cache-Control: max-age=300`) | public |
|
||||
| `GET` | `/api/version` (`{commit, builtAt}` of the running image, from the CI build; `dev` locally) | 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)
|
||||
- Spring Boot Actuator (`/actuator/health` only)
|
||||
- Tests: JUnit 5, Testcontainers (PostgreSQL), ArchUnit
|
||||
- CI: Forgejo Actions, multi-stage Docker image
|
||||
- CI: Gitea Actions (`.gitea/workflows/publish-auth.yml`), multi-stage Docker image
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -250,8 +254,8 @@ sequenceDiagram
|
||||
B->>A: POST /api/auth/login
|
||||
A-->>B: Set-Cookie tbp_access (15 min), tbp_refresh (30 days, path /api/auth)
|
||||
B->>C: API call with tbp_access
|
||||
C->>A: GET /.well-known/jwks.json (new kid only)
|
||||
A-->>C: Public keys (Ed25519)
|
||||
C->>A: GET /.well-known/jwks.json (unknown kid only)
|
||||
A-->>C: Current public key (Ed25519, one JWK)
|
||||
C-->>B: 200, or 401 when the access token expired
|
||||
B->>A: POST /api/auth/refresh with tbp_refresh
|
||||
A-->>B: New tbp_access and tbp_refresh, old refresh token rotated
|
||||
@@ -263,8 +267,8 @@ sequenceDiagram
|
||||
|
||||
1. `LoginService` checks the password. `LoginController` sets both HttpOnly cookies (`AuthCookies`), so no script
|
||||
can read them.
|
||||
2. Core verifies the access token with Auth's public keys only. `JwksController` publishes them; Core loads them
|
||||
again only for an unknown `kid`.
|
||||
2. Core verifies the access token with Auth's public keys only. `JwksController` publishes the current key only; Core
|
||||
loads it again only for an unknown `kid`.
|
||||
3. On a `401` the frontend calls `POST /api/auth/refresh` once and sends the request again.
|
||||
`RefreshTokenService` replaces the refresh token. A replaced token that comes back revokes the whole login.
|
||||
|
||||
@@ -332,7 +336,7 @@ sequenceDiagram
|
||||
A->>M: up to 20 due rows (account mails first, at most APP_MAIL_DAILY_LIMIT per 24 h, broadcast rows keep 10 free)
|
||||
M-->>A: ok: SENT / error: try again after 1, 2, 4 ... 60 min
|
||||
end
|
||||
Note over A: after 10 tries FAILED and a warning in the log; SENT rows are removed after 30 days
|
||||
Note over A: after 10 tries FAILED and a warning in the log, SENT rows are removed after 30 days
|
||||
```
|
||||
|
||||
### Registration flow
|
||||
@@ -429,7 +433,7 @@ other, then start them in this order (Core also starts without Auth and finds it
|
||||
3. **Frontend**: in `ToBePlayed-Frontend`, `npm install`, then `npm run dev`, and open `http://localhost:5173`
|
||||
(`localhost`, not `127.0.0.1`, so the browser sends the login cookies to every local port). The development
|
||||
values must be `VITE_API_URL=http://localhost:8080` and `VITE_CORE_API_URL=http://localhost:8090`.
|
||||
4. **Log in** with `dev@localhost` / `password123`. On the dashboard, start a Steam sync: the library fills with 8
|
||||
4. **Log in** with `dev@localhost` / `password123`. On the dashboard, start a Steam sync: the library fills with 30
|
||||
games, achievements, time to beat, moods, a wishlist and one speedrun game from recorded answers.
|
||||
|
||||
In Auth, `local` makes a new signing key at each start (`LocalKeysEnvironmentPostProcessor`, only in this
|
||||
@@ -449,18 +453,24 @@ Needs Java 25, Docker and your own keys.
|
||||
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` (the only open actuator endpoint).
|
||||
|
||||
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.
|
||||
Auth has no `application-dev.yml`: any profile but `local` (also none) runs the same way and takes the keys from the
|
||||
environment. `local` must not be combined with `prod`.
|
||||
|
||||
Only `tbp_access` gets the domain from `APP_AUTH_COOKIE_DOMAIN` (blank = host-only; production sets the parent
|
||||
domain, so Core gets it too). `tbp_refresh` (`Path=/api/auth`) and `tbp_steam_link` are always host-only. The cookies
|
||||
are `Secure` (`app.auth.cookie-secure`, default `true`). Chrome and Firefox accept `Secure` cookies on
|
||||
`http://localhost`; Safari drops them, so the `local` profile sets `cookie-secure: false`. With your own keys on Safari
|
||||
set `APP_AUTH_COOKIE_SECURE=false`. Open the frontend on `localhost`, not `127.0.0.1`.
|
||||
|
||||
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 |
|
||||
| `DB_PASSWORD` | Local PostgreSQL password |
|
||||
| `JWT_KID` | Key id in the token header (default `tbp-auth-1`); Auth refuses to start when it is blank |
|
||||
| `APP_AUTH_COOKIE_DOMAIN` | Domain of `tbp_access`, blank = host-only |
|
||||
| `APP_ACCOUNT_CLEANUP_DRY_RUN` | Default `true`: the nightly cleanup deletes nothing (it still sends the account states to Core); the 7-day purge of unconfirmed registrations runs anyway |
|
||||
| `APP_BUILD_COMMIT`, `APP_BUILD_TIME` | Feed `GET /api/version`; defaults `dev` and empty. CI passes `GIT_SHA` and `BUILD_TIME` as Docker build args |
|
||||
| `APP_PUBLIC_URL` | Auth's public address, the Steam OpenID realm (default `http://localhost:8080`) |
|
||||
| `APP_FRONTEND_URL` | Where the Steam link ends (default `http://localhost:5173`) |
|
||||
| `APP_CORE_INTERNAL_URL` | Core for the nightly account cleanup, required (local `http://localhost:8090`, Docker `http://tbp-core-service:8090`) |
|
||||
|
||||
Reference in new issue
Block a user