npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@am25/gate-next

v2.0.0

Published

AM25 Gate (OAuth2+OIDC Authentication) SDK for Next.js

Readme

@am25/gate-next

Server-side SDK for integrating Next.js 16+ applications with AM25 Gate IdP, an Identity Provider compatible with OAuth 2.0 and OpenID Connect.

Features

  • TypeScript-first: Full type definitions with exported interfaces
  • Server-first authentication: no React providers, no forced CSR
  • OAuth 2.0 + OIDC: Authorization Code flow with full scope support
  • Proxy for Next.js 16: Server-level route protection
  • httpOnly Cookie: Secure session shared across subdomains
  • React Helpers: Cached functions for Server Components
  • RS256 (JWKS): Token verification using public keys, no shared secrets
  • Assigned users: Fetch the users assigned to the current client app

Installation

# pnpm
pnpm add @am25/gate-next

# npm
npm install @am25/gate-next

# yarn
yarn add @am25/gate-next

Requirements

  • Next.js 16+
  • React 19+
  • An app registered as an OAuth client in Gate

Configuration

1. Environment variables

Create .env.local:

# Gate OAuth
GATE_ISSUER=https://gate.example.com
GATE_CLIENT_ID=your-client-id
GATE_CLIENT_SECRET=your-client-secret
GATE_REDIRECT_URI=https://myapp.example.com/api/auth/callback

# Cookie
COOKIE_DOMAIN=.example.com

# For client-side components (LoginButton)
NEXT_PUBLIC_GATE_ISSUER=https://gate.example.com
NEXT_PUBLIC_GATE_CLIENT_ID=your-client-id
NEXT_PUBLIC_GATE_REDIRECT_URI=https://myapp.example.com/api/auth/callback

You do not need JWT_SECRET. Tokens are verified using Gate's public key (JWKS).

2. Create API Routes

/api/auth/callback/route.ts

Exchanges the authorization code for tokens and sets the session cookie.

import { createCallbackHandler } from "@am25/gate-next";
import type { NextRequest } from "next/server";

const handler = createCallbackHandler({
  issuer: process.env.GATE_ISSUER!,
  clientId: process.env.GATE_CLIENT_ID!,
  clientSecret: process.env.GATE_CLIENT_SECRET!,
  redirectUri: process.env.GATE_REDIRECT_URI!,
  cookieDomain: process.env.COOKIE_DOMAIN,
  defaultRedirect: "/dashboard",
});

export async function GET(request: NextRequest) {
  return handler(request);
}
import { createCallbackHandler } from "@am25/gate-next";

const handler = createCallbackHandler({
  issuer: process.env.GATE_ISSUER,
  clientId: process.env.GATE_CLIENT_ID,
  clientSecret: process.env.GATE_CLIENT_SECRET,
  redirectUri: process.env.GATE_REDIRECT_URI,
  cookieDomain: process.env.COOKIE_DOMAIN,
  defaultRedirect: "/dashboard",
});

export async function GET(request) {
  return handler(request);
}

/api/auth/logout/route.ts

Clears the local cookie and optionally logs out from Gate (federated logout).

import { createLogoutHandler } from "@am25/gate-next";
import type { NextRequest } from "next/server";

const handler = createLogoutHandler({
  issuer: process.env.GATE_ISSUER,
  redirectUri: process.env.GATE_REDIRECT_URI!,
  cookieDomain: process.env.COOKIE_DOMAIN,
  redirectTo: "/",
});

export async function GET(request: NextRequest) {
  return handler(request);
}
import { createLogoutHandler } from "@am25/gate-next";

const handler = createLogoutHandler({
  issuer: process.env.GATE_ISSUER,
  redirectUri: process.env.GATE_REDIRECT_URI,
  cookieDomain: process.env.COOKIE_DOMAIN,
  redirectTo: "/",
});

export async function GET(request) {
  return handler(request);
}

3. Configure Proxy (Next.js 16)

The proxy protects routes by verifying the session cookie. If there is no valid session, the user is redirected to Gate to authenticate.

Create src/proxy.ts:

import { createGateProxy } from "@am25/gate-next";
import type { NextRequest } from "next/server";

