本文へ移動
cccskills
無料GitHub で公開

github-oauth-nango-integration

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

インストール方法を見る

含まれるファイル(1)

  • SKILL.md13.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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 TypeNango IntegrationPurposeStored In
User LogingithubAuthentication, identityusers.nangoConnectionId
Repo Accessgithub-app-oauthPR operations, file accessrepos.nangoConnectionId
FlowEndpointWebhook Type
LoginGET /auth/nango-sessionauth + github
Repo ConnectGET /auth/github-app-sessionauth + github-app-oauth
Data SyncN/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

MistakeFix
Using same connection for login and repo accessUse two integrations: github for login, github-app-oauth for repos
Not handling org approval pendingCheck for 403 error, set pendingInstallationRequest flag
Missing endUser.id in connectionAlways set in createConnectSession, update after user creation
Polling wrong connection IDStore incomingConnectionId separately for returning users
Not verifying webhook signatureAlways verify X-Nango-Signature with HMAC-SHA256
Keeping duplicate connectionsDelete 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.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use when you need Codex to coordinate multiple agents through Agent Relay for peer-to-peer messaging, lead/worker handoffs, or shared status tracking across sub-agents and terminals.

日本語の概要は準備中です。原文の説明を表示しています。

AgentWorkforce/relay8732026年10月11日 更新

Use when testing web applications with visual verification - automates Chrome browser interactions, element selection, and screenshot capture for confirming UI functionality

日本語の概要は準備中です。原文の説明を表示しています。

AgentWorkforce/relay8732026年10月11日 更新

Use when coordinating multiple AI agents with Agent Relay's workflow engine and need to pick the right orchestration pattern - covers the 10 core patterns (fan-out, pipeline, hub-spoke, consensus, mesh, handoff, cascade, dag, debate, hierarchical) plus 14 specialized ones, with decision framework and accurate SDK/YAML examples.

日本語の概要は準備中です。原文の説明を表示しています。

AgentWorkforce/relay8732026年10月11日 更新

Use when creating Agent Skills packages (SKILL.md format) for Codex CLI, GitHub Copilot, or Amp - provides the agentskills.io specification with frontmatter constraints, directory structure, and validation rules

日本語の概要は準備中です。原文の説明を表示しています。

AgentWorkforce/relay8732026年10月11日 更新

Use when creating or improving Claude Code agents. Expert guidance on agent file structure, frontmatter, persona definition, tool access, model selection, and validation against schema.

日本語の概要は準備中です。原文の説明を表示しています。

AgentWorkforce/relay8732026年10月11日 更新

Use when creating or publishing Claude Code hooks - covers executable format, event types, JSON I/O, exit codes, security requirements, and PRPM package structure

日本語の概要は準備中です。原文の説明を表示しています。

AgentWorkforce/relay8732026年10月11日 更新

AgentWorkforce のスキルをすべて見る

このスキルの問題を報告する