SAIKAT
Hire me
Open source@nukta/sso-sdk

@nukta/sso-sdkv0.1.2

Nukta SSO SDK: access-token verification, a NestJS guard and a login helper

485 / monthUNLICENSED licenseUpdated October 7, 2026

The official SDK for apps and APIs that use Nukta SSO, the OpenID Connect identity provider of the Nukta ecosystem.

  • Protect an API: verify Nukta access tokens (signature, issuer, audience, expiry, scopes, org) in NestJS or any Node.js framework.
  • Sign users in: server-side Authorization Code + PKCE for web apps (Next.js route handlers and similar), with refresh and logout.
  • Handle logout: verify back-channel logout tokens the SSO sends your app.

Full guides: https://docs.nukta.solutions/sso

npm install @nukta/sso-sdk
# or: pnpm add @nukta/sso-sdk / yarn add @nukta/sso-sdk

Requirements

  • Node.js 20 or newer; the package is ES modules only.
  • For the NestJS adapter: @nestjs/common and @nestjs/core 12, reflect-metadata and rxjs (already in every NestJS app). They are optional peer dependencies, so apps that don't use NestJS don't install them.

Entry points

Import For Main exports
@nukta/sso-sdk any API or server createAccessTokenVerifier, createLogoutTokenVerifier, requireScopes, requireOrg, bearerTokenFrom, NuktaAuthError
@nukta/sso-sdk/nestjs NestJS APIs NuktaAuthModule, NuktaAuthGuard, @RequireScopes(), @RequireOrg(), @NuktaPrincipal()
@nukta/sso-sdk/client web apps that sign users in createNuktaClient, sealTransaction, unsealTransaction

Issuers

Environment Issuer
Production https://accounts.nukta.app
Development https://accounts-dev.nukta.solutions

Your API's audience is its resource identifier, for example https://api.nukta.app/v1/billing, with scopes such as billing:read. Clients and APIs are registered by the Nukta platform team.


Protect a NestJS API

// app.module.ts
import { Module } from '@nestjs/common';
import { NuktaAuthModule } from '@nukta/sso-sdk/nestjs';

@Module({
  imports: [
    NuktaAuthModule.forRoot({
      issuer: 'https://accounts.nukta.app',
      audience: 'https://api.nukta.app/v1/billing',
    }),
  ],
})
export class AppModule {}
// invoices.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import type { Principal } from '@nukta/sso-sdk';
import {
  NuktaAuthGuard,
  NuktaPrincipal,
  RequireOrg,
  RequireScopes,
} from '@nukta/sso-sdk/nestjs';

@Controller('invoices')
@UseGuards(NuktaAuthGuard)
export class InvoicesController {
  @Get()
  @RequireScopes('billing:read')
  @RequireOrg()
  list(@NuktaPrincipal() user: Principal) {
    // user.subject, user.orgId, user.orgRole, user.actor ...
    return this.invoices.forOrg(user.orgId!);
  }
}

NuktaAuthModule.forRoot() is global, so the guard can be used in any module. Responses:

Situation Status WWW-Authenticate
No Authorization: Bearer … header 401 Bearer
Bad signature, wrong issuer or audience, expired, not an access token 401 Bearer error="invalid_token"
Missing scope from @RequireScopes() 403 Bearer error="insufficient_scope", scope="…"
@RequireOrg() but the token has no org 403 —

Protect any other Node.js API

import express from 'express';
import {
  bearerTokenFrom,
  createAccessTokenVerifier,
  NuktaAuthError,
  requireScopes,
} from '@nukta/sso-sdk';

const verifier = createAccessTokenVerifier({
  issuer: 'https://accounts.nukta.app',
  audience: 'https://api.nukta.app/v1/billing',
});

const app = express();

app.get('/invoices', async (req, res) => {
  try {
    const user = await verifier.verify(bearerTokenFrom(req.headers.authorization) ?? '');
    requireScopes(user, ['billing:read']);
    res.json(await invoicesFor(user.orgId));
  } catch (error) {
    if (error instanceof NuktaAuthError) {
      return res.status(error.code === 'invalid_token' ? 401 : 403).json({ error: error.code });
    }
    throw error;
  }
});

