# Single page application

Implement login, token management, and logout in your single page application (SPA) using Authorization Code with PKCE. SPAs run entirely in the browser and cannot securely store a `client_secret`, so they use PKCE (Proof Key for Code Exchange) to protect the authorization flow. This guide covers initiating login from your SPA, exchanging authorization codes for tokens, managing sessions, and implementing logout.

:::tip
[**Check out the example apps on GitHub**](https://github.com/scalekit-inc/multiapp-demo) to see Web, SPA, Desktop, and Mobile apps sharing a single Scalekit session.
:::

## Prerequisites

Before you begin, ensure you have:

- A Scalekit account with an environment configured
- Your environment URL (`ENV_URL`), e.g., `https://yourenv.scalekit.com`
- A SPA registered in Scalekit with a `client_id` ([Create one](/authenticate/fsa/multiapp/manage-apps))
- At least one redirect URL configured in **Dashboard > Developers > Applications > [Your App] > Redirects**

## High-level flow

```d2
shape: sequence_diagram

User
"Single page app (browser)"
Scalekit

User -> "Single page app (browser)": Click "Login"
"Single page app (browser)" -> Scalekit: Redirect to /oauth/authorize \n (+ state + PKCE challenge)
Scalekit -> "Single page app (browser)": Redirect to /callback with code + state
"Single page app (browser)" -> Scalekit: POST /oauth/token
Scalekit -> "Single page app (browser)": access_token, refresh_token, id_token
"Single page app (browser)" -> "Single page app (browser)": Store tokens + continue
```

## Step-by-step implementation

1. ## Initiate login or signup

   Initiate login by redirecting the user to Scalekit's hosted login page. Include the PKCE code challenge in the authorization request to protect against authorization code interception attacks.

   ```sh
   <ENV_URL>/oauth/authorize?
     response_type=code&
     client_id=<CLIENT_ID>&
     redirect_uri=<CALLBACK_URL>&
     scope=openid+profile+email+offline_access&
     state=<RANDOM_STATE>&
     code_challenge=<PKCE_CODE_CHALLENGE>&
     code_challenge_method=S256
   ```

   Generate and store these values before redirecting:

   - `state` — Validate this on callback to prevent CSRF attacks
   - `code_verifier` — A cryptographically random string you keep locally
   - `code_challenge` — Derived from the verifier using S256 hashing; send this in the authorization URL
**Why PKCE is required for SPAs:** SPAs are public clients that cannot keep a `client_secret` secure because all code runs in the browser. PKCE protects against authorization code interception attacks where an attacker captures the authorization code from the redirect URI. Without PKCE, anyone who intercepts the code could exchange it for tokens.

   For detailed parameter definitions, see [Initiate signup/login](/authenticate/fsa/implement-login).

2. ## Handle the callback and complete login

   After authentication, Scalekit redirects the user back to your callback URL with an authorization `code` and the `state` you sent.

   Your callback handler must:

   - Validate the returned `state` matches what you stored — this confirms the response is for your original request
   - Handle any error parameters before processing
   - Exchange the authorization code for tokens by including the `code_verifier`

   ```sh
   POST <ENV_URL>/oauth/token
   Content-Type: application/x-www-form-urlencoded

   grant_type=authorization_code&
   client_id=<CLIENT_ID>&
   code=<CODE>&
   redirect_uri=<CALLBACK_URL>&
   code_verifier=<PKCE_CODE_VERIFIER>
   ```

   ```json
   {
     "access_token": "...",
     "refresh_token": "...",
     "id_token": "...",
     "expires_in": 299
   }
   ```
**Authorization codes expire after one use:** Authorization codes are single-use and expire quickly (approximately 10 minutes). If you attempt to reuse a code or it expires, start a new login flow to obtain a fresh authorization code.

3. ## Manage sessions and token refresh

   Store tokens and validate them on each request. When access tokens expire, use the refresh token to obtain new ones without requiring the user to authenticate again.

   **Token roles**

   - **Access token** — Short-lived token (default 5 minutes) for authenticated API requests
   - **Refresh token** — Long-lived token to obtain new access tokens
   - **ID token** — JWT containing user identity claims; required for logout

   Store tokens client-side based on your security requirements. See [Token storage security](#token-storage-security) for guidance on choosing the right storage mechanism.

   When an access token expires, request new tokens:

   ```sh
   POST <ENV_URL>/oauth/token
   Content-Type: application/x-www-form-urlencoded

   grant_type=refresh_token&
   client_id=<CLIENT_ID>&
   refresh_token=<REFRESH_TOKEN>
   ```

   Validate access tokens by verifying:

   - Token signature using Scalekit's public keys (JWKS endpoint)
   - `iss` matches your Scalekit environment URL
   - `aud` includes your `client_id`
   - `exp` and `iat` are valid timestamps

   Public keys for signature verification:

   ```sh
   <ENV_URL>/keys
   ```

4. ## Implement logout

   Clear your local session and redirect to Scalekit's logout endpoint to invalidate the shared session.

   Your logout action must:

   - Extract the ID token before clearing local storage
   - Clear locally stored tokens from memory or storage
   - Redirect the browser to Scalekit's logout endpoint

   ```sh
   <ENV_URL>/oidc/logout?
     id_token_hint=<ID_TOKEN>&
     post_logout_redirect_uri=<POST_LOGOUT_REDIRECT_URI>
   ```
**Logout must be a browser redirect:** Use a browser redirect to the `/oidc/logout` endpoint, not an API call. The redirect ensures Scalekit's session cookie is sent with the request, allowing Scalekit to identify and terminate the correct session. API calls from JavaScript do not include the session cookie.

## Handle errors

When authentication fails, Scalekit redirects to your callback URL with error parameters instead of an authorization code:

```sh showLineNumbers=false wrap
/callback?error=access_denied&error_description=User+denied+access&state=<STATE>
```

Check for errors before processing the authorization code:

- Check if the `error` parameter exists in the URL
- Log the `error` and `error_description` for debugging
- Display a user-friendly message
- Provide an option to retry login

Common error codes:

| Error | Description |
|-------|-------------|
| `access_denied` | User denied the authorization request |
| `invalid_request` | Missing or invalid parameters (e.g., invalid PKCE challenge) |
| `server_error` | Scalekit encountered an unexpected error |

## Token storage security

SPAs run entirely in the browser where tokens are vulnerable to cross-site scripting (XSS) attacks. An attacker who successfully injects malicious JavaScript can read tokens from any accessible storage and use them to impersonate the user.

Choose a storage strategy based on your security requirements:

| Storage | Security | Trade-off |
|---------|----------|-----------|
| Memory (JavaScript variable) | Most secure — not accessible to XSS | Tokens lost on page refresh; requires silent refresh |
| Session storage | Moderate — cleared when tab closes | Accessible to XSS; persists during session |
| Local storage | Least secure — persists across sessions | Accessible to XSS; long exposure window |

**Recommendations:**

- For high-security applications, store tokens in memory and use silent refresh (iframe-based token renewal) to maintain sessions across page loads
- Always sanitize user inputs and use Content Security Policy (CSP) headers to mitigate XSS attacks
- Never log tokens or include them in error messages
**Never store tokens in local storage for sensitive applications:** Local storage is accessible to any JavaScript running on your page. If an attacker exploits an XSS vulnerability, they can read all tokens from local storage and fully compromise user accounts. For applications handling sensitive data, prefer memory storage with silent refresh.

## What's next

- [Set up a custom domain](/guides/custom-domain) for your authentication pages
- [Add enterprise SSO](/authenticate/auth-methods/enterprise-sso/) to support SAML and OIDC with your customers' identity providers