Skip to content

API keys for the public API, managed in the admin panel - #902

Open
sven-n wants to merge 2 commits into
masterfrom
claude/api-key-authentication
Open

API keys for the public API, managed in the admin panel#902
sven-n wants to merge 2 commits into
masterfrom
claude/api-key-authentication

Conversation

@sven-n

@sven-n sven-n commented Aug 25, 2026

Copy link
Copy Markdown
Member

Why

The public API under /apistatus, is-online/{account} and send/{server} — exists for external applications: a game launcher, a status page, a website. Since the admin panel authenticates its users, ConfigureAdminPanel puts every controller behind RequireAuthorization(), 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.

GET /api/status HTTP/1.1
X-Api-Key: <the key>

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:

Endpoint Needs
GET /api/status Viewer
GET /api/is-online/{account} Viewer
GET /api/send/{server}?msg= Operator

send is 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:Keys and OPENMU_API_KEY / OPENMU_API_KEY_ROLES are 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

  • Hashing. A plain unsalted SHA-256 is deliberate: a generated key is 32 random bytes and therefore not guessable, so a slow hash buys nothing, and it has to be computed on every API request. Same rationale as AdminUser.RecoveryCodeHashes.
  • Lookup. Stored keys are found by an indexed lookup on that hash. Configured keys can't be, so they are all compared with CryptographicOperations.FixedTimeEquals without an early break — neither the duration nor the number of comparisons says how much of a guessed key was right.
  • Schema. admin.ApiKey, next to admin.AdminUser and 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.
  • Last used. Written at most once per minute per key, not awaited, and the repository swallows its own errors — it must never slow or fail an API request.
  • The key itself is never written to the log.
  • A request without any key is NoResult(), not a failure, so the cookie still gets its chance on the same request.
  • The cookie handler answers 401/403 below /api instead of redirecting to the login page, which an API client can't use.
  • Like the panel itself, the API is open while no admin user exists at all (the initial setup mode). That's existing AdminAccessRequirement behaviour, 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.
  • The migration was generated with dotnet ef migrations add, not written by hand.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VZLARthjVEssPfKLacpaWw

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
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 25, 2026

Copy link
Copy Markdown

Deploying openmudocs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 13c5ebb
Status: ✅  Deploy successful!
Preview URL: https://b55eae73.openmudocs.pages.dev
Branch Preview URL: https://claude-api-key-authenticatio.openmudocs.pages.dev

View logs

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
@sven-n sven-n changed the title Authenticate the public API with an API key API keys for the public API, managed in the admin panel Aug 26, 2026
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.

2 participants