Get started

Quickstart

Add sign-in to a web app in five minutes with the JavaScript SDK, then call your API with the user's token.

View .md llms.txt

This walks through signing users in to a browser app with @9in-io/sdk, then calling your own API with their access token. For a server-rendered app, follow Server-rendered apps instead; the console steps are the same.

#1. Create a tenant and an application

  1. Sign in to the console and create a tenant, say acme. You get two issuers:
    • production: https://acme.9in.io
    • sandbox: https://acme-sandbox.9in.io (separate users; any password works and one-time codes are always 000000)
  2. Go to Applications → Create application → Single-Page App and set:
    • Allowed callback URLs: http://localhost:5173/callback
    • Allowed logout URLs: http://localhost:5173/
    • Allowed web origins: http://localhost:5173 (the browser calls the token endpoint directly, so its origin must be allowed for CORS)
  3. Copy the Client ID (app_…). Single-page apps have no client secret.

#2. Install the SDK

bash
npm install @9in-io/sdk

#3. Sign in and out

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

export const auth = new NineAuth({
  issuer: "https://acme-sandbox.9in.io",     // the sandbox while you develop
  clientId: "app_7f3k2m9q",
  redirectUri: `${location.origin}/callback`,
});

// A "Sign in" button
document.querySelector("#login")!.addEventListener("click", () => auth.login());

// A "Create account" button opens the sign-up form directly
document.querySelector("#signup")!.addEventListener("click", () => auth.signup());

// On /callback: finish signing in, then go where the user was heading
if (location.pathname === "/callback") {
  const { user, returnTo } = await auth.handleRedirect();
  console.log(`Signed in as ${user.email}`);
  history.replaceState(null, "", returnTo ?? "/");
}

// Anywhere
if (auth.isAuthenticated()) {
  const user = await auth.getUser();   // claims from the ID token: sub, email, name, picture…
}

// Sign out of your app and of 9in
document.querySelector("#logout")!.addEventListener("click", () => auth.logout({ returnTo: location.origin + "/" }));

login() redirects to the hosted sign-in page with authorization code + PKCE. handleRedirect() checks state, exchanges the code and stores the tokens; by default they are kept in localStorage and refreshed automatically (refresh tokens rotate on every use).

Open your app, click Sign in, and sign in to the sandbox with any email and any password.

#4. Call your API

Tell 9in about your API, so access tokens can be issued for it:

  1. In the console, go to APIs & Scopes → Create API. Set the identifier to your API's URL, e.g. https://api.acme.com, and add a scope such as orders:read.
  2. Request tokens for it in the browser:
ts
const auth = new NineAuth({
  issuer: "https://acme-sandbox.9in.io",
  clientId: "app_7f3k2m9q",
  redirectUri: `${location.origin}/callback`,
  audience: "https://api.acme.com",
  scope: "openid profile email offline_access orders:read",
});

const res = await auth.fetch("https://api.acme.com/orders");   // adds Authorization: Bearer <access token>
  1. Verify the token in your API (Node, Workers, Deno or Bun):
ts
import { verifyAccessToken } from "@9in-io/sdk/server";

export async function handle(request: Request) {
  const bearer = request.headers.get("authorization")?.replace(/^Bearer /i, "");
  const claims = await verifyAccessToken(bearer, {
    issuer: "https://acme-sandbox.9in.io",
    audience: "https://api.acme.com",
    scope: "orders:read",
  }); // throws NineError (invalid_token, insufficient_scope) otherwise
  return Response.json({ orders: await listOrders(claims.sub) });
}

verifyAccessToken checks the signature against your tenant's public keys (cached), the issuer, audience, expiry and scopes. To also notice revoked tokens within seconds, see Protecting your API.

#5. Go to production

  • Add your real URLs to the application (callback, logout and web origin).
  • Switch the issuer to https://acme.9in.io, or to your custom domain.
  • Users, sessions and activity in production are separate from the sandbox.

#Next