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本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
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
$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. 下の青いボタンを押して
github-oauth-nango-integration.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
github-oauth-nango-integrationフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
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 フロー:
- ログインフロー → ユーザーが認証 → ユーザー ID +
nangoConnectionIdを保存 - リポジトリフロー → 同じユーザーがアプリを承認 → リポジトリを保存 +
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:
- Login flow → User authenticates → Store user identity +
nangoConnectionId - 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
-
Create
githubintegration (for login):- Type: OAuth2
- Client ID/Secret: From GitHub OAuth App
- Scopes:
read:user,user:email
-
Create
github-app-oauthintegration (for repos):- Type: GitHub App
- App ID, Private Key, Client ID/Secret: From GitHub App
- Scopes:
repo,pull_request, etc.
-
Configure webhook URL:
https://your-domain/api/webhooks/nango -
Enable syncs:
pull-requests,commits,issues, etc.