Login & Auth Workflows

Single-Page Application OAuth Login Using Implicit Grant With JWTs In Local Storage

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)Local storage dropShopping cart loadJWT expiresAutomatic re-login (since session exists)Local storage dropShopping cart loadSSO login to forumsInitializeLogin (browser navigates away from SPA but auto-logs-in since the session exists)Local storage dropForums loadAttack vectorsStolen JWTGET /1(SPA HTML, CSS & JavaScript)2Check local storage forJWT3GET /oauth2/authorize {response_type=token}4(Login form HTML)5POST /oauth2/authorize (response_type=token)6302 Location: {redirect_uri w/ JWT in redirect URI and no refresh token}[SessionId HttpOnly w/ domain: example.fusionauth.io]7GET {redirect_uri w/o JWT in URI}8(SPA HTML, CSS & JavaScript)9Move JWT from URIto local storage10AJAX GET /api/load-shopping-cart(JWT from local storage)11(Shopping cart contents)12GET /oauth2/authorize {response_type=token}[SessionId HttpOnly w/ domain: example.fusionauth.io]13302 Location: {redirect_uri w/ JWT in redirect URI and no refresh token}[SessionId HttpOnly w/ domain: example.fusionauth.io]14GET {redirect_uri w/o JWT in URI}15(SPA HTML, CSS & JavaScript)16Move JWT from URIto local storage17AJAX GET /api/load-shopping-cart(JWT from local storage)18(Shopping cart contents)19GET /[No cookies]20(SPA HTML, CSS & JavaScript)21Check local storage forJWT22GET /oauth2/authorize {response_type=token}[SessionId HttpOnly w/ domain: example.fusionauth.io]23302 Location: {redirect_uri w/ JWT in redirect URI and no refresh token}[SessionId HttpOnly w/ domain: example.fusionauth.io]24GET {redirect_uri w/o JWT in URI}25(SPA HTML, CSS & JavaScript)26Move JWT from URIto local storage27AJAX GET /api/load-load-posts(JWT from local storage)28(Forum posts)29GET /api/load-shopping-cart(JWT)30(Shopping cart contents)31BrowserStoreForumsFusionAuthHacker

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 checks if there is a valid JWT in local storage. In this case, there isn't
  4. 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 token indicating that it is using the implicit grant
  5. FusionAuth responds with the HTML, CSS & JavaScript of the login page (including the form)
  6. The user inputs their credentials and clicks the submit button. The browser POSTs the form data to FusionAuth
  7. FusionAuth returns a redirect to the application backend's OAuth 2 redirect_uri. This redirect includes the access token (in our case a JWT) 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
  8. The browser requests the application backend's OAuth redirect_uri. This request does not include the JWT because it is after the # in the URL, which means the browser will not send it in the HTTP request to the application backend
  9. The application backend responds with the HTML, CSS & JavaScript of the application. During this step, the browser will initialize the single-page application. As part of the initialization of the application, the JWT will be pulled from the current URL of the browser
  10. The application running in the browser moves the JWT from the URI to local storage
  11. The browser requests the user's shopping cart via AJAX from the application backend and includes the JWT from local storage
  12. 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)
  13. A while later, the user's JWT expires and the application realizes it is expired. The application might automatically redirect the browser back to FusionAuth's OAuth 2 interface or wait until the user clicks the login link. Regardless, the browser requests the OAuth 2 login page from FusionAuth with a response_type of token indicating that it is using the implicit grant
  14. 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 a new JWT from FusionAuth in the URI
  15. The browser requests the application backend's OAuth redirect_uri. This request does not include the JWT because it is after the # in the URL, which means the browser will not send it in the HTTP request to the application backend
  16. The application backend responds with the HTML, CSS & JavaScript of the application. During this step, the browser will initialize the single-page application. As part of the initialization of the application, the JWT will be pulled from the current URL of the browser
  17. The application running in the browser moves the JWT from the URI to local storage
  18. The browser requests the user's shopping cart via AJAX from the application backend and includes the JWT from local storage
  19. 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)
  20. The browser requests the forums single-page application from the application backend. This is a standard SSO login that is fully supported by FusionAuth
  21. The application backend responds with the HTML, CSS & JavaScript of the application
  22. The browser loads the application and as part of the initialization process, it checks if there is a valid JWT in local storage. In this case, there isn't
  23. The application might automatically redirect the browser back to FusionAuth's OAuth 2 interface or wait until the user clicks the login link. Regardless, the browser requests the OAuth 2 login page from FusionAuth with a response_type of token indicating that it is using the implicit grant
  24. 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 a new JWT from FusionAuth in the URI
  25. The browser requests the application backend's OAuth redirect_uri. This request does not include the JWT because it is after the # in the URL, which means the browser will not send it in the HTTP request to the application backend
  26. The application backend responds with the HTML, CSS & JavaScript of the application. During this step, the browser will initialize the single-page application. As part of the initialization of the application, the JWT will be pulled from the current URL of the browser
  27. The application running in the browser moves the JWT from the URI to local storage
  28. The browser requests the user's forum posts from the application backend via AJAX and includes the JWT cookie
  29. 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)
  30. 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
  31. 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 workflow is less secure than other workflows because it is storing the user’s JWT in local storage. While local storage provides convenient storage for single-page applications, any JavaScript running on the page has access to it. If an attacker can inject JavaScript into the page, they can begin stealing user’s JWTs. The attacker might introduce JavaScript into an open source project through obfuscated code or through a backend exploit of some kind. Many platforms like Wordpress also allow plugins to add JavaScript includes to websites as well. Therefore, ensuring that your JavaScript is secure can be extremely difficult.

This workflow might still be a good solution for some applications. Developers should just weigh the risks associated with JWTs accessible to JavaScript versus the other workflows we have documented.

Additionally, since this workflow does not use refresh tokens (and cannot use refresh tokens according to the specification). Therefore, when the user’s session expires, they will need to log into the application again. This could be an automatic login, but it still requires the browser to take the user to the FusionAuth OAuth interface.

APIs used

Here are the FusionAuth APIs used in this example: