Claude Code Plugins

Community-maintained marketplace

Feedback

github-oauth-nango-integration

@AgentWorkforce/relay
8
0

Use when implementing GitHub OAuth + GitHub App authentication with Nango - provides two-connection pattern for user login and repo access with webhook handling

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name github-oauth-nango-integration
description Use when implementing GitHub OAuth + GitHub App authentication with Nango - provides two-connection pattern for user login and repo access with webhook handling

GitHub OAuth + Nango Integration

Overview

Implements dual-connection OAuth pattern: one for user identity (github integration), another for repository access (github-app-oauth integration). This separation enables secure login while maintaining granular repo permissions through GitHub App installations.

When to Use

  • Setting up GitHub OAuth login via Nango
  • Implementing GitHub App installation webhooks
  • Reconciling OAuth users with GitHub App installations
  • Building apps that need both user auth and repo access
  • Handling Nango sync webhooks for GitHub data

Why Two Connections?

GitHub has two different authentication mechanisms that serve different purposes:

GitHub OAuth App (github integration)

  • What it is: Traditional OAuth for user identity
  • What it gives you: User profile (name, email, avatar, GitHub ID)
  • What it DOESN'T give you: Access to repositories
  • Use for: Login, "Sign in with GitHub"

GitHub App (github-app-oauth integration)

  • What it is: Installable app with granular repo permissions
  • What it gives you: Access to specific repos the user installed it on
  • What it DOESN'T give you: User identity (it knows the installation, not who's using it)
  • Use for: Reading PRs, commits, files; posting comments; webhooks

The Reconciliation Problem

OAuth App alone:  "User john@example.com logged in" → but which repos can they access?
GitHub App alone: "Installation #12345 has access to repo X" → but who is the user?

Solution: Two separate OAuth flows linked by user ID:

  1. Login flow → User authenticates → Store user identity + nangoConnectionId
  2. Repo flow → Same user authorizes app → Store repos + link via ownerId

This lets you answer: "User john@example.com can access repos X, Y, Z"

Quick Reference

Connection Type Nango Integration Purpose Stored In
User Login github Authentication, identity users.nangoConnectionId
Repo Access github-app-oauth PR operations, file access repos.nangoConnectionId
Flow Endpoint Webhook Type
Login GET /auth/nango-session auth + github
Repo Connect GET /auth/github-app-session auth + github-app-oauth
Data Sync N/A (scheduled) sync

Implementation

1. Database Schema

// users table - stores login connection
export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  githubId: text('github_id').unique().notNull(),
  githubUsername: text('github_username').notNull(),
  email: text('email'),
  avatarUrl: text('avatar_url'),
  nangoConnectionId: text('nango_connection_id'),      // Permanent login connection
  incomingConnectionId: text('incoming_connection_id'), // Temp polling connection
  pendingInstallationRequest: timestamp('pending_installation_request'), // Org approval wait
});

// repos table - stores per-repo app connection
export const repos = pgTable('repos', {
  id: uuid('id').primaryKey().defaultRandom(),
  githubRepoId: text('github_repo_id').unique().notNull(),
  fullName: text('full_name').notNull(),
  installationId: uuid('installation_id').references(() => githubInstallations.id),
  ownerId: uuid('owner_id').references(() => users.id),
  nangoConnectionId: text('nango_connection_id'),  // App connection for this repo
});

// github_installations - tracks app installations
export const githubInstallations = pgTable('github_installations', {
  id: uuid('id').primaryKey().defaultRandom(),
  installationId: text('installation_id').unique().notNull(),
  accountType: text('account_type'),   // 'user' | 'organization'
  accountLogin: text('account_login'),
  installedById: uuid('installed_by_id').references(() => users.id),
});

2. Constants

// constants.ts
export const NANGO_INTEGRATION = {
  GITHUB_USER: 'github',              // Login only
  GITHUB_APP_OAUTH: 'github-app-oauth' // Repo access
} as const;

3. Login Flow Routes

// GET /auth/nango-session - Create login OAuth session
app.get('/auth/nango-session', async (c) => {
  const tempUserId = randomUUID();

  const { sessionToken } = await nangoClient.createConnectSession({
    end_user: { id: tempUserId },
    allowed_integrations: [NANGO_INTEGRATION.GITHUB_USER],
  });

  return c.json({ sessionToken, tempUserId });
});

