Give your users API keys
Issue API keys your database keeps only as digests, check the key on every API request, and give each key its own rate limit.
Last updated
After reading this guide, you will know
- What guides build API keys, and in what order to follow them
- How to check a key and publish it as
ctx.apiKeyfor every later guard - Why the key check runs before the rate limit
By the end of this recipe your users create API keys, see each one once, and call your API with it as a bearer token. The database keeps only each key's digest, a request is matched to its key before anything else runs, and every key spends its own rate-limit budget.
npm add @sdxc/crypto @sdxc/result remixIssue a key and keep its digest
Mint the key with randomToken and store only its SHA-256 digest, as
Mint secrets with randomToken
does for an invitation, with a prefix such as sk so a leaked key is recognizable. Show the key
once, on the page that created it.
Check the key on every request
Build the API and its problem catalog with
Build a JSON API with problem details, adding an unauthorized
entry (status: 401). Then match the bearer token to a key by its digest and publish the key,
so every later guard reads it:
import type { Middleware } from "remix/router";
import type { ApiKey } from "~/app/models/api-keys";
import { Hex, sha256 } from "@sdxc/crypto";
import { isFailure } from "@sdxc/result";
import { createContextKey } from "remix/router";
import { ApiKeys } from "~/app/models/api-keys";
import problems from "~/config/problems";
export const CurrentApiKey = createContextKey<ApiKey>();
declare module "remix/router" {
interface RequestContext {
apiKey: ApiKey;
}
}
export const authenticateApiKey: Middleware = async (ctx, next) => {
let header = ctx.request.headers.get("Authorization") ?? "";
let token = header.startsWith("Bearer ") ? header.slice(7) : "";
let digest = token === "" ? null : await sha256(token);
let apiKey =
digest === null || isFailure(digest)
? null
: await ApiKeys.findByTokenHash(ctx.db, Hex.encode(digest.data));
if (apiKey === null) {
let headers = { "WWW-Authenticate": 'Bearer error="invalid_token"' };
return problems.unauthorized({}, { headers });
}
ctx.set(CurrentApiKey, apiKey, { property: "apiKey" });
return next();
};ApiKeys is your own model over the keys table, and ApiKey its row, with at least an id.
Give each key its own budget
Add the limiter from Rate limit the API, keyed
on the key rather than the address: key: (ctx) => `api-key:${ctx.apiKey.id}`. Then make
each API route's guards [authenticateApiKey, apiRateLimit()].
Test it
Test each stage as its guide does:
Test the endpoints sends requests through the
router with the limiter's binding in place; add one without a key, which answers 401 with no
RateLimit-Policy header.
Where to go next
Idempotent writes and JSON Merge Patch — scope idempotency keys on
ctx.apiKey.id, so retries are safe per key.Decide who can do what — give each key scopes beyond being valid.
Build a public JSON API — the same API with OpenAPI and a typed client.