Guides

Error Handling & Security

Error codes, security guarantees, and best practices for production deployments of ioserver-oidc.

Error Handling & Security

Error codes

HTTP errors (OidcHttpMiddleware)

CodeHTTP statusMeaning
ERR_AUTH_TOKEN_REQUIRED401Missing Authorization header or value does not start with Bearer
ERR_AUTH_TOKEN_INVALID401JWT signature or claims verification failed (expired, wrong issuer, wrong audience, wrong/missing azp, disallowed algorithm, bad signature)
ERR_ACCOUNT_DISABLED403Token is valid but the local user account is disabled
ERR_USER_PROVISION_FAILED500users.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 messageMeaning
ERR_AUTH_TOKEN_REQUIREDNo token in auth.token or Authorization header
ERR_AUTH_TOKEN_INVALIDJWT verification failure
ERR_ACCOUNT_DISABLEDUser account is disabled
ERR_FORBIDDENAdmin 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:

  1. Send the token in socket.handshake.auth.token
  2. Before token expiry, have the client reconnect with the refreshed token
  3. Optionally implement a server-side ping that checks socket.features or 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.

Copyright © 2026