如何使用 GitHub 帐户实现社交登录?

Nat*_*les 25 authentication github github-api oauth-2.0

我的雇主要求我使用用户的 GitHub 帐户为我们的 Web 应用程序实现一个登录系统。我在网上查了一下,但无法找到如何使用 GitHub 帐户(而不是 Facebook 或 Google 帐户)执行此操作的明确说明。

Nat*_*les 42

我刚刚花了大约一周的时间来弄清楚如何做到这一点,所以我想我应该写一个解释来节省未来开发人员的时间。

\n

简短的(呃)答案

\n

您需要遵循GitHub 文档中的本指南(“授权 OAuth 应用程序”),并添加一些内容(如下所述)以使其能够作为用户身份验证的方法。

\n
    \n
  • 我实现了“ Web 应用程序流程”,以便我们的应用程序部署在我们公司的服务器上(我们可以将公司的 GitHub 应用程序的“客户端机密”保密),以及“设备流程 “当我们的应用程序将部署在客户的计算机上时(因为在这种情况下,我们将无法保守我们的“客户秘密”)。
  • \n
  • GitHub 的指南没有提及以下步骤(因为该指南并不是专门用于实现社交登录),但为了让社交登录正常工作,我还执行了以下操作:\n
      \n
    1. 我创建了一个users数据库表,其想法是用于登录的每个 GitHub 帐户都会在此表中拥有自己的相应行。\n\n
    2. \n
    3. 我创建了一个oauth_tokens数据库表来存储我们的后端从 GitHub 接收的所有 GitHub 访问令牌的副本。\n\n
    4. \n
    5. 我让后端向前端(用户)发送 GitHub 访问令牌,以便其将未来的请求作为其身份验证机制。\n
        \n
      • localStorage如果您希望用户即使在关闭登录的浏览器选项卡后仍保持登录状态,则前端应存储令牌。
      • \n
      \n
    6. \n
    7. 我在后端添加了中间件,对于每个传入请求,它都会在数据库中查找提供的访问令牌以查看它是否已过期,如果是,则尝试刷新它。如果成功刷新令牌,它将正常处理请求,并将新的访问令牌包含在前端正在关注的自定义响应标头中(我将其命名为 )x-updated-access-token。如果刷新令牌失败,它将中止请求并发送 401 响应,前端将其视为将用户重定向到登录页面的信号。\n
        \n
      • 将您的应用程序设置为仅允许未过期的访问令牌作为身份验证方法,这样用户就可以从 GitHub.com 的设置页面远程退出应用程序。
      • \n
      \n
    8. \n
    9. 我添加了前端代码来处理 GitHub 访问令牌的保存/更新/删除,无论是到 localStorage 还是从 localStorage 以及到后端的所有请求,以及重定向到 /login 路由(如果前端) end 找不到“access_token”localStorage 变量集。\n\n
    10. \n
    11. [更新] - 我的主管最终要求我更改代码的工作方式(根据我在上面的步骤中描述的内容),以便我们不使用数据库来存储和验证我们创建的访问令牌,而是向用户颁发我们的访问令牌自己签名的包含访问令牌的 JWT,并且用户通过每个 API 请求向我们发送该 JWT,而不是 GitHub 访问令牌本身。因为它是签名的 JWT(由我们使用私钥签名),所以我们可以确定它包含的访问令牌是用户登录我们的 Web 应用程序时生成的。我已经很久没有写代码了,无法写出详细信息,但我想在这里添加一个注释,以便人们至少知道可以通过这种方式实现事情。这种方法在概念上类似于 OpenID Connect,其中用户拥有一个令牌,可以检查该令牌由谁颁发。
    12. \n
    \n
  • \n
\n

更多信息

