> Markdown version of https://authpi.com/docs/quickstarts/typescript-backend/ — fetch the complete AuthPI docs index at https://authpi.com/llms.txt to discover all available pages.

# Server-side auth with the TypeScript SDK

Authenticate users in a Node.js backend with @authpi/idp — authorization-code flow with PKCE, callback handling, session refresh, and per-organization authorization, step by step.

In this quickstart you'll add AuthPI login to a server-side TypeScript application with [`@authpi/idp`](https://www.npmjs.com/package/@authpi/idp): redirect users to your hosted login page, exchange the callback code for tokens, keep the session fresh across requests, and make per-organization authorization decisions. Examples use Express, but the SDK is framework-agnostic and also runs on Bun, Deno, and Cloudflare Workers.

**Prerequisites:**

- An AuthPI account with an issuer ([sign up here](https://console.authpi.com/signup?ref=/docs/quickstarts/typescript-backend))
- Node.js 18+

## Step 1: Configure AuthPI resources

In the [console](https://console.authpi.com):

1. **Note your issuer URL** — `https://idp.authpi.com/{issuer_id}`, or your custom domain.
2. **Register a client** with application type "Web" (confidential). Add a redirect URI that exactly matches your callback route — for local development, `http://localhost:3000/callback`. Ensure the `authorization_code` grant and `code` response type are enabled.
3. **Copy the client ID and secret.** The secret is shown once — store it as an environment variable.
4. **Create a test user** in the issuer so you have someone to log in as.

## Step 2: Install and initialize

```bash
npm install @authpi/idp
```

```ts
// auth.ts
import { IdpClient } from '@authpi/idp';

export const idp = new IdpClient({
  issuerUrl: process.env.AUTHPI_ISSUER_URL!,   // https://idp.authpi.com/{issuer_id}
  clientId: process.env.AUTHPI_CLIENT_ID!,
  clientSecret: process.env.AUTHPI_CLIENT_SECRET!,
  redirectUri: 'http://localhost:3000/callback',
});
```

## Step 3: The login route

`createAuthorizationUrl` builds the hosted-login URL and generates the PKCE verifier, `state`, and `nonce`. Stash them in the user's session — the callback needs `state` (CSRF check) and `codeVerifier` (PKCE) — then redirect:

```ts
app.get('/login', async (req, res) => {
  const { url, codeVerifier, state, nonce } = await idp.createAuthorizationUrl({
    scopes: ['openid', 'profile', 'email'],
  });

  req.session.oauth = { codeVerifier, state, nonce };
  res.redirect(url);
});
```

The user lands on your issuer's hosted login page (brandable per issuer), authenticates, and comes back to your callback with a one-time code.

## Step 4: The callback

Verify the `state` matches what you stored, then exchange the code (with the PKCE verifier) for an authenticated **agent** — the SDK's handle on the logged-in user. The exchange goes directly to the token endpoint over TLS, and PKCE binds it to the verifier from Step 3:

```ts
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
  if (!req.session.oauth || state !== req.session.oauth.state) {
    return res.status(400).send('State mismatch');
  }

  const agent = await idp.exchangeCode(String(code), req.session.oauth.codeVerifier);

  req.session.tokens = agent.tokens;   // persist tokens; never expose them to the browser
  delete req.session.oauth;
  res.redirect('/dashboard');
});
```

## Step 5: Authenticate requests

On subsequent requests, rebuild the agent from the stored tokens. `createAgent` refreshes expired access tokens automatically — persist the rotated tokens in `onRefresh` (refresh tokens are single-use):

```ts
async function requireAuth(req, res, next) {
  if (!req.session.tokens) return res.redirect('/login');

  try {
    req.agent = await idp.createAgent(req.session.tokens, {
      onRefresh: async (newTokens) => {
        req.session.tokens = newTokens;
      },
      onRefreshError: async () => {
        req.session.destroy(() => {});
      },
    });
    next();
  } catch {
    res.redirect('/login');
  }
}

app.get('/dashboard', requireAuth, (req, res) => {
  res.json({ user: req.agent.profile });
});
```

## Step 6: Authorize with organizations

The agent carries the user's organization memberships from the [`organizations` claim](/docs/concepts/multi-org-tokens), so authorization checks are local — no extra API call:

```ts
app.post('/orgs/:orgId/projects', requireAuth, (req, res) => {
  if (!req.agent.hasAccessIn(req.params.orgId, 'write', 'projects')) {
    return res.status(403).json({ error: 'insufficient_scope' });
  }
  // create the project ...
});
```

Membership changes (added, removed, role changed, organization suspended) are recomputed at every token refresh — see [the propagation contract](/docs/guides/org-lifecycle) for the exact timing guarantees.

## Where to go next

- [IdP SDK — TypeScript](/docs/sdks/idp-typescript) — the full client reference, including `client_credentials` for machine-to-machine auth
- [Token claims reference](/docs/reference/token-claims) — everything inside `agent.tokens`
- [Manage and revoke sessions](/docs/guides/session-management) — listing sessions and logging users out everywhere
- [Validate tokens in your API](/docs/guides/validate-tokens) — verifying these tokens in your other services