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

How do I implement social login with GitHub accounts?

我的雇主要求我使用用户的 GitHub 帐户为我们的 Web 应用程序实施登录系统。我在网上四处张望,但未能找到关于如何使用 GitHub 帐户(而不是 Facebook 或 Google 帐户)执行此操作的明确解释。

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

简短(呃)答案

您需要遵循 GitHub 的文档(“授权 OAuth 应用程序”)中的 this guide,添加一些内容(如下所述)以允许它作为用户的方法工作身份验证。

  • 我实施了“web application flow" for when our application will be deployed on our company's servers (where we can keep our company's GitHub app's "client secret" a secret), and the "device flow”,用于确定何时将我们的应用程序部署到客户的计算机上(因为在那种情况下,我们将无法对“客户机密”保密)。
  • GitHub 的指南没有提到以下步骤(因为该指南并非专门用于实现社交登录),但为了使社交登录正常工作,我还执行了以下操作:
    1. 我创建了一个 users 数据库 table,其想法是用于登录的每个 GitHub 帐户在此 table 中都有自己的对应行。
      • 示例 users table 架构:
        id - INTEGER
        email - VARCHAR
        name - VARCHAR
        github_username - VARCHAR
        
    2. 我创建了一个 oauth_tokens 数据库 table 来存储我们的 back-end 从 GitHub 收到的所有 GitHub 访问令牌的副本。
    3. 我让 back-end 向 front-end(用户)发送 GitHub 访问令牌,以便它在未来的请求中作为其身份验证机制。
      • 如果您希望用户在关闭浏览器选项卡后仍保持登录状态,front-end 应将令牌存储在 localStorage 中。
    4. 我在 back-end 上添加了中间件——对于每个传入请求——在我们的数据库中查找提供的访问令牌以查看它是否已过期,如果已过期,则尝试刷新它。如果它成功刷新令牌,它会照常处理请求,并在自定义响应 header 中对 front-end 的响应中包含新的访问令牌 front-end 正在密切关注出于(我将其命名为 x-updated-access-token)。如果刷新令牌失败,它会中止请求并发送 401 响应,front-end 将其作为将用户重定向到登录页面的信号。
      • 将您的应用程序设置为仅允许未过期的访问令牌作为一种身份验证方法是必要的,这样用户就可以从 GitHub.com 的设置页面远程退出应用程序。
    5. 我添加了 front-end 代码来处理 GitHub 访问令牌的保存/更新/删除,to/from localStorage 以及对 [=430= 的所有请求],如果 front-end 没有找到“access_token”localStorage 变量集,则重定向到 /login 路由。