const gateProxy = createGateProxy({
  issuer: process.env.GATE_ISSUER!,
  clientId: process.env.GATE_CLIENT_ID!,
  redirectUri: process.env.GATE_REDIRECT_URI!,
  protectedPaths: ["/dashboard", "/settings"],
  publicPaths: ["/dashboard/public"],
});

export async function proxy(request: NextRequest) {
  return gateProxy(request);
}

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};
import { createGateProxy } from "@am25/gate-next";

const gateProxy = createGateProxy({
  issuer: process.env.GATE_ISSUER,
  clientId: process.env.GATE_CLIENT_ID,
  redirectUri: process.env.GATE_REDIRECT_URI,
  protectedPaths: ["/dashboard", "/settings"],
  publicPaths: ["/dashboard/public"],
});

export async function proxy(request) {
  return gateProxy(request);
}

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};

4. Create session helpers

Create src/lib/auth.ts:

import { createSessionHelpers } from "@am25/gate-next";

export const {
  getSession,
  getUser,
  isAuthenticated,
  requireAuth,
  requireAdmin,
} = createSessionHelpers({
  issuer: process.env.GATE_ISSUER!,
});
import { createSessionHelpers } from "@am25/gate-next";

export const {
  getSession,
  getUser,
  isAuthenticated,
  requireAuth,
  requireAdmin,
} = createSessionHelpers({
  issuer: process.env.GATE_ISSUER,
});

Usage

In Server Components

import { requireAuth } from "@/lib/auth";

export default async function DashboardPage() {
  const user = await requireAuth();

  return (
    <div>
      <h1>Hello, {user.name}</h1>
      <p>Email: {user.email}</p>
      {user.isAdmin && <span>You are an administrator</span>}
    </div>
  );
}

Syncing assigned Gate users

Gate exposes the users assigned to the current client app through /oauth/users. The SDK stores an am25_at httpOnly access token during the OAuth callback and uses it from server-side helpers.

import { createUserHelpers } from "@am25/gate-next";
import prisma from "@/lib/prisma";

const users = createUserHelpers({
  issuer: process.env.GATE_ISSUER!,
});

export async function syncGateUsers() {
  return users.syncUsers(async (gateUsers) => {
    await Promise.all(
      gateUsers.map((user) =>
        prisma.user.upsert({
          where: { gateId: user.id },
          update: { email: user.email, name: user.name, lastName: user.lastName },
          create: {
            gateId: user.id,
            email: user.email,
            name: user.name,
            lastName: user.lastName,
          },
        }),
      ),
    );
  });
}

Each app owns its roles and permissions. The SDK only fetches assigned Gate users so your app can manage local access data.

Confirming critical actions with OTP

Apps can request a short-lived step-up proof before running sensitive mutations. Add the step_up scope to the login flow, collect the user's OTP in your UI, then verify it server-side before executing the mutation.

import { createStepUpHelpers } from "@am25/gate-next";

const stepUp = createStepUpHelpers({
  issuer: process.env.GATE_ISSUER!,
  clientId: process.env.GATE_CLIENT_ID!,
});

export async function confirmDeleteProject(input: {
  projectId: string;
  code: string;
}) {
  const { proofToken } = await stepUp.verifyOtp({
    action: "project.delete",
    context: { projectId: input.projectId },
    code: input.code,
  });

  await stepUp.requireProof(proofToken, {
    action: "project.delete",
    context: { projectId: input.projectId },
  });

  // Run the critical mutation here.
}

For non-idempotent actions, include a unique intentId in context and validate the same context when requiring the proof. Each app owns the final UI, commonly an AlertDialog with an OTP input.

In Server Actions

"use server";

import { getUser } from "@/lib/auth";

interface CreatePostData {
  title: string;
  content: string;
}

export async function createPost(data: CreatePostData) {
  const user = await getUser();
  if (!user) throw new Error("Not authenticated");

  await prisma.post.create({
    data: {
      ...data,
      authorId: user.id,
    },
  });
}
"use server";

import { getUser } from "@/lib/auth";

export async function createPost(data) {
  const user = await getUser();
  if (!user) throw new Error("Not authenticated");

  await prisma.post.create({
    data: {
      ...data,
      authorId: user.id,
    },
  });
}

