Get started

Express

Callback, session middleware, route protection and sign-out for Express apps.

A complete server#

server.tstype-checked

// server.ts — an Express app
import express from 'express';
import {
  authMiddleware,
  handleCallback,
  handleSignOut,
  requireAuth,
  requirePermission,
  verifyWebhook,
  WebhookVerificationError,
} from '@3een/auth/express';
import type { AuthConfig } from '@3een/auth';

const config: AuthConfig = {
  clientId: process.env.AUTH_CLIENT_ID!,
  apiKey: process.env.AUTH_API_KEY!, // secret: server only
  apiUrl: process.env.AUTH_API_URL!,
  redirectUri: 'http://localhost:3000/dashboard',
};

const app = express();

// Webhooks first, with the raw body: the signature covers the exact bytes.
app.post('/webhooks/3een', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = verifyWebhook(req.body.toString('utf8'), req.header('3een-Signature'), process.env.AUTH_WEBHOOK_SECRET!);
    console.info('3een event', event.type);
    res.json({ received: true });
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      res.status(400).json({ error: error.message });
      return;
    }
    throw error;
  }
});

// The hosted page returns here with ?code=… (register this exact URL as a
// redirect URI). The code is redeemed with your API key, the session cookies
// are set, and the browser goes on to a same-origin redirect_uri or
// config.redirectUri.
app.get('/auth/callback', handleCallback(config));
app.post('/auth/sign-out', handleSignOut(config));

// Loads req.user for every request; answers 401 on non-public routes.
app.use(authMiddleware({ ...config, publicRoutes: ['/', '/auth/*', '/webhooks/*'] }));

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

// Verifies the token against the environment's keys itself: 401 without a
// session, 403 without the permission; the claims are left on req.auth.
app.delete('/posts/:id', requirePermission(config, 'posts:delete'), (_req, res) => {
  res.status(204).end();
});
app.get('/admin', requirePermission(config, { role: 'admin' }), (_req, res) => {
  res.json({ ok: true });
});

// A standalone check that does not need authMiddleware to have run.
app.get('/api/me', requireAuth(config), (req, res) => {
  res.json({ email: req.user?.email });
});

app.listen(3000);

The sign-in callback#

handleCallback(config) is the route the hosted page returns to. Register its full URL (here http://localhost:3000/auth/callback) as a redirect URI. It redeems ?code= with your API key, bound to that exact URL, sets the session cookies and redirects to a same-origin redirect_uri, or to config.redirectUri. A legacy ?token= is accepted only if it verifies against your environment's keys.

Protecting routes#

  • authMiddleware(options) loads req.user from the API for every request and answers 401 on routes outside publicRoutes.
  • requireAuth used bare expects authMiddleware to have run; requireAuth(config) checks the session itself.
  • requirePermission(config, …checks) verifies the token locally against the environment's keys: 401 without a session, 403 with { error, missing } without the permission. The claims are left on req.auth.

Sign-out#

handleSignOut(config) revokes the session with the API (a failure there is logged, not surfaced), clears the cookie and answers { success: true }.