我刚花了一周的时间来弄清楚如何做到这一点,所以我想写一篇解释来节省未来开发人员的时间。
简短(一些)答案
您需要遵循 GitHub 文档中的 this guide(“授权 OAuth 应用程序”),并进行一些添加(下面解释),以使其作为用户身份验证的方法工作。
我为我们公司的服务器实现了 "
web应用程序流程"(在那里我们可以保持我们公司的GitHub应用程序的“客户端密钥”为秘密),并为当我们的应用程序部署在客户端计算机上时实现了 "
设备流程"(因为在这种情况下,我们将无法保持我们的“客户端密钥”为秘密)。
GitHub的指南没有提及以下步骤(因为该指南不是专门用于实施社交登录),但要使社交登录起作用,我还做了以下工作:
- 我创建了一个名为
users
的数据库表,其想法是每个用于登录的GitHub帐户都将在此表中拥有自己对应的行。
- 示例
users
表模式:
```
id - 整数
email - VARCHAR
name - VARCHAR
github_id - VARCHAR
```
- 我创建了一个名为
oauth_tokens
的数据库表,以存储我们后端从GitHub接收到的所有GitHub访问令牌的副本。
- 这是必需的,以
防止其他恶意网站使用用户授权的有效GitHub访问令牌模仿我们的用户。
- 示例
oauth_tokens
表模式:
```
id - 整数
user_id - 整数
access_token - VARCHAR
expires_at - DATETIME
refresh_token - VARCHAR
refresh_token_expires_at - DATETIME
device_code - VARCHAR<-用于“设备流程”。我让后端立即在开始设备流程时向前端发送设备代码,然后让前端使用该代码轮询后端,直到后端从GitHub收到访问令牌,此时前端丢弃设备代码并将访问令牌用作其身份验证令牌。
```
- 我让后端向前端(用户)发送GitHub访问令牌,以便它可以作为其身份验证机制与未来请求一起呈现。
- 如果要使用户在关闭其登录的浏览器选项卡后仍保持登录状态,则应将前端存储在
localStorage
中。
- 我在后端添加了中间件,以便对于每个传入请求,查找我们的数据库中提供的访问令牌是否已过期,如果已过期,则尝试刷新该令牌。如果成功刷新令牌,则按正常方式处理请求,并在响应中包含新的访问令牌,前端会注意到自定义响应头(我命名为
x-updated-access-token
)。如果无法刷新令牌,则中止请求并发送401响应,前端将其作为重定向用户到登录页面的信号进行接受。
- 仅允许未过期的访问令牌用作身份验证方法是使用户能够从其GitHub.com上的设置页面远程注销应用程序的必要条件。
- 我添加了前端代码,以处理保存/更新/删除GitHub访问令牌,无论是从/到localStorage还是到所有向后端的请求,并在前端没有设置“access_token”localStorage变量时重定向到/login路由。
- 如果您想深入了解代码,请参考以下代码。基本上,我使用此文章作为“web应用程序流程”的前端代码应如何工作的大致指南:
OpenID Connect Client by Example - Codeburst.io
- [更新] - 最终我的主管要求我更改代码的工作方式(从上面步骤中描述的方式),这样我们不是使用数据库存储和验证已创建的
更多信息
- To clarify some vocabulary: The goal here is to do user authentication via social login. Social login is a type of single-sign on.
- The first thing you should understand is that--as of the time I'm writing this--GitHub has not set itself up to be a provider of social login in the way Facebook and Google have.
- Facebook and Google both have developed special JavaScript libraries that you can use to implement social login without needing to write any(?) login-specific back-end code. GitHub has no such library, and from what I can tell it's not even possible for a third party to develop such a library because GitHub's API doesn't offer the functionality required to make such a library possible (specifically, they seem to support neither the "implicit flow" nor OpenID Connect).
- The next thing you should understand is that--as of the time I'm writing this--GitHub's API does not support the use of OpenID Connect to implement social login using GitHub accounts.
- When I started doing research into how to implement social login I was confused by the fact that the most-recent online guides were saying that 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). I contacted GitHub and they confirmed that their 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.
- Thus, the way web apps will generally want to implement social login with GitHub accounts is to use the OAuth 2.0 flow that most websites used before OpenID Connect, which most online resources call the "authorization code flow", but which GitHub's docs refer to as the "web application flow". It's just as secure but requires some more work/code than the other methods to implement properly. The takeaway is that implementing social login with GitHub is going to take more time than using an Identity Provider like Facebook or Google that have streamlined the process for developers.
- If you (or your boss) still want to use GitHub for social login even after understanding it's going to take more time, it's worth spending some time to watch some explanations of how the OAuth 2.0 flow works, why OpenID Connect was developed (even though GitHub doesn't seem to support it), and become familiar with some key technical terms, as it'll make it easier to understand the GitHub guide.
- OAuth 2.0
- The best explanation of OAuth 2.0 that I found was this one by Okta: An Illustrated Guide to OAuth and OpenID Connect
- The most important technical terms:
- Identity Provider - This is GitHub, Facebook, Google, etc.
- Client - This is your app; specifically, the back-end part of your app.
- Authorization Code - "A short-lived temporary code the Client gives the [Identity Provider] in exchange for an Access Token."
- Access Token: This is what lets your app ask GitHub for information about the user.
- You may also find this graph helpful:
![enter image description here](https://istack.dev59.com/fDQ3k.webp)
- The slide title is "OIDC Authorization Code Flow" but the same flow is used for a non-OIDC OAuth 2.0 authorization code flow, with the only difference being that step 10 doesn't return an ID token, just the access token and refresh token.
- The fact that step 11 is highlighted in green isn't significant; it's just the step the presenter wanted to highlight for this particular slide.
- The graph shows the "Identity Provider" and "Resource Server" as separate entities, which might be confusing. In our case they're both GitHub's API; the "Identity Provider" is the part of GitHub's API that gets us an access token, and the "Resource Server" is the part of GitHub's API that we can send the access token to to take actions on behalf of the user (e.g. asking about their profile).
- Source: Introduction to OAuth 2.0 and OpenID Connect (PowerPoint slides) - PragmaticWebSecurity.com
- OpenID Connect (OIDC)
- Again, GitHub doesn't seem to support this, but it's mentioned a lot online, so you may be curious to know what's going on here / what problem it solves / why GitHub doesn't support it.
- The best explanation I've seen for why OpenID Connect was introduced and why it would be preferred over plain OAuth 2.0 for authentication is my own summary of a 2012 ThreadSafe blog post: Why use OpenID Connect instead of plain OAuth2?.
- The short answer is that before OIDC existed, pure-frontend social login JavaScript libraries (like Facebook's) were using plain OAuth 2.0, but this method was open to an exploit where a malicious web app could have a user sign into their site (for example, using Facebook login) and then use the generated (Facebook) access token to impersonate that user on any other site that accepted that (Facebook) access token as a method of authentication. OIDC prevents that exploit.
- But GitHub doesn't have a pure-frontend social login JavaScript library, so it doesn't need to support OpenID Connect to address that exploit. You just need to make sure your app's back-end is keeping track of which GitHub access tokens it has generated rather than just trusting any valid GitHub access token it receives.
- While doing research I came across HelloJS and wondered if I could use it to implement social login. From what I can tell, the answer is "not securely".
- The first thing to understand is that when you use HelloJS, it is using the same authentication code flow I describe above, except HelloJS has its own back-end ("proxy") server set up to allow you to skip writing the back-end code normally needed to implement this flow, and the HelloJS front-end library allows you to skip writing all the front-end code normally needed.
- The problem with using HelloJS for social login is the back-end server/proxy part: there seems to be no way to prevent the kind of attack that OpenID Connect was created to prevent: the end result of using HelloJS seems to be a GitHub access token, and there seems to be no way for your app's back-end to tell whether that access token was created by the user trying to log into your app or if it was created when the user was logging into some other malicious app (which is then using that access token to send requests to your app, impersonating the user).
- If your app doesn't use a back-end then you could be fine, but most apps do rely on a back-end to store user-specific data that should only be accessible to that user.
- You could get around this problem if you were able to query the proxy server to double-check which access tokens it had generated, but HelloJS doesn't seem to have a way to do this out-of-the-box, and if you decide to create your own proxy server so that you can do this, you seem to be ending up in a more-complicated situation than if you'd just avoided HelloJS from the beginning.
- HelloJS instead seems to be intended for situations where your front-end just wants to query the GitHub API on behalf of the user to get information about their account, like their user details or their list of repositories, with no expectation that your back-end will be using the user's GitHub access token as a method for that user to access their private information on your back-end.
- To implement the "web application flow" I used the following article as a reference, although it didn't perfectly map to what I needed to do with GitHub: OpenID Connect Client by Example - Codeburst.io
- Keep in mind that this guide is for implementing the OpenID Connect authentication flow, which is similar-to-but-not-the-same-as the flow we need to use for GitHub.
- The code here was especially helpful for getting my front-end code working properly.
- GitHub does not allow for the use of a "nonce" as described in this guide, because that is a feature specific to (some implementations of?) OpenID Connect, and GitHub's API does not support the use of a nonce in the same way that Google's API does.
- To implement the "device flow" I used the following article as inspiration: Using the OAuth 2.0 device flow to authenticate users in desktop apps
- The key quote is this: "Basically, when you need to authenticate, the device will display a URL and a code (it could also display a QR code to avoid having to copy the URL), and start polling the identity provider to ask if authentication is complete. You navigate to the URL in the browser on your phone or computer, log in when prompted to, and enter the code. When you’re done, the next time the device polls the IdP, it will receive a token: the flow is complete."
示例代码
我正在开发的应用程序在前端使用Vue + Quasar + TypeScript,在后端使用Python + aiohttp。显然,您可能无法直接使用代码,但是希望将其作为参考,可以让您更快地使自己的代码工作,并了解最终产品的外观。
由于Stack Overflow的帖子长度限制,我无法在此答案正文中包含代码,因此我将在单独的GitHub Gists中链接代码。
App.vue
这是“父组件”,整个前端应用程序都包含在其中。它具有处理“Web应用程序流程”期间的情况的代码,即用户在授权我们的应用程序后被GitHub重定向回我们的应用程序。它从URL查询参数中获取授权代码并将其发送到我们应用程序的后端,后者将授权代码发送到GitHub以换取访问令牌和刷新令牌。
axios.ts
这是大部分
axios.ts
的代码。这是我放置代码的位置,该代码将GitHub访问令牌添加到所有请求到我们应用程序的后端(如果前端在localStorage中找到这样的令牌),以及查看来自我们应用程序的后端的任何响应是否已刷新访问令牌的代码。
auth.py
这是包含登录过程中用于“Web应用程序流程”和“设备流程”的所有路由的后端文件。如果路由URL包含“oauth”,则为“Web应用程序流程”,如果路由URL包含“device”,则为“设备流程”;我只是遵循GitHub的示例。
middleware.py
这是包含评估所有传入请求以查看呈现的GitHub访问令牌是否为我们应用程序数据库中的令牌并且尚未过期的中间件函数的后端文件。刷新访问令牌的代码在此文件中。
Login.vue
这是显示“登录页面”的前端组件。它具有“Web应用程序流程”和“设备流程”的代码。
我的应用程序中实现的两个登录流程概述:
Web应用程序流程
- 用户访问 http://mywebsite.com/
- 前端代码检查是否存在
access_token
localStorage 变量(这表明用户已经登录),如果没有找到,则将用户重定向到 /login 路由。
- 请参见
App.vue:mounted()
和 App.vue:watch:authenticated()
- 在登录页面/视图中,用户单击“使用 GitHub 登录”按钮。
- 前端设置一个随机的
state
localStorage 变量,然后将用户重定向到 GitHub 的 OAuth 应用程序授权页面,其中包含我们应用程序的客户端 ID 和随机的 state
变量作为 URL 查询参数。
- 请参见
Login.vue:redirectUserToGitHubWebAppFlowLoginLink()
- 用户登录 GitHub(如果尚未登录),授权我们的应用程序,并带有身份验证代码和状态变量作为 URL 查询参数重定向回 http://mywebsite.com/。
- 每次加载应用程序时,应用程序都会查找这些 URL 查询参数,并在看到它们时确保
state
变量与存储在 localStorage 中的变量匹配,如果匹配,则将授权代码 POST 到我们的后端。
- 请参见
App.vue:mounted()
和 App.vue:sendTheBackendTheAuthorizationCodeFromGitHub()
- 我们应用程序的后端接收 POST 授权代码,然后非常快地执行以下操作:
- 注意:以下步骤在
auth.py:get_web_app_flow_access_token_and_refresh_token()
中。
- 它将授权代码发送到 GitHub,以交换访问令牌和刷新令牌(以及它们的过期时间)。
- 它使用访问令牌查询 GitHub 的“/user”端点,以获取用户的 GitHub ID、电子邮件地址和名称。
- 它查看我们的数据库,以查看是否有一个具有检索到的 GitHub ID 的用户,如果没有,则创建一个。
- 它为新检索的访问令牌创建一个新的“oauth_tokens”数据库记录,并将其与用户记录关联。
- 最后,它将访问令牌发送到前端,作为对前端请求的响应。
- 前端接收响应,在 localStorage 中设置一个
access_token
变量,并将 authenticated
Vue 变量设置为 true
,该变量一直被应用程序监视,触发前端将用户从“登录”视图重定向到需要用户进行身份验证的“应用程序”视图。
- 请参见
App.vue:sendTheBackendTheAuthorizationCodeFromGitHub()
和 App.vue:watch:authenticated()
设备流程
- 用户进入http://mywebsite.com/
- 前端代码检查本地存储中是否存在
access_token
变量(这表明用户已经登录),没有找到,则重定向用户到/login路由。
- 参见
App.vue:mounted()
和App.vue:watch:authenticated()
- 在登录页面/视图中,用户点击“使用GitHub登录”按钮。
- 前端发送请求到我们应用的后端,请求用户在登录GitHub账号时将要输入的用户代码。
- 参见
Login.vue:startTheDeviceLoginFlow()
- 后端接收到该请求并:
- 参见
auth.py:get_device_flow_user_code()
- 向GitHub发送新的
user_code
请求。
- 创建异步任务轮询GitHub,以查看用户是否已经输入了
user_code
。
- 向用户发送包含从GitHub获取到的
user_code
和device_code
的响应信息。
- 前端接收到我们应用后端的响应,并:
- 将
user_code
和device_code
存储在Vue变量中。
- 参见
Login.vue:startTheDeviceLoginFlow()
device_code
也保存在本地存储中,以便如果用户关闭了浏览器窗口,那么重新打开一个新的,他们不需要重新开始登录过程。
- 向用户显示
user_code
。
- 参见
Login.vue
模板代码块中的<div v-if="deviceFlowUserCode">
- 显示一个按钮,点击该按钮将打开GitHub URL,用户可以在其中输入
user_code
(它将在新选项卡中打开页面)。
- 显示一个QR码,链接到相同的GitHub链接,以便如果用户在计算机上使用应用程序并希望在手机上输入代码,则可以这样做。
- 应用程序使用接收到的
device_code
设置一个deviceFlowDeviceCode
变量。 应用程序代码的另一部分不断检查是否已经设置了该变量,并且当它看到已经设置了该变量时,它开始轮询后端,以查看后端是否已从GitHub接收到access_token
。
- 参见
Login.vue:watch:deviceFlowDeviceCode()
和Login.vue:repeatedlyPollTheBackEndForTheAccessTokenGivenTheDeviceCode()
- 用户单击上述按钮或使用手机扫描QR码,并在
https://github.com/login/device
上输入用户代码,同时登录他们的GitHub帐户,无论是在运行此应用程序的同一设备上还是其他设备上(例如他们的手机)。
- 后端在每隔几秒钟轮询GitHub时收到
access_token
和refresh_token
,并像描述“Web应用程序流程”时那样,发送请求到GitHub的“/user”端点以获取用户数据,然后获取或创建用户数据库记录,最后创建一个新的oauth_tokens
数据库记录。
- 参见
auth.py:_repeatedly_poll_github_to_check_if_the_user_has_entered_their_code()
- 前端在每隔几秒钟轮询我们应用程序后端时,最终从后端接
id
字段作为唯一标识符,因为它似乎应该在调用 GitHub 的/user
端点时返回:https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28 我更新了我的答案,使用 GitHub ID 而不是用户名。 - Nathan Wailes