Requirements
- Target platform
- OpenClaw
- Install method
- Manual import
- Extraction
- Extract archive
- Prerequisites
- OpenClaw
- Primary doc
- SKILL.md
Provides expert OAuth 2.0 implementation, troubleshooting, and token management for Twenty CRM with Google/Microsoft OAuth and email/calendar sync integration.
Provides expert OAuth 2.0 implementation, troubleshooting, and token management for Twenty CRM with Google/Microsoft OAuth and email/calendar sync integration.
Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.
I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete.
I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run.
Author: Generated from extensive OAuth debugging sessions in OpenCode Last Updated: 2026-02-08 Version: 1.0
name: twenty-oauth-mastery description: Expert-level OAuth authentication knowledge for Twenty CRM including implementation, troubleshooting, and best practices expertise_level: Expert/Mastery category: Authentication applicable_to: - Twenty CRM authentication - Google/Microsoft OAuth - Token refresh management - Domain restrictions - Email/Calendar sync integration prerequisites: - Knowledge of TypeScript/JavaScript - Understanding of OAuth 2.0 protocol - Familiarity with NestJS framework keywords: - oauth - authentication - twenty-crm - google-oauth - microsoft-oauth - token-refresh - sync-integration - domain-restriction
You should use this skill when working on: โ Implementing new OAuth providers โ Fixing OAuth login issues โ Setting up automatic Gmail/Calendar sync after OAuth โ Debugging token refresh failures โ Configuring domain restrictions โ Troubleshooting redirect loops
IssueFile to CheckQuick FixRedirect loopauth.service.tsRebuild: npx nx build twenty-server.co domain blockedgoogle-auth.controller.tsAdd to allowlist: ['company.com', 'company.co']Sync not startinggoogle.auth.strategy.tsReturn tokens in validate()Cookie not readableController cookie settingsSet httpOnly: falseInfinite loopSignInUpGlobalScopeFormEffect.tsxTrack processed token signatures
Key Files: twenty/packages/twenty-server/src/engine/core-modules/auth/ Structure: auth/ โโโ strategies/ # Passport strategies (Google, Microsoft) โโโ controllers/ # OAuth endpoints and callbacks โโโ services/ # Auth logic, sync setup, token management โโโ guards/ # Auth guards and validation โโโ utils/ # Scope configuration, utilities
Passport Strategy Pattern (MUST FOLLOW) @Injectable() export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { constructor(twentyConfigService: TwentyConfigService) { super({ clientID: twentyConfigService.get('AUTH_GOOGLE_CLIENT_ID'), clientSecret: twentyConfigService.get('AUTH_GOOGLE_CLIENT_SECRET'), callbackURL: twentyConfigService.get('AUTH_GOOGLE_CALLBACK_URL'), scope: getGoogleApisOauthScopes(), passReqToCallback: true, // ๐ด CRITICAL: Required for request state }); } async validate( request: GoogleRequest, _accessToken: string, _refreshToken: string, profile: GoogleProfile, ) { // ๐ด CRITICAL: Include tokens in return object // Without this, automatic sync setup fails return { ...profile, accessToken: _accessToken, refreshToken: _refreshToken, hostedDomain: request.query.hosted_domain || profile.emails?.[0]?.value?.split('@')[1], }; } } Why This Matters: passReqToCallback: true: Enables access to request state Token preservation: Required for OAuthSyncService to work
Issue 1: Redirect Loop After OAuth Symptoms: OAuth completes but user stuck on welcome page Root Causes: Backend not compiled: Source has fix, container running old JavaScript Fix: npx nx build twenty-server docker restart fratres-twenty Missing isSingleDomainMode: Redirect logic not in compiled code Check: docker exec fratres-twenty cat /app/dist/engine/core-modules/auth/services/auth.service.js | grep isSingleDomainMode Cookie domain mismatch: Cookie not accessible Fix: // auth.service.ts - Remove explicit domain attribute res.cookie('tokenPair', JSON.stringify(authTokens), { path: '/', secure: true, sameSite: 'lax', httpOnly: false, // ๐ด Must be false for JavaScript access }); Issue 2: Domain Enforcement Blocking .co Users Symptoms: @company.co rejected, only @company.com allowed Three Places to Fix: Google Strategy (google.auth.strategy.ts): // โ WRONG - Hardcoded hd: 'company.com' // โ CORRECT - Remove hd parameter // (no hd parameter) Controller (google-auth.controller.ts): // โ WRONG - Hardcoded check if (hostedDomain !== 'company.com') { throw ... } // โ CORRECT - Allowlist const allowedOAuthDomains = ['company.com', 'company.co']; if (!hostedDomain || !allowedOAuthDomains.includes(hostedDomain)) { throw new UnauthorizedException( `Only ${allowedOAuthDomains.map(d => `@${d}`).join(', ')} allowed` ); } Database (workspaceMetadata table): INSERT INTO "workspaceMetadata" ("id", "workspaceId", "key", "value", "createdAt", "updatedAt") VALUES (gen_random_uuid(), 'workspace-id', 'approvedAccessDomains', '["company.com", "company.co"]', NOW(), NOW()); Issue 3: Automatic Sync Not Triggered Symptoms: User logs in but connected account/sync channels not created Root Cause: Tokens lost in validate() method Fix: // google.auth.strategy.ts validate() async validate(request, accessToken, refreshToken, profile) { // โ WRONG - Tokens lost return { ...profile }; // โ CORRECT - Tokens preserved return { ...profile, accessToken, refreshToken, }; } Additional Checks: Verify auth.service.ts calls oauthSyncService.setupSyncForOAuthUser() after login Verify tokens are passed to sync service Check Google scopes include gmail.readonly and calendar.events Verify CALENDAR_PROVIDER_GOOGLE_ENABLED=true Issue 4: Frontend Token Processing Loop Symptoms: SignInUpGlobalScopeFormEffect runs repeatedly, infinite API calls Root Cause: Same token processed multiple times Fix: // SignInUpGlobalScopeFormEffect.tsx useEffect(() => { const tokenPairFromUrl = getAuthPairFromUrl(); if (tokenPairFromUrl) { const tokenSignature = JSON.stringify(tokenPairFromUrl); // ๐ด CRITICAL: Skip if already processed if (processedTokenSignatures.current.has(tokenSignature)) { return; } // Track this signature processedTokenSignatures.current.add(tokenSignature); // Now process the token setAuthTokens(tokenPairFromUrl); } }, []);
When to Use: Users should have Gmail/Calendar auto-connected after OAuth login Implementation: Create OAuthSyncService: async setupSyncForOAuthUser(input: { workspaceId: string; userId: string; workspaceMemberId: string; email: string; accessToken: string; refreshToken: string; scopes: string[]; }) { // 1. Create/update connected account with tokens // 2. Create message channel // 3. Create calendar channel (if enabled) // 4. Queue initial sync jobs } Integrate into AuthService: // auth.service.ts:signInUpWithSocialSSO() const { redirectUrl, authTokens } = await this.generateTokens(...); // ๐ด CRITICAL: Call sync setup BEFORE redirect if (provider === 'google') { try { await this.oauthSyncService.setupSyncForOAuthUser({ workspaceId, userId, email: user.email, accessToken: authTokens.authToken.accessToken, refreshToken: authTokens.authToken.refreshToken, scopes: user.scopes || [], }); } catch (error) { // Log error but don't fail login this.logger.error('Failed to setup OAuth sync', error); } } return { redirectUrl, authTokens }; Critical: Use try/catch to prevent sync setup from failing login Check for existing channels (prevent duplication) Only run for specific providers/domains if needed
Token Refresh Pattern: async refreshTokens(refreshToken: string): Promise<ConnectedAccountTokens> { const oAuth2Client = new google.auth.OAuth2(clientId, clientSecret); oAuth2Client.setCredentials({ refresh_token: refreshToken }); try { const { token } = await oAuth2Client.getAccessToken(); // ๐ด CRITICAL: Preserve original refresh token // Google may not return a new one return { accessToken: token, refreshToken: refreshToken, }; } catch (error) { throw parseGoogleOAuthError(error); } } Error Handling: export const parseGoogleOAuthError = (error: unknown) => { const gaxiosError = error as GaxiosError; const code = gaxiosError.response?.status; const reason = gaxiosError.response?.data?.error; switch (code) { case 400: if (reason === 'invalid_grant') { // ๐ด FATAL: Refresh token expired/revoked return new ConnectedAccountRefreshAccessTokenException( 'invalid_grant', ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN, ); } break; case 401: return new ConnectedAccountRefreshAccessTokenException( 'unauthorized', ConnectedAccountRefreshAccessTokenExceptionCode.UNAUTHORIZED, ); case 429: // ๐ด RETRYABLE: Rate limit error return new ConnectedAccountRefreshAccessTokenException( 'rate_limit', ConnectedAccountRefreshAccessTokenExceptionCode.RATE_LIMIT_ERROR, ); } return new ConnectedAccountRefreshAccessTokenException('unknown', ...); };
Unit Testing (Token Refresh) describe('GoogleAPIRefreshAccessTokenService', () => { it('should refresh token successfully', async () => { const mockRefreshToken = 'valid-refresh-token'; const mockNewAccessToken = 'new-access-token'; jest.spyOn(google.auth, 'OAuth2').mockImplementation(() => ({ setCredentials: jest.fn(), getAccessToken: jest.fn().mockResolvedValue({ token: mockNewAccessToken }), })); const result = await service.refreshTokens(mockRefreshToken); expect(result.accessToken).toBe(mockNewAccessToken); expect(result.refreshToken).toBe(mockRefreshToken); // Original preserved }); }); Cookie Injection Test (Playwright) // Test: frontend reads and processes cookie await context.addCookies([{ name: 'tokenPair', value: JSON.stringify({ authToken: { accessToken: 'fake-token' } }), domain: 'isearch.1791technology.com', path: '/', secure: true, sameSite: 'Lax', }]); await page.goto('https://isearch.1791technology.com'); // Check console logs const logs = await page.evaluate(() => window.tokenPairLogs || []); assert(logs.includes('tokenPairPayload from cookies: found')); assert(logs.includes('Setting auth tokens...'));
Required Environment Variables: # Google OAuth AUTH_GOOGLE_ENABLED=true AUTH_GOOGLE_CLIENT_ID=849758856044-54v9md2rt6ucthch26p8g4etotcb8gth.apps.googleusercontent.com AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-... AUTH_GOOGLE_CALLBACK_URL=https://yourdomain.com/auth/google/redirect # Calendars/Email CALENDAR_PROVIDER_GOOGLE_ENABLED=true MESSAGING_PROVIDER_GMAIL_ENABLED=true # Billing (disable for self-hosted) IS_BILLING_ENABLED=false Google Cloud Console: Redirect URIs: https://yourdomain.com/auth/google/redirect Authorized Origins: https://yourdomain.com
Before Deploying: TypeScript source updated Unit tests passing Type check: npx nx typecheck twenty-server Build: npx nx build twenty-server Verify compiled JavaScript has changes (check dist/ folder) Copy dist/ to container Restart container Check health: curl -f /healthz After Deploying: Test OAuth flow manually Check browser console Verify redirect to dashboard (not welcome) Check connected account in database Verify sync channels created (if applicable)
Step 1: Verify Container Running New Code docker ps | grep fratres-twenty docker exec fratres-twenty cat /app/dist/engine/core-modules/auth/services/auth.service.js | grep isSingleDomainMode Step 2: Check Google Cloud Console Redirect URIs match production URL Client ID and secret correct OAuth consent screen configured Step 3: Check Environment docker exec fratres-twenty env | grep AUTH_GOOGLE docker exec fratres-twenty env | grep CALENDAR_PROVIDER Step 4: Test OAuth Entry Point curl -v https://yourdomain.com/auth/google | grep Location # Should redirect to accounts.google.com with correct client_id Step 5: Check Database (Sync Issues) -- Check connected accounts SELECT id, handle, provider, "accessToken" IS NOT NULL FROM "connectedAccount" WHERE handle = 'user@example.com'; -- Check sync channels SELECT id, "syncStatus" FROM "messageChannel" WHERE "connectedAccountId" = 'account-id'; Step 6: Check Logs docker logs fratres-twenty --tail 100 | grep -i oauth
Forgetting to rebuild - Source changes don't auto-compile Hardcoding domains - Use allowlists instead Setting httpOnly: true - Frontend can't read tokenPair cookie Losing tokens in validate() - Must return accessToken/refreshToken Not preserving refresh tokens - Google may not return new ones Missing passReqToCallback: true - Can't access request state Not testing with real OAuth - Mock tests miss edge cases Skipping health checks - Container running old code unnoticed
Debug Path: Check oauth-sync.service.ts exists and is called Verify tokens passed through validate() Check scopes include gmail.readonly and calendar.events Verify CALENDAR_PROVIDER_GOOGLE_ENABLED=true Check connected account in database Verify sync channels with syncStatus=ONGOING Common Fix: Return tokens in validate() method
Debug Path: Check google.auth.strategy.ts for hardcoded hd parameter Check google-auth.controller.ts domain validation Check auth.service.ts domain allowlist Check workspaceMetadata.approvedAccessDomains in database Common Fixes: Remove hardcoded hd parameter Update controller/service allowlists Insert domain into database
Debug Path: Check isSingleDomainMode logic in auth.service.ts Check compiled auth.service.js has logic Check computeRedirectURI returns AppPath.Index Check cookie httpOnly attribute Common Fixes: Rebuild backend: npx nx build twenty-server Ensure redirect to dashboard: AppPath.Index Set httpOnly: false on cookie
# Build backend npx nx build twenty-server # Build frontend npx nx build twenty-front # Typecheck npx nx typecheck twenty-server # Restart container docker restart fratres-twenty # Check logs docker logs fratres-twenty --tail 100 # Health check curl -f https://yourdomain.com/healthz # Test OAuth redirect curl -v https://yourdomain.com/auth/google
This skill provides expert-level OAuth knowledge for Twenty CRM covering: Architecture: Twenty's OAuth using Passport strategies Common Issues: 5+ major issues with detailed fixes Automatic Sync: Gmail/Calendar sync after OAuth Token Management: Refresh patterns and error handling Testing: Unit and integration test patterns Configuration: Required environment variables Deployment: Step-by-step checklist Troubleshooting: Systematic workflow Use this skill when: Implementing new OAuth provider Fixing OAuth login issues Setting up automatic sync integration Debugging token refresh failures Configuring domain restrictions Troubleshooting redirect loops
Workflow acceleration for inboxes, docs, calendars, planning, and execution loops.
Largest current source with strong distribution and engagement signals.