Authentication
Stable and running in production; the API is close to final before 1.0.
The auth option enables authentication. Its value is a '<strategy>:<provider>' string or an object. It registers the provider routes and, on every request, verifies the credential and populates ctx.user. It does not block unauthenticated requests; guard those yourself (see Protecting routes). A store is required for every strategy except key.
auth: 'cookie:github'
// or, for finer control:
auth: { strategy: 'cookie', providers: ['github', 'google'] }- strategy:
cookie,token,jwtorkey(see Strategies). - provider:
email,github,google,microsoft,discord,facebook,apple. The string form takes one; use the object form'sprovidersarray for several.keyis provider-less.
For a step-by-step setup, see the Login with GitHub guide.
Configuring auth
The string form expands to the object form, which exposes every field:
export default server({
auth: {
strategy: 'cookie',
providers: ['google', 'github'],
redirect: '/dashboard',
store: users,
session: sessions,
cleanUser: (user) => {
const { internalId, ...rest } = user;
return rest;
},
},
});| Field | Type | Default | Description |
|---|---|---|---|
strategy | 'cookie' | 'token' | 'jwt' | 'key' | (required) | How the session credential is issued and checked. See Strategies. |
providers | Provider[] | [] | OAuth providers and/or email to enable. |
key | string | AUTH_KEY env | Shared secret for the key strategy. |
redirect | string | '/user' | Where a cookie login redirects the browser. |
store | Store | server store, user: prefix | Where users are kept. |
session | Store | server store, auth: prefix | Where sessions are kept. |
cleanUser | (user) => user | strips password | Transforms the user before it is stored and before it reaches ctx.user. |
Users and sessions
Auth uses two key-value stores, both derived from your store by default:
- Users: the
user:prefix. An OAuth user is keyed by its provider id, an email user by its email. - Sessions: the
auth:prefix, lasting a week. The cookie or bearer token is only a session id; the user is looked up from it on each request.
OAuth providers
Each provider reads its credentials from the environment:
| Provider | Env vars |
|---|---|
github | GITHUB_ID, GITHUB_SECRET |
google | GOOGLE_ID, GOOGLE_SECRET |
microsoft | MICROSOFT_ID, MICROSOFT_SECRET |
discord | DISCORD_ID, DISCORD_SECRET |
facebook | FACEBOOK_ID, FACEBOOK_SECRET |
apple | APPLE_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_PRIVATE_KEY |
Set each provider's authorized callback URL to https://<your-host>/auth/callback/<provider>. The flow is CSRF-protected with a short-lived state cookie.
Apple signs its client secret as a short-lived ES256 JWT from APPLE_PRIVATE_KEY, and its callback is a POST.
Email and password
The email provider keeps users in your store, with no third party. Passwords are hashed, never stored or returned in clear.
| Method | Route | Body | Notes |
|---|---|---|---|
POST | /auth/register/email | { email, password, ...fields } | email must contain @, password ≥ 8 chars. Extra fields are stored on the user. The session is active on success. |
POST | /auth/login/email | { email, password } | |
PUT | /auth/password/email | { previous, updated } | Changes the password for the signed-in user. |
Register and login finish the session the same way as OAuth (a cookie or a token, per strategy).
Strategies
cookie(browsers): on success a signed,HttpOnlyauthenticationcookie is set (SameSite=Lax, plusSecurein production) and the browser is redirected toredirect.token(APIs, mobile): login responds with JSON containing atoken(an opaque id stored server-side); send it asAuthorization: Bearer <token>.jwt(stateless APIs): login responds with atokenthat is a signed JWT (HS256, using yoursecret) carrying the session itself. No server-side session is stored, so it scales without a shared store. Trade-offs: a token can't be revoked before it expires (one week), changingsecretinvalidates all of them, andsecretmust be set; an unset per-process secret breaks every token on restart. Sent asAuthorization: Bearer <jwt>.key(machine-to-machine): a single shared secret, no users or sessions (see Shared-secret key).
Shared-secret key
For service-to-service access with no users, the key strategy checks one shared secret sent as Authorization: Bearer <key> against AUTH_KEY (or auth.key) in constant time. There are no providers, store, sessions or login/logout routes.
// AUTH_KEY=s3cr3t-api-key in the environment
const requireKey = (ctx) => {
if (!ctx.user) return 401;
};
server({ auth: 'key' })
.get('/data', requireKey, () => ({ ok: true }));- Pass the secret inline with the object form instead of the env var:
auth: { strategy: 'key', key: 'secret' }. - On a match,
ctx.useris the fixed marker{ id: 'key', strategy: 'key', provider: 'key' }(no user record, so noemail). - No header leaves the request anonymous (
ctx.userundefined); a wrong or malformed header responds with401. - Booting without a key configured throws, so a missing
AUTH_KEYfails fast at startup rather than running unprotected.
The user object
ctx.user is undefined when nobody is signed in. When present, every user carries id, provider and strategy; the rest depends on the provider:
| Provider | ctx.user |
|---|---|
OAuth (github, google, …) | { id, name, email, picture, provider, strategy } |
email | { id, email, ...fields, provider, strategy } |
key | { id: 'key', provider: 'key', strategy: 'key' } |
Type ctx.user with a second generic to server(); see ctx.user.
Protecting routes
Auth only verifies the credential and sets ctx.user; it never rejects a request on its own. Guard the routes you want to protect with a ctx.user check:
const requireUser = (ctx) => {
if (!ctx.user) return 401;
};
export default server({ store, auth: 'cookie:github' })
.get('/public', () => 'anyone')
.get('/account', requireUser,
(ctx) => `Hello ${ctx.user.email}`);Responses
| Situation | Status |
|---|---|
| No credential | anonymous (ctx.user is undefined) |
| Invalid or expired token / cookie | 401 |
| Wrong email or password, or an unverifiable JWT | 500 |
Failed OAuth state check | 403 |
POST /auth/logout ends the session for cookie/token. Stateless jwt has nothing to revoke server-side (the client discards the token); key has no session.
Routes
| Method | Route | Provider |
|---|---|---|
GET | /auth/login/<provider> | OAuth |
GET | /auth/callback/<provider> | OAuth (Apple POST) |
POST | /auth/register/email | |
POST | /auth/login/email | |
PUT | /auth/password/email | |
POST | /auth/logout | all except key |
The key strategy registers none of these; it only checks the Authorization header on your own routes.