feat(oauth): OAuth identity sign-in with cross-origin token exchange#1141
feat(oauth): OAuth identity sign-in with cross-origin token exchange#1141theothersideofgod wants to merge 18 commits into
Conversation
Code Review — OAuth Identity Sign-InNice work on the overall architecture here. The secrets handling (encrypted_secrets module for client secrets, HMAC-SHA256 state signing with timing-safe comparison), the database integration (identity provider allowlist guard, proper JWT context on dedicated db client), and the cross-origin vs same-origin token flow are all well-designed. PR #1117 (Cookie lifecycle & CSRF) has been reviewed and is good to merge. This PR (#1141) needs the following fixes before we can merge it. Once addressed, please rebase onto Must Fix1. Test env var mismatch — callback tests don't actually validate callback logic
// Test sets:
process.env.OAUTH_STATE_SECRET = 'test-secret-key-for-testing';
// But the code reads (oauth.ts line 34):
const secret = process.env.OAUTH_SECRET;Wrong env var name. Fix: Change 2. Dead code — standalone
|
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
Code Review ResponseThanks for the detailed review @devin-ai-integration! Must Fix ✅ All Done
Should Fix ✅ All Done
Follow-Up Items ✅ All Done
Commit SummaryReady for rebase onto |
fe3be65 to
65eec98
Compare
…true - Pass remember_me=true to sign_in_identity and sign_up_identity - Pass rememberMe=true to getSessionCookieConfig - Add device_token to sign_up_identity call - Ensures cookie and DB session both use remember_me_duration (30 days) Fixes PR #1141 follow-up items #4 and #8: - #4: sign_up_identity now receives device_token - #8: Cookie and session duration now synchronized Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add emailVerified field to OAuthProfile - Add emailVerified mapping to providers - Track email verification for GitHub
Implement OAuth routes that wire the @constructive-io/oauth package to the database's sign_in_identity/sign_up_identity private functions. Routes: - GET /auth/providers - list configured providers - GET /auth/:provider - initiate OAuth flow - GET /auth/:provider/callback - handle OAuth callback Features: - Signed state with HMAC for CSRF protection and redirect_uri storage - Shadow attack defense: reject unverified emails for auto-signup - MFA flow: redirect to /auth/mfa when mfa_required=true - Session and device token cookie setting Refs: #750 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- OAuth middleware unit tests - Multi-tenant isolation tests
- Read OAuth provider config from identity_providers table - Resolve encrypted_secrets schema from metaschema - Use encrypted_secrets.get() to decrypt client secret
Instead of using a static baseUrl config, dynamically derive it from
req.protocol and req.get('host'). This ensures each tenant's OAuth
callback URL matches their subdomain.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add /error route to prevent conflict with /:provider
- Use next('router') to skip entire router for /error
- Correct parameter order for sign_up_identity call
- Make cookie and token modes mutually exclusive - Set JWT context for correct session user-agent/origin - Use dedicated db client for sign_in_identity JWT context - Use session-level set_config for JWT context
OAUTH_SECRET is now required for server to start. No fallback, no warning - just fail fast if not configured. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change OAUTH_STATE_SECRET to OAUTH_SECRET to match the production code. This fixes 10 failing tests that were incorrectly hitting INVALID_STATE instead of testing the actual callback logic. Co-Authored-By: Claude <noreply@anthropic.com>
These functions used pool.query() directly without JWT context setup. The actual callback handler uses a dedicated dbClient with proper set_config calls for user_agent and origin audit fields. Co-Authored-By: Claude <noreply@anthropic.com>
Remove the 'message' query parameter that exposed raw error.message content (potentially database errors, function names, or internal details) in the redirect URL. The generic 'CALLBACK_FAILED' error code is sufficient for the frontend, and detailed errors are still logged server-side. Co-Authored-By: Claude <noreply@anthropic.com>
Move OAuthRoutesConfig interface to graphql/types package and add oauth property to ConstructiveOptions, removing the need for `(opts as any).oauth` type assertion. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…true - Pass remember_me=true to sign_in_identity and sign_up_identity - Pass rememberMe=true to getSessionCookieConfig - Add device_token to sign_up_identity call - Ensures cookie and DB session both use remember_me_duration (30 days) Fixes PR #1141 follow-up items #4 and #8: - #4: sign_up_identity now receives device_token - #8: Cookie and session duration now synchronized Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
… secret lookup The getIdentityProvider query was using ip.id to retrieve the encrypted OAuth client secret, but secrets are stored under ip.client_secret_id. This caused PROVIDER_NOT_CONFIGURED errors when the IDs didn't match. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…identity The database functions expect (remember_me, device_token) but the code was passing (device_token, remember_me). Fixed the SQL parameter types and argument order to match the database function signatures. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1b0cf82 to
60d33de
Compare
|
@pyramation Hi, Dan please let devin review this pr again. thanks |
Follow-Up Review — Verifying FixesAll 9 items from the original review have been addressed. Nice work on the quick turnaround. Here's the verification: Must Fix — All Confirmed Fixed
Should Fix — All Confirmed Fixed
Follow-Up Items — All Confirmed Addressed
Additional Commits (not from original review)
New Issue Found:
|
The catch block had manual dbClient.release() calls before return statements, but JavaScript's finally block always executes even after a return. This caused the connection to be released twice, potentially corrupting pool state under concurrent load. Fix: Remove manual release calls and let finally handle cleanup. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Hi, @pyramation. Hi, @devin-ai-integration Please review the latest fix (commit de1f282). Fix for Devin's Follow-Up ReviewCommit Issue:
|
Resolve pnpm-lock.yaml conflict by regenerating Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Hi, @pyramation Hi, @devin-ai-integration Please review the latest fix (commit de1f282). conflict fixed |
| ip.display_name, | ||
| ip.enabled, | ||
| ip.client_id, | ||
| "${encryptedSecretsSchema}".get(ip.client_secret_id, 'oauth_client_secret') as client_secret, |
There was a problem hiding this comment.
I think this must be an old version of constructive-db secrets ?
| const requireVerifiedEmail = oauthConfig?.requireVerifiedEmail ?? true; | ||
|
|
||
| // Rate limiters for OAuth endpoints (disabled in development/test) | ||
| const skipRateLimit = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; |
There was a problem hiding this comment.
we should be able to user our env var tools for this, I think getNodeEnv() from pgpm/env/src/env.ts
| // Rate limiters for OAuth endpoints (disabled in development/test) | ||
| const skipRateLimit = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; | ||
|
|
||
| const oauthInitLimiter = rateLimit({ |
There was a problem hiding this comment.
we should look into if this conflicts with our rate limiting, probably good to have this too, but we also rate limit other places. Where does it store this?
| await dbClient.query(` | ||
| SELECT set_config('jwt.claims.user_agent', $1, false), | ||
| set_config('jwt.claims.origin', $2, false) | ||
| `, [userAgent, targetOrigin]); |
There was a problem hiding this comment.
you should not be doing this manually
| return res.redirect(errorUrl.toString()); | ||
| } | ||
|
|
||
| const privateSchema = req.api.rlsModule.privateSchema.schemaName; |
There was a problem hiding this comment.
this is where we get one of the API's private schemas, that doesn't mean that's where secrets live, that doesn't mean that's where identity lives either.
| }; | ||
|
|
||
| const signInSql = ` | ||
| SELECT * FROM "${privateSchema}".sign_in_identity( |
There was a problem hiding this comment.
const privateSchema = req.api.rlsModule.privateSchema.schemaName;
is this the same privateSchema? Why would you assume sign_in_identity is on there?
| } | ||
|
|
||
| // Call sign_up_identity (using same client with JWT context) | ||
| const signUpSql = ` |
There was a problem hiding this comment.
is signup only based on a failed signin? is there any better way to detect?
DB Settings Available for OAuth Middlewareconstructive-db PR #1301 (now merged) added three new columns to New settings (from PR #1301) → replace hardcoded constants
Existing settings → replace hardcoded values
|
| Setting | Why |
|---|---|
OAUTH_SECRET (env var) |
Server-level HMAC signing secret — correct as env var |
OAUTH_STATE_COOKIE = 'oauth_state' |
Internal implementation detail, not tenant-configurable |
How to read these settings
The settings are available via req.api.authSettings in the middleware, which is the singleton row from app_settings_auth. For example:
const authSettings = req.api.authSettings;
const stateMaxAge = authSettings.oauth_state_max_age; // interval as string, e.g. '00:10:00'
const requireVerified = authSettings.oauth_require_verified_email;
const errorPath = authSettings.oauth_error_redirect_path;
const allowSignup = authSettings.allow_identity_sign_up;
const cookieSecure = authSettings.cookie_secure;
// etc.|
This PR has been superseded by #1220 which rewrites the OAuth middleware to use module loaders from Key changes in #1220:
|
Summary
Implements OAuth identity sign-in with support for GitHub/Google/Apple providers, cross-origin token exchange, and multi-tenant support.
Based on #749 (Cookie/CSRF infrastructure).
Architecture
Commits
Cookie/CSRF Infrastructure (#749)
7f9b7a4960eefc2f1f37bd133c29e393e6bb1a0c73b1190bcc3310d29b0e35fOAuth Sign-In (#750)
5be361f6c752b69c147f6563c5b9d5646e7dda188ad200ab5b22a513Key Design
1. Cross-Origin vs Same-Origin
signInCrossOriginfor access_token2. Security
OAUTH_SECRETenvironment variable required3. Multi-Tenant
identity_providerstable)encrypted_secrets)Environment Variables
OAUTH_SECRETTests
oauth.test.ts(939 lines) - middleware, multi-tenant isolation, error handling🤖 Generated with Claude Code