avirweb-twenty-oauth-mastery

ClawSkills 作者 clawskills

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install clawskills:clawskills~avirweb-twenty-oauth-mastery
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/clawskills%3Aclawskills~avirweb-twenty-oauth-mastery/file -o avirweb-twenty-oauth-mastery.md
# Twenty CRM OAuth Mastery Skill

**Author**: Generated from extensive OAuth debugging sessions in OpenCode  
**Last Updated**: 2026-02-08  
**Version**: 1.0

---

## Skill Metadata

```yaml
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
```

---

## Quick Start

### When to Use This Skill

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  

### Quick Reference for Common Issues

| Issue | File to Check | Quick Fix |
|-------|---------------|-----------|
| Redirect loop | `auth.service.ts` | Rebuild: `npx nx build twenty-server` |
| .co domain blocked | `google-auth.controller.ts` | Add to allowlist: `['company.com', 'company.co']` |
| Sync not starting | `google.auth.strategy.ts` | Return tokens in validate() |
| Cookie not readable | Controller cookie settings | Set `httpOnly: false` |
| Infinite loop | `SignInUpGlobalScopeFormEffect.tsx` | Track processed token signatures |

---

## Core Knowledge

### 1. Twenty CRM OAuth Architecture

**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
```

---

### 2. Critical Code Patterns

#### Passport Strategy Pattern (MUST FOLLOW)

```typescript
@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

---

### 3. Common Issues & Solutions

#### Issue 1: Redirect Loop After OAuth

**Symptoms**: OAuth completes but user stuck on welcome page

**Root Causes**:

1. **Backend not compiled**: Source has fix, container running old JavaScript
  
   **Fix**:
   ```bash
   npx nx build twenty-server
   docker restart fratres-twenty
   ```

2. **Missing isSingleDomainMode**: Redirect logic not in compiled code

   **Check**:
   ```bash
   docker exec fratres-twenty cat /app/dist/engine/core-modules/auth/services/auth.service.js | grep isSingleDomainMode
   ```

3. **Cookie domain mismatch**: Cookie not accessible

   **Fix**:
   ```typescript
   // 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**:

1. **Google Strategy** (`google.auth.strategy.ts`):
   ```typescript
   // ❌ WRONG - Hardcoded
   hd: 'company.com'
   
   // ✅ CORRECT - Remove hd parameter
   // (no hd parameter)
   ```

2. **Controller** (`google-auth.controller.ts`):
   ```typescript
   // ❌ 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`
     );
   }
   ```

3. **Database** (`workspaceMetadata` table):
   ```sql
   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**:
```typescript
// 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**:

1. Verify `auth.service.ts` calls `oauthSyncService.setupSyncForOAuthUser()` after login
2. Verify tokens are passed to sync service
3. Check Google scopes include `gmail.readonly` and `calendar.events`
4. 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**:
```typescript
// 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);
  }
}, []);
```

---

### 4. OAuth Sync Integration

**When to Use**: Users should have Gmail/Calendar auto-connected after OAuth login

**Implementation**:

1. **Create OAuthSyncService**:
   ```typescript
   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
   }
   ```

2. **Integrate into AuthService**:
   ```typescript
   // 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)