Login button (Client Component)

"use client";

import { getLoginUrl } from "@am25/gate-next";

export function LoginButton() {
  const handleLogin = () => {
    const url = getLoginUrl({
      issuer: process.env.NEXT_PUBLIC_GATE_ISSUER!,
      clientId: process.env.NEXT_PUBLIC_GATE_CLIENT_ID!,
      redirectUri: process.env.NEXT_PUBLIC_GATE_REDIRECT_URI!,
      returnTo: "/dashboard",
    });
    window.location.href = url;
  };

  return <button onClick={handleLogin}>Log in</button>;
}
"use client";

import { getLoginUrl } from "@am25/gate-next";

export function LoginButton() {
  const handleLogin = () => {
    const url = getLoginUrl({
      issuer: process.env.NEXT_PUBLIC_GATE_ISSUER,
      clientId: process.env.NEXT_PUBLIC_GATE_CLIENT_ID,
      redirectUri: process.env.NEXT_PUBLIC_GATE_REDIRECT_URI,
      returnTo: "/dashboard",
    });
    window.location.href = url;
  };

  return <button onClick={handleLogin}>Log in</button>;
}

Logout link

export function LogoutButton() {
  return <a href="/api/auth/logout">Log out</a>;
}

Scopes

Gate supports the following OIDC scopes:

| Scope | Claims included in the token | | --------- | ------------------------------------- | | openid | sub (required for OIDC) | | profile | name, lastName, picture | | email | email | | users | Allows fetching assigned users | | step_up | Allows OTP confirmation for critical actions |

The default scope is openid profile email users. Add step_up when the app needs OTP confirmation for critical actions.

User data

The object returned by getUser() implements the GateUser interface:

interface GateUser {
  id: string;        // JWT sub
  email: string;     // requires "email" scope
  name: string;      // requires "profile" scope
  lastName: string;  // requires "profile" scope
  picture: string | null; // requires "profile" scope
  isAdmin: boolean;
}

Difference between getSession and getUser

| Function | Returns | User ID | Recommended use | | -------------- | -------------------- | ------------- | ----------------- | | getSession() | JWTPayload \| null | session.sub | Access raw claims | | getUser() | GateUser \| null | user.id | Business logic |

Use getUser() for business logic. Use getSession() only if you need direct access to JWT claims.

API Reference

createGateProxy(options)

Creates a proxy to protect routes in Next.js 16.

| Option | Type | Required | Default | Description | | ---------------- | -------- | -------- | ----------------------------------------- | ----------------------------------- | | issuer | string | Yes | | Gate server URL | | clientId | string | Yes | | App Client ID | | redirectUri | string | Yes | | Callback URI | | protectedPaths | string[] | No | ["/dashboard"] | Routes to protect | | publicPaths | string[] | No | [] | Public routes inside protectedPaths | | cookieName | string | No | "am25_sess" | Cookie name | | scopes | string[] | No | ["openid", "profile", "email", "users"] | Scopes requested during redirect |

Returns null if the route does not require protection or the session is valid. Returns NextResponse.redirect if authentication is required.

createCallbackHandler(options)

Creates the handler to exchange the authorization code for tokens.

| Option | Type | Required | Default | Description | | ----------------- | ------ | -------- | --------------- | ------------------------------------- | | issuer | string | Yes | | Gate server URL | | clientId | string | Yes | | Client ID | | clientSecret | string | Yes | | Client Secret | | redirectUri | string | Yes | | Callback URI (must match Gate config) | | cookieName | string | No | "am25_sess" | Session cookie name | | accessCookieName | string | No | "am25_at" | Access token cookie name | | cookieDomain | string | No | | Cookie domain (e.g. .example.com) | | cookieMaxAge | number | No | 2592000 (30d) | Duration in seconds | | defaultRedirect | string | No | "/dashboard" | Route after login |

The handler stores the session_token (or access_token as fallback) in an httpOnly session cookie. It also stores access_token in a separate httpOnly cookie with a maximum age of 1 hour for server-side SDK helpers.

createLogoutHandler(options)

Creates the logout handler.

