Here’s my basic understanding of how to do client-side OAuth, meaning we want JavaScript running in the browser to be able to authenticate to a service (e.g., GitHub) and then access that service directly from the browser. The key security requirement is that the client-side JavaScript can’t know the client secret because the whole point of secrets as that they be secret. Assume in what follows that our web server is running on example.com. Also note that all these requests should be over HTTPS.
The browser makes a request to our web server to initiate the login process at a URL like:
For example, the user clicks a "Login" button which triggers a request to the
endpoint. Or possibly our client-side Javascript notices whenever we are on a
page that requires authentication and we don't have a Github access token in our
session storage and kicks things off by setting window.location.href
to the
login URL.
In either case, before making the request (i.e. in the onclick of the button or
in the auto-login code), we store the current value of window.location.href
in
sessionStorage as preLoginURL
or something.
The server generates a secure random STATE
parameter (i.e. a cryptographically
secure random string) to prevent Cross Site Request Forgery (CSRF) attacks and
stores it in the user's server-side session.
The server then constructs the GitHub authorization URL with the state
parameter included and sends a redirect response to send the browser there:
https://github.com/login/oauth/authorize?client_id=CLIENT_ID &redirect_uri=REDIRECT_URI &scope=repo,user &state=STATE
-
CLIENT_ID
is the ID created by GitHub when we set up our OAuth app which the server knows from it's configuration (e.g. dotenv or whatever) -
REDIRECT_URI
is the URL on our web server that GitHub should redirect the browser to after the user has authenticated. Also from configuration. It has probably been configured as part of the OAuth app and Github may reject this request if it doesn't match. -
scope
parameter tells GitHub what scopes we are asking for so it can inform the user. -
STATE
is the secure random value produced by our web server.
The user sees a page served by github.com that asks them to log in and authorize
the app to have the requested access. If the user approves, GitHub redirects the
browser to the REDIRECT_URI
with a code
and state
parameter:
https://example.com/callback?code=SOMECODE&state=STATE
-
SOMECODE
is an opaque code generated by the GitHub server. -
STATE
is the same value that was passed in the authorization url.
The purpose of this step is to communicate these two pieces of information back to our web server, associated with this user. When the browser is redirected it sends the two query params but also it's cookies for our web server which allows our web server to identify it's session.
Our web server receives the callback request, and verifies that the state
parameter matches the one in the user’s session. If it doesn’t match returns an
error response.
The goal at this point is to trade in the code generated by Github for an access token and to pass it back to the the client-side Javascript so it can use it to access the Github API.
The key thing that happens at this step is our web server unites the code given to us by Github (which Github will have kept track of and associated with the CLIENT_ID) with the CLIENT_ID and the CLIENT_SECRET which allows Github to know that this is a legit request from code owned by the owner of the OAuth app. So our web-server posts a request to:
https://github.com/login/oauth/access_token
passing the CLIENT_ID, CLIENT_SECRET, code, and for good measure the REDIRECT_URL so Github can double check that it matches. Presumably Github does something roughly like, looks up the CLIENT_ID it stored associated with the code when it generated the code and then checks that the CLIENT_SECRET and REDIRECT_URL are the correct values for that CLIENT_ID. Assuming so it responds with an access token.
Now our web server needs to send some response to the browser that allows it to communicate the access token. The simplest and most secure way is to set a Secure, HttpOnly cookie containing the token on the successful response to the /callback endpoint.
Finally we want our user to end up back on the page they were on when they
started logging in. If we stored the starting page in sessionStorage under
preLoginURL as suggested in “User/client initiates login” we can get back there
if the response to /callback
contains Javascript that sets
window.location.href
to that stored URL and the user will be back where they
started, now with a Github access token stored in sessionStorage.