Login & Auth Workflows
Single-Page Application Native Login To FusionAuth With JWTs And Refresh Tokens In Local Storage
By Brian Pontarelli
This workflow example is used by single-page applications using a native login form inside the webapp. This login form uses an AJAX
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
[] --> cookiessequenceDiagram autonumber participant Browser participant Store participant Forums participant FusionAuth participant Hacker Note over Browser,Hacker: Initialize Browser->>Store: GET / Store->>Browser: (SPA HTML, CSS & JavaScript) Browser->>Browser: Check local storage for JWT Note over Browser,Hacker: Login (inside SPA) Browser->>Browser: Render login form Browser->>FusionAuth: AJAX POST /api/login FusionAuth->>Browser: (JWT)<br/>[Refresh token HttpOnly w/ domain: example.fusionauth.io] Note over Browser,Hacker: Local storage drop Browser->>Browser: Move JWT from response body to local storage Note over Browser,Hacker: Shopping cart load Browser->>Store: AJAX GET /api/load-shopping-cart<br/>(JWT from local storage) Store->>Browser: (Shopping cart contents) Note over Browser: JWT expires Note over Browser,Hacker: Token refresh Browser->>FusionAuth: AJAX POST /api/jwt/refresh<br/>[Refresh token HttpOnly w/ domain example.fusionauth.io] FusionAuth->>Browser: (JWT) Note over Browser,Hacker: Local storage drop Browser->>Browser: Move JWT from response body to local storage Note over Browser,Hacker: Shopping cart load Browser->>Store: AJAX GET /api/load-shopping-cart<br/>(JWT from local storage) Store->>Browser: (Shopping cart contents) Note over Browser: Refresh token expires Note over Browser,Hacker: Re-login Browser->>FusionAuth: AJAX POST /api/jwt/refresh<br/>[Refresh token w/ domain example.fusionauth.io] FusionAuth->>Browser: 404 Missing Browser->>Browser: Login same as above Note over Browser,Hacker: SSO login to forums - not provided by FusionAuth for this workflow Note over Browser,Hacker: Initialize Browser->>Forums: GET /<br/>[No cookies] Forums->>Browser: (SPA HTML, CSS & JavaScript) Browser->>Browser: Check local storage for JWT Note over Browser,Hacker: Login (inside SPA) Browser->>Browser: Render login form Browser->>FusionAuth: AJAX POST /api/login<br/>[Refresh token HttpOnly w/ domain: example.fusionauth.io - FOR WRONG APP] FusionAuth->>Browser: (JWT)<br/>[New Refresh token HttpOnly w/ domain: example.fusionauth.io] Browser->>Browser: Refresh token cookie from Store gets<br/>clobbered by refresh token for Forums Note over Browser,Hacker: Local storage drop Browser->>Browser: Move JWT from response body to local storage Note over Browser,Hacker: Forums load Browser->>Forums: AJAX GET /api/load-load-posts<br/>(JWT from local storage) Forums->>Browser: (Forum posts) Note over Browser,Hacker: Attack vectors Note over Browser,Hacker: Stolen refresh token Hacker->>FusionAuth: POST /api/jwt/refresh<br/>[Refresh token w/ domain example.fusionauth.io] FusionAuth->>Hacker: (JWT) Hacker->>Store: GET /api/load-shopping-cart<br/>(JWT from response body) Store->>Hacker: (Shopping cart contents) Note over Browser,Hacker: Stolen JWT Hacker->>Store: GET /api/load-shopping-cart<br/>(JWT) Store->>Hacker: (Shopping cart contents)
Explanation
- The browser requests the shopping cart single-page application from the application backend
- The application backend responds with the HTML, CSS & JavaScript of the application
- 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
- The application renders the login form
- The user inputs their credentials and clicks the submit button. The browser AJAX
POSTs the form data directly to the Login API in FusionAuth - FusionAuth returns a 200 status code stating that the credentials were okay. It also returns a JWT in JSON and a refresh token cookie with the domain of the FusionAuth server (which could be different than the application backend)
- The application running in the browser moves the JWT from the JSON response to local storage
- The browser requests the user's shopping cart via AJAX from the application backend and includes the JWT from local storage
- 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)
- A while later, the user’s JWT expires and the user clicks on their shopping cart again. The browser recognizes that the JWT has expired and makes a request directly to the JWT refresh API in FusionAuth. This request includes the refresh token cookie
- FusionAuth looks up the refresh token and returns a new JWT
- The application running in the browser moves the JWT from the JSON response to local storage
- The browser requests the user's shopping cart via AJAX from the application backend and includes the JWT from local storage
- 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)
- A while later, the user’s refresh token expires and the user clicks on their shopping cart again. The browser recognizes that the JWT has expired and makes a request directly to the JWT refresh API in FusionAuth. This request includes the refresh token cookie
- Since the refresh token has expired, FusionAuth returns a 404 status code
- At this point, the application can allow the user to log in the same way they did above
- The browser requests the forums single-page application from the application backend. This is a standard SSO login, but because of the way this workflow manages cookies and identities, FusionAuth does not provide SSO capabilities automatically
- The application backend responds with the HTML, CSS & JavaScript of the application
- 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
- The application renders the login form
- The user inputs their credentials and clicks the submit button. The browser AJAX
POSTs the form data directly to the Login API in FusionAuth. The refresh token cookie from the Store application is sent to FusionAuth here as well. **NOTE** this refresh token cookie is for the wrong application - FusionAuth returns a 200 status code stating that the credentials were okay. It also returns a JWT in JSON and a refresh token cookie with the domain of the FusionAuth server (which could be different than the application backend)
- The browser updates the cookie that stores the refresh token to the new cookie value for the forums. This clobbers the refresh token for the store and will force the user to log into the store next time they open that application
- The application running in the browser moves the JWT from the JSON response to local storage
- The browser requests the user's forum posts via AJAX from the application backend and includes the JWT from local storage
- 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 (usually as JSON)
- This is an attack vector where the attacker has stolen the user's refresh token. Here, the attacker can request directly to the JWT refresh API in FusionAuth since it is the same request the browser is making. The attacker includes the refresh token cookie in the request
- FusionAuth looks up the refresh token and returns a new JWT
- The attacker requests the user's shopping cart with the JWT
- 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)
- 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
- 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 tokens (JWTs and refresh tokens). 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 local storage of JWTs versus the other workflows we have documented.
APIs used
Here are the FusionAuth APIs used in this example: