New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@novatic/auth

Package Overview
Dependencies
Maintainers
1
Versions
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@novatic/auth

Self-contained OIDC auth for Next.js (App Router) with encrypted sessions, automatic token refresh and a mountable auth BFF.

latest
Source
npmnpm
Version
1.0.0
Version published
Maintainers
1
Created
Source

@novatic/auth

Autenticación OIDC autocontenida para apps Next.js (App Router). Gestiona todo el proceso: flujo Authorization Code, intercambio del code en el callback, creación de la sesión cifrada, retorno del token con refresh automático, y el logout (RP-initiated) — todo recibiendo por parámetros lo que necesita. Tu app solo decide qué hacer con la información del token.

No lee process.env: tú pasas issuer, client, secret, redirect_uri y destinos.

Server-only. Importa next/server y next/headers; úsalo solo en route handlers, server actions y proxy.ts. Nunca desde componentes cliente.

Requisitos

  • Next.js ^16 (peer dependency).
  • Node.js >= 20.

1. Instalación

pnpm add @novatic/auth
npm i @novatic/auth

No necesitas transpilePackages: el paquete llega compilado (dist/ + tipos).

2. Quickstart: de cero a login funcionando

Crea un único módulo de instancia en tu app, p. ej. src/libs/auth.ts:

import { createAuth, normalizeLoginMethod, resolveCallbackUrl } from '@novatic/auth'

export const auth = createAuth({
  // ── Identidad de tu SP ante el Identity Server ──
  issuerBaseUrl: process.env.NEXT_PUBLIC_IS_BASE_URL!,     // https://is.ejemplo.com
  clientId: process.env.OIDC_CLIENT_ID!,
  // Helpers puros: la app lee env, la lib normaliza/valida (ver §3bis).
  loginMethod: normalizeLoginMethod(process.env.OIDC_TOKEN_ENDPOINT_AUTH_METHOD, {
    envName: 'OIDC_TOKEN_ENDPOINT_AUTH_METHOD',            // el error cita la variable
  }),
  clientSecret: process.env.OIDC_CLIENT_SECRET,            // solo con 'basic' | 'post'
  // OIDC_REDIRECT_URI admite ruta (dev) o URL absoluta (K8s) — sin concatenar a mano
  callbackUrl: resolveCallbackUrl({
    redirectUri: process.env.OIDC_REDIRECT_URI,
    appUrl: process.env.NEXT_PUBLIC_APP_URL,
  }),

  // ── Sesión ──
  scopes: (process.env.OIDC_SCOPES ?? 'openid').split(' '),
  sessionSecret: process.env.SESSION_SECRET!,
  cookiePrefix: process.env.COOKIE_PREFIX ?? 'myapp',      // evita colisión entre apps

  // ── Destinos del flujo (los decides tú) ──
  postLoginRedirect: process.env.POST_LOGIN_URL ?? '/dashboard',
  postLogoutFallback: process.env.POST_LOGOUT_URL ?? '/',
})

Monta el BFF interno en un solo archivo src/app/api/auth/[...action]/route.ts:

import { auth } from '@/libs/auth'

export const runtime = 'nodejs'
const bff = auth.bff()

export const GET = bff.GET
export const POST = bff.POST

Y las rutas de login/registro (páginas → route handlers):

// src/app/login/route.ts
import { auth } from '@/libs/auth'
export const runtime = 'nodejs'
export const GET = auth.handlers.login

// src/app/register/route.ts
import { auth } from '@/libs/auth'
export const runtime = 'nodejs'
export const GET = auth.handlers.register

Hecho. Tienes login, callback, refresh, logout y me funcionando, con el redirect_uri registrado en tu SP apuntando a /api/auth/callback.

3. Referencia de createAuth(config)