// GET /auth/nango/status/:connectionId - Poll login completion
app.get('/auth/nango/status/:connectionId', async (c) => {
  const { connectionId } = c.req.param();

  // Check if user exists with this incoming connection
  const user = await userRepo.findByIncomingConnectionId(connectionId);
  if (!user) {
    return c.json({ ready: false });
  }

  // Issue JWT and return
  const token = authService.issueToken(user);
  await userRepo.clearIncomingConnectionId(user.id);

  return c.json({ ready: true, token, user });
});

4. App OAuth Flow Routes

// GET /auth/github-app-session - Create app OAuth session (authenticated)
app.get('/auth/github-app-session', authMiddleware, async (c) => {
  const user = c.get('user');

  const { sessionToken } = await nangoClient.createConnectSession({
    end_user: { id: user.id, email: user.email },
    allowed_integrations: [NANGO_INTEGRATION.GITHUB_APP_OAUTH],
  });

  return c.json({ sessionToken });
});

// GET /auth/github-app/status/:connectionId - Poll repo sync
app.get('/auth/github-app/status/:connectionId', authMiddleware, async (c) => {
  const user = c.get('user');

  // Check for pending org approval
  if (user.pendingInstallationRequest) {
    return c.json({ ready: false, pendingApproval: true });
  }

  // Check if repos synced
  const repos = await repoRepo.findByOwnerId(user.id);
  return c.json({ ready: repos.length > 0, repos });
});

5. Auth Webhook Handler

// auth-webhook-service.ts
export async function handleAuthWebhook(payload: NangoAuthWebhook): Promise<boolean> {
  const { connectionId, providerConfigKey, endUser } = payload;

  if (providerConfigKey === NANGO_INTEGRATION.GITHUB_USER) {
    return handleLoginWebhook(connectionId, endUser);
  }

  if (providerConfigKey === NANGO_INTEGRATION.GITHUB_APP_OAUTH) {
    return handleAppOAuthWebhook(connectionId, endUser);
  }

  return false;
}

async function handleLoginWebhook(connectionId: string, endUser?: EndUser) {
  // Fetch GitHub user info via Nango
  const githubUser = await nangoService.getGitHubUser(connectionId);

  // Check if user exists
  const existingUser = await userRepo.findByGitHubId(String(githubUser.id));

  if (existingUser) {
    // Returning user - store temp connection for polling
    await userRepo.update(existingUser.id, {
      incomingConnectionId: connectionId,
    });
    // Delete duplicate connection later
    await nangoService.deleteConnection(connectionId);
  } else {
    // New user - create record
    const user = await userRepo.create({
      githubId: String(githubUser.id),
      githubUsername: githubUser.login,
      email: githubUser.email,
      avatarUrl: githubUser.avatar_url,
      nangoConnectionId: connectionId,
      incomingConnectionId: connectionId,
    });

    // Update connection with real user ID
    await nangoService.patchConnection(connectionId, {
      end_user: { id: user.id, email: user.email },
    });
  }

  return true;
}

async function handleAppOAuthWebhook(connectionId: string, endUser?: EndUser) {
  const userId = endUser?.id;
  if (!userId) throw new Error('No user ID in app OAuth webhook');

  const user = await userRepo.findById(userId);
  if (!user) throw new Error('User not found');

  try {
    // Fetch repos user has access to
    const repos = await githubService.getInstallationReposRaw(connectionId);

    // Sync repos to database
    for (const repo of repos) {
      await repoRepo.upsert({
        githubRepoId: String(repo.id),
        fullName: repo.full_name,
        ownerId: user.id,
        nangoConnectionId: connectionId,
      });
    }

    // Trigger Nango syncs
    await nangoService.triggerSync(connectionId, ['pull-requests', 'commits']);

  } catch (error) {
    if (error.status === 403) {
      // Org approval pending
      await userRepo.update(user.id, {
        pendingInstallationRequest: new Date(),
      });
      return true; // Graceful degradation
    }
    throw error;
  }

  return true;
}

