Skip to content

feat: harden JWT auth with claim validation, JWKS, clock-skew, token refresh#212

Open
queentiffany1111-cloud wants to merge 1 commit into
RiftCore00:devfrom
queentiffany1111-cloud:feat/jwt-auth-hardening-197
Open

feat: harden JWT auth with claim validation, JWKS, clock-skew, token refresh#212
queentiffany1111-cloud wants to merge 1 commit into
RiftCore00:devfrom
queentiffany1111-cloud:feat/jwt-auth-hardening-197

Conversation

@queentiffany1111-cloud

Copy link
Copy Markdown

Summary

Hardens the JWT authentication layer (src/auth.js) to close several security gaps present in the original implementation and adds a
token-refresh protocol to src/server.js so long-lived WebSocket connections (e.g. fleet-tracking clients) can rotate their credentials
without disconnecting.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Problem

The previous verifyConnection implementation only checked JWT signature and expiry. This left four concrete vulnerabilities and one
operational gap:

  1. Algorithm confusion — jwt.verify was called without an algorithms restriction, meaning an attacker could craft a token with alg:
    "none" or swap to a different algorithm family to bypass signature verification.
  2. Missing claim validation — iss (issuer) and aud (audience) claims were never checked. A JWT minted by any service sharing the same
    HMAC secret (e.g. an internal SSO) would be unconditionally accepted by the gateway.
  3. No clock-skew tolerance — jwt.verify used library defaults with no tolerance window. Clients whose clocks are even 1 second ahead
    received Token has expired on tokens that were nominally still valid.
  4. No asymmetric key support — only HMAC symmetric secrets were supported. Production deployments behind an API gateway or identity
    provider (Auth0, Keycloak, AWS Cognito) require JWKS-based key rotation using RSA or EC public keys.
  5. No token refresh — when a JWT expired, the client was disconnected with close code 4001 and had to reconnect out-of-band, causing gaps
    in live tracking streams.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Changes

src/auth.js

  • async conversion — verifyConnection is now async to support JWKS HTTP fetching.
  • Algorithm restriction — jwt.verify is always called with algorithms: ["HS256", "RS256", "ES256"], rejecting any other algorithm
    including none.
  • alg: none pre-check — token header is decoded first; tokens with alg: "none" are rejected immediately before any library call, with the
    error message "Invalid token: unsupported algorithm".
  • Claim validation — validates iss against AUTH_ISSUER env var and aud against AUTH_AUDIENCE env var when set. Issuer is pre-validated
    manually before calling jwt.verify to ensure the correct error is returned (the jsonwebtoken library checks aud before iss internally,
    which would produce misleading errors).
  • Clock-skew tolerance — passes clockTolerance: clockSkewMs / 1000 to jwt.verify, defaulting to ±30 seconds, configurable via
    AUTH_CLOCK_SKEW_MS.
  • JWKS support — when AUTH_JWKS_URI is set, fetches the JWKS document and resolves the signing key by matching the token's kid header.
    Supports both https:// and http:// transports. Keys are converted to Node.js KeyObject via crypto.createPublicKey({ format: "jwk", key:
    jwk }) — no new dependencies added. HMAC (AUTH_SECRET) flow is preserved as the default when AUTH_JWKS_URI is not set.
  • JWKS caching — the JWKS document is cached in memory with a configurable TTL (default 10 minutes via AUTH_JWKS_CACHE_TTL). Stale cache
    is returned on background refresh errors to avoid auth outages during transient JWKS endpoint failures.

src/server.js

  • async connection callback — wss.on("connection", ...) is now async so await verifyConnection(token) works correctly.
  • token_refresh handler — new case "token_refresh" in the message switch. The client sends { type: "token_refresh", token: "" }.
    The server verifies the new token, updates actualClientId in-place, and replies with { type: "token_refresh_ok" }. On failure it returns
    { type: "error", payload: { message: "..." } } and keeps the connection open. actualClientId is promoted from const to let to allow
    identity updates on refresh.

src/validator.js

  • Added token_refresh to the messageSchema discriminated union with a token: z.string().min(1) field, and a 2048-byte size limit in
    MESSAGE_SIZE_LIMITS.

tests/auth-hardening.test.js (new)

Covers all new hardening paths:

  • Rejects token with invalid iss claim
  • Rejects token with invalid aud claim
  • Rejects token with nbf 31 seconds in the future (outside ±30s skew)
  • Accepts token with exp 29 seconds in the past (within ±30s skew)
  • Rejects token with alg: "none"
  • JWKS: verifies token signed with RSA private key against a mock JWKS HTTP server
  • JWKS: rejects token with an unknown kid

tests/refresh.test.js (new)

Integration tests against a live createServer instance:

  • Successfully refreshes a valid token and receives token_refresh_ok
  • Updates client identity (actualClientId) after refresh — verified via room membership
  • Returns error frame for an invalid refresh token, connection remains open

Testing

Test Files: 21 passed | 1 skipped (22)
Tests: 173 passed | 15 skipped (188)

The 15 skipped tests are PostgreSQL integration tests that require a live database — skipped by design in CI without a DB. All auth,
server, refresh, and hardening tests pass. npm run lint passes with zero errors.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Out of Scope

  • Full OAuth2/OIDC integration
  • Token introspection (RFC 7662)
  • Refresh token rotation
  • Changes to room-manager.js, rate-limiter.js, conn-rate-limiter.js, logger.js

closes #197

…refresh (RiftCore00#197)

- Validate iss and aud claims against AUTH_ISSUER/AUTH_AUDIENCE env vars
- Add clock-skew tolerance (±30s, configurable via AUTH_CLOCK_SKEW_MS)
- Add JWKS endpoint support (http+https) with TTL caching and RSA/EC key support
- Explicitly reject alg:none tokens before jwt.verify
- Pre-validate iss claim to ensure correct error ordering over aud check
- Restrict allowed algorithms to HS256, RS256, ES256
- Add token_refresh message handler in server.js; updates client identity on success
- Make wss connection callback async to support await verifyConnection
- Add auth-hardening.test.js and refresh.test.js test suites
- All 173 tests pass, lint clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden JWT authentication with claim validation, JWKS endpoint support, clock-skew tolerance, and token-refresh protocol

1 participant