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.
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:
- Reads the
Authorization: Bearer <token>header. - Calls Syntropy Auth's userinfo endpoint (
GET /oidc/userinfo) with the token. - If valid, populates
req.syntropyUserwith the returned claims and callsnext(). - If the token is missing or invalid,
req.syntropyUseris leftundefined. OnlyrequireSyntropyAuthblocks the request —syntropyAuthalone always continues.
The userinfo endpoint validates the token's signature and expiry server-side. No local JWT verification is needed.
Configuration options
| Option | Type | Required | Description |
|---|---|---|---|
baseUrl | string | ✓ | Base URL of the Syntropy Auth service |
clientId | string | ✓ | OAuth client ID registered in the dashboard |
clientSecret | string | OAuth 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.
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.