jpskill.com
🛠️ 開発・MCP コミュニティ

github-oauth-nango-integration

Nangoを使ってGitHub OAuthとGitHub App認証を実装し、ユーザーログインとリポジトリへのアクセスを、Webhook処理を含めて二つの接続パターンで実現するSkill。

📜 元の英語説明(参考)

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

🇯🇵 日本人クリエイター向け解説

一言でいうと

Nangoを使ってGitHub OAuthとGitHub App認証を実装し、ユーザーログインとリポジトリへのアクセスを、Webhook処理を含めて二つの接続パターンで実現するSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o github-oauth-nango-integration.zip https://jpskill.com/download/17078.zip && unzip -o github-oauth-nango-integration.zip && rm github-oauth-nango-integration.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17078.zip -OutFile "$d\github-oauth-nango-integration.zip"; Expand-Archive "$d\github-oauth-nango-integration.zip" -DestinationPath $d -Force; ri "$d\github-oauth-nango-integration.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して github-oauth-nango-integration.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → github-oauth-nango-integration フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

[Skill 名] github-oauth-nango-integration

GitHub OAuth + Nango 連携

概要

二重接続 OAuth パターンを実装します。1つはユーザー ID 用(github 連携)、もう1つはリポジトリアクセス用(github-app-oauth 連携)です。この分離により、セキュアなログインが可能になり、GitHub App のインストールを通じてきめ細かいリポジトリ権限を維持できます。

どのような時に使うか

  • Nango 経由で GitHub OAuth ログインを設定する
  • GitHub App インストール Webhook を実装する
  • OAuth ユーザーと GitHub App インストールを調整する
  • ユーザー認証とリポジトリアクセスの両方が必要なアプリを構築する
  • GitHub データ用の Nango 同期 Webhook を処理する

なぜ2つの接続が必要なのか?

GitHub には、異なる目的を果たす2つの異なる認証メカニズムがあります。

GitHub OAuth App (github integration)

  • これは何か: ユーザー ID 用の従来の OAuth
  • 何が得られるか: ユーザープロファイル(名前、メールアドレス、アバター、GitHub ID)
  • 何が得られないか: リポジトリへのアクセス
  • 用途: ログイン、「GitHub でサインイン」

GitHub App (github-app-oauth integration)

  • これは何か: きめ細かいリポジトリ権限を持つインストール可能なアプリ
  • 何が得られるか: ユーザーがインストールした特定のリポジトリへのアクセス
  • 何が得られないか: ユーザー ID(インストールはわかるが、誰が使用しているかはわからない)
  • 用途: PR、コミット、ファイルの読み取り、コメントの投稿、Webhook

調整の問題

OAuth App 単独: "ユーザー john@example.com がログインしました" → しかし、どのリポジトリにアクセスできるのか?
GitHub App 単独: "インストール #12345 はリポジトリ X へのアクセス権を持っています" → しかし、ユーザーは誰なのか?

解決策: ユーザー ID でリンクされた2つの別々の OAuth フロー:

  1. ログインフロー → ユーザーが認証 → ユーザー ID + nangoConnectionId を保存
  2. リポジトリフロー → 同じユーザーがアプリを承認 → リポジトリを保存 + ownerId 経由でリンク

これにより、「ユーザー john@example.com はリポジトリ X、Y、Z にアクセスできます」という質問に答えることができます。

クイックリファレンス

接続タイプ Nango 連携 目的 保存場所
ユーザーログイン github 認証、ID users.nangoConnectionId
リポジトリアクセス github-app-oauth PR 操作、ファイルアクセス repos.nangoConnectionId
フロー エンドポイント Webhook タイプ
ログイン GET /auth/nango-session auth + github
リポジトリ接続 GET /auth/github-app-session auth + github-app-oauth
データ同期 N/A (スケジュール) sync

実装

1. データベーススキーマ

// users table - ログイン接続を保存
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'),      // 永続的なログイン接続
  incomingConnectionId: text('incoming_connection_id'), // 一時的なポーリング接続
  pendingInstallationRequest: timestamp('pending_installation_request'), // 組織の承認待ち
});

// repos table - リポジトリごとのアプリ接続を保存
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'),  // このリポジトリのアプリ接続
});

// github_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.ts
export const NANGO_INTEGRATION = {
  GITHUB_USER: 'github',              // ログインのみ
  GITHUB_APP_OAUTH: 'github-app-oauth' // リポジトリアクセス
} as const;

3. ログインフローのルート

// GET /auth/nango-session - ログイン OAuth セッションを作成
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 - ログイン完了をポーリング
app.get('/auth/nango/status/:connectionId', async (c) => {
  const { connectionId } = c.req.param();

  // この受信接続を持つユーザーが存在するか確認
  const user = await userRepo.findByIncomingConnectionId(connectionId);
  if (!user) {
    return c.json({ ready: false });
  }

  // JWT を発行して返す
  const token = authService.issueToken(user);
  await userRepo.clearIncomingConnectionId(user.id);

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

4. App OAuth フローのルート

// GET /auth/github-app-session - アプリ OAuth セッションを作成 (認証済み)
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 - リポジトリ同期をポーリング
app.get('/auth/github-app/status/:connectionId', authMiddleware, async (c) => {
  const user = c.get('user');

  // 組織の承認待ちを確認
  if (user.pendingInstallationRequest) {
    return c.json({ ready: false, pendingApproval: true });
  }

  // リポジトリが同期されたか確認
  const repos = await repoRepo.findByOwnerId(user.id);
  return c.json({ ready: repos.length > 0, repos });
});

5. Auth Webhook ハンドラー

// auth-webhook-service.ts
export 
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

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.