Skip to Content
Deneva MCPComponentsOauth State

OAuth state service

Source: src/auth/oauth-state.service.ts Table: oauth_states

Storage and cleanup primitives for the OAuth 2.0 PKCE + state flow. Phase 1 ships only the storage layer. Phase 2 adds the HTTP routes (/auth/:platform/start, /auth/:platform/callback) that drive these helpers.

What “state” and “PKCE” guard against

ThreatDefence
CSRF on the OAuth callback (an attacker tricks a user into completing their flow with the user’s browser)The state parameter — random 32 bytes, server-generated, single-use. Mismatch = reject.
Authorization-code interception on the wire (e.g. a malicious mobile app intercepts the redirect)PKCE: the server commits to a code_verifier upfront and sends only its SHA-256 (code_challenge) to the auth provider. The token exchange must present the original verifier.

API

createOAuthState(tenantId, platform): Promise<{ state, codeVerifier, codeChallenge }> consumeOAuthState(state): Promise<row> // throws if unknown / expired / already-consumed startOAuthStateCleanup(): NodeJS.Timeout // background interval, replaced by Inngest in Phase 4 _cleanupOAuthStatesNow(): Promise<void> // test helper, runs the cleanup synchronously

Lifecycle of one row

createOAuthState() INSERT (state, code_verifier, tenant_id, platform, expires_at = now + 10m) consumeOAuthState(state): DELETE WHERE state = ? AND expires_at > now() RETURNING * ├─ row returned → delivered to caller (single-use enforced) └─ no row → throw "Invalid or expired OAuth state"

If consumeOAuthState is called twice for the same state, the second call gets zero rows back (DELETE is the consumption) and throws. Replay attacks are not possible.

Cleanup

A 5-minute interval runs DELETE ... WHERE expires_at < now(). This is best-effort — even if it never ran, expired rows would still be unconsumable thanks to the expires_at > now() clause in consumeOAuthState. The interval is purely about keeping the table small.

Phase 4 replaces the in-process interval with an Inngest cron so the cleanup runs once globally, not once per Node process.

Tests

tests/integration/oauth-state.test.ts:

  • create writes a row; consume returns and deletes it.
  • Second consume for the same state throws.
  • Expired rows are removed by _cleanupOAuthStatesNow.