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).
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.
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.
users
database table, with the idea being that each GitHub account used to log in would have its own corresponding row in this table.
users
table schema:
id - INTEGER
email - VARCHAR
name - VARCHAR
github_id - VARCHAR
oauth_tokens
database table to store a copy of all of the GitHub access tokens that our back-end receives from GitHub.
oauth_tokens
table schema:
id - INTEGER
user_id - INTEGER
access_token - VARCHAR
expires_at - DATETIME
refresh_token - VARCHAR
refresh_token_expires_at - DATETIME
device_code - VARCHAR <-- Used for the "device flow". I have the back-end send the
front-end the device code immediately upon starting the device flow, and I then
have the front-end poll the back-end with it until the back-end has received
the access token from GitHub, at which point the front-end discards the device
code and uses the access token as its authentication token.
localStorage
if you want the user to remain logged in even after they close the browser tab they logged in with.x-updated-access-token
). If it fails to refresh the token, it aborts the request and sends a 401 response that the front-end takes as a signal to redirect the user to the login page.
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.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.
App.vue:mounted()
and App.vue:watch:authenticated()
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.
Login.vue:redirectUserToGitHubWebAppFlowLoginLink()
state
variable matches what it stored in localStorage, and if so, it POSTs the authorization code to our back-end.
App.vue:mounted()
and App.vue:sendTheBackendTheAuthorizationCodeFromGitHub()
auth.py:get_web_app_flow_access_token_and_refresh_token()
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).
App.vue:sendTheBackendTheAuthorizationCodeFromGitHub()
and App.vue:watch:authenticated()
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.
App.vue:mounted()
and App.vue:watch:authenticated()
Login.vue:startTheDeviceLoginFlow()
auth.py:get_device_flow_user_code()
user_code
.user_code
yet.user_code
and device_code
that it got from GitHub.user_code
and device_code
in Vue variables.
Login.vue:startTheDeviceLoginFlow()
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.user_code
to the user.
Login.vue
in the template code block starting <div v-if="deviceFlowUserCode">
user_code
(it will open the page in a new tab).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.
Login.vue:watch:deviceFlowDeviceCode()
and Login.vue:repeatedlyPollTheBackEndForTheAccessTokenGivenTheDeviceCode()
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).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.
auth.py:_repeatedly_poll_github_to_check_if_the_user_has_entered_their_code()
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).
Login.vue:repeatedlyPollTheBackEndForTheAccessTokenGivenTheDeviceCode()