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, jwt or key (see Strategies).
  • provider: email, github, google, microsoft, discord, facebook, apple. The string form takes one; use the object form's providers array for several. key is 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;
    },
  },
});
FieldTypeDefaultDescription
strategy'cookie' | 'token' | 'jwt' | 'key'(required)How the session credential is issued and checked. See Strategies.
providersProvider[][]OAuth providers and/or email to enable.
keystringAUTH_KEY envShared secret for the key strategy.
redirectstring'/user'Where a cookie login redirects the browser.
storeStoreserver store, user: prefixWhere users are kept.
sessionStoreserver store, auth: prefixWhere sessions are kept.
cleanUser(user) => userstrips passwordTransforms 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:

ProviderEnv vars
githubGITHUB_ID, GITHUB_SECRET
googleGOOGLE_ID, GOOGLE_SECRET
microsoftMICROSOFT_ID, MICROSOFT_SECRET
discordDISCORD_ID, DISCORD_SECRET
facebookFACEBOOK_ID, FACEBOOK_SECRET
appleAPPLE_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.

MethodRouteBodyNotes
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, HttpOnly authentication cookie is set (SameSite=Lax, plus Secure in production) and the browser is redirected to redirect.
  • token (APIs, mobile): login responds with JSON containing a token (an opaque id stored server-side); send it as Authorization: Bearer <token>.
  • jwt (stateless APIs): login responds with a token that is a signed JWT (HS256, using your secret) 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), changing secret invalidates all of them, and secret must be set; an unset per-process secret breaks every token on restart. Sent as Authorization: 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.user is the fixed marker { id: 'key', strategy: 'key', provider: 'key' } (no user record, so no email).
  • No header leaves the request anonymous (ctx.user undefined); a wrong or malformed header responds with 401.
  • Booting without a key configured throws, so a missing AUTH_KEY fails 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:

Providerctx.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

SituationStatus
No credentialanonymous (ctx.user is undefined)
Invalid or expired token / cookie401
Wrong email or password, or an unverifiable JWT500
Failed OAuth state check403

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

MethodRouteProvider
GET/auth/login/<provider>OAuth
GET/auth/callback/<provider>OAuth (Apple POST)
POST/auth/register/emailemail
POST/auth/login/emailemail
PUT/auth/password/emailemail
POST/auth/logoutall except key

The key strategy registers none of these; it only checks the Authorization header on your own routes.