Sign users in (Next.js and other server-side web apps)

Sign-in runs on your server and tokens stay there, never in localStorage.

// lib/auth.ts
import { createNuktaClient } from '@nukta/sso-sdk/client';

export const auth = await createNuktaClient({
  issuer: process.env.NUKTA_ISSUER!, // https://accounts.nukta.app
  clientId: process.env.NUKTA_CLIENT_ID!,
  clientSecret: process.env.NUKTA_CLIENT_SECRET, // confidential clients only
  redirectUri: 'https://billing.nukta.app/auth/callback',
  postLogoutRedirectUri: 'https://billing.nukta.app/',
  scope: 'openid email profile offline_access billing:read',
  resource: 'https://api.nukta.app/v1/billing',
});
// app/auth/login/route.ts
import { sealTransaction } from '@nukta/sso-sdk/client';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { auth } from '@/lib/auth';

export async function GET(request: Request) {
  const returnTo = new URL(request.url).searchParams.get('returnTo') ?? '/';
  const { url, transaction } = await auth.startLogin({ returnTo });

  (await cookies()).set(
    'nukta_tx',
    await sealTransaction(transaction, process.env.NUKTA_TX_SECRET!),
    { httpOnly: true, secure: true, sameSite: 'lax', path: '/auth', maxAge: 600 },
  );
  redirect(url.href);
}
// app/auth/callback/route.ts
import { unsealTransaction } from '@nukta/sso-sdk/client';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { auth } from '@/lib/auth';

export async function GET(request: Request) {
  const jar = await cookies();
  const transaction = await unsealTransaction(
    jar.get('nukta_tx')!.value,
    process.env.NUKTA_TX_SECRET!,
  );
  jar.delete('nukta_tx');

  const tokens = await auth.finishLogin(new URL(request.url), transaction);
  await saveSession(tokens); // your own encrypted, HttpOnly session
  redirect(transaction.returnTo);
}

Refresh before the access token expires (10 minutes) and always store the new refresh token: refresh tokens rotate, and reusing an old one revokes the session.

const next = await auth.refresh(session.refreshToken);

Sign out by sending the user to the SSO, which asks whether to sign out of this app only or everywhere:

redirect(auth.logoutUrl({ idTokenHint: session.idToken }).href);

NUKTA_TX_SECRET is 32 random bytes, base64url-encoded:

openssl rand -base64 32 | tr '+/' '-_' | tr -d '='

Handle back-channel logout

When a user signs out (or their password changes, or their account is disabled), the SSO POSTs a signed logout_token to your registered back-channel logout URL:

// app/auth/backchannel-logout/route.ts
import { createLogoutTokenVerifier } from '@nukta/sso-sdk';

const verifier = createLogoutTokenVerifier({
  issuer: process.env.NUKTA_ISSUER!,
  clientId: process.env.NUKTA_CLIENT_ID!,
});

export async function POST(request: Request) {
  const form = await request.formData();
  const { subject, sessionId } = await verifier.verify(String(form.get('logout_token')));
  await destroySessions({ userId: subject, ssoSessionId: sessionId });
  return new Response(null, { status: 200 });
}

Match sessionId to the sid claim of the ID token you stored at sign-in.


API reference

createAccessTokenVerifier(options)

Returns { verify(token: string): Promise<Principal> }. Throws NuktaAuthError with code invalid_token for anything that isn't a valid access token for your API.

Option Default Description
issuer required The SSO issuer URL.
audience required Your API's resource identifier.
jwksUri <issuer>/oidc/jwks Where the signing keys are published.
keys remote JWKS A jose key source, for tests or keys loaded another way.
clockToleranceSeconds 5 Allowed clock difference.

Checks: RS256 signature, typ: at+jwt (an ID token is refused), iss, aud, exp, and that sub is present. The key set is cached and refetched when a token names an unknown key, so key rotation needs no restart.

Principal

