GitHubOAuth与Nango集成 github-oauth-nango-integration

GitHub OAuth与Nango集成是一种软件开发技能,用于实现GitHub用户身份认证和仓库访问的双重OAuth连接模式。该技能涉及使用Nango平台集成GitHub OAuth应用和GitHub应用,通过分离用户登录和仓库权限管理,构建安全的第三方应用接入方案。关键技术包括OAuth2.0认证、Webhook处理、数据库同步、JWT令牌管理和前端集成。适用于需要GitHub第三方登录、仓库数据访问、代码审查自动化、CI/CD集成等场景的开发者。

后端开发 0 次安装 0 次浏览 更新于 3/1/2026

name: github-oauth-nango-integration description: 使用Nango实现GitHub OAuth + GitHub App身份验证 - 提供用户登录和仓库访问的双连接模式,包含Webhook处理

GitHub OAuth + Nango 集成

概述

实现双连接OAuth模式:一个用于用户身份(github集成),另一个用于仓库访问(github-app-oauth集成)。这种分离使得在通过GitHub App安装维护细粒度仓库权限的同时,实现安全登录。

何时使用

  • 通过Nango设置GitHub OAuth登录
  • 实现GitHub App安装Webhook
  • 协调OAuth用户与GitHub App安装
  • 构建需要用户认证和仓库访问的应用
  • 处理GitHub数据的Nango同步Webhook

为何需要两个连接?

GitHub有两种不同的认证机制,服务于不同目的:

GitHub OAuth应用(github集成)

  • 是什么:用于用户身份的传统OAuth
  • 提供什么:用户资料(姓名、邮箱、头像、GitHub ID)
  • 不提供什么:仓库访问权限
  • 用途:登录,“使用GitHub登录”

GitHub应用(github-app-oauth集成)

  • 是什么:具有细粒度仓库权限的可安装应用
  • 提供什么:访问用户安装该应用的特定仓库
  • 不提供什么:用户身份(它知道安装,但不知道谁在使用)
  • 用途:读取PR、提交、文件;发布评论;Webhook

协调问题

仅OAuth应用:  “用户john@example.com已登录” → 但他们可以访问哪些仓库?
仅GitHub应用: “安装#12345可以访问仓库X” → 但用户是谁?

解决方案:通过用户ID链接的两个独立OAuth流程:

  1. 登录流程 → 用户认证 → 存储用户身份 + nangoConnectionId
  2. 仓库流程 → 同一用户授权应用 → 存储仓库 + 通过ownerId链接

这让你可以回答:“用户john@example.com可以访问仓库X, Y, Z”

快速参考

连接类型 Nango集成 目的 存储位置
用户登录 github 认证、身份 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表 - 存储登录连接
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表 - 存储每个仓库的应用连接
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. 应用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. 认证Webhook处理器

// 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) {
  // 通过Nango获取GitHub用户信息
  const githubUser = await nangoService.getGitHubUser(connectionId);

  // 检查用户是否存在
  const existingUser = await userRepo.findByGitHubId(String(githubUser.id));

  if (existingUser) {
    // 返回用户 - 存储临时连接用于轮询
    await userRepo.update(existingUser.id, {
      incomingConnectionId: connectionId,
    });
    // 稍后删除重复连接
    await nangoService.deleteConnection(connectionId);
  } else {
    // 新用户 - 创建记录
    const user = await userRepo.create({
      githubId: String(githubUser.id),
      githubUsername: githubUser.login,
      email: githubUser.email,
      avatarUrl: githubUser.avatar_url,
      nangoConnectionId: connectionId,
      incomingConnectionId: connectionId,
    });

    // 使用真实用户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('应用OAuth Webhook中没有用户ID');

  const user = await userRepo.findById(userId);
  if (!user) throw new Error('用户未找到');

  try {
    // 获取用户有权访问的仓库
    const repos = await githubService.getInstallationReposRaw(connectionId);

    // 将仓库同步到数据库
    for (const repo of repos) {
      await repoRepo.upsert({
        githubRepoId: String(repo.id),
        fullName: repo.full_name,
        ownerId: user.id,
        nangoConnectionId: connectionId,
      });
    }

    // 触发Nango同步
    await nangoService.triggerSync(connectionId, ['pull-requests', 'commits']);

  } catch (error) {
    if (error.status === 403) {
      // 组织批准待处理
      await userRepo.update(user.id, {
        pendingInstallationRequest: new Date(),
      });
      return true; // 优雅降级
    }
    throw error;
  }

  return true;
}

6. 带签名验证的Webhook路由

// webhooks.ts
app.post('/api/webhooks/nango', async (c) => {
  const signature = c.req.header('X-Nango-Signature');
  const body = await c.req.text();

  // 验证签名
  const expectedSignature = createHmac('sha256', NANGO_SECRET_KEY)
    .update(body)
    .digest('hex');

  if (signature !== expectedSignature) {
    return c.json({ error: '无效签名' }, 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. 前端集成

// 登录流程
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') {
        // 轮询完成状态
        const result = await pollForAuth(event.payload.connectionId);
        if (result.ready) {
          localStorage.setItem('token', result.token);
          navigate('/dashboard');
        }
      }
    },
  });
}

// 仓库连接流程(登录后)
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('等待组织管理员批准...');
        } else if (result.ready) {
          setRepos(result.repos);
        }
      }
    },
  });
}

完整流程示意图

用户登录:
  前端 → GET /auth/nango-session
           → Nango.openConnectUI(sessionToken)
           → 用户授权GitHub
           → Nango Webhook (类型: auth, providerConfigKey: github)
           → 后端创建/更新用户
           → 前端轮询 /auth/nango/status/:connectionId
           → 返回JWT令牌

仓库连接(已认证):
  前端 → GET /auth/github-app-session (带JWT)
           → Nango.openConnectUI(sessionToken)
           → 用户授权GitHub应用
           → Nango Webhook (类型: auth, providerConfigKey: github-app-oauth)
           → 后端获取仓库,同步到数据库
           → 前端轮询 /auth/github-app/status/:connectionId
           → 返回仓库列表

数据同步(后台):
  Nango → 计划任务每4小时同步
        → Webhook (类型: sync, 模型: GithubPullRequest)
        → 后端处理增量更新

常见错误

错误 修复方法
对登录和仓库访问使用相同连接 使用两个集成:github用于登录,github-app-oauth用于仓库
未处理组织批准待处理 检查403错误,设置pendingInstallationRequest标志
连接中缺少endUser.id 始终在createConnectSession中设置,用户创建后更新
轮询错误的连接ID 为返回用户单独存储incomingConnectionId
未验证Webhook签名 始终使用HMAC-SHA256验证X-Nango-Signature
保留重复连接 返回用户认证后删除临时连接

环境变量

# 必需
NANGO_SECRET_KEY=你的nango密钥
JWT_SECRET=你的jwt密钥-至少32字符
DATABASE_URL=postgres://...

# 在Nango仪表板中配置
# - github集成:OAuth应用凭据
# - github-app-oauth集成:GitHub应用凭据

Nango仪表板设置

  1. 创建github集成(用于登录):

    • 类型:OAuth2
    • 客户端ID/密钥:来自GitHub OAuth应用
    • 范围:read:user, user:email
  2. 创建github-app-oauth集成(用于仓库):

    • 类型:GitHub应用
    • 应用ID、私钥、客户端ID/密钥:来自GitHub应用
    • 范围:repo, pull_request
  3. 配置Webhook URLhttps://你的域名/api/webhooks/nango

  4. 启用同步pull-requests, commits, issues