Error Handling & Security
Error Handling & Security
Error codes
HTTP errors (OidcHttpMiddleware)
| Code | HTTP status | Meaning |
|---|---|---|
ERR_AUTH_TOKEN_REQUIRED | 401 | Missing Authorization header or value does not start with Bearer |
ERR_AUTH_TOKEN_INVALID | 401 | JWT signature or claims verification failed (expired, wrong issuer, wrong audience, wrong/missing azp, disallowed algorithm, bad signature) |
ERR_ACCOUNT_DISABLED | 403 | Token is valid but the local user account is disabled |
ERR_USER_PROVISION_FAILED | 500 | users.findOrCreate threw an unexpected error |
Response shape:
{
"statusCode": 401,
"error": "Unauthorized",
"code": "ERR_AUTH_TOKEN_REQUIRED",
"message": "Bearer token required."
}
Socket.IO errors (OidcSocketMiddleware, OidcSocketAdminMiddleware)
Socket.IO errors call next(new Error("ERR_*")), which triggers a connect_error event on the client:
| Error message | Meaning |
|---|---|
ERR_AUTH_TOKEN_REQUIRED | No token in auth.token or Authorization header |
ERR_AUTH_TOKEN_INVALID | JWT verification failure |
ERR_ACCOUNT_DISABLED | User account is disabled |
ERR_FORBIDDEN | Admin role required (OidcSocketAdminMiddleware) |
Client-side handling:
socket.on("connect_error", (err) => {
switch (err.message) {
case "ERR_AUTH_TOKEN_REQUIRED":
// Redirect to login
break;
case "ERR_AUTH_TOKEN_INVALID":
// Token expired — refresh and reconnect
break;
case "ERR_FORBIDDEN":
// Not an admin — show access-denied UI
break;
default:
console.error("Unexpected connection error:", err.message);
}
});
Handling token expiry on long-lived connections
JWTs are verified at connection time. A Socket.IO connection that was established with a valid token will remain open even after the token expires — the middleware only runs during the handshake.
To enforce token lifetime on long-lived connections:
- Send the token in
socket.handshake.auth.token - Before token expiry, have the client reconnect with the refreshed token
- Optionally implement a server-side ping that checks
socket.featuresor a timestamp injected at connection time
// Server: record connection time
class AuthService extends BaseService {
async "connect:init"(socket: any, _data: unknown) {
socket.connectedAt = Date.now();
}
}
// Server: periodic check via a Watcher
class TokenWatcher extends BaseWatcher {
private timer: ReturnType<typeof setInterval> | null = null;
async watch() {
this.timer = setInterval(() => {
// Disconnect sockets connected more than 1 hour ago
const io = (this.app as any).io;
io.sockets.sockets.forEach((socket: any) => {
if (Date.now() - socket.connectedAt > 3_600_000) {
socket.disconnect(true);
}
});
}, 60_000);
}
stop() {
if (this.timer) { clearInterval(this.timer); this.timer = null; }
}
}
Security guarantees
Signature verification — Every token is verified against the remote JWKS using jose.jwtVerify, restricted to an asymmetric algorithm allow-list (RS*, PS*, ES*, EdDSA). Tokens signed with a symmetric algorithm (HS*) or none are rejected to prevent algorithm-confusion attacks. Override the list via OidcConfig.algorithms.
Claim validation — iss (issuer) and aud (audience) are always validated. aud matches OidcConfig.appSlug by default, or OidcConfig.audience (a resource URL) in RFC 8707 mode. The azp (authorized party) claim is bound to OidcConfig.appSlug — mandatory in resource-audience mode — to prevent token substitution between different applications sharing the same auth-service.
Key rotation — JWKS keys are cached per URI. jose automatically re-fetches the key set when signature verification fails with the cached keys, with a minimum 5-minute cooldown to prevent hammering the JWKS endpoint.
No secret storage — Access tokens are never written to disk, a database, or a cache. Verification happens in-memory on every request.
Account disablement — Disabled accounts are checked after token verification, not before. A valid token does not guarantee access — the local users table is the authoritative source for account status.
HTTPS requirement
Tokens transmitted over plain HTTP can be intercepted. Always deploy behind TLS in production. Using Traefik as a reverse proxy with automatic certificate management is a common pattern in this project ecosystem — see the IOServer Docker deployment guide.