\n
    \n
  • 澄清一些词汇:这里的目标是通过社交登录进行用户身份验证。社交登录是一种单点登录
  • \n
  • 您应该了解的第一件事是,截至我撰写本文时,GitHub 尚未将自己定位为 Facebook 和 Google 那样的社交登录提供商。\n
      \n
    • FacebookGoogle都开发了特殊的 JavaScript 库,您可以使用它们来实现社交登录,而无需编写任何(?)特定于登录的后端代码。GitHub 没有这样的库,据我所知,第三方甚至不可能开发这样的库,因为 GitHub 的 API 不提供使这样的库成为可能所需的功能(特别是,他们似乎既不支持“隐式流”也不支持 OpenID Connect)。
    • \n
    \n
  • \n
  • 接下来您应该了解的是,截至我撰写本文时,GitHub 的 API 不支持使用 OpenID Connect 来实现使用 GitHub 帐户的社交登录。\n
      \n
    • 当我开始研究如何实现社交登录时,我对最新的在线指南说OpenID Connect是当前最佳实践方式感到困惑。如果您使用的身份提供商(例如 GitHub)支持它(即他们的 API 可以返回 OpenID Connect ID 令牌),则这是事实。我联系了 GitHub,他们确认他们的 API 目前无法从我们需要请求的端点返回 OpenID Connect ID 令牌,尽管他们似乎支持在其他地方使用 OpenID Connect 令牌。他们的 API
    • \n
    \n
  • \n
  • 因此,Web 应用程序通常希望使用 GitHub 帐户实现社交登录的方式是使用大多数网站在 OpenID Connect 之前使用的 OAuth 2.0 流程,大多数在线资源将其称为“授权代码流程”,但 GitHub 的文档称为“ Web 应用程序流程”。它同样安全,但需要比其他方法更多的工作/代码才能正确实现。结论是,使用 GitHub 实现社交登录将比使用 Facebook 或 Google 等身份提供商花费更多的时间,而 Facebook 或 Google 已经简化了开发人员的流程
  • \n
  • 如果您(或您的老板)仍然想使用 GitHub 进行社交登录,即使了解它需要更多时间,那么值得花一些时间观看一些关于 OAuth 2.0 流程如何工作、为什么使用 OpenID 的解释Connect 已开发(尽管 GitHub 似乎不支持它),并熟悉一些关键技术术语,因为这将使您更容易理解 GitHub 指南。\n
      \n
    • OAuth 2.0\n
        \n
      • 我发现 OAuth 2.0 的最佳解释是 Okta 的:An Illustrated Guide to OAuth and OpenID Connect \n
          \n
        • 最重要的技术术语:\n
            \n
          • 身份提供商 - 这是 GitHub、Facebook、Google 等。
          • \n
          • 客户端 - 这是您的应用程序;具体来说,是应用程序的后端部分。
          • \n
          • 授权代码 - “客户端向[身份提供商]提供的短暂临时代码,以换取访问令牌。”
          • \n
          • 访问令牌:这可以让您的应用程序向 GitHub 请求有关用户的信息。
          • \n
          \n
        • \n
        \n
      • \n
      • 您可能还会发现此图很有帮助:\n
          \n
        • 在此输入图像描述
        • \n
        • 幻灯片标题是“OIDC 授权代码流程”,但相同的流程用于非 OIDC OAuth 2.0 授权代码流程,唯一的区别是步骤 10 不返回 ID 令牌,只返回访问令牌和刷新令牌。
        • \n
        • 第 11 步以绿色突出显示这一事实并不重要;这正是演示者想要在这张特定幻灯片中强调的步骤。
        • \n
        • 该图将“身份提供者”和“资源服务器”显示为单独的实体,这可能会令人困惑。在我们的例子中,它们都是 GitHub 的 API;“身份提供者”是 GitHub API 的一部分,它为我们获取访问令牌,而“资源服务器”是 GitHub API 的一部分,我们可以将访问令牌发送到它以代表我们执行操作用户(例如询问他们的个人资料)。
        • \n
        • 来源:OAuth 2.0 和 OpenID Connect 简介(PowerPoint 幻灯片)- PragmaticWebSecurity.com
        • \n
        \n
      • \n
      \n
    • \n
    • OpenID 连接 (OIDC)\n
        \n
      • 再说一次,GitHub 似乎不支持这一点,但网上提到了很多,所以你可能会好奇知道这里发生了什么/它解决了什么问题/为什么 GitHub 不支持它。
      • \n
      • 关于为什么引入 OpenID Connect 以及为什么它比普通 OAuth 2.0 更适合进行身份验证,我看到的最好解释是我自己对 2012 年 ThreadSafe 博客文章的总结:为什么使用 OpenID Connect 而不是普通 OAuth2?.\n
          \n
        • 简而言之,在 OIDC 存在之前,纯前端社交登录 JavaScript 库(例如 Facebook 的)使用的是普通 OAuth 2.0,但此方法容易受到攻击,恶意 Web 应用程序可能会让用户登录其网站(例如,使用 Facebook 登录),然后使用生成的 (Facebook) 访问令牌在接受该 (Facebook) 访问令牌作为身份验证方法的任何其他网站上模拟该用户。OIDC 可防止该漏洞利用。\n\n
        • \n
        • 但 GitHub 没有纯前端社交登录 JavaScript 库,因此不需要支持 OpenID Connect 来解决该漏洞。您只需要确保应用程序的后端跟踪它生成的 GitHub 访问令牌,而不是仅仅信任它收到的任何有效的 GitHub 访问令牌。
        • \n
        \n
      • \n
      \n
    • \n
    \n
  • \n
  • 在做研究时,我遇到了HelloJS,想知道是否可以用它来实现社交登录。据我所知,答案是“不安全”。\n
      \n
    • 首先要了解的是,当您使用 HelloJS 时,它使用与我上面描述的相同的身份验证代码流程,除了 HelloJS 设置了自己的后端(“代理”)服务器以允许您跳过编写后端实现此流程通常需要的代码,HelloJS 前端库允许您跳过编写通常需要的所有前端代码。
    • \n
    • 使用 HelloJS 进行社交登录的问题在于后端服务器/代理部分:似乎没有办法阻止OpenID Connect 旨在阻止的攻击类型:使用 HelloJS 的最终结果似乎是 GitHub 访问令牌,并且您的应用程序的后端似乎无法判断该访问令牌是由尝试登录您的应用程序的用户创建的,还是在用户登录其他恶意应用程序时创建的(然后使用该访问令牌向您的应用发送请求,模拟用户)。\n
        \n
      • 如果您的应用程序不使用后端,那么您可能没问题,但大多数应用程序确实依赖后端来存储仅应由该用户访问的用户特定数据。
      • \n
      • 如果您能够查询代理服务器以仔细检查它生成的访问令牌,则可以解决此问题,但 HelloJS 似乎没有办法开箱即用地执行此操作,并且如果如果您决定创建自己的代理服务器以便能够执行此操作,那么您似乎会遇到比从一开始就避免使用 HelloJS 更复杂的情况。
      • \n
      \n
    • \n
    • 相反,HelloJS 似乎适用于这样的情况:您的前端只想代表用户查询 GitHub API 以获取有关其帐户的信息,例如用户详细信息或存储库列表,而不期望您的后端将使用用户的 GitHub 访问令牌作为该用户在您的后端访问其私人信息的方法。
    • \n
    \n
  • \n
  • 为了实现“Web 应用程序流程”,我使用了以下文章作为参考,尽管它没有完全映射到我需要使用 GitHub 执行的操作:OpenID Connect 客户端示例 - Codeburst.io \n
      \n
    • 请记住,本指南用于实现 OpenID Connect 身份验证流程,该流程与我们需要用于 GitHub 的流程类似但不相同。
    • \n
    • 这里的代码对于让我的前端代码正常工作特别有帮助。
    • \n
    • GitHub 不允许使用本指南中描述的“随机数”,因为这是特定于 OpenID Connect(某些实现?)的功能,并且 GitHub 的 API 不支持在与 Google 的 API 的做法相同。
    • \n
    \n
  • \n
  • 为了实现“设备流”,我使用以下文章作为灵感:使用 OAuth 2.0 设备流对桌面应用程序中的用户进行身份验证\n
      \n
    • 关键引用是这样的:“基本上,当您需要进行身份验证时,设备将显示一个 URL 和一个代码(它还可以显示一个 QR 代码以避免复制 URL),并开始轮询身份提供商以询问是否身份验证已完成。您在手机或计算机上的浏览器中导航到该 URL,出现提示时登录,然后输入代码。\xe2\x80\x99 完成后,设备下次轮询 IdP 时,它将收到令牌:流程已完成。”
    • \n
    \n
  • \n
