Login & Auth Workflows

Single-Page Application OAuth Login Using Authorization Code Grant With JWTs And Refresh Tokens - Recommended

By Brian Pontarelli

This workflow is used by single-page applications using the FusionAuth OAuth login interface. The single-page application navigates away from its interface and over to FusionAuth’s OAuth interface. Once the user completes their login, FusionAuth redirects back to the single-page application. This requires that the single-page application re-initialize itself, but the browser should cache the application files and be able to restart it quickly. Below is a diagram that describes the primary components of this workflow and how they interact. Keep in mind that not every interaction is covered here, just the primary login interactions. At the bottom of the diagram is a discussion of the key steps.

For all of our examples, we use a store and a forum for the same company. The store requires a user to login to view their shopping cart and the forum requires the user to login to view forum posts. We also provide a couple of example attack vectors that hackers could use if portions of the system are compromised. These cases might be theoretical or based on known exploits such as XSS (cross-site scripting).

Diagram

Legend

() --> request/response bodies
{} --> request parameters
[] --> cookies
BrowserStoreForumsFusionAuthHackerInitializeLogin (browser navigates away from SPA)Shopping cart loadJWT expiresShopping cart loadRefresh tokenexpiresRe-loginSSO login to forumsInitializeLogin (browser navigates away from SPA but auto-logs-in since the session exists)Forum loadAttack vectorsStolen refresh tokenStolen JWTGET /1(SPA HTML, CSS & JavaScript)2AJAX GET /api/user[No cookies]3404 Missing4GET /oauth2/authorize {response_type=code}5(Login form HTML)6POST /oauth2/authorize {response_type=code}7302 Location: {redirect_uri w/ code}[SessionId HttpOnly w/ domain: example.fusionauth.io]8GET {redirect_uri w/ code}9POST /oauth2/token(code, client_secret)10200 Ok(Refresh token and JWT)11302 Location: /[Refresh token and JWT HttpOnly w/ domain: store.example.com]12GET /13(SPA HTML, CSS & JavaScript)14AJAX GET /api/user[Refresh token and JWT HttpOnly w/ domain: store.example.com]15200 Ok(User)16AJAX GET /api/load-shopping-cart[Refresh token and JWT HttpOnly w/ domain: store.example.com]17(Shopping cart contents)18AJAX GET /api/load-shopping-cart[Refresh token and JWT HttpOnly w/ domain: store.example.com]19POST /oauth2/token or POST /api/jwt/refresh(grant_type=refresh and refresh token)20(JWT)21(Shopping cart contents)[New JWT HttpOnly w/ domain: store.example.com]22AJAX GET /api/load-shopping-cart[Refresh token and JWT HttpOnly w/ domain: store.example.com]23POST /oauth2/token or POST /api/jwt/refresh(grant_type=refresh and refresh token)24404 Missing25401 Not Authorized26Login same as above27GET /[No cookies]28(SPA HTML, CSS & JavaScript)29GET /api/user[No cookies]30404 Missing31GET /oauth2/authorize {response_type=code}[SessionId HttpOnly w/ domain: example.fusionauth.io]32302 Location: {redirect_uri w/ code}[SessionId HttpOnly w/ domain: example.fusionauth.io]33GET {redirect_uri w/ code}34POST /oauth2/token(code, client_secret)35200 Ok(Refresh token and JWT)36302 Location: /[Refresh token and JWT HttpOnly w/ domain: forums.example.com]37GET /38(SPA HTML, CSS & JavaScript)39AJAX GET /api/user[Refresh token and JWT HttpOnly w/ domain: forums.example.com]40200 Ok(User)41AJAX GET /api/load-posts[Refresh token and JWT HttpOnly w/ domain: forums.example.com]42(Forum posts)43GET /api/load-shopping-cart[Refresh token and bad JWT HttpOnly w/ domain: store.example.com]44POST /oauth2/token or POST /api/jwt/refresh(grant_type=refresh and refresh token)45(JWT)46(Shopping cart contents)[New JWT HttpOnly w/ domain: store.example.com]47GET /api/load-shopping-cart[JWT HttpOnly w/ domain: store.example.com]48(Shopping cart contents)49BrowserStoreForumsFusionAuthHacker