更多信息

  • 澄清一些词汇:这里的目标是user authentication via social login. Social login is a type of single-sign on.
  • 您应该了解的第一件事是——在我撰写本文时——GitHub 尚未将自己设置为社交登录提供商Facebook 和 Google 有 的方式。
    • Facebook and Google 都开发了特殊的 JavaScript 库,您可以使用这些库实现社交登录,而无需编写任何(?)login-specific back-end 代码。 GitHub 没有这样的库,据我所知,第三方甚至不可能开发这样的库,因为 GitHub 的 API 不提供使这样的库成为可能(具体来说,它们似乎既不支持“隐式流”也不支持 OpenID Connect)。
  • 接下来你应该明白的是——在我写这篇文章的时候——GitHub's API does not seem to support the use of OpenID Connect to implement social login using GitHub accounts
    • 当我开始研究如何实施社交登录时,most-recent 在线指南说 OpenID Connect was the current best-practice way to do it. And this is true, if the Identity Provider (e.g. GitHub) you're using supports it (i.e. their API can return OpenID Connect ID tokens). As far as I can tell, GitHub's API doesn't currently have the ability to return OpenID Connect ID tokens from the endpoints we'd need to request them from, although it does seem they support the use of OpenID Connect tokens elsewhere in their API.
    • 令我感到困惑
  • 因此,网络应用程序通常希望使用 GitHub 帐户实现社交登录的方式是使用大多数网站在 OpenID Connect 之前使用的 OAuth 2.0 流程,大多数在线资源称之为“authorization code flow", but which GitHub's docs refer to as the "web application flow”。它同样安全,但需要比其他方法更多 work/code 才能正确实施。要点是,使用 GitHub 实现社交登录比使用 Facebook 或 Google 等简化开发人员流程的身份提供程序需要更多时间
  • 如果您(或您的老板)仍然想使用 GitHub 进行社交登录,即使在了解这将花费更多时间之后,值得花一些时间观看一些关于 OAuth 2.0 流程如何工作的解释, 为什么要开发 OpenID Connect(尽管 GitHub 似乎不支持它),并熟悉 wth 一些关键的技术术语,因为它会让 GitHub 指南更容易理解。
    • OAuth 2.0
      • 我发现 OAuth 2.0 的最佳解释是 Okta 的解释:An Illustrated Guide to OAuth and OpenID Connect
        • 最重要的技术术语:
          • 身份提供者 - 这是 GitHub、Facebook、Google 等
          • 客户端 - 这是您的应用程序;具体来说,您应用的 back-end 部分。
          • 授权码 - “short-lived 客户端为 [Identity Provider] 提供的临时代码以换取访问令牌。”
          • 访问令牌:这是让您的应用程序向 GitHub 询问有关用户的信息的原因。
      • 您可能还会发现此图表有帮助:
        • 幻灯片标题是“OIDC 授权代码流程”,但相同的流程用于 non-OIDC OAuth 2.0 授权代码流程,唯一的区别是第 10 步没有 return一个 ID 令牌,只有访问令牌和刷新令牌。
        • 第 11 步以绿色突出显示这一事实并不重要;这只是演示者想要为此特定幻灯片突出显示的步骤。
        • 该图将“身份提供者”和“资源服务器”显示为单独的实体,这可能会造成混淆。在我们的例子中,它们都是 GitHub 的 API; “身份提供者”是 GitHub 的 API 的一部分,它为我们提供访问令牌,“资源服务器”是 GitHub 的 API 的一部分我们可以发送访问令牌以代表用户采取行动(例如询问他们的个人资料)。
        • 来源:Introduction to OAuth 2.0 and OpenID Connect (PowerPoint slides) - PragmaticWebSecurity.com
    • OpenID 连接 (OIDC)
      • 同样,GitHub似乎不​​支持这个,但网上提到很多,所以你可能想知道这里发生了什么/它解决了什么问题/为什么GitHub不支持。
      • 我所看到的关于为什么引入 OpenID Connect 以及为什么它比普通 OAuth 2.0 更适合身份验证的最佳解释是我自己对 2012 ThreadSafe 博客的总结 post:Why use OpenID Connect instead of plain OAuth2? .
        • 简短的回答是,在 OIDC 存在之前,pure-frontend 社交登录 JavaScript 库(如 Facebook 的)使用的是普通 OAuth 2.0,但这种方法容易受到恶意网络应用程序的攻击可以让用户登录他们的站点(例如,使用 Facebook 登录),然后使用生成的 (Facebook) 访问令牌在接受该 (Facebook) 访问令牌作为身份验证方法的任何其他站点上模拟该用户。 OIDC 阻止了这种利用。
        • 但是 GitHub 没有 pure-frontend 社交登录 JavaScript 库,因此它不需要支持 OpenID Connect 来解决该漏洞。您只需要确保您的应用程序的 back-end 正在跟踪它生成的 GitHub 访问令牌,而不是仅仅信任它收到的任何有效 GitHub 访问令牌。
  • 在做研究时,我遇到了 HelloJS,想知道我是否可以用它来实现社交登录。据我所知,答案是“不安全”。
    • 首先要了解的是,当您使用 HelloJS 时,它使用的是与我在上面描述的相同的身份验证代码流程,除了 HelloJS 有自己的 back-end(“代理”)服务器设置以允许您跳过编写实现此流程通常需要的 back-end 代码,HelloJS front-end 库允许您跳过编写通常需要的所有 front-end 代码。
    • 使用HelloJS进行社交登录的问题是back-end server/proxy部分:似乎没有办法阻止the kind of attack that OpenID Connect was created to prevent:使用HelloJS的最终结果似乎是GitHub 访问令牌,您的应用程序的 back-end 似乎无法判断该访问令牌是否是由尝试登录 您的 的用户创建的应用程序,或者如果它是在用户登录到其他恶意应用程序时创建的(然后使用该访问令牌向您的应用程序发送请求,冒充用户)。
      • 如果您的应用程序不使用 back-end 那么您可能没问题,但大多数应用程序确实依赖 back-end 来存储 user-specific 数据,这些数据应该只有那个用户。
      • 如果您能够向代理服务器查询 double-check 它生成了哪些访问令牌,您就可以解决这个问题,但是 HelloJS 似乎没有办法做到这一点 out-of-the-box,并且如果您决定创建自己的代理服务器以便执行此操作,与从一开始就避免使用 HelloJS 相比,您似乎会陷入 more-complicated 的境地
    • HelloJS 似乎适用于您的 front-end 只想代表用户查询 GitHub API 以获取有关他们帐户的信息的情况,例如他们的用户详细信息或他们的存储库列表,并不期望您的 back-end 会使用用户的 GitHub 访问令牌作为该用户访问他们在您的 back-end 上的私人信息的方法。
  • 为了实现“Web 应用程序流程”,我使用了以下文章作为参考,尽管它并没有完全映射到我需要用 GitHub 做的事情:OpenID Connect Client by Example - Codeburst.io
    • 请记住,本指南用于实施 OpenID Connect 身份验证流程,这是 similar-to-but-not-the-same-as 我们需要用于 GitHub 的流程。
    • 此处的代码对让我的 front-end 代码正常工作特别有帮助。
    • GitHub 不允许使用本指南中描述的“nonce”,因为这是特定于(某些实现?)OpenID Connect 和 GitHub 的功能s API 不支持像 Google 的 API 那样使用随机数。
  • 为了实现“设备流程”,我使用了以下文章作为灵感:Using the OAuth 2.0 device flow to authenticate users in desktop apps
    • 关键引用是这样的:“基本上,当您需要进行身份验证时,设备会显示一个 URL 和一个代码(它也可以显示一个 QR 码以避免必须复制 URL),并开始轮询身份提供者以询问身份验证是否完成。您在 phone 或计算机上的浏览器中导航至 URL,在出现提示时登录,然后输入代码。完成后,下次设备轮询 IdP 时,它将收到一个令牌:流程已完成。"

示例代码

  • 我正在开发的应用程序在 front-end 上使用 Vue + Quasar + TypeScript,在 back-end 上使用 Python + aiohttp。显然,您可能无法直接使用代码,但希望将其用作参考将使您对最终产品的外观有足够的了解,以便您可以 more-quickly 让自己的代码正常工作。
  • 由于 Stack Overflow 的 post 长度限制,我不能将代码包含在这个答案的 body 中,所以我 link 单独编写代码GitHub要点。
  • App.vue
    • 这是 'parent component',整个 front-end 应用程序都包含在其中。它具有处理“Web 应用程序流程”期间用户在授权我们的应用程序后被 GitHub 重定向回我们的应用程序的情况的代码。它从 URL 查询参数中获取授权代码,并将其发送到我们应用程序的 back-end,后者又将授权代码发送到 GitHub,以换取访问令牌和刷新令牌。
  • axios.ts
    • 这是 axios.ts 中的大部分代码。这是我放置代码的地方,该代码将 GitHub 访问令牌添加到我们应用程序 back-end 的所有请求(如果 front-end 在 localStorage 中找到这样的令牌),以及代码查看来自我们应用程序 back-end 的任何响应,以查看访问令牌是否已刷新。
  • auth.py
    • 这是 back-end 文件,其中包含在“Web 应用程序流”和“设备流”的登录过程中使用的所有路由。如果路由 URL 包含“oauth”,则用于“web 应用程序流”,如果路由 URL 包含“device”,则用于“设备流”;我只是按照 GitHub 的例子。
  • middleware.py
    • 这是 back-end 文件,其中包含评估所有传入请求的中间件功能,以查看所提供的 GitHub 访问令牌是否在我们应用程序的数据库中,并且尚未过期。刷新访问令牌的代码在这个文件中。
  • Login.vue
    • 这是显示“登录页面”的 front-end 组件。它具有“Web 应用程序流”和“设备流”的代码。

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

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