\n

示例代码

\n
    \n
  • 我正在开发的应用程序在前端使用 Vue + Quasar + TypeScript,在后端使用 Python + aiohttp。显然,您可能无法直接使用代码,但希望使用它作为参考将使您对最终产品应该是什么样子有足够的了解,以便您可以更快地让自己的代码工作。
  • \n
  • 由于 Stack Overflow 的帖子长度限制,我无法将代码包含在本答案的正文中,因此我将代码链接到各个 GitHub Gists 中。
  • \n
  • 应用程序.vue \n
      \n
    • 这是包含整个前端应用程序的“父组件”。它的代码可以处理“Web 应用程序流程”期间的情况,即用户在授权我们的应用程序后被 GitHub 重定向回我们的应用程序。它从 URL 查询参数中获取授权代码并将其发送到我们应用程序的后端,后端又将授权代码发送到 GitHub 以换取访问令牌和刷新令牌。
    • \n
    \n
  • \n
  • axios.ts \n
      \n
    • 这是来自 的大部分代码axios.ts。这是我放置代码的地方,该代码将 GitHub 访问令牌添加到我们应用程序后端的所有请求(如果前端在 localStorage 中找到这样的令牌),以及查看来自的任何响应的代码我们的应用程序的后端查看访问令牌是否已刷新。
    • \n
    \n
  • \n
  • auth.py \n
      \n
    • 这是后端文件,其中包含“Web 应用程序流”和“设备流”登录过程中使用的所有路由。如果路由 URL 包含“oauth”,则适用于“Web 应用程序流”;如果路由 URL 包含“device”,则适用于“设备流”;我只是按照 GitHub 的示例进行操作。
    • \n
    \n
  • \n
  • 中间件.py \n
      \n
    • 这是包含中间件函数的后端文件,该函数评估所有传入请求,以查看所提供的 GitHub 访问令牌是否在我们应用程序的数据库中,并且尚未过期。刷新访问令牌的代码位于此文件中。
    • \n
    \n
  • \n
  • 登录.vue \n
      \n
    • 这是显示“登录页面”的前端组件。它具有“Web 应用程序流程”和“设备流程”的代码。
    • \n
    \n
  • \n
