Build your app

Single-page apps

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

View .md llms.txt

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:

SettingExampleWhy
Allowed callback URLshttps://app.acme.com/callbackWhere 9in may send the code. Exact match.
Allowed logout URLshttps://app.acme.com/Where 9in may send users after sign-out (post_logout_redirect_uri).
Allowed web originshttps://app.acme.comThe 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.

#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.