| Option | Type | Required | Default | Description | | -------------- | ------ | -------- | ------------- | ------------------------------------------- | | redirectUri | string | Yes | | Callback URI (used to determine app origin) | | issuer | string | No | | Gate URL (enables federated logout) | | cookieName | string | No | "am25_sess" | Cookie name | | cookieDomain | string | No | | Cookie domain | | redirectTo | string | No | "/" | Route after logout |

Federated logout: If issuer is provided, logout redirects to Gate to close the user's global session across all apps. Otherwise, it only clears the local cookie.

createSessionHelpers(options)

Creates helpers to access the session in Server Components.

| Option | Type | Required | Default | Description | | ------------ | ------ | -------- | ------------- | --------------- | | issuer | string | Yes | | Gate server URL | | cookieName | string | No | "am25_sess" | Cookie name |

Returns a SessionHelpers object:

| Helper | Returns | Description | | ---------------------- | ---------------------- | ---------------------------------------- | | getSession() | JWTPayload \| null | Raw JWT payload | | getUser() | GateUser \| null | Formatted user data | | isAuthenticated() | boolean | Whether a session exists | | requireAuth() | GateUser | User data, throws if not authenticated | | requireAdmin() | GateUser | User data, throws if not admin |

All functions are cached per request using React.cache().

getLoginUrl(options)

Generates the URL to start the OAuth flow.

| Option | Type | Required | Default | Description | | ------------- | -------- | -------- | ----------------------------------------- | ------------------------------ | | issuer | string | Yes | | Gate server URL | | clientId | string | Yes | | Client ID | | redirectUri | string | Yes | | Callback URI | | scopes | string[] | No | ["openid", "profile", "email", "users"] | Scopes to request | | returnTo | string | No | | Route to return to after login |

getLogoutUrl(options)

Generates the URL for the local logout endpoint.

| Option | Type | Required | Default | Description | | ---------------- | ------ | -------- | -------------------- | -------------------- | | logoutEndpoint | string | No | "/api/auth/logout" | Logout handler route | | returnTo | string | No | | URL after logout |

createAuthConfig(config)

Creates a reusable configuration that encapsulates getLoginUrl and getLogoutUrl.

import { createAuthConfig } from "@am25/gate-next";

const auth = createAuthConfig({
  issuer: process.env.NEXT_PUBLIC_GATE_ISSUER!,
  clientId: process.env.NEXT_PUBLIC_GATE_CLIENT_ID!,
  redirectUri: process.env.NEXT_PUBLIC_GATE_REDIRECT_URI!,
  scopes: ["openid", "profile", "email", "users"],
});

const loginUrl = auth.getLoginUrl("/dashboard");
const logoutUrl = auth.getLogoutUrl("/");

createUserHelpers(options)

Creates server-side helpers to fetch users assigned to the current Gate client and sync them into the app.

| Option | Type | Required | Default | Description | | ------------------ | ------ | -------- | ----------- | ------------------------ | | issuer | string | Yes | | Gate server URL | | accessCookieName | string | No | "am25_at" | Access token cookie name |

| Helper | Returns | Description | | ------------- | ----------------------- | ----------------------------------- | | getUsers() | Promise<GateAssignedUser[]> | Fetches users from /oauth/users | | syncUsers() | Promise<GateAssignedUser[]> | Fetches users and calls your sync |

createStepUpHelpers(options)

Creates server-side helpers for Gate OTP step-up challenges and proof validation.

| Option | Type | Required | Default | Description | | ------------------ | ------ | -------- | ----------- | ------------------------ | | issuer | string | Yes | | Gate server URL | | clientId | string | Yes | | App Client ID | | cookieName | string | No | "am25_sess" | Session token cookie name | | accessCookieName | string | No | "am25_at" | Access token fallback cookie name |

| Helper | Returns | Description | | ------------------- | ------------------------------------ | ------------------------------------------ | | createChallenge() | Promise<StepUpChallenge> | Creates a Gate challenge for an action | | verifyChallenge() | Promise<StepUpProof> | Verifies a pre-created challenge | | verifyOtp() | Promise<StepUpProof> | Creates a fresh challenge and verifies OTP | | verifyProof() | Promise<StepUpProofPayload \| null> | Validates a proof token with JWKS | | requireProof() | Promise<StepUpProofPayload> | Validates a proof token or throws |