\n

我的应用程序中实现的两个登录流程的摘要:

\n
Web 应用程序流程
\n
    \n
  1. 用户访问http://mywebsite.com/
  2. \n
  3. 前端代码检查是否有access_tokenlocalStorage 变量(这表明用户已经登录),但没有找到,因此将用户重定向到 /login 路由。\n
      \n
    • 参见App.vue:mounted()App.vue:watch:authenticated()
    • \n
    \n
  4. \n
  5. 在登录页面/视图中,用户单击“使用 GitHub 登录”按钮。
  6. \n
  7. 前端设置一个随机的statelocalStorage 变量,然后使用我们应用的客户端 ID 和随机变量作为 URL 查询参数将用户重定向到 GitHub 的 OAuth 应用授权页面state。\n
      \n
    • Login.vue:redirectUserToGitHubWebAppFlowLoginLink()
    • \n
    \n
  8. \n
  9. 用户登录 GitHub(如果尚未登录),授权我们的应用程序,并使用身份验证代码和状态变量作为 URL 查询参数重定向回http://mywebsite.com/ 。
  10. \n
  11. 应用程序每次加载时都会查找这些 URL 查询参数,当它看到它们时,它会确保该state变量与其存储在 localStorage 中的内容相匹配,如果是这样,它会将授权代码 POST 到我们的后端。\n
      \n
    • 参见App.vue:mounted()App.vue:sendTheBackendTheAuthorizationCodeFromGitHub()
    • \n
    \n
  12. \n
  13. 我们的应用后端收到 POST 的授权代码,然后很快:\n
      \n
    • 注:以下步骤在auth.py:get_web_app_flow_access_token_and_refresh_token()
    • \n
    \n
      \n
    1. 它将授权代码发送到 GitHub,以换取访问令牌和刷新令牌(及其过期时间)。
    2. \n
    3. 它使用访问令牌查询 GitHub 的“/user”端点,以获取用户的 GitHub ID、电子邮件地址和姓名。
    4. \n
    5. 它会在我们的数据库中查找是否有具有检索到的 GitHub ID 的用户,如果没有,则创建一个。
    6. \n
    7. 它为新检索的访问令牌创建新的“oauth_tokens”数据库记录,并将其与用户记录关联。
    8. \n
    9. 最后,它在响应前端请求时将访问令牌发送到前端。
    10. \n
    \n
  14. \n
  15. 前端收到响应,access_token在 localStorage 中设置一个变量,并将authenticatedVue 变量设置为true,应用程序会不断监视该变量,并触发前端将用户从“登录”视图重定向到“ app”视图(即应用程序中要求用户进行身份验证的部分)。\n
      \n
    • 参见App.vue:sendTheBackendTheAuthorizationCodeFromGitHub()App.vue:watch:authenticated()
    • \n
    \n
  16. \n
