API keys for the public API, managed in the admin panel - #902
Open
sven-n wants to merge 2 commits into
Open
Conversation
The public API under /api - the server status, the online state of an account and the global message - is meant for external applications like a game launcher or a status page. Since the admin panel authenticates its users, that API was only reachable with the authentication cookie of the panel, which such an application can't get: it would have to go through a login form and a second factor. It now accepts an API key as well, in an "X-Api-Key" header or as a bearer token. The keys are configured under "AdminPanel:Api:Keys", or with the OPENMU_API_KEY environment variable for a single key, like the bootstrap user is. They are not stored in the database on purpose: the API has to work before the game database exists. A key carries the same roles as a user and defaults to the least privileged one, so a status page can be given a key which can only read. The endpoints which report something require the viewer role, while the global message - the only endpoint which does something - requires the operator role. Keys shorter than 32 characters are refused and logged, because the key travels with every request. All configured keys are compared in constant time, and the key itself is never written to the log. The cookie handler now answers with 401 and 403 below /api instead of redirecting to the login page, which an API client can't use anyway. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VZLARthjVEssPfKLacpaWw
Deploying openmudocs with
|
| Latest commit: |
13c5ebb
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b55eae73.openmudocs.pages.dev |
| Branch Preview URL: | https://claude-api-key-authenticatio.openmudocs.pages.dev |
The keys of the public API were configured only, which meant a restart to add or revoke one, and the key in plain text in a configuration file. They are now created and revoked on an own page next to the users, which needs the administrator role. A created key is generated from a cryptographic random number and shown exactly once - only its SHA-256 hash is stored, like the recovery codes of a user. A plain hash without a salt is enough and necessary here: the key is not guessable, and it has to be looked up on every request of the API. The list shows each key by its name and its leading characters, so keys can be told apart without knowing them, together with the last time each was used. A key which is not used anymore is therefore visible as such. That timestamp is written at most once per minute per key and never fails a request. A key can be disabled instead of deleted, which stops it from working without touching the configuration of the application which uses it, and its role can be changed without handing out a new key. The keys live in the "admin" schema next to the users, and for the same reason: they grant access to server functions, and no game server database role may read them. The configured keys keep working. The panel needs a database to store keys in, and the API has to work before that database exists - the same reason the panel has a bootstrap user. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VZLARthjVEssPfKLacpaWw
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.
Why
The public API under
/api—status,is-online/{account}andsend/{server}— exists for external applications: a game launcher, a status page, a website. Since the admin panel authenticates its users,ConfigureAdminPanelputs every controller behindRequireAuthorization(), so the only way into that API is the panel's authentication cookie.An external application can't get that cookie. It would have to go through the login form and, where it's set up, a TOTP second factor — which a server-to-server client can't do, and shouldn't be able to do with a secret it holds.
What
A second authentication scheme next to the cookie, and a page to manage its keys.
Authorization: Bearer <the key>works too, for clients which only speak that.The page
API keys, next to Users, requiring the administrator role. Create a key for an application, pick its role, and copy it — it's shown exactly once, because only its SHA-256 hash is stored, the same way recovery codes are.
The list shows each key by its name and its leading characters, so keys can be told apart without knowing them, plus the last time each was used — a key whose Last used stays empty is one you can delete. A key can be disabled rather than deleted, which stops it immediately without touching the application's configuration, and its role can be changed without handing out a new key.
Roles
A key carries the same roles as a user and defaults to the least privileged one:
GET /api/statusGET /api/is-online/{account}GET /api/send/{server}?msg=sendis the only endpoint which does something rather than reporting something, so it asks for the operator role. That's a tightening compared to before, where any authenticated panel user could use it.A signed in panel user can still use the whole API with its own roles, which is handy while trying things out in the browser.
Configured keys still work
The panel needs a database to store keys in, and the API has to work before that database exists — the same reason the panel has a bootstrap user. So
AdminPanel:Api:KeysandOPENMU_API_KEY/OPENMU_API_KEY_ROLESare still read, and the compose files pass the variables through. Configured keys can't be managed in the panel, which the docs say.Details worth a look in review
AdminUser.RecoveryCodeHashes.CryptographicOperations.FixedTimeEqualswithout an early break — neither the duration nor the number of comparisons says how much of a guessed key was right.admin.ApiKey, next toadmin.AdminUserand for the same reason: no game server database role may read it. Both tables share the context, so the migration is still driven by the admin user repository.NoResult(), not a failure, so the cookie still gets its chance on the same request.401/403below/apiinstead of redirecting to the login page, which an API client can't use.AdminAccessRequirementbehaviour, and the docs mention it.Possible follow-ups
Per-key IP allowlists, an expiry date, and rate limiting. None of them are needed to use the API, and each adds UI, so they seemed better kept separate.
Testing
dotnet build src/MUnique.OpenMU.sln -p:ci=true— succeeds, 0 errors.dotnet test tests/MUnique.OpenMU.Web.Tests— 38 passed, 0 failed. The 12 new tests cover configured and stored key lookup, rejection of unknown, too-short and disabled keys, the role build-up, that only a hash is stored, that the last-usage write is throttled, that generated keys are unique and prefixed, both header forms, and the no-key / wrong-key results.dotnet ef migrations add, not written by hand.🤖 Generated with Claude Code
https://claude.ai/code/session_01VZLARthjVEssPfKLacpaWw