pasbydocs
OIDC API

Next.js integration

@finsel-dgi/pasby-next — OIDC login routes, handshake, and getEID for App Router.

@finsel-dgi/pasby-next wraps the full OIDC sequence for Next.js 14+ App Router: PKCE, session start, handshake callback, encrypted cookies, and claim fetch.

npm: @finsel-dgi/pasby-next · Repository: github.com/Finsel-DGI/pasby-nextjs

Underlying HTTP contract: OIDC quickstart.


Install

npm install @finsel-dgi/pasby-next

Peer dependencies: next 14+, react 18+, axios.

If you link the package from a monorepo, add to next.config:

transpilePackages: ["@finsel-dgi/pasby-next"],

Environment variables

VariablePurpose
PASBY_CLIENT_SECRETApp secret (x-access-secret)
PASBY_CONSUMER_KEYOrganisation API key (x-api-key)
PASBY_CLIENT_IDApp id (resource body)
SECRET_GENSymmetric key for JWE around PKCE verifier + access token cookies
PASBY_LOGIN_REDIRECTFallback path after handshake if state cookie is missing
PASBY_LOGOUT_REDIRECTTarget after logout (default /)

Register OAuth callback in Console as:

https://<your-host>/api/eid/handshake


1. API route

Create a catch-all route that handles login, handshake, and logout:

// app/api/eid/[auth]/route.ts
import { handler } from "@finsel-dgi/pasby-next/server";
import { NextRequest } from "next/server";

const pasbyHandler = handler(
  {
    claims: ["naming.given", "naming.family", "contact.email"],
    action: "login",
    payload: "Sign in to your app",
  },
  "/auth/error",
);

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ auth: string }> },
) {
  return pasbyHandler(request, { params: await params });
}
PathPhase
GET /api/eid/loginStarts OIDC session (start session), redirects to pasby
GET /api/eid/handshakeExchange + sets session cookies
GET /api/eid/logoutClears cookies, redirects to PASBY_LOGOUT_REDIRECT

2. Login button

"use client";

import { LoginButton } from "@finsel-dgi/pasby-next";

export function AuthBar() {
  return (
    <LoginButton
      variant="dark"
      action="login"
      fallbackPath="/dashboard"
    />
  );
}
PropDescription
fallbackPathPost-handshake redirect (URL-encoded as state)
actionlogin or identify
variantoriginal | light | dark | darktext
tenantIdRequired when using createPasbyHandler (multi-tenant)

The button navigates to:

GET /api/eid/login?redirect=false&state=<encodeURIComponent(fallbackPath)>


3. Read the signed-in user

// app/dashboard/page.tsx
import { getEID } from "@finsel-dgi/pasby-next/server";
import { cookies } from "next/headers";

export default async function Dashboard() {
  const user = await getEID(await cookies());

  if (!user) {
    return <p>Not signed in</p>;
  }

  return (
    <div>
      <p>NIN: {user.national}</p>
      <p>Email: {user.claims?.contact?.email}</p>
    </div>
  );
}

getEID calls resource using cookies set at handshake. Returns User from @finsel-dgi/pasby-react.


4. Logout

"use client";
import { useRouter } from "next/navigation";

export function Logout() {
  const router = useRouter();
  return (
    <button type="button" onClick={() => router.push("/api/eid/logout")}>
      Sign out
    </button>
  );
}

Multi-tenant / injected config

When credentials vary per tenant (Infisical, Vault, DB row), use createPasbyHandler instead of handler:

import {
  createPasbyHandler,
  PASBY_TENANT_COOKIE,
  pasbyConfigFromEnv,
} from "@finsel-dgi/pasby-next/server";
import type { ResolvePasbyContext } from "@finsel-dgi/pasby-next/server";

const resolveContext: ResolvePasbyContext = async (req, phase) => {
  if (phase === "login") {
    const tenantId = req.nextUrl.searchParams.get("tenant")?.trim();
    if (!tenantId) return null;
    const config = await loadPasbyRuntimeConfigForTenant(tenantId);
    if (!config) return null;
    return { config, tenantId };
  }
  if (phase === "handshake") {
    const tenantId = req.cookies.get(PASBY_TENANT_COOKIE)?.value?.trim();
    if (!tenantId) return null;
    const config = await loadPasbyRuntimeConfigForTenant(tenantId);
    if (!config) return null;
    return { config, tenantId };
  }
  return { config: pasbyConfigFromEnv() };
};

export const GET = createPasbyHandler(
  {
    claims: ["naming.given", "contact.email"],
    action: "login",
    payload: "Sign-in request",
  },
  "/error",
  resolveContext,
);

Pass tenantId on LoginButton so login can resolve the correct PasbyRuntimeConfig.


Server exports

Import from @finsel-dgi/pasby-next/server:

ExportPurpose
handlerSingle-tenant App Router handler (env-based)
createPasbyHandlerMulti-tenant / injected config
getEIDLoad User from cookies
pasbyConfigFromEnvBuild PasbyRuntimeConfig from env
PASBY_TENANT_COOKIECookie name for tenant scoping

Client exports (@finsel-dgi/pasby-next): LoginButton, PasbyButton, Logo, WordMark.


On this page