Field Type Description
subject string The user id (for service tokens, the client id).
clientId string The app the token was issued to.
scopes ReadonlySet<string> Granted scopes.
orgId string | undefined The org the user signed in for.
orgRole string | undefined owner, admin or member in orgId.
actor { subject: string } | undefined Set when Nukta staff impersonate the user; record it as the real actor.
authTime number | undefined Seconds since epoch of the last interactive sign-in.
acr string | undefined urn:nukta:acr:pwd or urn:nukta:acr:mfa.
amr readonly string[] Sign-in methods, for example ['pwd', 'otp', 'mfa'].
tokenId string | undefined The token's jti.
expiresAt Date When the token expires.

requireScopes(principal, scopes) and requireOrg(principal)

Throw NuktaAuthError (insufficient_scope with requiredScopes, or org_required). requireOrg also narrows orgId and orgRole to string.

bearerTokenFrom(header)

Reads the token from an Authorization header value (the Bearer scheme is case-insensitive). Returns undefined when there is none.

NuktaAuthError

error.code is 'invalid_token' | 'insufficient_scope' | 'org_required'; error.requiredScopes lists the scopes a route needed.

createLogoutTokenVerifier(options)

Returns { verify(token): Promise<{ subject, sessionId, tokenId }> }.

Option Default Description
issuer required The SSO issuer URL.
clientId required Your app's client id (the token's audience).
jwksUri, keys as above
maxAgeSeconds 300 Refuse older tokens. Keep tokenId briefly to refuse replays.

Checks: RS256 signature, typ: logout+jwt, iss, aud, age, the back-channel logout event, sub or sid present, and no nonce.

NestJS: NuktaAuthModule.forRoot(options)

Takes the same options as createAccessTokenVerifier. Provides NuktaAuthGuard and the verifier (injection token NUKTA_AUTH_VERIFIER) globally. The guard puts the principal on request.nuktaPrincipal.

  • @RequireScopes(...scopes): every listed scope must be granted.
  • @RequireOrg(): the token must carry an org.
  • @NuktaPrincipal(): parameter decorator returning the Principal.

createNuktaClient(options)

Async (it reads the issuer's discovery document). Returns { startLogin, finishLogin, refresh, logoutUrl }.

Option Default Description
issuer required The SSO issuer URL.
clientId required Your client id.
clientSecret none Confidential clients only; public clients use PKCE alone.
redirectUri required Exactly as registered.
postLogoutRedirectUri none Exactly as registered.
scope openid Add offline_access for refresh tokens, and your API scopes.
resource none The API the access token is for.
allowInsecureRequests false Development only: allow an http:// issuer.
  • startLogin({ returnTo?, orgId?, prompt? }) → { url, transaction }. Creates the PKCE verifier, state and nonce. returnTo keeps only app-relative paths (no open redirects). orgId signs in for a specific org; prompt can be login or consent.
  • finishLogin(callbackUrl, transaction) → TokenSet. Checks state, nonce and PKCE, exchanges the code and validates the ID token.
  • refresh(refreshToken) → TokenSet. Rejects with invalid_grant when the user signed out, was disabled or removed your app: treat it as signed out.
  • logoutUrl({ idTokenHint?, state? }) → URL of the SSO's logout page.

TokenSet is { accessToken, refreshToken?, idToken?, expiresAt?, claims? }, where claims are the ID token claims (sub, email, org_id, org_role, sid, ...).

sealTransaction(transaction, secret) and unsealTransaction(sealed, secret)

Encrypt and decrypt the login transaction (A256GCM, valid 10 minutes) for a short-lived HttpOnly cookie, so the PKCE verifier is never readable in the browser. secret must be 32 random bytes, base64url-encoded; a tampered or expired value is rejected.


Security notes

  • Verify every request; never trust a token's claims without verify().
  • One token is valid for one API: use your own audience.
  • Access tokens are stateless and live 10 minutes. For instant reactions, handle back-channel logout and the SSO's events.
  • Tokens issued during staff impersonation carry actor and never authTime or acr; refuse them on routes only the user may use.

Changelog

0.1.2

0.1.1

  • Full README: setup per framework, back-channel logout, and an API reference.

0.1.0

  • First release: access-token and logout-token verification, the NestJS adapter, and the web sign-in client.

License

Proprietary (UNLICENSED). © Nukta Solutions. For use in Nukta products.

↑↓ navigate↵ openesc closeCtrl K toggles this menu