Explanation

  1. The browser requests the shopping cart single-page application from the application backend
  2. The application backend responds with the HTML, CSS & JavaScript of the application
  3. The browser loads the application and as part of the initialization process, it makes a request to the application backend to see if the user is logged in
  4. The application backend responds with a 404 indicating the user is not logged in
  5. The user clicks the login link and the browser navigates away from the single-page application to FusionAuth's OAuth 2 interface. The browser requests the OAuth 2 login page from FusionAuth with a response_type of code indicating that it is using the authorization code grant
  6. FusionAuth responds with the HTML, CSS & JavaScript of the login page (including the form)
  7. The user inputs their credentials and clicks the submit button. The browser POSTs the form data to FusionAuth
  8. FusionAuth returns a redirect to the application backend's OAuth 2 redirect_uri. This redirect includes the authorization code from FusionAuth. Also, this response includes a session id for the FusionAuth OAuth 2 interface as an HTTP cookie. This cookie is HttpOnly, which prevents JavaScript from accessing it, making it less vulnerable to theft
  9. The browser requests the application backend's OAuth redirect_uri with the authorization code from FusionAuth
  10. The application backend calls FusionAuth's OAuth 2 token endpoint with the authorization code and optionally the client_secret
  11. FusionAuth verifies the authorization code and client_secret. It returns a 200 along with a JWT and refresh token in JSON
  12. The application backend returns a redirect back to the single-page application. The JWT and refresh token from FusionAuth are written back to the browser in HTTP cookies. These cookies are HttpOnly, which prevents JavaScript from accessing them, making them less vulnerable to theft. Additionally, all requests from the browser to the application backend will include these cookies so that the backend can use them
  13. The browser requests the shopping cart single-page application from the application backend
  14. The application backend responds with the HTML, CSS & JavaScript of the application
  15. The browser loads the application and as part of the initialization process, it makes a request to the application backend to see if the user is logged in
  16. The application backend responds with a 200 and the User object (usually in JSON)
  17. The browser requests the user's shopping cart via AJAX from the application backend and includes the JWT and refresh token cookies
  18. The application backend verifies the JWT and then uses the JWT to identify the user. Once the user is identified, the backend looks up the user's shopping cart from the database (or similar location). Finally, the application backend returns the user's shopping cart contents (usually as JSON)
  19. A while later, the user's JWT expires and the user clicks on their shopping cart again. The browser requests the shopping cart from the application backend via AJAX and sends the JWT and refresh token to the application backend
  20. The application backend verifies the JWT and realizes it is expired. Since the browser also sent across the refresh token, the application backend calls the JWT refresh API in FusionAuth with the refresh token
  21. FusionAuth looks up the refresh token and returns a new JWT
  22. The application backend responds with the user's shopping cart contents (usually as JSON) that the browser renders. It also includes the new JWT as a cookie that replaces the old JWT in the browser
  23. A while later, the user's refresh token expires and the user clicks on their shopping cart again. The browser requests the shopping cart from the application backend via AJAX and sends the JWT and refresh token to the application backend
  24. The application backend verifies the JWT and realizes it is expired. Since the browser also sent across the refresh token, the application backend calls the JWT refresh API in FusionAuth with the refresh token
  25. Since the refresh token has expired, FusionAuth returns a 404 status code
  26. Since FusionAuth returned a 404 status code, the application backend returns a 401 that indicates the user is no longer logged in
  27. At this point, the application can allow the user to log in the same way they did above
  28. The browser requests the forum single-page application from the application backend. This is a standard SSO login that is fully supported by FusionAuth
  29. The application backend responds with the HTML, CSS & JavaScript of the application
  30. The browser loads the application and as part of the initialization process, it makes a request to the application backend to see if the user is logged in
  31. The application backend responds with a 404 indicating the user is not logged in
  32. The user clicks the login link and the browser navigates away from the single-page application to FusionAuth's OAuth 2 interface. The browser requests the OAuth 2 login page from FusionAuth with a response_type of code indicating that it is using the authorization code grant. Additionally, the session cookie that was set during the first login is also sent by the browser to FusionAuth
  33. FusionAuth realizes that the user already has a session and is already logged in. Therefore, it returns a redirect to the application backend's OAuth 2 redirect_uri. This redirect includes the authorization code from FusionAuth
  34. The browser requests the application backend's OAuth redirect_uri with the authorization code from FusionAuth
  35. The application backend calls FusionAuth's OAuth 2 token endpoint with the authorization code and optionally the client_secret
  36. FusionAuth verifies the authorization code and client_secret. It returns a 200 along with a JWT and refresh token in JSON. **NOTE**: all of this happens without any user interaction, hence the SSO nature of this login
  37. The application backend receives the 200 from FusionAuth. The application backend returns a redirect back to the single-page application. The JWT and refresh token from FusionAuth are written back to the browser in HTTP cookies. These cookies are HttpOnly, which prevents JavaScript from accessing them, making them less vulnerable to theft. Additionally, all requests from the browser to the application backend will include these cookies so that the backend can use them
  38. The browser requests the forums single-page application from the application backend
  39. The application backend responds with the HTML, CSS & JavaScript of the application
  40. The browser loads the application and as part of the initialization process, it makes a request to the application backend to see if the user is logged in
  41. The application backend responds with a 200 and the User object (usually in JSON)
  42. The browser requests the user's forum posts from the application backend via AJAX and includes the JWT and refresh token cookies
  43. The application backend verifies the JWT and then uses the JWT to identify the user. Once the user is identified, the backend looks up the user's forum posts from the database (or similar location). Finally, the application backend returns the user's forum posts that the browser renders (usually as JSON)
  44. This is an attack vector where the attacker has stolen the user's refresh token. Here, the attacker requests the user's shopping cart with the stolen refresh token and an invalid JWT
  45. The application backend verifies the JWT and realizes it is invalid. Since the attacker also sent across the refresh token, the application backend calls the JWT refresh API in FusionAuth with the refresh token
  46. FusionAuth looks up the refresh token and returns a new JWT
  47. The application backend uses the JWT to look up the user's shopping cart. It responds to the attacker with the user's shopping cart (usually as JSON). It also includes the new JWT as a cookie that attacker can now use
  48. This is an attack vector where the attacker has stolen the user's JWT. Here, the attacker requests the user's shopping cart with the stolen JWT
  49. The application backend verifies the JWT and then uses the JWT to identify the user. Once the user is identified, the backend looks up the user's shopping cart from the database (or similar location). Finally, the application backend returns the user's shopping cart to the attacker (usually as JSON)

Security considerations

This is one of the safest and most feature rich login workflow in FusionAuth. It has the benefit that passwords are only provided directly to FusionAuth. It also has the benefit of full SSO capabilities when the user is automatically logged into the forum application by FusionAuth. Finally, the JWT and refresh tokens are HttpOnly cookies that are domain locked to the application backend that needs them.

One downside to this workflow is that it causes the user to leave the single-page application and navigate to the FusionAuth OAuth interface. The effects of this are minimized as long as the browser caches the single-page application.

APIs used

Here are the FusionAuth APIs used in this example: