ioserver-oidc
v0.1.4
Published
IOServer middleware set for OIDC/OAuth2 JWT authentication via JWKS (auth-service compatible)
Maintainers
Readme
ioserver-oidc
OIDC/OAuth2 JWT middleware set for IOServer.
Protects Fastify HTTP routes and Socket.IO namespaces by verifying access tokens issued by auth-service (BetterAuth + OAuth2 provider) via remote JWKS — zero secret storage on the application side.
Table of contents
- Features
- Requirements
- Installation
- Quick start
- Environment variables
- API reference
- Request / socket context
- Error codes
- Security notes
- Contributing
- License
Features
- ✅ Verifies RS256 / ES256 JWT access tokens via remote JWKS (no secret distribution)
- ✅ Validates
iss,aud, and expiry claims - ✅ In-process JWKS key cache — one HTTP round-trip per key rotation
- ✅ Auto-provisions local user records via
appHandle.users.findOrCreate(...) - ✅ Rejects disabled accounts (403)
- ✅ Injects
sub,userId,userRole,roles,permissions,featureson every authenticated request/socket - ✅ Admin role guard for Socket.IO namespaces
- ✅ Full TypeScript declarations; ESM-only
Requirements
| Dependency | Version | | ---------- | ------- | | Node.js | ≥ 20 | | ioserver | ≥ 2.0.0 | | jose | ≥ 6.0.0 |
Installation
# npm
npm install ioserver-oidc
# pnpm
pnpm add ioserver-oidc
# yarn
yarn add ioserver-oidcjose is bundled as a direct dependency — no extra installation required.
Quick start
1. Register the config manager
import {
OidcConfigManager,
OidcHttpMiddleware,
OidcSocketMiddleware,
OidcSocketAdminMiddleware,
} from "ioserver-oidc";
import { IOServer } from "ioserver";
const server = new IOServer({
/* your IOServer options */
});
// Reads AUTH_SERVICE_URL + AUTH_SERVICE_APP_SLUG from process.env
server.addManager({ name: "oidcConfig", manager: OidcConfigManager });2. Protect HTTP controllers
server.addController({
name: "profile",
controller: ProfileController,
middlewares: [OidcHttpMiddleware], // ← JWT-required
prefix: "/profile",
});3. Protect Socket.IO services
// Any authenticated user
server.addService({
name: "chat",
service: ChatService,
middlewares: [OidcSocketMiddleware],
});
// Admin-only namespace
server.addService({
name: "users",
service: UserService,
middlewares: [OidcSocketMiddleware, OidcSocketAdminMiddleware],
});4. Read the injected context in your handlers
// HTTP (Fastify)
fastify.get("/me", async (request) => {
const req = request as any;
return { userId: req.userId, role: req.userRole };
});
// Socket.IO
socket.on("ping", () => {
console.log(socket.userId, socket.userRole);
});Environment variables
| Variable | Required | Default | Description |
| ----------------------- | -------- | ---------------------------------- | --------------------------------------------------------------------- |
| AUTH_SERVICE_URL | ✅ | — | Public base URL of your auth-service. E.g. https://auth.example.com |
| AUTH_SERVICE_APP_SLUG | ✅ | — | OAuth2 client_id / app slug registered in auth-service |
| AUTH_SERVICE_JWKS_URI | ❌ | <AUTH_SERVICE_URL>/api/auth/jwks | Override the JWKS endpoint |
| AUTH_SERVICE_ISSUER | ❌ | <AUTH_SERVICE_URL> | Override the expected iss claim |
All variables are read once at server startup by OidcConfigManager.start().
If OidcConfigManager is not registered, each middleware reads the same
variables lazily on first request (without caching between restarts).
API reference
OidcConfigManager
Extends BaseManager. Reads environment variables and exposes the resolved
OidcConfig to sibling middlewares via appHandle.oidcConfig.getConfig().
server.addManager({ name: "oidcConfig", manager: OidcConfigManager });The name must be
"oidcConfig"— the middlewares look forappHandle.oidcConfigby that exact key.
OidcHttpMiddleware
Extends BaseMiddleware. Verifies the Authorization: Bearer <token> header on
every inbound Fastify request.
Flow:
- Extracts the Bearer token from
Authorizationheader - Verifies JWT signature via JWKS (
iss+aud+ expiry) - Calls
appHandle.users.findOrCreate(sub, { email, name })if available - Rejects disabled accounts with
403 - Injects auth context onto the request object
Returns 401 on missing/invalid tokens, 403 on disabled accounts,
500 if user provisioning fails.
OidcSocketMiddleware
Same as OidcHttpMiddleware but for Socket.IO connections.
Token is read from (in order):
socket.handshake.auth.token— preferred, set by the Vue/web clientsocket.handshake.headers.authorization(Bearerprefix) — fallback
Calls appHandle.session.registerSocket(userId, socketId, sub) when the
session manager is available.
Rejects with new Error("ERR_AUTH_TOKEN_REQUIRED") or
"ERR_AUTH_TOKEN_INVALID" on failure.
OidcSocketAdminMiddleware
Role guard. Must be placed after OidcSocketMiddleware in the middlewares
array (relies on socket.roles/socket.userRole being already set).
Rejects with new Error("ERR_FORBIDDEN") when the user does not hold
the "admin" role.
verifyOidcToken
Low-level function — use this if you need to verify a token outside of the IOServer middleware system.
import { verifyOidcToken } from "ioserver-oidc";
const ctx = await verifyOidcToken(rawJwt, {
authServiceUrl: "https://auth.example.com",
appSlug: "my-app",
});
// ctx → OidcUserContextThrows a jose JWTVerifyError (or subclass) on any verification failure.
Types
import type { OidcConfig, OidcUserContext, OidcFeatures } from "ioserver-oidc";OidcConfig
interface OidcConfig {
authServiceUrl: string; // e.g. "https://auth.example.com"
appSlug: string; // OAuth2 client_id (= app slug)
jwksUri?: string; // Override JWKS endpoint
issuer?: string; // Override expected `iss` claim
}OidcUserContext
interface OidcUserContext {
userId: string; // Local DB user ID (after findOrCreate)
sub: string; // OIDC sub claim
email: string | null;
name: string | null;
userRole: string; // First element of roles[], fallback "user"
roles: string[];
permissions: string[];
features: OidcFeatures; // Record<string, unknown>
}Request / socket context
After successful authentication the following properties are available:
| Property | Type | Source |
| ------------- | ------------------------ | ----------------------- |
| sub | string | JWT sub claim |
| userId | string | Local DB users.id |
| userRole | string | roles[0] or "user" |
| roles | string[] | JWT roles claim |
| permissions | string[] | JWT permissions claim |
| features | Record<string,unknown> | JWT features claim |
In TypeScript, cast the Fastify request or Socket.IO socket to any (or
augment the types in your app) to access these properties.
Error codes
| Code | HTTP / Socket | Meaning |
| --------------------------- | ------------- | ------------------------------------------ |
| ERR_AUTH_TOKEN_REQUIRED | 401 / reject | No Authorization header or auth token |
| ERR_AUTH_TOKEN_INVALID | 401 / reject | JWT signature / claims verification failed |
| ERR_USER_DISABLED | 403 | User account is disabled in the local DB |
| ERR_USER_PROVISION_FAILED | 500 | findOrCreate threw an error |
| ERR_FORBIDDEN | — / reject | User lacks the required role |
Security notes
- Access tokens are never stored — they are verified in-memory on every request/connection using the cached JWKS.
- JWKS keys are fetched lazily and cached per URI. The
joselibrary automatically re-fetches keys on signature verification failure (key rotation) with a minimum 5-minute cooldown. - The
aud(audience) claim is always validated againstOidcConfig.appSlugto prevent token substitution attacks between different applications sharing the same auth-service instance. - The
iss(issuer) claim is validated againstOidcConfig.authServiceUrl(or the explicit override).
Contributing
- Fork the repo and create a branch:
git checkout -b feat/my-feature - Make your changes (TypeScript in
src/) - Build:
pnpm run build - Open a Pull Request against
main
