authenticationgithuboauth-2.0github-api

How do I implement social login with GitHub accounts?


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).


Solution

  • 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 this guide in GitHub's docs ("Authorizing OAuth Apps"), with some additions (explained below) to allow it to work as a method of user authentication.

    More information

    Example code

    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 id, email address, and name.
      3. It looks in our database to see if we have a user with the retrieved GitHub id, 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()