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
Best use case
github-oauth-nango-integration is best used when you need a repeatable AI agent workflow instead of a one-off prompt. It is especially useful for teams working in multi. Use when implementing GitHub OAuth + GitHub App authentication with Nango - provides two-connection pattern for user login and repo access with webhook handling
Use when implementing GitHub OAuth + GitHub App authentication with Nango - provides two-connection pattern for user login and repo access with webhook handling
Users should expect a more consistent workflow output, faster repeated execution, and less time spent rewriting prompts from scratch.
Practical example
Example input
Use the "github-oauth-nango-integration" skill to help with this workflow task. Context: Use when implementing GitHub OAuth + GitHub App authentication with Nango - provides two-connection pattern for user login and repo access with webhook handling
Example output
A structured workflow result with clearer steps, more consistent formatting, and an output that is easier to reuse in the next run.
When to use this skill
- Use this skill when you want a reusable workflow rather than writing the same prompt again and again.
When not to use this skill
- Do not use this when you only need a one-off answer and do not need a reusable workflow.
- Do not use it if you cannot install or maintain the related files, repository context, or supporting tools.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/github-oauth-nango-integration/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How github-oauth-nango-integration Compares
| Feature / Agent | github-oauth-nango-integration | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Use when implementing GitHub OAuth + GitHub App authentication with Nango - provides two-connection pattern for user login and repo access with webhook handling
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```bash
# 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.Related Skills
github-release-assistant
Generate bilingual GitHub release documentation (README.md + README.zh.md) from repo metadata and user input, and guide release prep with git add/commit/push. Use when the user asks to write or polish README files, create bilingual docs, prepare a GitHub release, or mentions release assistant/README generation.
github-repo-search
帮助用户搜索和筛选 GitHub 开源项目,输出结构化推荐报告。当用户说"帮我找开源项目"、"搜一下GitHub上有什么"、"找找XX方向的仓库"、"开源项目推荐"、"github搜索"、"/github-search"时触发。
github-actions-docs
Use when users ask how to write, explain, customize, migrate, secure, or troubleshoot GitHub Actions workflows, workflow syntax, triggers, matrices, runners, reusable workflows, artifacts, caching, secrets, OIDC, deployments, custom actions, or Actions Runner Controller, especially when they need official GitHub documentation, exact links, or docs-grounded YAML guidance.
stripe-integration
Implement Stripe payment processing for robust, PCI-compliant payment flows including checkout, subscriptions, and webhooks. Use when integrating Stripe payments, building subscription systems, or implementing secure checkout flows.
paypal-integration
Integrate PayPal payment processing with support for express checkout, subscriptions, and refund management. Use when implementing PayPal payments, processing online transactions, or building e-commerce checkout flows.
payment-integration
Integrate Stripe, PayPal, and payment processors. Handles checkout flows, subscriptions, webhooks, and PCI compliance. Use PROACTIVELY when implementing payments, billing, or subscription features.
hubspot-integration
Expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects. Covers Node.js and Python SDKs. Use when: hubspot, hubspot api, hubspot crm, hubspot integration, contacts api.
github-issue-creator
Convert raw notes, error logs, voice dictation, or screenshots into crisp GitHub-flavored markdown issue reports. Use when the user pastes bug info, error messages, or informal descriptions and wants a structured GitHub issue. Supports images/GIFs for visual evidence.
github-automation
Automate GitHub repositories, issues, pull requests, branches, CI/CD, and permissions via Rube MCP (Composio). Manage code workflows, review PRs, search code, and handle deployments programmatically.
github-actions-templates
Create production-ready GitHub Actions workflows for automated testing, building, and deploying applications. Use when setting up CI/CD with GitHub Actions, automating development workflows, or creating reusable workflow templates.
address-github-comments
Use when you need to address review or issue comments on an open GitHub Pull Request using the gh CLI.
github-elements-tracking
This skill should be used when the user asks to "track work across sessions", "create an epic", "manage issue waves", "post a checkpoint", "claim an issue", "recover from compaction", "coordinate multiple agents", "update memory bank", "store large documents", or mentions GitHub Issues as persistent memory, multi-session work, context survival, agent collaboration, SERENA MCP memory, or project-level context. Provides complete protocols for using GitHub Issues as permanent memory that survives context exhaustion, with integrated SERENA MCP memory bank for project-level context and large document storage.