npm.io
0.10.10 • Published 1h ago

@authagonal/bff

Licence
MIT
Version
0.10.10
Deps
1
Size
115 kB
Vulns
0
Weekly
0
Stars
4

@authagonal/bff

Backend-for-Frontend (BFF) for SPAs that authenticate with Authagonal, for Node — Express and Next.js.

Your React/Vue/Svelte app should never hold access or refresh tokens: anything in JS-reachable storage is exposed to XSS. This package is a confidential OIDC client you run on your own backend. It runs the authorization-code + PKCE flow server-side, keeps the tokens in a server-side session, and gives the browser nothing but an httpOnly session cookie — the pattern the IETF OAuth 2.0 for Browser-Based Apps BCP recommends. It's the Node twin of the .NET Authagonal.Bff package and speaks the same protocol.

npm install @authagonal/bff

Express

import express from 'express';
import { authagonalBff } from '@authagonal/bff/express';

const app = express();
app.set('trust proxy', 1); // if behind a reverse proxy / ingress

app.use(authagonalBff({
  authority:     'https://acme.authagonal.io',   // your tenant auth host
  clientId:      process.env.BFF_CLIENT_ID!,
  clientSecret:  process.env.BFF_CLIENT_SECRET!,
  scope:         ['openid', 'profile', 'email', 'offline_access'], // offline_access enables refresh
  cookieSecret:  process.env.BFF_COOKIE_SECRET!, // used to encrypt the session cookie
  postLogoutRedirectUri: 'https://app.acme.com/',
}));

app.use(express.static('dist')); // your SPA
app.listen(3000);

Next.js (App Router)

app/bff/[...bff]/route.ts:

import { createBffRoute } from '@authagonal/bff/next';

export const { GET, POST } = createBffRoute({
  authority:    'https://acme.authagonal.io',
  clientId:     process.env.BFF_CLIENT_ID!,
  clientSecret: process.env.BFF_CLIENT_SECRET!,
  scope:        ['openid', 'profile', 'email', 'offline_access'],
  cookieSecret: process.env.BFF_COOKIE_SECRET!,
  postLogoutRedirectUri: 'https://app.acme.com/',
});

export const runtime = 'nodejs'; // server-side session + client secret

Endpoints (mounted under /bff)

Route Purpose
GET /bff/login?returnUrl=/ Start login; redirects to Authagonal.
GET /bff/callback OIDC redirect URI (handled for you).
GET /bff/user { isAuthenticated, claims, sessionExpiresAt }. Requires the anti-forgery header.
GET|POST /bff/logout Ends the session locally + at Authagonal.
POST /bff/backchannel-logout OIDC back-channel logout consumer (kills sessions).

Register a BFF client in the Authagonal portal (confidential + PKCE + offline_access) with redirect URI https://app.acme.com/bff/callback and post-logout redirect https://app.acme.com/. For subject-wide "log out everywhere", register it with BackChannelLogoutSessionRequired=false.

From the browser

Every non-navigation call must carry a static anti-forgery header (defends against CSRF alongside SameSite=Lax):

const me = await fetch('/bff/user', { headers: { 'X-Authagonal-Bff': '1' } }).then(r => r.json());
if (!me.isAuthenticated) location.href = '/bff/login?returnUrl=' + encodeURIComponent(location.pathname);

Log in / out by navigating (not fetching): location.href = '/bff/login' / '/bff/logout'.

Sessions & scaling

Sessions default to an in-memory store, fine for a single instance. Pass a shared sessionStore (implement IBffSessionStore, e.g. over Redis) to run more than one instance.

Extension points (the hosted seam)

sessionStore (IBffSessionStore), cookieProtector (ICookieProtector), and the core OidcClient are all replaceable. See docs/bff.md in the authagonal-cloud repo for the full protocol contract.