Home > Enterprise >  How do I implement social login with GitHub accounts?
How do I implement social login with GitHub accounts?

Time:04-07

I've been asked by my employer to implement a log-in system for our web application using users' GitHub accounts. I've looked around online but I haven't been able to find a clear explanation of how to go about doing this with GitHub accounts (as opposed to with Facebook or Google accounts).

CodePudding user response:

I just spent about a week's worth of effort figuring out how to do this, so I thought I'd write up an explanation to save future developers time.

The short(er) answer

You'll want to follow enter image description here

  • 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."
  • Example code

    • The app I'm working on uses Vue Quasar TypeScript on the front-end, and Python aiohttp on the back-end. Obviously you may not be able to use the code directly, but hopefully using it as a reference will give you enough of an idea of what the finished product should look like that you can more-quickly get your own code working.
    • Because of Stack Overflow's post length limits, I can't include the code in the body of this answer, so instead I'm linking the code in individual GitHub Gists.
    • App.vue
      • This is the 'parent component' which the entire front-end application is contained within. It has code that handles the situation during the "web application flow" where the user has been redirected by GitHub back to our application after authorizing our application. It takes the authorization code from the URL query parameters and sends it to our application's back-end, which in turn sends the authorization code to GitHub in exchange for the access token and refresh token.
    • axios.ts
      • This is most of the code from axios.ts. This is where I put the code that adds the GitHub access token to all requests to our app's back-end (if the front-end finds such a token in localStorage), as well as the code that looks at any responses from our app's back-end to see if the access token has been refreshed.
    • auth.py
      • This is the back-end file that contains all the routes used during the login process for both the "web application flow" and the "device flow". If the route URL contains "oauth" it's for the "web application flow", and if the route URL contains "device" it's for the "device flow"; I was just following GitHub's example there.
    • middleware.py
      • This is the back-end file that contains the middleware function that evaluates all incoming requests to see if the presented GitHub access token is one in our app's database, and hasn't yet expired. The code for refreshing the access token is in this file.
    • Login.vue
      • This is the front-end component that displays the "Login page". It has code for both the "web application flow" as well as the "device flow".

    Summary of the two login flows as implemented in my application:

    The web application flow
    1. The user goes to http://mywebsite.com/
    2. The front-end code checks whether there's an access_token localStorage variable (which would indicate the user has already logged in), and doesn't find one, so it redirects the user to the /login route.
      • See App.vue:mounted() and App.vue:watch:authenticated()
    3. At the Login page/view, the user clicks the "Sign in with GitHub" button.
    4. The front-end sets a random state localStorage variable, then redirects the user to GitHub's OAuth app authorization page with our app's client ID and the random state variable as URL query parameters.
      • See Login.vue:redirectUserToGitHubWebAppFlowLoginLink()
    5. The user signs into GitHub (if they're not already signed in), authorizes our application, and is redirected back to http://mywebsite.com/ with an authentication code and the state variable as URL query parameters.
    6. The app is looking for those URL query parameters every time it loads, and when it sees them, it makes sure the state variable matches what it stored in localStorage, and if so, it POSTs the authorization code to our back-end.
      • See App.vue:mounted() and App.vue:sendTheBackendTheAuthorizationCodeFromGitHub()
    7. Our app's back-end receives the POSTed authorization code and then very quickly:
      • Note: the steps below are in auth.py:get_web_app_flow_access_token_and_refresh_token()
      1. It sends the authorization code to GitHub in exchange for the access token and refresh token (as well as their expiration times).
      2. It uses the access token to query GitHub's "/user" endpoint to get the user's GitHub username, email address, and name.
      3. It looks in our database to see if we have a user with the retrieved GitHub username, and if not, creates one.
      4. It creates a new "oauth_tokens" database record for the newly-retrieved access tokens and associates it with the user record.
      5. Finally, it sends the access token to the front-end in the response to the front-end's request.
    8. The front-end receives the response, sets an access_token variable in localStorage, and sets an authenticated Vue variable to true, which the app is constantly watching out for, and which triggers the front-end to redirect the user from the "login" view to the "app" view (i.e. the part of the app that requires the user to be authenticated).
      • See App.vue:sendTheBackendTheAuthorizationCodeFromGitHub() and App.vue:watch:authenticated()
    The device flow
    1. The user goes to http://mywebsite.com/
    2. The front-end code checks whether there's an access_token localStorage variable (which would indicate the user has already logged in), and doesn't find one, so it redirects the user to the /login route.
      • See App.vue:mounted() and App.vue:watch:authenticated()
    3. At the Login page/view, the user clicks the "Sign in with GitHub" button.
    4. The front-end sends a request to our app's back-end asking for the user code that the user will enter while signed into their GitHub account.
      • See Login.vue:startTheDeviceLoginFlow()
    5. The back-end receives this request and:
      • See auth.py:get_device_flow_user_code()
      1. Sends a request to GitHub asking for a new user_code.
      2. Creates an asynchronous task polling GitHub to see if the user has entered the user_code yet.
      3. Sends the user a response with the user_code and device_code that it got from GitHub.
    6. The front-end receives the response from our app's back-end and:
      1. It stores the user_code and device_code in Vue variables.
        • See Login.vue:startTheDeviceLoginFlow()
        • The device_code is also saved to localStorage so that if the user closes the browser window that has the "log in" page open and then opens up a new one, they won't need to restart the login process.
      2. It displays the user_code to the user.
        • See Login.vue in the template code block starting <div v-if="deviceFlowUserCode">
      3. It shows a button that will open the GitHub URL where the user can enter the user_code (it will open the page in a new tab).
      4. It shows a QR code that links to the same GitHub link, so that if the user is using the application on a computer and wants to enter the code on their phone, they can do that.
      5. The app uses the received device_code to set a deviceFlowDeviceCode variable. A separate part of the code in the app is constantly checking to see if that variable has been set, and when it sees that it has, it begins polling the back-end to see if the back-end has received the access_token yet from GitHub.
        • See Login.vue:watch:deviceFlowDeviceCode() and Login.vue:repeatedlyPollTheBackEndForTheAccessTokenGivenTheDeviceCode()
    7. The user either clicks the aforementioned button or scans the QR code with their phone, and enters the user code at https://github.com/login/device while logged into their GitHub account, either on the same device this application is running on or some other device (like their phone).
    8. The back-end, while polling GitHub every few seconds as previously mentioned, receives the access_token and refresh_token, and as mentioned while describing the "web app flow", sends a request to GitHub's "/user" endpoint to get user data, then gets or creates a user db record, and then creates a new oauth_tokens db record.
      • See auth.py:_repeatedly_poll_github_to_check_if_the_user_has_entered_their_code()
    9. The front-end, while polling our application's back-end every few seconds, finally receives a response from the back-end with the access_token, sets an access_token variable in localStorage, redirects the user to the "app" view (i.e. the part of the app that requires the user to be authenticated).
      • See Login.vue:repeatedlyPollTheBackEndForTheAccessTokenGivenTheDeviceCode()
    • Related