# Single-page apps

> Sign users in from React, Vue, Svelte or plain JavaScript with authorization code + PKCE, and call your API.

Source: https://9in.io/docs/single-page-apps/

A single-page app runs entirely in the browser, so it cannot keep a secret. It signs users in with the **authorization code flow and PKCE**, exchanges the code from the browser, and keeps the tokens in the browser. `@9in-io/sdk` does all of it; any certified OIDC library (oidc-client-ts, angular-auth-oidc-client…) works too.

## Set up the application

In the console, **Applications → Create application → Single-Page App**, then on its **Settings**:

| Setting | Example | Why |
|---|---|---|
| Allowed callback URLs | `https://app.acme.com/callback` | Where 9in may send the code. Exact match. |
| Allowed logout URLs | `https://app.acme.com/` | Where 9in may send users after sign-out (`post_logout_redirect_uri`). |
| Allowed web origins | `https://app.acme.com` | The browser calls `/oauth/token` itself; without the origin the request fails with a CORS error. |

Add `http://localhost:5173/callback` and `http://localhost:5173` too while you develop (plain http is allowed for localhost).

## Sign in

```ts
import { NineAuth } from "@9in-io/sdk";

export const auth = new NineAuth({
  issuer: import.meta.env.VITE_NINE_ISSUER,        // https://acme.9in.io
  clientId: import.meta.env.VITE_NINE_CLIENT_ID,   // app_…
  redirectUri: `${location.origin}/callback`,
  // scope: "openid profile email offline_access"   (default)
  // audience: "https://api.acme.com"               access tokens for your API
});

await auth.login({ returnTo: location.pathname });  // or auth.signup() for the sign-up form
```

`login()` accepts `prompt` (`"login"` to force re-authentication, `"none"` to check silently, `"consent"`, `"select_account"`), `loginHint` (pre-fills the email), `screen: "signup"`, `returnTo` and extra `params`.

## Handle the callback

```ts
// on /callback
const { user, returnTo } = await auth.handleRedirect();
router.replace(returnTo ?? "/");
```

`handleRedirect()` checks `state` and the issuer, exchanges the code with the PKCE verifier, stores the tokens and removes `code` from the address bar. If the user cancelled or something failed it throws a `NineError` with the OAuth error (for example `access_denied`).

## Use the session

```ts
auth.isAuthenticated();            // true with a usable access token or a refresh token
await auth.getUser();              // ID token claims: sub, email, email_verified, name, picture…
await auth.fetchUserInfo();        // fresh claims from /oauth/userinfo
await auth.getAccessToken();       // refreshed (and rotated) when it expires within a minute
await auth.fetch("/api/orders");   // fetch() with Authorization: Bearer …
await auth.logout({ returnTo: location.origin + "/" });   // revokes the refresh token, ends the 9in session
```

Tokens are kept in `localStorage` by default. Pass `storage` (anything with `getItem`, `setItem` and `removeItem`) to keep them elsewhere, for example in memory only.

## React

```tsx
import { createContext, useContext, useEffect, useState } from "react";
import type { User } from "@9in-io/sdk";
import { auth } from "./auth";

const UserContext = createContext<User | null | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null | undefined>(undefined);
  useEffect(() => {
    (async () => {
      if (location.pathname === "/callback") {
        const { returnTo } = await auth.handleRedirect();
        history.replaceState(null, "", returnTo ?? "/");
      }
      setUser(auth.isAuthenticated() ? await auth.getUser() : null);
    })().catch(() => setUser(null));
  }, []);
  return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}

export const useUser = () => useContext(UserContext);   // undefined while loading, null when signed out
```

## Call your API

Ask for tokens for your API with `audience` and its scopes, then send them with `auth.fetch()` (or `Authorization: Bearer ${await auth.getAccessToken()}`):

```ts
const auth = new NineAuth({
  issuer, clientId, redirectUri,
  audience: "https://api.acme.com",
  scope: "openid profile email offline_access orders:read",
});
```

Your API verifies them as described in [Protecting your API](https://9in.io/docs/protecting-apis/).

## Good to know

- **Single sign-on:** a user signed in to 9in from another app isn't asked again; `login({ prompt: "login" })` forces it.
- **Refresh tokens rotate:** each refresh returns a new refresh token. Reusing an old one revokes the whole family, which signs the user out everywhere that family was used, so don't share tokens between tabs by copying them; the SDK coordinates refreshes for you.
- **Sign-out everywhere:** `logout()` ends the 9in session too, so other apps using the same session are signed out on their next check.
