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/commonand@nestjs/core12,reflect-metadataandrxjs(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 thePrincipal.
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,stateandnonce.returnTokeeps only app-relative paths (no open redirects).orgIdsigns in for a specific org;promptcan beloginorconsent.finishLogin(callbackUrl, transaction)→TokenSet. Checksstate,nonceand PKCE, exchanges the code and validates the ID token.refresh(refreshToken)→TokenSet. Rejects withinvalid_grantwhen the user signed out, was disabled or removed your app: treat it as signed out.logoutUrl({ idTokenHint?, state? })→URLof 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
actorand neverauthTimeoracr; refuse them on routes only the user may use.
Changelog
0.1.2
- Docs links point to https://docs.nukta.solutions.
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.