The app must request the step_up scope during login before using these helpers.

verifyTokenWithJWKS(token, issuer, expectedTyp)

Verifies a JWT using Gate’s public key (JWKS). Used internally by the SDK but available for manual verification.

| Parameter | Type | Required | Description | | ------------- | ------ | -------- | -------------------------------------------------- | | token | string | Yes | JWT to verify | | issuer | string | Yes | Gate server URL | | expectedTyp | string | No | Expected header type (e.g. "st+jwt", "at+jwt") |

clearJWKSCache(issuer)

Clears the JWKS public key cache. Useful if Gate rotates its keys.

| Parameter | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------- | | issuer | string | No | Issuer URL. If omitted, clears all cache |

Exported types

All option interfaces and return types are exported for use in your own code:

import type {
  GateUser,
  SessionHelpers,
  SessionHelpersOptions,
  GateProxyOptions,
  CallbackHandlerOptions,
  LogoutHandlerOptions,
  LoginUrlOptions,
  LogoutUrlOptions,
  AuthConfig,
  AuthConfigOptions,
} from "@am25/gate-next";

Authentication flow

User            App (proxy)            Gate (IdP)           App (callback)
  |                  |                      |                      |
  | GET /dashboard   |                      |                      |
  | ---------------> |                      |                      |
  |                  |                      |                      |
  |                  | No valid cookie      |                      |
  |                  | Redirect to Gate     |                      |
  | <--------------- |                      |                      |
  |                  |                      |                      |
  | Login on Gate                           |                      |
  | --------------------------------------->|                      |
  |                  |                      |                      |
  | Redirect with authorization code       |                      |
  | <---------------------------------------|                      |
  |                  |                      |                      |
  | GET /api/auth/callback?code=xxx                               |
  | -------------------------------------------------------------->|
  |                  |                      |                      |
  |                  |                      | POST /oauth/token    |
  |                  |                      |<---------------------|
  |                  |                      |                      |
  |                  |                      | Returns tokens       |
  |                  |                      |--------------------->|
  |                  |                      |                      |
  | Set-Cookie: am25_sess (httpOnly, RS256)                       |
  | <--------------------------------------------------------------|
  |                  |                      |                      |
  | Redirect to /dashboard                                         |
  | ---------------> |                      |                      |
  |                  |                      |                      |
  |                  | Verify token (JWKS) |                      |
  |                  | -------------------> |                      |
  |                  |                      |                      |
  |                  | Public key (cache)  |                      |
  |                  | <------------------- |                      |
  |                  |                      |                      |
  | Page OK          |                      |                      |
  | <--------------- |                      |                      |

Token verification (RS256)

The SDK verifies tokens using Gate’s public key obtained from the JWKS endpoint:

GET {issuer}/.well-known/jwks.json
  • Only Gate has the private key (used to sign tokens)
  • Apps only need the public key (used to verify tokens)
  • The public key is automatically cached in memory

Domain cookies

Apps share sessions by domain:

  • Apps on *.example.com → cookie on .example.com
  • Apps on *.example.com → cookie on .example.com

Each domain has its own session. They do not cross.

Access control

Gate manages access at two levels:

Per application: In the Gate dashboard you configure which users can access each app. Administrators automatically have access to all apps. If an unauthorized user tries to authenticate, Gate returns a 403 error.

Per app roles and permissions: Each client app owns its own role and permission model. Gate only authenticates users and controls which users can access each app.

Internal vs third-party clients

Gate distinguishes two types of OAuth clients:

| Type | Consent | Use case | | -------------------------- | ------------------- | ----------------------------------- | | Internal (first-party) | No, auto-approved | Apps within the AM25 ecosystem | | Third-party | Yes, consent screen | External apps integrating with Gate |

Configured in the Gate dashboard when creating or editing a client.

Compatibility with standard libraries

Gate is an OAuth 2.0 and OpenID Connect compatible Identity Provider. Besides this SDK, you can integrate it with any library that supports OIDC Discovery:

Discovery: {issuer}/.well-known/openid-configuration
JWKS:      {issuer}/.well-known/jwks.json