Appearance
API keys for your users
Some callers can't sign in through a browser: a script, a CI job, or an MCP client that doesn't support OAuth. API keys cover them. Each key belongs to one person, works at the servers they pick, and carries only the permissions they choose from the ones they hold.
bash
curl https://api.example.com/posts -H "Authorization: Bearer cms_8Kq2..."Your API checks keys the same way it checks access tokens, and your routes don't need to know the difference.
Turn them on
- Open APIs and MCP servers and find the API keys for your users card. Turn keys on and choose:
- Prefix: the start of every key, such as
cms. 2 to 10 lowercase letters or digits, starting with a letter. A new prefix only applies to new keys. - Longest lifetime: the longest expiry people can pick, up to 3650 days. 365 days by default. Choose no limit to also let people make keys that never expire. A new maximum only applies to new keys.
- Sensitive permissions: whether keys may carry permissions you marked sensitive. Off by default. Turning it off later takes sensitive permissions off existing keys at their next check.
- Prefix: the start of every key, such as
- Open each API or MCP server that should accept keys and create its server credentials: a client ID that starts with
rs_and a secret it uses to check keys. The secret is shown once. Rotating it gives a new one and keeps the old one working for 24 hours. You can also set requests per minute for each person's keys there. - Add
api_keys=TruetoTokenAuthorMcpAuthon that server.
Keys need a plan that includes them. See pricing.
Where people create keys
On their account page. Once keys are on, your users get an API keys page under Account. It appears for people who hold at least one permission that some server exposes, not counting sensitive ones unless you allow them on keys. It lists each key with its name, the start of the key, its servers and permissions, where it was made, when it was last used and when it expires.
Creating a key asks for a name, the servers, the permissions and an expiry within your longest lifetime. It asks them to confirm it's them, then shows the key once, with a copy button and a curl example for the first server they picked. The page never saves the key in the browser, and closing the panel clears it. From the same page they can rename, roll and revoke a key, or revoke all of them. Rolling and revoking ask before they act.
Inside your app. Your web app can offer the same thing on its own settings page. See manage keys from your app.
When a key is created, its owner gets an email that says where it was made, on their account page or in which of your apps, with a link to their API keys page where they can revoke it.
What a key can do
When someone creates a key, they choose:
- Servers: which of your APIs and MCP servers it works at. A key is refused everywhere else.
- Permissions: only ones they hold and the chosen servers expose.
- Expiry: up to your longest lifetime.
Every time a key is used, MicroAuth works out its permissions again: the ones the key lists, that the calling server exposes, and that the person still holds. Take a role away, or suspend the person, and their keys lose that access within 30 seconds. A key whose permissions have all been taken away stays active with an empty scope, so routes that need no permission still accept it. Revoke it to stop it completely.
A key stops working, without being deleted, while the person is suspended, the service is paused, you turn keys off, or the plan doesn't include them. People can still see, rename and revoke their keys while that's the case.
Rename, roll and revoke
- Rename changes the label people see.
- Roll gives the key a new secret with the same settings. The person chooses whether the old secret stops working within 30 seconds or keeps working for 1 or 24 hours, so a CI job can switch without downtime. Rolling again ends any older secret within 30 seconds too, so a key never has more than two. Like creating a key, it asks them to confirm it's them. An expired key can't be rolled.
- Revoke stops it at your APIs within 30 seconds, the time they may cache an answer. People can revoke one key or all of theirs.
Keys that were used in the last 30 days get a reminder email 7 days before they expire. Expired keys stay in the list until they're deleted, 90 days after they expire. Revoked keys leave the list at once and are deleted 90 days later.
What owners see
Open a person under Users to see their keys: where each was created, its permissions and servers, when it was last used and when it expires. You can revoke one or all of them. You can't create keys for people, so nobody can act as someone else. From a script, the platform API lists a person's keys and revokes one or all of them.
The Activity tab records keys being created, rolled and revoked, and server credentials being created and their secrets rotated. A key created in one of your apps names the app, such as "Created API key cms_Ab12Cd in Acme Web". It never shows a key, only its ID and the start of it.
Accept keys in your API
python
from fastapi import Depends, FastAPI
from microauth_fastapi import TokenAuth, TokenClaims
app = FastAPI()
api = TokenAuth(app, api_keys=True)
@app.post("/posts")
async def create_post(caller: TokenClaims = Depends(api.require("posts:write"))):
return {"author": caller.sub, "via": caller.via}bash
MICROAUTH_ISSUER=https://acme.microauth.dev
MICROAUTH_RESOURCE=https://api.example.com
MICROAUTH_RESOURCE_CLIENT_ID=...
MICROAUTH_RESOURCE_SECRET=...- A bearer value with a dot is a JWT and gets the local access token check. Anything else is treated as a key.
- A key with the wrong shape or checksum gets
401without a call to MicroAuth. require(...)works the same for both.caller.viais"access_token"or"api_key", andcaller.key_idnames the key. Keys have nocaller.client_id, because no app is involved.- Without both server credentials,
TokenAuth(api_keys=True)andMcpAuth(api_keys=True)refuse to start. - Answers are cached for as long as MicroAuth's
Cache-Controlallows, 30 seconds today, in memory, or in Redis whenMICROAUTH_REDIS_URLis set. - While MicroAuth can't be reached, a key that was active keeps working for 10 more minutes past its cached answer, set with
stale_if_error. A key the SDK hasn't seen before gets503withRetry-After: 5. - The per person limit is counted across all of a person's keys at this server, so making more keys doesn't raise it. A caller over it gets
429withRetry-After. - After 20 unknown, revoked or mistyped keys from one client address in a minute, that address gets
429without MicroAuth being asked. Behind a proxy, passclient_address=so the SDK sees the real address.
To let the same routes serve your web app's browser sessions too, pass your UserAuth: TokenAuth(app, api_keys=True, sessions=auth). Then one API serves your UI and your users' scripts, with the same permission checks, and caller.via is "session" for browser requests. A request with an Authorization header is judged by that header alone.
Not using Python? Any language can check keys with one HTTP call. See check tokens and keys from any language.
Manage keys from your app
With UserAuth(app, api_keys=True), your web app can offer the same key management on its own settings page through auth.api_keys. The SDK asks for the reserved microauth:api_keys scope and keeps the access token in the session cookie. Every helper takes the request first and acts for whoever is signed in on it:
python
import html
from fastapi import Depends, FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from microauth_fastapi import ReauthenticationRequired, User, UserAuth
app = FastAPI()
auth = UserAuth(app, api_keys=True)
@app.post("/settings/api-keys")
async def create_key(request: Request, user: User = Depends(auth.login_required)):
try:
new = await auth.api_keys.create(
request,
name="CI deploys",
resources=["https://api.example.com"],
permissions=["posts:write"],
expires_in_days=90,
)
except ReauthenticationRequired as exc:
return RedirectResponse(exc.login_url, status_code=303)
return HTMLResponse(
f"<p>Copy your new key now. It won't be shown again.</p><pre>{html.escape(new.secret)}</pre>",
headers={"Cache-Control": "no-store"},
)- The helpers are
options(request),list(request),create(request, name=..., resources=[...], permissions=[...], expires_in_days=...),rename(request, key_id, name),roll(request, key_id, old_secret_lasts_hours=0),revoke(request, key_id)andrevoke_all(request). The SDK reference has what each returns. - Only web applications can get
microauth:api_keys. Single page, native and third party apps can't, so a token held in a browser can never create keys.api_keys=Truecan't be combined withresource. - Creating or rolling a key from your app needs a sign in from the last 10 minutes. When it's older, the helper raises
ReauthenticationRequired. Send the browser to itslogin_url, which signs the person in again and brings them back to the same page. - The secret is shown once, in
new.secret. Send that page withCache-Control: no-storeand never store or log the secret. - A refusal raises
AccountAPIError, whosedetailyou can show the person as it is. When MicroAuth can't be reached, or fails on its side, the helper raisesAccountAPIUnavailableinstead, a subclass withstatus_code503.
The calls go to /api/v1/me/api-keys on your sign in service, with the person's access token as a Bearer token. That token needs the microauth:api_keys scope, and its aud must be your issuer alone, so request it without a resource. A token issued for one of your APIs is refused. microauth:api_keys isn't listed in your discovery document, so don't look for it there.
| Call | What it does | Answer |
|---|---|---|
GET /api/v1/me/api-keys/options | The servers and permissions this person can put on a key, and the longest a key may last | 200 |
GET /api/v1/me/api-keys | Their keys that aren't revoked, newest first | 200 |
POST /api/v1/me/api-keys | Creates a key from name, resources, permissions and expires_in_days, which may be null for no expiry when the options say allow_no_expiry | 201 with key and the whole secret |
PATCH /api/v1/me/api-keys/{key} | Renames a key, with name | 200 with the key |
POST /api/v1/me/api-keys/{key}/roll | Rolls a key, with old_secret_lasts_hours of 0, 1 or 24 | 200 with key and the new secret |
DELETE /api/v1/me/api-keys/{key} | Revokes a key | 204 |
DELETE /api/v1/me/api-keys | Revokes all their keys | 204 |
When something is wrong with the token itself, the answer has a WWW-Authenticate header:
401withinvalid_tokenwhen the token is missing, invalid or expired.403withinsufficient_scopeandscope="microauth:api_keys"when the app didn't get the scope.401withinsufficient_user_authenticationandmax_age="600"when creating or rolling a key and the sign in is older than 10 minutes.
Creating a key answers 403 while keys are off for the service, 409 at 25 keys, 429 after 20 new keys in a day, and 422 naming the field when the name, servers, permissions or expiry aren't allowed. Each person can make 20 requests a second, in bursts of up to 60, and 1000 changes an hour.
Limits
- 25 active keys per person, and 20 new keys per person per day.
- Up to 10 servers and 100 permissions per key.
- Names up to 64 characters.
Key format
Keys look like cms_8Kq2ZtR1vX9mB4nC7dF0gH3jL6pS5wY2aE8uT1oQ0tdX69: your prefix, an underscore, 40 random characters and a 6 character checksum. The checksum lets code spot a mistyped key without a network call. The key is shown once, and MicroAuth stores only its hash. The format in detail.