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.
Last updated 2026-07-01
In this quickstart you’ll add AuthPI login to a server-side TypeScript application with @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)
- Node.js 18+
Step 1: Configure AuthPI resources
In the console:
- Note your issuer URL —
https://idp.authpi.com/{issuer_id}, or your custom domain. - 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 theauthorization_codegrant andcoderesponse type are enabled. - Copy the client ID and secret. The secret is shown once — store it as an environment variable.
- Create a test user in the issuer so you have someone to log in as.
Step 2: Install and initialize
npm install @authpi/idp
// 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 the returned object in the user’s session; the callback helper uses it for the CSRF, PKCE, and nonce checks.
app.get('/login', async (req, res) => {
const auth = await idp.createAuthorizationUrl({
scopes: ['openid', 'profile', 'email'],
});
req.session.oauth = auth;
res.redirect(auth.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
Validate the callback, then exchange the code for an authenticated agent — the SDK’s handle on the logged-in user. exchangeCallback checks state, uses the stored PKCE verifier, exchanges the code, and verifies the ID-token nonce when an ID token is present:
app.get('/callback', async (req, res) => {
if (!req.session.oauth) return res.status(400).send('Missing OAuth session');
const callbackUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
const agent = await idp.exchangeCallback(callbackUrl, req.session.oauth);
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):
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, so authorization checks are local — no extra API call:
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 for the exact timing guarantees.
Where to go next
- IdP SDK — TypeScript — the full client reference, including
client_credentialsfor machine-to-machine auth - Token claims reference — everything inside
agent.tokens - Manage and revoke sessions — listing sessions and logging users out everywhere
- Validate tokens in your API — verifying these tokens in your other services