6. Webhook Route with Signature Verification

// webhooks.ts
app.post('/api/webhooks/nango', async (c) => {
  const signature = c.req.header('X-Nango-Signature');
  const body = await c.req.text();

  // Verify signature
  const expectedSignature = createHmac('sha256', NANGO_SECRET_KEY)
    .update(body)
    .digest('hex');

  if (signature !== expectedSignature) {
    return c.json({ error: 'Invalid signature' }, 401);
  }

  const payload = JSON.parse(body);

  if (payload.type === 'auth') {
    const success = await handleAuthWebhook(payload);
    return c.json({ success });
  }

  if (payload.type === 'sync') {
    await processSyncWebhook(payload);
    return c.json({ success: true });
  }

  return c.json({ success: false });
});

7. Frontend Integration

// Login flow
async function handleLogin() {
  const res = await fetch('/api/auth/nango-session');
  const { sessionToken } = await res.json();

  const nango = new Nango({ connectSessionToken: sessionToken });

  nango.openConnectUI({
    onEvent: async (event) => {
      if (event.type === 'connect') {
        // Poll for completion
        const result = await pollForAuth(event.payload.connectionId);
        if (result.ready) {
          localStorage.setItem('token', result.token);
          navigate('/dashboard');
        }
      }
    },
  });
}

// Repo connection flow (after login)
async function handleConnectRepos() {
  const res = await fetch('/api/auth/github-app-session', {
    headers: { Authorization: `Bearer ${token}` },
  });
  const { sessionToken } = await res.json();

  const nango = new Nango({ connectSessionToken: sessionToken });

  nango.openConnectUI({
    onEvent: async (event) => {
      if (event.type === 'connect') {
        const result = await pollForRepos(event.payload.connectionId);
        if (result.pendingApproval) {
          showMessage('Waiting for org admin approval...');
        } else if (result.ready) {
          setRepos(result.repos);
        }
      }
    },
  });
}

Complete Flow Diagram

USER LOGIN:
  Frontend → GET /auth/nango-session
           → Nango.openConnectUI(sessionToken)
           → User authorizes GitHub
           → Nango webhook (type: auth, providerConfigKey: github)
           → Backend creates/updates user
           → Frontend polls /auth/nango/status/:connectionId
           → Returns JWT token

REPO CONNECTION (authenticated):
  Frontend → GET /auth/github-app-session (with JWT)
           → Nango.openConnectUI(sessionToken)
           → User authorizes GitHub App
           → Nango webhook (type: auth, providerConfigKey: github-app-oauth)
           → Backend fetches repos, syncs to DB
           → Frontend polls /auth/github-app/status/:connectionId
           → Returns repos list

DATA SYNCS (background):
  Nango → Scheduled sync every 4 hours
        → Webhook (type: sync, model: GithubPullRequest)
        → Backend processes incremental updates

Common Mistakes

Mistake Fix
Using same connection for login and repo access Use two integrations: github for login, github-app-oauth for repos
Not handling org approval pending Check for 403 error, set pendingInstallationRequest flag
Missing endUser.id in connection Always set in createConnectSession, update after user creation
Polling wrong connection ID Store incomingConnectionId separately for returning users
Not verifying webhook signature Always verify X-Nango-Signature with HMAC-SHA256
Keeping duplicate connections Delete temp connection after returning user authenticates

Environment Variables

# Required
NANGO_SECRET_KEY=your-nango-secret-key
JWT_SECRET=your-jwt-secret-min-32-chars
DATABASE_URL=postgres://...

# Configure in Nango Dashboard
# - github integration: OAuth App credentials
# - github-app-oauth integration: GitHub App credentials

Nango Dashboard Setup

  1. Create github integration (for login):

    • Type: OAuth2
    • Client ID/Secret: From GitHub OAuth App
    • Scopes: read:user, user:email
  2. Create github-app-oauth integration (for repos):

    • Type: GitHub App
    • App ID, Private Key, Client ID/Secret: From GitHub App
    • Scopes: repo, pull_request, etc.
  3. Configure webhook URL: https://your-domain/api/webhooks/nango

  4. Enable syncs: pull-requests, commits, issues, etc.