@antzsoft/wso2-auth-backend
v1.0.0
Published
Node.js backend SDK for Antz Central User Service (WSO2 IS 7.2.0) — M2M SCIM2 user management, JWT verification, admin & self-service password flows.
Readme
@antzsoft/wso2-auth-backend
Node.js backend SDK for Antz Central User Service (WSO2 IS 7.2.0). Wraps the
machine-to-machine (client_credentials) SCIM2 user-management API, admin and
self-service password flows, local JWT verification via JWKS, and OIDC
session/logout helpers — everything a backend needs to integrate without
hand-rolling fetch calls against WSO2 directly.
Companion to @antzsoft/wso2-auth-web (browser/frontend)
and @antzsoft/wso2-auth-reactnative — this
package is the server-side counterpart, built from the same API surface
documented in docs/backend-api-integration-guide.md.
Contents
- Installation
- Quick Start
- Configuration
- User Management
- Bulk Update & Delete
- Password Flows
- JWT Verification
- Session Helpers
- Error Types
- Environment Routing
Installation
npm install @antzsoft/wso2-auth-backendRequires Node.js 18+ (uses the global fetch). Ships as dual ESM + CommonJS —
import and require() both work.
Quick Start
import { AntzBackendClient } from "@antzsoft/wso2-auth-backend";
const antz = new AntzBackendClient({
baseUrl: process.env.WSO2_BASE_URL!, // e.g. https://auth.antzsystems.com
tenant: process.env.WSO2_TENANT!, // "prod" | "dev" | "uat"
clientId: process.env.WSO2_CLIENT_ID!,
clientSecret: process.env.WSO2_CLIENT_SECRET!,
});
const user = await antz.users.createUser({
userName: "[email protected]",
email: "[email protected]",
givenName: "John",
familyName: "Doe",
phone: "+919876543210",
});The M2M token is fetched lazily on the first call and cached in memory (refreshed ~60s before it expires) — you never need to manage it yourself.
Configuration
interface AntzBackendConfig {
baseUrl: string; // "https://auth.antzsystems.com"
tenant: string; // "prod" | "dev" | "uat"
clientId: string;
clientSecret: string;
scope?: string; // defaults to the full user-mgt scope set
audience?: string; // expected `aud` claim on verified tokens; omit to skip the check
tokenRefreshMarginSeconds?: number; // default 60
jwksCacheMaxAgeMs?: number; // default 24h
}Store clientId/clientSecret in your secrets manager, not in source or
plain .env files committed to the repo.
User Management
antz.users — see Section 3–5 of the integration guide for full request/response shapes.
// Create — omit `password` to use the auto-generated-password + SMS/email notification flow
await antz.users.createUser({ userName: "alice", phone: "+919876543210" });
// Lookup — email/username use SCIM filters; phone uses a dedicated lookup endpoint
const { exists, user } = await antz.users.validateUserExists("email", "[email protected]");
const byPhone = await antz.users.validateUserExists("phone", "+919876543210");
await antz.users.getUser(wso2UserId);
await antz.users.listUsers({ startIndex: 1, count: 20 }); // no sortBy — unsupported by WSO2 IS 7.2.0
// Update — only supplied fields change
await antz.users.updateUser(wso2UserId, { active: false }); // deactivate
await antz.users.updateUser(wso2UserId, { unlockAccount: true }); // unlock after failed logins
await antz.users.updateUser(wso2UserId, { antzzooids: ["ZOO-001"] }); // replace the full Zoo ID list
await antz.users.updateUser(wso2UserId, { addAntzzooids: ["ZOO-003"] }); // append without removing existing
await antz.users.deleteUser(wso2UserId); // permanent — prefer { active: false } in most casesantzuserid is single-value; antzzooids is multi-value — pass an array even
for one Zoo ID, and use addAntzzooids (SCIM op: add) instead of
antzzooids (SCIM op: replace) when you want to append rather than
overwrite.
Bulk Update & Delete
WSO2's SCIM2 Bulk API batches independent per-user operations into one HTTP
call. Not atomic — already-applied operations are not rolled back if a
later one fails. You must already know the target wso2-uuids.
const results = await antz.users.bulkUpdateUsers([
{ id: wso2UserId1, active: false },
{ id: wso2UserId2, email: "[email protected]" },
]);
// results: [{ bulkId, ok, code, detail? }, ...] — check each entry, an overall success does not imply every op succeeded
await antz.users.bulkDeleteUsers([wso2UserId1, wso2UserId2]); // permanentPassword Flows
antz.password
// Admin reset — silent, no notification
await antz.password.adminResetPassword(wso2UserId, "NewPassword123!");
// Admin reset — WSO2 notifies the user via SMS/email with the new password
// (requires the M2M token to be JWT, not opaque)
await antz.password.adminResetPasswordAndNotify(wso2UserId, "NewPassword123!");
// Self-service — requires the USER's own Bearer token, never the M2M token
const otpStatus = await antz.password.sendChangePasswordOtp(userAccessToken);
if (otpStatus.ok || otpStatus.code === "OTP_NOT_ENABLED") {
const result = await antz.password.changePassword(
userAccessToken,
"OldPassword123!",
"NewPassword456@",
otpStatus.ok ? "123456" : undefined,
);
}
// Or throw instead of returning a result object:
await antz.password.changePasswordOrThrow(userAccessToken, "OldPassword123!", "NewPassword456@");JWT Verification
antz.jwt — local verification against WSO2's JWKS, no network call per
request. The JWKS response is cached (default 24h) and refetched automatically
on a kid cache miss (key rotation).
try {
const claims = await antz.jwt.verifyAccessToken(bearerToken);
// claims.sub, claims.email, claims.phone_number, claims.scope, ...
} catch (err) {
if (err instanceof AntzTokenVerificationError) {
// err.code: "TOKEN_EXPIRED" | "INVALID_SIGNATURE" | "CLAIM_MISMATCH" | "INVALID_TOKEN"
}
}
// Reshaped into common fields:
const verified = await antz.jwt.extractVerifiedClaims(bearerToken);
// { wso2Id, email, phone, firstName, lastName, username, tenant, scopes, issuedAt, expiresAt, clientId }Express middleware example
async function authMiddleware(req, res, next) {
const header = req.headers.authorization ?? "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
if (!token) return res.status(401).json({ code: "MISSING_TOKEN" });
try {
req.user = await antz.jwt.verifyAccessToken(token);
next();
} catch (err) {
const code = err instanceof AntzTokenVerificationError ? err.code : "INVALID_TOKEN";
res.status(401).json({ code });
}
}Decode without verifying (debugging only)
import { decodeTokenUnsafe } from "@antzsoft/wso2-auth-backend";
decodeTokenUnsafe(token); // no signature check — never use for authorization decisionsIntrospection (opaque tokens only)
const result = await antz.jwt.introspect(token, clientId, clientSecret);
if (!result.active) { /* expired or revoked */ }Prefer verifyAccessToken for JWTs — introspection costs a network round-trip
to WSO2 on every call.
Session Helpers
antz.session and the standalone assertExpectedUser guard — for backends
that drive the OIDC flow directly (no Antz frontend SDK), e.g. server-rendered
integrations like odoo or ThingsBoard.
Cross-app SSO mismatch guard
WSO2 keeps one login session per browser (commonAuthId), shared across every
app. A second app's /authorize call can silently get back a token for
whichever user is already signed in elsewhere — regardless of which username
that app just collected and verified. Call this right after exchanging the
authorization code, passing the username your app expected:
import { assertExpectedUser, AntzSessionUserMismatchError } from "@antzsoft/wso2-auth-backend";
try {
assertExpectedUser(tokens.id_token, expectedUsername);
} catch (err) {
if (err instanceof AntzSessionUserMismatchError) {
// tell the user to log out of the other app first
}
}Fail-open by design — a no-op if there's nothing to compare (e.g. the
id_token's sub is a bare UUID and no email/username claim is
configured on the WSO2 application).
Logout — local vs. full
// Local (app-only): revoke this app's refresh token; other apps stay signed in
await antz.session.revokeToken(refreshToken);
// Full (SSO-wide), back-channel: revoke + end the WSO2 session record
await antz.session.fullLogout(refreshToken, idToken);
// Full, front-channel: redirect the user's browser (required to clear the commonAuthId cookie)
const logoutUrl = antz.session.buildFrontChannelLogoutUrl(idToken, postLogoutRedirectUri);
res.redirect(logoutUrl);Error Types
All errors extend AntzAuthError:
| Class | Thrown when |
|---|---|
| AntzTokenError | M2M token request failed |
| AntzApiError | Any non-2xx SCIM/REST response (carries status/body) |
| AntzUserExistsError | createUser hit a 409 conflict |
| AntzUserNotFoundError | A single-user endpoint returned 404 |
| AntzTokenVerificationError | JWT verification failed (.code: TOKEN_EXPIRED / INVALID_SIGNATURE / CLAIM_MISMATCH / INVALID_TOKEN / MISSING_TOKEN) |
| AntzSessionUserMismatchError | Cross-app SSO session guard tripped |
| AntzChangePasswordError | changePasswordOrThrow failed (.code, .status) |
Environment Routing
Set WSO2_TENANT (prod | dev | uat) as an environment variable so no
code changes are needed when promoting across environments. Users are fully
isolated per tenant — never mix tenant configs within a single backend
deployment.
