JWT + Third-party Oauth in Single Page App

JWT + Third-party Oauth in Single Page App

Imagine you run a single page app at example.com that communicates with backends over restful API and is authenticated with JWT tokens managed by you, but identities are managed by third-party OAuth vendors (Google, Facebook, GitHub, etc). Integration of JWT and Oauth has proven nontrivial. Especially, how exactly does one issue the JWT token back to the client after the client successfully authenticated with the third-party OAuth vendor?

To appreciate this challenge fully, please endure a brief introduction of the OAuth workflow.

The API server does not manage user credentials. The client sends the credentials to the OAuth vendor and receives a redirect URL and a one-time passcode. The redirect URL is a subdomain of yours and is pre-registered by your app with the OAuth vendor. Its purpose is to relay the passcode back to API server so the server could use this code in exchange for the OAuth token. Then the server might use the token to request for some basic user info and then issue a JWT token for this user.

How do we send this token back to the client? The client is still waiting for a server response to its “share code” request. It is worthwhile to note that such request is done by the browser hitting the redirect URL as instructed by the OAuth vendor, so the browser is waiting for a response that it can render. Hence, replying the JWT token in JSON over HTTP just does not cut it. If the server replies just an HTML with javascript assets, all subsequent requests by the client will still be unauthorized.

I have seen so many hacks on this problem.

  • Store session id in cookie and store the token in session on the server side. Then you must make sure session persistence if your API servers are horizontally scaled. If a user logged in on replica A and subsequent requests hit replica B, the user should not be asked to log in again. The use of cookie also defeats the entire point of JWT based API server authentication, since it is not mobile app friendly.

  • Pass the token through URL params by redirect elsewhere before redirecting to the final logged-in page. In the following example, the callback from OAuth vendor will first redirect the client to /saveToken with JWT token as param. The client extracts the token from the path and at the same time, the server responds dashboard.html. Not only is doing so not secure (referrer HTTP header might keep the token), it also requires a lot more work on the frontend client code to control redirect routings to extract JWT token before sending GET requests to /saveToken.

1
2
3
4
5
6
7
8
const auth = passport.authenticate('google', {
  failureRedirect: '/login',
  session: false,
})
app.get('/auth/google/callback', auth, (req, res) => {
  const jwt = createJWTFromUserDATA(req.user)
  res.redirect('/saveToken?jwt='+jwt)
})

The best solution that I encountered is the following, which stores the JWT token in local storage and then redirects to the main page with no change to service code and just a few lines of HTML.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const auth = passport.authenticate('google', {
  failureRedirect: '/login',
  session: false,
})
app.get('/auth/google/callback', auth, (req, res) => {
  const jwt = createJWTFromUserDATA(req.user)
  const htmlWithEmbeddedJWT = `
<html>
  <script>
    window.localStorage.setItem('JWT', '${jwt}');
    window.location.href = '/';
  </script>
</html>`
  res.send(htmlWithEmbeddedJWT)
})