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
DeepseekModel
Curated skill
Quality Excellent · 90
v1.0.0
Get
https://deepseekmodel.com/api/download.php?id=agentworkforce-relay-claude-skills-github-oauth-nango-integration-skill-md&format=skill
Download .skill
Standard format with system_prompt and model_config, ready for any agent framework
The actual content of the system_prompt field in the .skill file.
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: 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
Keywords that activate this skill. Click one to copy it.
This skill does not provide trigger words.
The downloaded .skill package contains the following fields.
| Field | Description |
|---|---|
| format | Format tag (skill/v1) |
| skill_id | Unique skill ID |
| name | Skill name |
| version | Version |
| description | Description |
| category | Categories (array) |
| trigger_words | Trigger words |
| tags | Tags |
| source | Source |
| source_url | Source URL (this page) |
| exported_at | Exported at (set per download) |
| system_prompt | System prompt body |
| model_config | Model config: provider / model / temperature / max_tokens / top_p |
| examples | Examples |
| install_guide | Import guide for Coze / Dify / Claude / custom frameworks |
The same skill can be exported in different platform formats.