Skip to main content

Express Adapter

@eclosion-tech/syntropy-auth-express provides Express middleware that validates Bearer tokens issued by Syntropy Auth and populates req.syntropyUser with the verified user claims.

This is designed for backend API services — not browser-facing apps. For Next.js, use the Next.js adapter instead.

No CORS configuration needed

Because this adapter runs server-to-server (your Express backend → Syntropy Auth), there are no browser CORS restrictions. You do not need to add your server's IP or hostname to the Syntropy Auth allowed origins list. Allowed origins only apply to browser-originated requests.

npm install @eclosion-tech/syntropy-auth-express

Basic usage

import express from "express";
import { syntropyAuth, requireSyntropyAuth } from "@eclosion-tech/syntropy-auth-express";

const app = express();

const authConfig = {
baseUrl: process.env.SYNTROPY_AUTH_URL!, // e.g. "https://auth.syntropy.chat"
clientId: process.env.SYNTROPY_AUTH_CLIENT_ID!,
};

// Validate the Bearer token on every request (populates req.syntropyUser)
app.use(syntropyAuth(authConfig));

// Public route — user may or may not be authenticated
app.get("/public", (req, res) => {
res.json({ user: req.syntropyUser ?? null });
});

// Protected route — 401 if no valid token
app.get("/private", requireSyntropyAuth(authConfig), (req, res) => {
res.json({ user: req.syntropyUser });
});

TypeScript augmentation

The middleware extends the Express Request type. If you are using TypeScript, add the following declaration to your project:

// src/types/express.d.ts
import type { SyntropyUserInfo } from "@eclosion-tech/syntropy-auth";

declare global {
namespace Express {
interface Request {
syntropyUser?: SyntropyUserInfo;
}
}
}

How it works

On each request the middleware:

  1. Reads the Authorization: Bearer <token> header.
  2. Calls Syntropy Auth's userinfo endpoint (GET /oidc/userinfo) with the token.
  3. If valid, populates req.syntropyUser with the returned claims and calls next().
  4. If the token is missing or invalid, req.syntropyUser is left undefined. Only requireSyntropyAuth blocks the request — syntropyAuth alone always continues.

The userinfo endpoint validates the token's signature and expiry server-side. No local JWT verification is needed.

Configuration options

OptionTypeRequiredDescription
baseUrlstringBase URL of the Syntropy Auth service
clientIdstringOAuth client ID registered in the dashboard
clientSecretstringOAuth client secret (only needed for confidential clients)

User object shape

interface SyntropyUserInfo {
sub: string; // stable Syntropy Auth user ID
email: string;
email_verified: boolean;
name?: string;
preferred_username?: string;
given_name?: string;
family_name?: string;
updated_at?: number;
org?: {
id: string; // owner ID (org or project UUID)
pool_id: string; // user pool isolation boundary
owner_type: "org" | "project";
name: string; // human-readable pool name
};
}

Protecting individual routes

Use requireSyntropyAuth as route-level middleware:

import { requireSyntropyAuth } from "@eclosion-tech/syntropy-auth-express";

router.get("/orders", requireSyntropyAuth, async (req, res) => {
const userId = req.syntropyUser!.sub;
const orders = await db.getOrdersForUser(userId);
res.json(orders);
});

Using with organization-scoped tokens

If the access token was issued with the org scope, req.syntropyUser.org will contain the user pool identity. Use this to enforce organization or project-level access control:

app.get("/org-data", requireSyntropyAuth, (req, res) => {
const org = req.syntropyUser!.org;
if (!org) {
return res.status(403).json({ error: "Token missing org scope" });
}
// org.id — owner UUID (org or project)
// org.pool_id — user pool boundary
// org.owner_type — "org" or "project"
// ... fetch scoped data
});

Core SDK (@eclosion-tech/syntropy-auth)

For environments that are neither Next.js nor Express (Workers, Deno, Bun, custom servers), use the framework-agnostic core SDK directly:

npm install @eclosion-tech/syntropy-auth
import { SyntropyAuthClient } from "@eclosion-tech/syntropy-auth";

const auth = new SyntropyAuthClient({
baseUrl: "https://auth.syntropy.chat",
clientId: "my-client-id",
clientSecret: "my-client-secret",
redirectUri: "https://myapp.com/callback",
});

// 1. Start login — redirect the user to this URL
const { url, codeVerifier, state } = await auth.createAuthorizationUrl();
// Store codeVerifier and state in the user's session

// 2. Handle the callback — exchange code for tokens
const tokens = await auth.exchangeCode({
code: req.query.code,
codeVerifier, // retrieved from session
});

// 3. Fetch user info
const user = await auth.getUserInfo(tokens.access_token);

// 4. Refresh when needed
const refreshed = await auth.refreshToken({ refreshToken: tokens.refresh_token });

// 5. Revoke tokens on logout
await auth.revokeToken(tokens.access_token, "access_token");

// 6. Build end-session URL
const logoutUrl = auth.getLogoutUrl("https://myapp.com");

The core SDK handles PKCE (code verifier + challenge generation) automatically. All methods use the standard fetch API, making it compatible with any modern JS runtime.

CORS when using the core SDK in the browser

If you use the core SDK directly in a browser (e.g. a Vite/SPA app), your site's origin must be registered in the Syntropy dashboard under Settings → Auth Clients → Configure → Allowed Origins. Otherwise the browser will block requests to the auth service.

The OIDC discovery endpoint (GET /.well-known/openid-configuration) and JWKS endpoint are always CORS-open. All other endpoints (token, userinfo, etc.) require an explicit origin registration.