fix(server): treat expired HTTP Bearer token as no-auth instead of 401 by 22f · Pull Request #7196 · surrealdb/surrealdb · GitHub
Skip to content

fix(server): treat expired HTTP Bearer token as no-auth instead of 401#7196

Open
22f wants to merge 1 commit intosurrealdb:mainfrom
22f:fix/http-jwt-expiry-deadlock
Open

fix(server): treat expired HTTP Bearer token as no-auth instead of 401#7196
22f wants to merge 1 commit intosurrealdb:mainfrom
22f:fix/http-jwt-expiry-deadlock

Conversation

@22f
Copy link
Copy Markdown
Contributor

@22f 22f commented Mar 30, 2026

Thank you for submitting this pull request. We really appreciate you spending the time to work on SurrealDB. 🚀 🎉

What is the motivation?

When an HTTP client's JWT expires, it enters a deadlock: signin() and invalidate() — the operations needed to recover — are also rejected with 401 because the auth middleware (SurrealAuth) validates the Bearer token on all incoming requests before the endpoint handler runs.

The SDK automatically attaches the Authorization: Bearer <token> header on every request, including recovery calls. The client has no way to obtain a fresh JWT without creating a brand-new connection (bypassing the stale Bearer header).

The deadlock flow:

  1. Client signs in via HTTP → receives JWT with expiry
  2. JWT expires
  3. Client calls signin() with valid credentials
  4. SDK attaches the expired JWT as Authorization: Bearer <expired>
  5. Middleware: expired token → 401 → signin handler never runs
  6. Client is stuck

WebSocket is unaffected — WS auth is per-RPC-method, not middleware-based.

Historical context:

What does this change do?

In check_auth() (surrealdb/server/src/ntw/auth.rs), when Bearer token validation returns ExpiredToken, the middleware now treats it as equivalent to no token — the request proceeds with an unauthenticated (anonymous) session. The endpoint handler then decides what to do:

  • /signin: receives anonymous session + credentials from body → verifies → issues new JWT ✓
  • /sql and other protected endpoints: anonymous session → permissions error (not 401) ✓
  • /invalidate: clears the (already empty) session → succeeds ✓

This is not a security downgrade:

  • An expired token provides zero security value
  • The anonymous session is identical to not sending any Authorization header at all
  • All endpoints enforce permissions based on the session's auth level, not the presence of a Bearer header
  • Invalid tokens (bad signature, wrong claims, etc.) still return 401 as before

Uses the existing is_expired_token_error() helper from surrealdb_core::iam.

What is your testing strategy?

New integration test expired_jwt_does_not_block_signin in tests/http_integration.rs with three scenarios:

  1. Signin with expired BearerPOST /signin with expired JWT in Authorization header → must return 200 (not 401)
  2. SQL with expired BearerPOST /sql with expired JWT → must not return 401 at middleware level (handler decides permissions)
  3. Full recovery flow — sign in normally, then re-signin carrying an expired token → must succeed (reproduces the exact production deadlock)

All existing tests pass. Fork CI results: 22f/surrealdb#2

  • ✅ HTTP / WebSocket / CLI integration tests
  • ✅ clippy, format, docs, revision
  • ✅ KVS engines (mem, rocksdb, surrealkv), Rust SDK (all engines)
  • ✅ SurrealQL tests (rocksdb, surrealkv, tikv)
  • ⚠️ SurrealQL tests (mem) — pre-existing flaky failure in access/record/variables.surql (same on upstream main)

Is this related to any issues?

Does this change need documentation?

  • No documentation needed

Does this change make any alterations to environment variables or CLI commands?

  • No changes made to env vars

Have you read the Contributing Guidelines?

When an HTTP client's Bearer JWT expires, the auth middleware rejected
ALL requests with 401 — including signin and invalidate, which are the
operations needed to recover. This created a deadlock where the client
could not obtain a fresh token without creating a brand new connection.

The fix treats an expired Bearer token as equivalent to no token,
falling back to an unauthenticated session. This lets recovery
endpoints (signin, invalidate) proceed normally while other endpoints
still enforce permissions via their handlers. WebSocket connections
were unaffected as they handle auth per-RPC-method.

Uses the existing `is_expired_token_error()` helper from core::iam.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@22f 22f requested a review from a team as a code owner March 30, 2026 14:34
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@github-actions github-actions Bot added the community PRs from the community. This label is used to ensure all community PRs get reviewed. label Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community PRs from the community. This label is used to ensure all community PRs get reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: HTTP clients with expired JWT cannot signin or invalidate (auth deadlock)

1 participant