Skip to content

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 ​

  1. 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.
  2. 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.
  3. Add api_keys=True to TokenAuth or McpAuth on 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 401 without a call to MicroAuth.
  • require(...) works the same for both. caller.via is "access_token" or "api_key", and caller.key_id names the key. Keys have no caller.client_id, because no app is involved.
  • Without both server credentials, TokenAuth(api_keys=True) and McpAuth(api_keys=True) refuse to start.
  • Answers are cached for as long as MicroAuth's Cache-Control allows, 30 seconds today, in memory, or in Redis when MICROAUTH_REDIS_URL is 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 gets 503 with Retry-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 429 with Retry-After.
  • After 20 unknown, revoked or mistyped keys from one client address in a minute, that address gets 429 without MicroAuth being asked. Behind a proxy, pass client_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) and revoke_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=True can't be combined with resource.
  • 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 its login_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 with Cache-Control: no-store and never store or log the secret.
  • A refusal raises AccountAPIError, whose detail you can show the person as it is. When MicroAuth can't be reached, or fails on its side, the helper raises AccountAPIUnavailable instead, a subclass with status_code 503.

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.

CallWhat it doesAnswer
GET /api/v1/me/api-keys/optionsThe servers and permissions this person can put on a key, and the longest a key may last200
GET /api/v1/me/api-keysTheir keys that aren't revoked, newest first200
POST /api/v1/me/api-keysCreates a key from name, resources, permissions and expires_in_days, which may be null for no expiry when the options say allow_no_expiry201 with key and the whole secret
PATCH /api/v1/me/api-keys/{key}Renames a key, with name200 with the key
POST /api/v1/me/api-keys/{key}/rollRolls a key, with old_secret_lasts_hours of 0, 1 or 24200 with key and the new secret
DELETE /api/v1/me/api-keys/{key}Revokes a key204
DELETE /api/v1/me/api-keysRevokes all their keys204

When something is wrong with the token itself, the answer has a WWW-Authenticate header:

  • 401 with invalid_token when the token is missing, invalid or expired.
  • 403 with insufficient_scope and scope="microauth:api_keys" when the app didn't get the scope.
  • 401 with insufficient_user_authentication and max_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.

MicroAuth is a product of Zyref, LLC.