ParámetroTipoDefaultCuándo tocarlo
issuerBaseUrlstring— (obligatorio)Base del IS. De aquí se derivan authorize, discovery y SCIM2.
clientIdstring— (obligatorio)Tu SP ante el IS.
loginMethod'pkce' | 'basic' | 'post'— (obligatorio)Cómo autentica la app en el token endpoint: 'pkce' = cliente público + PKCE S256 (sin secret); 'basic'/'post' = confidencial, credenciales en header Authorization o en el body. Sin default y sin fallback: el IS rechaza más de un método por petición (RFC 6749 §2.3).
clientSecretstring | undefined—Server-only. Requerido con 'basic'/'post'; ignorado con 'pkce'.
callbackUrlstring— (obligatorio)redirect_uri registrado en el IS (absoluto).
scopesstring[]['openid']P. ej. ['openid','email','profile'].
sessionSecretstring— (obligatorio)SESSION_SECRET (hex 32+ bytes). Fail-fast si falta.
cookiePrefixstring'oidc'Prefijo de TODAS las cookies de auth. Úsalo distinto por app si comparten dominio.
sessionCookieNamestring`${prefix}-session`Nombre explícito de la cookie de sesión.
sessionTtlSecondsnumber86400TTL de la sesión cifrada.
postLoginRedirectstring'/select-organization'A dónde va el usuario tras login exitoso.
postLogoutFallbackstring'/'Destino si no hay cookie de coordinación de logout.
loginPathstring'/login'Ruta de login (para el guard y reintentos).
routesPartial<IsRoutes>derivado de issuerBaseUrlSobreescribe paths no estándar del IS (ver §6).
mapClaimsToUser(claims) => DefaultUsersub/email/nombreMapea claims del id_token al shape de usuario de tu app.

Fail-fast: si falta issuerBaseUrl, clientId, callbackUrl o sessionSecret, createAuth() lanza con mensaje claro en boot (no silencioso). Igual con loginMethod: ausente o fuera de pkce | basic | post → throw; y 'basic'/'post' sin clientSecret → throw.

3bis. Helpers para apps (la lib interpreta, la app lee env)

La lib nunca lee process.env (ni en los helpers): tu app lee la env y pasa el valor crudo; los helpers normalizan, validan y resuelven. Usarlos en todas las apps garantiza idéntica normalización, fail-fast y contrato ruta/URL.

normalizeLoginMethod(raw, { envName? })

Normaliza (trim + lowercase) y valida el valor de OIDC_TOKEN_ENDPOINT_AUTH_METHOD. Lanza en boot citando la variable si envName se pasa (fail-fast sin default ni fallback).

import { createAuth, normalizeLoginMethod } from '@novatic/auth'

createAuth({
  // ...
  loginMethod: normalizeLoginMethod(process.env.OIDC_TOKEN_ENDPOINT_AUTH_METHOD, {
    envName: 'OIDC_TOKEN_ENDPOINT_AUTH_METHOD',
  }),
})

resolveCallbackUrl({ redirectUri, appUrl })

Resuelve el redirect_uri registrado contra el IS. Soportadas dos formas del valor de OIDC_REDIRECT_URI:

  • ruta (/api/auth/callback, dev local) → se une a appUrl sin dobles /;
  • URL absoluta (ConfigMap de K8s) → passthrough.
createAuth({
  // ...
  callbackUrl: resolveCallbackUrl({
    redirectUri: process.env.OIDC_REDIRECT_URI,
    appUrl: process.env.NEXT_PUBLIC_APP_URL,
  }),
})

Los defaults de NEGOCIO (cookiePrefix, postLoginRedirect, postLogoutFallback) no son de la lib: cada app los pasa en createAuth desde sus propias envs.

4. El BFF interno (lo que monta auth.bff())

Todos viven bajo /api/auth/[...action]:

EndpointMétodoQué hace
/api/auth/callbackGETValida state (anti-CSRF) → intercambia code → tokens → sesión cifrada → 302 a postLoginRedirect. Maneja: reintento non-silent tras error=login_required (silente), logout RP-initiated (-logoutcb), fallback post-logout, y clasifica fallos de red (→ /auth/error) vs protocolo (→ login).
/api/auth/login—Ver §2 (route handler aparte en /login).
/api/auth/logoutPOSTLimpia sesión local + devuelve endSessionUrl del IS en JSON (la app navega con window.location).
/api/auth/refreshPOSTRenueva el access token con el refresh token; invalida sesión si falla.
/api/auth/meGETUsuario ligero de la sesión + organizations + expiración. Sustituible (ver §5).

