@mks2508/auth-oidc-elysia
Pocket ID OIDC plugin for Elysia. PKCE Authorization Code flow, JWKS validation, session JWT (HS256), requireAuth() guard.
Install
bun add @mks2508/auth-oidc-elysia elysia @elysiajs/jwt
Usage
import { Elysia } from 'elysia'
import { createAuthPlugin, requireAuth } from '@mks2508/auth-oidc-elysia'
const app = new Elysia()
.use(createAuthPlugin({
issuerUrl: 'https://auth-provider.example.com',
clientId: 'my-app',
clientSecret: process.env.OIDC_CLIENT_SECRET,
redirectUri: 'https://admin.example.com/auth/callback/oidc',
sessionSecret: process.env.OIDC_SESSION_SECRET!,
adminSubs: ['<allowed-user-uuid>'],
adminUiUrl: 'https://admin.example.com',
cookieDomain: '.example.com',
}))
.get('/admin/data', () => ({ secret: 42 }), { beforeHandle: requireAuth() })
.listen(3000)
Config options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
issuerUrl |
string | Yes | — | Pocket ID issuer URL |
clientId |
string | Yes | — | OIDC client_id |
clientSecret |
string | No | — | OIDC client_secret (omit for PKCE-only public clients) |
redirectUri |
string | Yes | — | OIDC callback URI registered in Pocket ID |
sessionSecret |
string | Yes | — | HS256 secret for session JWT (32+ chars) |
adminSubs |
string[] | No | [] | Allowed Pocket ID user subs. Empty = any authenticated user |
adminUiUrl |
string | No | / |
Post-login redirect URL |
cookieDomain |
string | No | host-only | Cookie domain for cross-subdomain sessions |
bypass |
boolean | No | false |
Bypass auth entirely (local dev only) |
sessionTtl |
number | No | 604800 |
Session JWT TTL in seconds (7 days) |
trustedOrigins |
string[] | No | [] |
Extra trusted origins for token-mode return_to validation (e.g. SPA web domains). Merged with loopback CLI defaults + adminUiUrl |
cookieSecure |
boolean | No | true |
Set Secure flag on cookies. Set false only for local http dev (no TLS) |
audience |
string | string[] | No | — | Expected aud claim value(s) for raw access tokens. When set, tokens whose aud doesn't match are rejected (fail-closed) |
allowedClientIds |
string[] | No | [] |
Allowlist of OIDC client_ids permitted to call (derived from aud[0]). When set, unlisted clients are rejected (fail-closed) |
Auth routes
| Route | Method | Auth | Description |
|---|---|---|---|
/auth/health |
GET | Public | Health check + issuer URL |
/auth/status |
GET | Public | Returns session info |
/auth/login/oidc |
GET | Public | Initiate PKCE flow |
/auth/callback/oidc |
GET | Public | Exchange code, set session |
/auth/logout |
POST | Public | Clear session cookie |
CLI loopback (PKCE only)
For CLI tools (gh-style device auth): pass ?token_mode=true&return_to=http://127.0.0.1:54321/cb to /auth/login/oidc. The callback returns the session token in a URL fragment instead of setting a cookie — safe for local server capture.
GET /auth/login/oidc?token_mode=true&return_to=http://127.0.0.1:54321/cb
→ 302 https://auth-provider.example.com/authorize?...
→ 302 http://127.0.0.1:54321/cb#token=<session_jwt>
M2M (client_credentials) validation
Machine callers (a CI/build runner, another service) authenticate with a raw
Authorization: Bearer <access_token> obtained via Pocket ID's
client_credentials grant — no session cookie, no PKCE flow. Pocket ID issues
these tokens with sub = "client-<client_id>" and aud = [client_id].
Configure audience and/or allowedClientIds to lock down which callers are
accepted; both checks are fail-closed and, when configured, reject the same
way an invalid token does (requireAuth() → 401):
const app = new Elysia()
.use(createAuthPlugin({
issuerUrl: 'https://auth-provider.example.com',
clientId: 'release-hub',
redirectUri: 'https://hub.example.com/auth/callback/oidc',
sessionSecret: process.env.OIDC_SESSION_SECRET!,
audience: 'release-hub-api', // reject tokens not issued for this API
allowedClientIds: ['build-runner-01'], // only this M2M client may call
}))
.post('/ingest/artifact', ({ clientId, isM2M }) => {
return { received: true, clientId, isM2M };
}, { beforeHandle: requireAuth() });
IAuthContext exposes two derived fields on requests authenticated via a raw
access token (JWKS-verified — not via a session cookie):
isM2M—truewhensubhas the Pocket ID M2M shapeclient-<client_id>clientId— the caller's OIDC client_id, read from the JWTaudclaim (aud[0])
Exports
import {
createAuthPlugin, // Main plugin factory
requireAuth, // beforeHandle guard
AuthErrorCode, // Error code enum
authError, // Result<never, ResultError> factory
type IAuthConfig, // Plugin config interface
type IAuthContext, // Request context after middleware
type IAuthStatusResponse, // /auth/status response shape
type ISessionPayload, // Session JWT payload shape
} from '@mks2508/auth-oidc-elysia'
License
MIT