Security Finding: SEC-19 — OAuth State Parameter (CSRF) Delegation
Security Finding: SEC-19 — OAuth State Parameter (CSRF) Delegation
Version: v1.0.87
Category: Authentication & Session (auth_session)
Severity: Medium — Requires follow-up action
Affected file: src/platform/auth/providers.ts
Background
Calmony Pay supports four OAuth 2.0 providers for authentication: Google, GitHub, Microsoft Entra, and Okta. The OAuth 2.0 specification requires that a state parameter be generated by the application before redirecting the user to the provider, and verified on callback to guard against cross-site request forgery (CSRF) attacks on the authentication flow.
The Finding
State parameter handling — both generation and verification — is delegated entirely to Auth.js v5 (beta). The application itself contains no assertion, test, or explicit documentation confirming that this protection is active and correctly applied across all four providers.
Because Auth.js v5 is currently in beta, this creates a visibility gap: if Auth.js were to introduce a regression in state handling, the application would have no independent safeguard or observable signal to detect it.
What This Means
| Concern | Detail |
|---|---|
| CSRF protection source | Auth.js v5 beta internals only |
| App-layer verification | None |
| Providers affected | Google, GitHub, Microsoft Entra, Okta |
| Test coverage | No test asserts rejection of missing/mismatched state |
| Audit logging | No provider/state logging on sign-in events |
Recommended Remediation
1. Add Test Coverage
Write an end-to-end or unit test that confirms the OAuth callback endpoint rejects requests where the state parameter is absent or does not match the originally issued value. Coverage should span at least one provider, with the intent to extend to all four.
// Example: assert that a callback with a tampered state is rejected
it('rejects OAuth callback with mismatched state parameter', async () => {
const response = await simulateOAuthCallback({
provider: 'google',
state: 'tampered-or-missing',
});
expect(response.status).toBe(400); // or equivalent rejection
});
2. Document the Auth.js Dependency
Add an inline comment or README note to src/platform/auth/providers.ts (and the relevant Auth.js configuration file) explicitly stating:
- That CSRF state parameter protection is provided by Auth.js.
- The exact Auth.js version in use.
- A reference to the relevant Auth.js source or documentation confirming state enforcement is enabled by default.
This makes the security assumption visible to future maintainers and surfaced during dependency upgrade reviews.
// src/platform/auth/providers.ts
//
// SECURITY NOTE (SEC-19):
// OAuth CSRF protection via the `state` parameter is handled by Auth.js v5 (beta).
// Auth.js generates and verifies the state parameter automatically for all providers.
// Ref: https://authjs.dev/reference/core — confirm state behaviour on every major upgrade.
// If upgrading Auth.js, verify state enforcement has not regressed before deploying.
3. Add Audit Logging via signIn Callback
Implement a custom signIn event callback to log the provider and relevant session context on each authentication. This provides an observable signal if state-related errors occur in production.
events: {
async signIn({ user, account }) {
// Audit log: provider and user on every successful sign-in
console.info('[auth] signIn', {
provider: account?.provider,
userId: user?.id,
timestamp: new Date().toISOString(),
});
},
},
Note: This logging step does not replace state verification — it supplements it by making successful authentications observable.
Current Status
No code changes were shipped in v1.0.87. This release documents the finding and recommended remediation steps. Follow-up work to add test coverage, inline documentation, and audit logging should be tracked and addressed before Auth.js v5 exits beta or before any Auth.js dependency upgrade.