me por defecto vs enriquecido

El me por defecto solo devuelve la sesión ligera. Si tu app necesita perfil completo (SCIM2 /Me, etc.), pásale tu propio handler:

const bff = auth.bff({ me: myMeHandler })

El handler recibe (req) => Response y puede usar auth.getSession() y auth.getValidAccessToken(). Esto mantiene la lógica de negocio en tu app, montada a través de la librería.

5. Consumo del token (el contrato clave)

La librería te entrega el token válido; tú decides qué hacer con él:

const token = await auth.getValidAccessToken()
// → access token válido; si expira en <5min lo REFRESCA solo y persiste en la sesión
// → null si no hay sesión o el refresh falló (sesión invalidada)

Patrón típico (inyectar Bearer en el cliente BFF → backend de negocio):

backendClient.interceptors.request.use(async (config) => {
  const token = await auth.getValidAccessToken()
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

Otros miembros de la instancia:

MiembroPara qué
auth.getSession()Sesión server-side (usa next/headers). Solo en route handlers/server actions.
auth.getValidAccessToken()Token válido con auto-refresh.
auth.hasValidSession()true si hay tokens válidos.
auth.beginOidcFlow(req, { silent })Inicia el flujo manualmente (state/nonce + 302).
auth.buildEndSessionUrl(state?)URL de logout RP-initiated del IS.
auth.unsealFromRequest(req)Lee la cookie sin next/headers (para proxy).
auth.createRouteGuard(opts)Guard de rutas para proxy.ts.

Personalización del usuario (mapClaimsToUser)

createAuth({
  // ...
  mapClaimsToUser: (claims) => ({
    sub: String(claims.sub),
    email: String(claims.email),
    given_name: String(claims.given_name),
    family_name: String(claims.family_name),
    // campos extra que tu app necesite...
  }),
})

El tipo de sesión es genérico: auth.getSession<TuUser>().

6. Multi-app y rutas del IS

cookiePrefix

Dos apps bajo el mismo dominio compartirían cookies si usan el mismo prefijo. Pon un cookiePrefix distinto por app ('portal', 'tienda'). La librería deriva todas las cookies de ese prefijo.

Dominios distintos

Cada app se despliega a su dominio y registra su callbackUrl como redirect_uri en el SP del IS. No hay nada compartido en runtime — el artifact de cada app es independiente.

Rutas no estándar del IS

Por defecto se derivan de issuerBaseUrl (con sabor WSO2, sobreescribible):

  • authorize: ${issuerBaseUrl}/oauth2/authorize
  • discovery: ${issuerBaseUrl}/oauth2/token/.well-known/openid-configuration
  • scimMe: ${issuerBaseUrl}/scim2/Me

Si tu IS se desvía, sobreescribe solo lo necesario:

createAuth({
  // ...
  routes: {
    authorize: 'https://is.ejemplo.com/custom/authorize',
    // discovery y scimMe usan el default
  },
})

end_session_endpoint y token se resuelven automáticamente vía discovery.

7. Guard de rutas (proxy.ts)

Next.js 16 sustituye middleware.ts por proxy.ts. Usa el guard de la lib:

// src/proxy.ts
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/libs/auth'

const guard = auth.createRouteGuard({
  protectedRoutes: ['/dashboard', '/account'],
  requiredCookies: [
    { cookie: 'myapp-org', redirectTo: '/select-organization', routes: ['/dashboard'] },
  ],
  authenticatedHome: '/dashboard',
})

export default async function proxy(request: NextRequest) {
  const res = await guard(request)
  if (res) return res          // redirección (no autenticado / falta org)
  return NextResponse.next()
}

Opciones: protectedRoutes, requiredCookies (opcional routes para limitar a rutas concretas), authenticatedHome, loginPath, refreshPath. El guard dispara refresh proactivo en background cuando el token está por vencer.

8. Seguridad (resumen)

  • Tokens nunca tocan el navegador: viven solo en la iron-session cifrada (AES, httpOnly).
  • Un solo método de auth en el token endpoint, declarado con loginMethod: 'pkce' = cliente público (sin secret) con PKCE S256; 'basic'/'post' = confidencial con client_secret server-only. PKCE y secret son excluyentes en la mayoría de proveedores estrictos (RFC 6749 §2.3) — por eso no hay default ni fallback: se declara SIEMPRE.
  • state/nonce efímeros anti-CSRF; validación de firma/JWKS/iss/aud/exp por openid-client.
  • Logout distingue respuesta de login vs logout mediante marcador -logoutcb.
  • Clasificación de errores: fallo de red/infra del IS no reintenta el flujo (evita bucle).

9. Troubleshooting

SíntomaCausa probableFix
createAuth() faltan parámetros...No pasaste un campo obligatorio desde la appPásalos desde envs de tu app; la lib no lee process.env.
Sesiones de dos apps se pisanMismo cookiePrefix bajo un dominioUsa cookiePrefix distinto por app.
Login bucle infinitoIS inalcanzable (red/DNS/TLS)La lib redirige a /auth/error?reason=idp-unreachable (no a login). Revisa conectividad al IS.
redirect_uri does not matchcallbackUrl ≠ URI registrada en el SPRegistra https://tu-app/api/auth/callback en el IS.
El callback falla con invalid_request "MUST NOT use more than one authentication method"loginMethod desalineada con cómo está registrado el SP (el IS ve credenciales por dos vías)Declara el método real del SP: 'pkce' si es cliente público con PKCE; 'basic'/'post' si es confidencial. Solo uno.
Token no se refrescaSin refreshToken o revocadogetValidAccessToken() devuelve null; tu app debe pedir re-login.

10. Estructura interna del package

src/
├── index.ts          # createAuth() + re-exports de tipos
├── types.ts          # AuthConfig, Session, GuardOptions, Auth
├── core/
│   ├── oidc.ts         # cliente OIDC POR INSTANCIA (closure, no singleton global)
│   ├── login-method.ts # vocabulario 'pkce'|'basic'|'post' + traducción RFC 7591
│   │                   # + normalizeLoginMethod (helper puro para apps)
│   ├── callback-url.ts # resolveCallbackUrl (helper puro: ruta o URL absoluta)
│   ├── pkce.ts         # par verifier/challenge S256 (RFC 7636, node:crypto)
│   └── errors.ts       # clasificación red-vs-protocolo
├── session.ts        # iron-session: options + getServerSession + unsealFromRequest
├── token.ts          # getValidAccessToken / hasValidSession (auto-refresh)
├── bff/
│   ├── handlers.ts   # login/callback/logout/refresh/me
│   ├── router.ts     # dispatch /api/auth/[...action]
│   └── post-logout.ts
└── guards.ts         # createRouteGuard

11. Integrar la librería en una app nueva

La app no escribe ni una línea de OIDC: solo instala, instancia, monta y registra:

  • Instalar: pnpm add @novatic/auth (requiere next ^16 como peer).
  • Instanciar una sola vez en src/libs/auth.ts (ver §2): createAuth({...}) con normalizeLoginMethod + resolveCallbackUrl.
  • Montar el BFF en src/app/api/auth/[...action]/route.ts (auth.bff()).
  • Login/registro como route handlers (auth.handlers.login / .register).
  • Guard en proxy.ts con auth.createRouteGuard({...}) e inyección de token en el cliente BFF con auth.getValidAccessToken() (ver §4 y §7).
  • Registrar el redirect_uri https://<tu-dominio>/api/auth/callback en el SP del Identity Server.

📄 Licencia

MIT — ver LICENSE.

Keywords

nextjs

FAQs

Package last updated on 20 Sep 2026

Related posts