\n
设备流程
\n
    \n
  1. 用户访问http://mywebsite.com/
  2. \n
  3. 前端代码检查是否有access_tokenlocalStorage 变量(这表明用户已经登录),但没有找到,因此将用户重定向到 /login 路由。\n
      \n
    • 参见App.vue:mounted()App.vue:watch:authenticated()
    • \n
    \n
  4. \n
  5. 在登录页面/视图中,用户单击“使用 GitHub 登录”按钮。
  6. \n
  7. 前端向我们应用的后端发送请求,询问用户在登录其 GitHub 帐户时将输入的用户代码。\n
      \n
    • Login.vue:startTheDeviceLoginFlow()
    • \n
    \n
  8. \n
  9. 后端收到此请求并:\n
      \n
    • auth.py:get_device_flow_user_code()
    • \n
    \n
      \n
    1. 向 GitHub 发送请求,请求新的user_code.
    2. \n
    3. 创建一个异步任务轮询 GitHub 以查看用户是否已进入user_code
    4. \n
    5. 向用户发送从 GitHub 获得的带有user_code和 的响应。device_code
    6. \n
    \n
  10. \n
  11. 前端接收来自我们应用后端的响应并且:\n
      \n
    1. user_code它将和存储device_code在 Vue 变量中。\n
        \n
      • Login.vue:startTheDeviceLoginFlow()
      • \n
      • 该信息device_code也会保存到 localStorage 中,这样,如果用户关闭打开“登录”页面的浏览器窗口,然后打开一个新页面,则无需重新启动登录过程。
      • \n
      \n
    2. \n
    3. user_code向用户显示 。\n
        \n
      • 请参阅Login.vue模板代码块中的开头<div v-if="deviceFlowUserCode">
      • \n
      \n
    4. \n
    5. 它显示一个按钮,该按钮将打开 GitHub URL,用户可以在其中输入user_code(它将在新选项卡中打开页面)。
    6. \n
    7. 它显示了一个链接到同一 GitHub 链接的二维码,因此,如果用户在计算机上使用该应用程序并想要在手机上输入该代码,他们就可以这样做。
    8. \n
    9. 应用程序使用接收到的数据device_code来设置deviceFlowDeviceCode变量。应用程序中代码的一个单独部分不断检查该变量是否已设置,当它发现已设置时,它开始轮询后端以查看后端是否已从access_tokenGitHub 接收到 Yet。 \n
        \n
      • 参见Login.vue:watch:deviceFlowDeviceCode()Login.vue:repeatedlyPollTheBackEndForTheAccessTokenGivenTheDeviceCode()
      • \n
      \n
    10. \n
    \n
  12. \n
  13. 用户可以单击上述按钮或使用手机扫描二维码,然后在https://github.com/login/device登录其 GitHub 帐户时输入用户代码,无论是在运行此应用程序的同一设备上还是在其他设备(例如手机)上。
  14. \n
  15. 后端在如前所述每隔几秒轮询 GitHub 时,接收access_tokenrefresh_token,并且如在描述“Web 应用程序流程”时提到的,向 GitHub 的“/user”端点发送请求以获取用户数据,然后获取或创建用户数据库记录,然后创建新的oauth_tokens数据库记录。\n
      \n
    • auth.py:_repeatedly_poll_github_to_check_if_the_user_has_entered_their_code()
    • \n
    \n
  16. \n
  17. 前端每隔几秒轮询我们应用程序的后端,最终收到后端的响应,在 localStorage 中access_token设置一个access_token变量,将用户重定向到“app”视图(即部分要求用户进行身份验证的应用程序的名称)。\n
      \n
    • Login.vue:repeatedlyPollTheBackEndForTheAccessTokenGivenTheDeviceCode()
    • \n
    \n
  18. \n
\n

  • 这个答案值得写在自己的博客文章中:) (7认同)
  • tl;dr GitHub 不支持 id_token,因此您必须针对其用户配置文件 API 端点硬编码使用访问令牌才能获得“sub”的等效项。 (4认同)
  • 当用户更改 github 用户名时,您如何处理这种情况? (3认同)
  • @leangaurav 嗯...非常好的问题。该代码不处理这种情况。最好使用用户的 GitHub `id` 字段作为唯一标识符,因为这似乎应该通过调用 GitHub 的 `/user` 端点返回:https://docs.github.com/en/rest/ users/users?apiVersion=2022-11-28 我更新了答案以使用 GitHub ID 而不是用户名。 (2认同)

归档时间:

查看次数:

5563 次

最近记录:

2 年,9 月 前