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

@antzsoft/wso2-auth-web

v1.4.5

Published

Framework-agnostic WSO2 IS auth client — login, logout, token refresh, change password. Works with React, Vue, Next.js, or plain JS.

Readme

@antzsoft/wso2-auth-web

Framework-agnostic OAuth2 / PKCE client for WSO2 Identity Server 7.x.

Handles login, logout, token refresh, change password (with OTP), and auto-logout on session expiry. Works with React, Vue 3, Next.js (with or without proxy routes), and plain JavaScript/TypeScript.


What's new in v1.4.3

Network error resilience — no unintended logout on offline/timeout:

  • Proactive refresh timer no longer logs out on network failure: Previously, any error during the scheduled refresh (offline, DNS failure, slow connection timeout) was treated identically to an expired refresh token — causing immediate logout. The timer now distinguishes TypeError (no network) and AbortError (timeout) from auth errors (4xx invalid_grant). On network error the timer reschedules silently; only genuine auth failures trigger logout.
  • 5xx server errors no longer cause logout: If WSO2 IS returns a 500 during token refresh (server temporarily down), the SDK now retries on the next cycle instead of logging the user out. The refresh token is still valid in this case.
  • Tab visibility and restore also resilient: The visibility-change handler and on-mount session restore both handle network errors gracefully — the user stays authenticated with their stale token until connectivity returns.
  • Session poll distinguishes network from 401: The poll skips the refresh attempt entirely on network errors and retries on the next tick. Only a confirmed 401 (token revoked) triggers the logout flow.

What's new in v1.4.1 / v1.4.2

sessionPollIntervalSeconds — remote revocation detection:

  • New opt-in config option. When set (e.g. sessionPollIntervalSeconds: 180), the SDK polls GET /api/users/v1/me/session-info every N seconds. This endpoint uses DB-backed token validation and returns 401 immediately when WSO2 revokes tokens (e.g. password changed on another device), even while the JWT signature is still valid.
  • On 401 from the poll: attempts a silent refresh first (transient error guard); if refresh also fails, calls onSessionExpired → logout.
  • Poll is paused automatically when the browser tab is hidden (visibilityState === 'hidden') and resumes on tab focus.
  • Default: 0 (disabled).

What's new in v1.4.0

Multiple concurrent device/browser sessions — no more session flip-flop:

WSO2 IS by default enforces one active token row per (user, app, scope). A second browser login would silently expire the first browser's DB row, causing 401s on that session until its proactive refresh timer fired — then the first session recovered and the second broke. They kept flipping.

This version adds sessionPollIntervalSeconds support and is the first version compatible with the WSO2 server-side fix ([oauth.jwt.renew_token_without_revoking_existing] enable = true). With that server config applied, each login gets a unique token_binding_ref UUID, so all browser/device sessions coexist independently.

Also: ChangePasswordAsync now calls LogoutAsync on success (Blazor pattern, matches React Native behaviour).


What's new in v1.3.6

Session expiry and daily check — reliability fixes:

  • onSessionExpired fires correctly on tab/browser reopen after expiry: Previously, if the browser was closed and reopened after the refresh token had expired, onSessionExpired was silently skipped because the everAuthenticated guard was false on the fresh mount (restore hadn't succeeded yet). The adapter now also checks whether refresh_token or id_token exists in storage — if so, a prior session is present and must be cleaned up (revoke + end_session), so onSessionExpired fires correctly regardless of everAuthenticated.

  • logout() works correctly after re-login: _logoutInProgress was set to true on the first logout and never reset. On subsequent logins and logouts within the same client singleton lifetime, client.logout() silently returned as a no-op — tokens were not revoked and the WSO2 SSO session was not killed. The flag is now reset after tokens are cleared.

  • Daily catch-up no longer fires immediately after login: The catch-up check (fires on mount when the scheduled time has already passed today) now skips if the user logged in after today's check time — meaning the token was freshly issued and cannot be expiring soon. Uses antz_auth_login_time (epoch ms, written at exchangeCode(), stored in localStorage). If login was before the check time (e.g. logged in at 8 AM, check time is 9:43 AM), the catch-up correctly fires when the app reopens after 9:43 AM.


What's new in v1.3.0

Session expiry callbacks — fully reliable across all frameworks and multi-instance setups:

  • onSessionExpired — React multi-instance fix: The React adapter now registers the hook-level onSessionExpired callback on the shared client object (same pattern as onDailyExpiryWarning since v1.2.15). In apps with multiple useAntzAuth() instances (e.g. AuthSessionGuard + DashboardPage), whichever instance's refresh timer fires will always find and invoke the callback.

  • refreshTokens() deduplication: When multiple useAntzAuth() instances share a client and both schedule a proactive refresh timer for the same expiresAt, the second call joins the already-in-flight promise instead of making a duplicate network request. Eliminates the double token API calls and double session-info API calls previously observed in multi-instance setups.

  • Daily expiry catch-up fix — no more immediate logout after login: antz_auth_last_daily_check is now written to localStorage at login time (in exchangeCode()) if not already set. Previously, logout() cleared this key, so the daily catch-up check fired immediately on every login when the configured check time had already passed that day — triggering onDailyExpiryWarning and logging the user out. Now the check correctly fires at most once per day.

  • antz_auth_last_daily_check moved to localStorage: The key previously fell through to sessionStorage (cleared on tab close), causing the catch-up to re-fire whenever the browser was reopened after the configured check time. It is now in localStorage so it persists across tab close and browser restarts within the same calendar day.

  • antz_auth_last_daily_check survives logout: _clearTokens() no longer removes this key, preventing the catch-up from re-firing after a logout+login cycle on the same day.


What's new in v1.2.8

onSessionExpired callback + Daily expiry check:

  • onSessionExpired — new callback in useAntzAuth(client, { onSessionExpired }) (React & Vue). Fires when the SDK detects the refresh token is dead. Replaces the loading → unauthenticated status-watch pattern in AuthSessionGuard with a simpler, explicit callback. The status-watch approach still works as a fallback.

  • Daily expiry check — new opt-in feature (enableDailyExpiryCheck: true). Fires onDailyExpiryWarning once per day at a configurable local time (default 5 AM) when the refresh token will expire within a configurable window (default 24 h). Distinct from onSessionExpired — this is a proactive warning, not a hard expiry. The SDK does not auto-logout; your callback decides the UX. Handles all cases: app open continuously (setTimeout), tab closed and reopened (catch-up on mount), tab hidden and restored (visibility listener).

  • Refresh token expiry stored after logingetSessionInfo() is called fire-and-forget after every login and after every token refresh. The refresh token's absolute expiry is persisted in localStorage under antz_auth_refresh_expires_at so the daily check always has a current value without a network call. Cleared on logout().

New config fields: enableDailyExpiryCheck, dailyCheckHour, dailyCheckMinute, expiryWarningWindowSeconds, onDailyExpiryWarning.


What's new in v1.2.15

onDailyExpiryWarning — multi-instance propagation fix (React):

In Next.js App Router and any app with multiple useAntzAuth() calls for the same client (e.g. a root layout AuthSessionGuard + individual page components), only one hook instance's daily-check timer fires — whichever reached "authenticated" first. Previously, if the page component's instance fired the timer and that instance had no onDailyExpiryWarning callback, the callback registered in AuthSessionGuard was silently skipped.

The React adapter now propagates the hook-level onDailyExpiryWarning to the shared client object when it is set. Any hook instance whose timer fires will use the callback registered by whichever instance has it — regardless of which instance won the race. This means you only need to register onDailyExpiryWarning once (in AuthSessionGuard), not in every component.

expiryWarningWindowHours renamed to expiryWarningWindowSeconds:

The config field now accepts seconds instead of hours, making it practical to use small values for testing (e.g. 300 for a 5-minute window) without waiting a full hour. The default is 86400 (24 hours). Rename the field in your AntzAuthClient config if you were passing it explicitly — the default behaviour is unchanged.


What's new in v1.2.7

logout() — fundamental fix for SSO session not being killed (cross-origin CORS):

The previous approach used fetch(POST /oidc/logout, { credentials: 'include' }) to terminate the WSO2 SSO session. This is a cross-origin request (your app origin ≠ auth.antzsystems.com). Browsers block the session cookie (commonAuthId) on cross-origin fetch unless the server responds with Access-Control-Allow-Credentials: true and a specific Access-Control-Allow-Origin — which WSO2 does not send for /oidc/logout. So the cookie was never transmitted, WSO2 could not find the SSO session, and the session stayed alive. This caused silent re-login on every browser and every machine — some appeared to work by coincidence (e.g. if WSO2's own page handled the redirect differently).

logout() now uses a browser GET navigation to WSO2's end_session endpoint with id_token_hint and post_logout_redirect_uri as query parameters. A browser navigation is not a CORS request — cookies are sent normally, WSO2 receives commonAuthId, kills the session, and redirects back to post_logout_redirect_uri. No WSO2 logout confirmation page is shown when post_logout_redirect_uri is registered.

Required WSO2 Console configuration:

Add your app's base URL as an allowed logout callback in WSO2 Console:

Applications → [your app] → Protocol → Allowed logout callback URLs → add https://yourapp.com (or http://localhost:3001 for local dev)

This must match postLogoutRedirectUri in your AntzAuthClient config (defaults to redirectUri if not set).

Network tab — what you'll see now:

| Before (v1.2.6 and earlier) | After (v1.2.7) | |---|---| | revoke (fetch) | revoke (fetch) | | oauth2_logout.do (navigation — wrong) | Navigation to oidc/logout?id_token_hint=... | | Broken — SSO session not killed | WSO2 kills session, redirects back to app |

The oidc/logout navigation is not a fetch so it does not appear as a separate network entry — the whole page navigates to WSO2 and back, just like the initial login redirect.


What's new in v1.2.6

logout() — fixed oauth2_logout.do browser navigation in Safari:

The POST /oidc/logout call now includes post_logout_redirect_uri in the request body. Without it, WSO2 IS responds with a 302 redirect to its internal oauth2_logout.do page. Safari follows this redirect as a full browser navigation — not as a transparent fetch redirect — which cancelled the remaining JavaScript execution: the revoke call (if still in flight) was aborted, and window.location.href never fired. Adding post_logout_redirect_uri tells WSO2 to redirect back to the app after session termination; the fetch follows the redirect silently within JS and returns normally.

Dashboard sample app — fixed double-logout useEffect:

The useEffect in the dashboard page that watches status === "unauthenticated" was using a bare equality check. This fired on every render where status was unauthenticated — including during the manual sign-out path where client.logout() was already in progress. Changed to a transition guard (authenticated → unauthenticated) using a prevStatus ref, so it only fires when the proactive refresh timer or tab-visibility check detects mid-session expiry, not during a manual sign-out that's already being handled.


What's new in v1.2.5

logout() — fixed silent re-login in Safari and some Chrome builds:

Both fetch calls inside logout() (POST /oauth2/revoke and POST /oidc/logout) now use keepalive: true. Without this flag, browsers that start navigation before a fetch response arrives (Safari, and some Chrome installs on certain platforms) would cancel the in-flight requests. The end_session POST to WSO2 was never delivered, leaving the commonAuthId SSO session cookie alive. On the next login() call, WSO2 found a valid SSO session and silently re-authenticated the user without showing the credentials prompt.

keepalive: true instructs the browser to complete the request even if the page navigates away — the same mechanism used by navigator.sendBeacon, but with full POST body and credentials: include support.

React adapter — logout() race condition fixed:

The React logout() wrapper no longer sets status = "unauthenticated" before client.logout() reads the tokens from storage. Previously, the state update triggered a React re-render that could fire effects while client.logout() was still reading refresh_token and id_token, creating a narrow race window. State is now cleared after client.logout() returns (navigation inside client.logout() means this line only runs if postLogoutRedirectUri is the current page).


What's new in v1.2.3

id_token now persists in localStorage (via SplitStorageAdapter):

The id_token is now stored in localStorage alongside the refresh_token. This is required so that logout() can pass id_token_hint to POST /oidc/logout when the app is reopened after the refresh token has expired — at that point sessionStorage is empty, but id_token must still be available to kill the WSO2 SSO session.

Updated storage layout:

| Token | Storage | Survives browser close? | |-------|---------|------------------------| | refresh_token | localStorage | Yes — persists for the 24h refresh token TTL | | id_token | localStorage | Yes — required for SSO session termination on next open | | access_token, expiry | sessionStorage | No — cleared on tab/browser close |

refreshTokens() no longer clears tokens on failure:

Previously, refreshTokens() called _clearTokens() in its catch block — this wiped id_token from storage before logout() could read it, causing the POST /oidc/logout call to send no id_token_hint, leaving the WSO2 SSO session alive. The catch block is now a clean return null — tokens are only cleared by logout() after it has used them.

AuthSessionGuard — required in your root layout:

Apps must mount an AuthSessionGuard component (or equivalent) once in their root layout. This guard watches for the loading → unauthenticated status transition — which is the signal that the app was reopened after the refresh token expired — and calls logout() to kill the WSO2 SSO session before the user clicks Sign In. Without this guard, the SSO session is never terminated on the "reopen after expiry" path, and WSO2 silently re-authenticates the user on the next login() call.

See the AuthSessionGuard — required root layout component section for the exact pattern for each framework.


What's new in v1.2.0

SplitStorageAdapter is now the default — 24-hour sessions that survive browser close:

The default storage strategy changed from SessionStorageAdapter (everything in sessionStorage, lost on tab/browser close) to SplitStorageAdapter (split between localStorage and sessionStorage):

| Token | Storage | Survives browser close? | |-------|---------|------------------------| | refresh_token | localStorage | Yes — persists for the 24h refresh token TTL | | id_token | localStorage | Yes — required for SSO session termination (see v1.2.3) | | access_token, expiry | sessionStorage | No — cleared on tab/browser close |

This means users who reopen your app within 24 hours are silently restored — no login screen. After 24 hours (or after an explicit logout), they are prompted to log in again.

Silent session restore on reopen — fixed:

Previously, the React and Vue adapters would bail on restore if the access token was expired (e.g. after >15 min with the tab closed), without attempting a silent refresh via the refresh token. This caused a spurious unauthenticated flash and unnecessary re-login on reopen. The restore logic now always calls getAccessToken(), which silently refreshes using the stored refresh token when the access token is expired. The user sees status = 'loading' briefly, then status = 'authenticated' — no login screen.

SSO session correctly killed on refresh token expiry — FORCE_LOGIN removed:

With sessionStorage as the default, closing the browser cleared all tokens before logout() could fire — WSO2's SSO session cookie (commonAuthId) stayed alive. On next open, the app called login(), WSO2 found a valid SSO session, and silently authenticated the user (no login screen). The old workaround was a FORCE_LOGIN flag that appended prompt=login to the next /authorize call.

With SplitStorageAdapter, the refresh token survives browser close. On next open, getAccessToken() detects the expired access token, attempts a refresh — if the refresh token is also expired, refreshTokens() returns null, handleExpired() fires, logout() runs, and the SSO session is terminated via POST /oidc/logout before the user ever clicks Sign In. By the time login() is called, WSO2 has no alive SSO session to reuse. No prompt=login flag is needed.

The FORCE_LOGIN internal key has been removed. No changes required in your app code.

SplitStorageAdapter exported from package root:

import { SplitStorageAdapter } from "@antzsoft/wso2-auth-web";

You only need this if you want to reference the adapter explicitly. No changes needed if you rely on the default.

Required app-side changes when upgrading from v1.1.x:

The package handles session restore correctly, but your app's startup code must also be updated. Any code that uses client.isAuthenticated() as a gate at page load or in route guards will break — it returns false synchronously before the silent refresh has run.

See the Migration: startup and route guard patterns section for exact before/after examples for React, Vue, Next.js, and Vanilla JS.


What's new in v1.1.17+

getSessionInfo() — query token expiry durations from WSO2:

Calls GET /api/users/v1/me/session-info on WSO2 IS and returns the configured expiry durations for the current access and refresh tokens. Useful for showing users when their session will end, or for building adaptive refresh strategies.

const info = await client.getSessionInfo();
// {
//   access_token_expires_at: 1745616400,
//   access_token_expires_in_seconds: 3542,
//   refresh_token_expires_at: 1745702800,
//   refresh_token_expires_in_seconds: 89942,
// }

Throws:

  • AntzSessionExpiredError — access token is expired or missing
  • AntzApiError — any other error from the endpoint

What's new in v1.1.15+

decodeToken() — decode any JWT access token locally:

A new named export decodeToken decodes any JWT string without a network call. Useful for displaying access token claims (sub, roles, tenant, exp, iss, etc.) on a dashboard:

import { decodeToken } from "@antzsoft/wso2-auth-web";

const token = await client.getAccessToken();
const claims = decodeToken(token!);
// { sub: "...", roles: [...], tenant: "dev", exp: 1745612800, ... }

Returns Record<string, unknown> | null. Returns null if the string is not a valid JWT or decoding fails.


What's new in v1.1.10+

logout() is now fully fetch-based — no browser redirect to WSO2:

Previously logout() redirected the browser to WSO2's /oidc/logout endpoint, which could show a "Are you sure you want to log out?" confirmation page. Starting in v1.1.10, logout is handled entirely via fetch:

  1. POST /oauth2/revoke — revokes the refresh token server-side
  2. POST /oidc/logout with credentials: 'include' — terminates the WSO2 SSO session using the browser's session cookie, with no browser redirect and no confirmation page

logout() is now async and returns Promise<void>:

Update your call sites if you need to await it:

// Before (v1.1.9 and earlier)
logout: () => void

// After (v1.1.10+)
logout: () => Promise<void>

React adapter returns accessToken:

useAntzAuth() now returns accessToken: string | null — a reactive state value that updates automatically after every background refresh. No need to call getAccessToken() just to display the current token.

Vue adapter returns status and accessToken:

useAntzAuth() now returns status (reactive ref matching the React adapter) and accessToken in addition to the existing fields.

React 18 Strict Mode protection:

useAntzCallback and useAntzAuth's session-restore effect are both protected by useRef run-once guards — token exchange and session restore fire exactly once per navigation, even in Strict Mode's double-mount development behavior.


Contents


What's in the package

| Export | Description | |--------|-------------| | AntzAuthClient | Core client class — works in any JS environment | | useAntzAuth | React hook (from @antzsoft/wso2-auth-web/react) | | useAntzCallback | React callback hook (from @antzsoft/wso2-auth-web/react) | | useAntzAuth | Vue 3 composable (from @antzsoft/wso2-auth-web/vue) | | useAntzCallback | Vue 3 callback composable (from @antzsoft/wso2-auth-web/vue) | | SplitStorageAdapter | Default — refresh token in localStorage (24h persistence), access token in sessionStorage | | SessionStorageAdapter | All tokens in sessionStorage — cleared on tab/browser close | | LocalStorageAdapter | All tokens in localStorage — survives page reload, higher XSS exposure | | MemoryStorageAdapter | In-memory storage — SSR / testing, lost on page reload | | decodeToken | Decodes any JWT string locally — no network call. Returns all payload claims | | Error classes | Typed errors for every failure scenario |


Installation

# npm
npm install @antzsoft/wso2-auth-web

# yarn
yarn add @antzsoft/wso2-auth-web

# pnpm
pnpm add @antzsoft/wso2-auth-web

For React projects, react >= 18 must be installed (peer dependency).
For Vue projects, vue >= 3 must be installed (peer dependency).


Configuration

import { AntzAuthClient } from "@antzsoft/wso2-auth-web";

const client = new AntzAuthClient({
  baseUrl:     "https://auth.antzsystems.com",   // WSO2 IS base URL, no trailing slash
  clientId:    "your-client-id",                 // OAuth2 client_id from WSO2 Console
  redirectUri: "https://yourapp.com/callback",   // Must be registered in WSO2 Console
  tenant:      "dev",                            // Tenant: "dev" | "uat" | "prod" — omit for carbon.super
  scopes:      ["openid", "profile", "email", "roles"], // OAuth2 scopes
  proxyUrl:    "/api/auth/change-password",      // Optional — see CORS section below
  storage:     new SplitStorageAdapter(),        // Optional — default: SplitStorageAdapter
  postLogoutRedirectUri: "https://yourapp.com",  // Optional — default: redirectUri
});

Config options

| Option | Type | Required | Description | |--------|------|----------|-------------| | baseUrl | string | Yes | WSO2 IS base URL (e.g. https://auth.antzsystems.com) | | clientId | string | Yes | OAuth2 client_id registered in WSO2 Console | | redirectUri | string | Yes | Callback URL — must exactly match WSO2 Console registration | | tenant | string | No | Tenant domain (dev, uat, prod). Omit for root org (carbon.super) | | scopes | string[] | No | OAuth2 scopes. Default: ["openid", "profile", "email"] | | proxyUrl | string | No | Base URL for same-origin proxy routes. Required when WSO2 CORS is not configured. See CORS section | | storage | StorageAdapter | No | Token storage adapter. Default: SplitStorageAdapter (refresh token in localStorage, access token in sessionStorage) | | postLogoutRedirectUri | string | No | Where the browser navigates after logout. Default: redirectUri. | | refreshBufferSeconds | number | No | Seconds before access token expiry to proactively refresh. Default: 60 | | enableDailyExpiryCheck | boolean | No | Enable the once-per-day refresh token expiry warning. Default: false | | dailyCheckHour | number | No | Local hour (0–23) to run the daily check. Default: 5 (5 AM) | | dailyCheckMinute | number | No | Local minute (0–59) to run the daily check. Default: 0 | | expiryWarningWindowSeconds | number | No | Fire onDailyExpiryWarning if the refresh token expires within this many seconds. Default: 86400 (24 h) | | onDailyExpiryWarning | () => void | No | Called by the daily check when the refresh token is expiring soon. Register this in useAntzAuth() instead for access to React/Vue context (router, toasts, etc.) — see Daily Expiry Check. |


Core API

login()

Starts the PKCE authorization code flow. Generates verifier + challenge, saves them to storage, then redirects the browser to WSO2's /authorize endpoint.

await client.login();

// With custom scopes
await client.login(["openid", "profile", "email", "roles"]);

This method redirects the browser — it never returns.


handleCallback()

Call this on your redirect/callback page. Reads code and state from the URL, validates state (CSRF protection), and exchanges the code for tokens.

const tokens = await client.handleCallback();
// tokens: { access_token, refresh_token, id_token, expires_in, ... }

// Or pass the URL explicitly (useful in SSR or testing)
const tokens = await client.handleCallback("https://yourapp.com/callback?code=...&state=...");

Throws:

  • AntzAuthError — state mismatch (CSRF), missing code, or authorization error from WSO2
  • AntzTokenError — missing PKCE verifier (session lost) or token exchange failed

getAccessToken()

Returns the current access token. Silently refreshes it if it is expiring within refreshBufferSeconds (default: 60 seconds). Returns null if not authenticated or if the refresh token has expired.

const token = await client.getAccessToken();
if (!token) {
  // Not authenticated or session expired — redirect to login
}

refreshTokens()

Explicitly refreshes tokens using the stored refresh token. Returns null and clears the session if the refresh token is missing or expired.

const tokens = await client.refreshTokens();
// tokens: TokenSet | null

getUser()

Decodes user claims from the stored id_token locally — no network call.

const user = client.getUser();
// {
//   sub: "9f3eed57-...",
//   email: "[email protected]",
//   name: "John Doe",
//   given_name: "John",
//   family_name: "Doe",
//   username: "dev_john",
//   tenant: "dev",
//   roles: ["admin", "user"]
// }

Returns null if not authenticated.


isAuthenticated()

Synchronous check — returns true if an access token exists in storage right now and has not expired. No network call, no refresh attempt.

isAuthenticated(): boolean
// true  — access token present and not yet expired
// false — access token missing or expired (does NOT attempt refresh)

When to use it:

Only inside an already-running authenticated session — never at page load or in route guards.

| Situation | Use | |-----------|-----| | App startup, page load, route guard | await getAccessToken() or watch status | | Polling interval (already mounted, session known) | isAuthenticated() as a cheap skip guard | | Tab visibility handler (already mounted) | isAuthenticated() as a cheap skip guard |

Correct — skip a polling check if clearly not logged in:

setInterval(async () => {
  if (!client.isAuthenticated()) return; // already gone — no point refreshing
  const token = await client.getAccessToken();
  if (!token) client.logout();
}, 30_000);

Wrong — synchronous gate at page load:

// After browser close, sessionStorage is wiped — this always returns false
// before getAccessToken() has had a chance to restore via localStorage refresh token
useEffect(() => {
  if (client.isAuthenticated()) router.replace("/dashboard"); // ✗
}, []);

Correct — wait for async restore:

const { status } = useAntzAuth(client);
useEffect(() => {
  if (status === "authenticated") router.replace("/dashboard"); // ✓
}, [status]);

Why this matters with SplitStorageAdapter: After browser close, the access token is gone from sessionStorage but the refresh token is still in localStorage. isAuthenticated() returns false immediately. getAccessToken() sees the missing access token, finds the refresh token, silently exchanges it, and returns a fresh access token — restoring the session without showing the login page.


logout()

Revokes tokens, terminates the WSO2 SSO session server-side, clears all local storage, and navigates to postLogoutRedirectUri. Everything happens via fetch — there is no browser redirect to WSO2, no confirmation page.

await client.logout();

What it does, in order:

  1. POST /oauth2/revoke — revokes the refresh token (invalidates it on the WSO2 server). Uses keepalive: true so the request completes even after the navigation in step 3 starts.
  2. Clears all tokens from local storage
  3. Navigates the browser to GET /oidc/logout?id_token_hint=...&post_logout_redirect_uri=... — a real browser navigation (not fetch), so WSO2 receives the session cookie without CORS restrictions. WSO2 kills the SSO session and redirects back to post_logout_redirect_uri.

Why a browser navigation instead of fetch for end_session:

POST /oidc/logout with credentials: 'include' is a cross-origin request. Browsers block the session cookie on cross-origin fetch unless the server responds with Access-Control-Allow-Credentials: true — which WSO2 does not send for /oidc/logout. A GET navigation is not a CORS request and always sends cookies normally.

Required WSO2 Console configuration:

post_logout_redirect_uri must be registered in WSO2 Console:

Applications → [your app] → Protocol → Allowed logout callback URLs → add your app's base URL

This must match postLogoutRedirectUri in your client config (defaults to redirectUri if not set).

Idempotent — safe to call from auto-logout and manual logout:

logout() is guarded internally against concurrent calls. If status transitions to 'unauthenticated' (triggering auto-logout) at the same time the user clicks "Sign out" (triggering manual logout), only one logout sequence runs. You do not need any debounce logic in your components.

// Manual logout button
<button onClick={() => logout()}>Sign out</button>

// Auto-logout when session expires (React)
useEffect(() => {
  if (status === "unauthenticated") logout();
}, [status, logout]);

// Both paths call the same logout() — identical behavior

postLogoutRedirectUri:

Controls where the browser navigates after tokens are cleared. Defaults to redirectUri.

const client = new AntzAuthClient({
  redirectUri:           "http://localhost:3000/callback",
  postLogoutRedirectUri: "http://localhost:3000",  // optional, defaults to redirectUri
});

sendOtp()

Requests WSO2 to send a one-time password to the user's registered mobile/email before a password change. Only needed when OTP is enabled for the tenant/app in WSO2 Console.

const { otpRequired, message } = await client.sendOtp();

if (!otpRequired) {
  // OTP is disabled for this app — call changePassword() directly without an OTP
}

if (otpRequired) {
  // OTP was sent — show OTP input to the user
  // message may contain a hint like "OTP sent to +91XXXXXXXX90"
}

Returns: { otpRequired: boolean, message?: string }

Throws:

  • AntzSessionExpiredError — access token expired
  • AntzApiError — no contact info on account or send failure

When proxyUrl is set:
Calls POST {proxyUrl}/send-otp (your same-origin proxy).
When not set, calls WSO2 directly (requires CORS to be configured).


changePassword()

Changes the logged-in user's password.

// Without OTP (when OTP is disabled for the app)
await client.changePassword(currentPassword, newPassword);

// With OTP (when OTP is enabled — call sendOtp() first)
await client.changePassword(currentPassword, newPassword, otp);

Throws:

| Error | When | |-------|------| | AntzInvalidCredentialsError | currentPassword is wrong | | AntzInvalidOtpError | OTP code is wrong | | AntzOtpExpiredError | OTP has passed its 5-minute TTL | | AntzOtpRequiredError | OTP enabled but not provided, or sendOtp() was not called | | AntzOtpMaxAttemptsError | 3 wrong OTP codes — OTP invalidated, call sendOtp() again | | AntzPasswordPolicyError | New password fails WSO2 policy (complexity, history) | | AntzSessionExpiredError | Access token expired | | AntzApiError | Other WSO2 error |

When proxyUrl is set:
Calls POST {proxyUrl} (your same-origin proxy).
When not set, calls WSO2 directly (requires CORS to be configured).


decodeToken()

Decodes a JWT string locally — no network call, no signature verification. Returns all payload claims as a plain object, or null if the input is not a valid JWT.

import { decodeToken } from "@antzsoft/wso2-auth-web";

const token = await client.getAccessToken();
if (token) {
  const claims = decodeToken(token);
  // {
  //   sub: "9f3eed57-...",
  //   roles: ["admin", "user"],
  //   tenant: "dev",
  //   exp: 1745612800,
  //   iss: "https://auth.antzsystems.com/t/dev/oauth2/token",
  //   ...
  // }
}

This is a standalone utility — it is not a method on AntzAuthClient. Import it directly from the package root. Useful for displaying token debug info in dashboards without making any API calls.


getSessionInfo()

Returns the configured expiry durations for the current access and refresh tokens. Calls GET /api/users/v1/me/session-info on WSO2 IS using the current Bearer token.

import type { SessionInfo } from "@antzsoft/wso2-auth-web";

const info: SessionInfo = await client.getSessionInfo();

console.log(info.access_token_expires_at);        // epoch seconds
console.log(info.access_token_expires_in_seconds); // seconds remaining
console.log(info.refresh_token_expires_at);        // epoch seconds
console.log(info.refresh_token_expires_in_seconds); // seconds remaining

// Human-readable date:
const expiresAt = new Date(info.access_token_expires_at * 1000);
console.log(expiresAt.toLocaleString());

Returns: Promise<SessionInfo>

interface SessionInfo {
  access_token_expires_at: number;        // epoch timestamp (seconds)
  access_token_expires_in_seconds: number; // seconds remaining
  refresh_token_expires_at: number;        // epoch timestamp (seconds)
  refresh_token_expires_in_seconds: number; // seconds remaining
}

Throws:

  • AntzSessionExpiredError — access token is expired or not present
  • AntzApiError — any other non-2xx response from WSO2

forgotPassword()

Redirects to WSO2 My Account portal for self-service password reset.

client.forgotPassword();        // opens in new tab (default)
client.forgotPassword(false);   // redirects current tab

apiFetch()

Makes an authenticated fetch to your backend API. Automatically attaches the Bearer token and silently refreshes if needed. Throws AntzSessionExpiredError on 401.

const data = await client.apiFetch<{ name: string }>("/api/profile");

// With custom options
const result = await client.apiFetch("/api/data", {
  method: "POST",
  body: JSON.stringify({ key: "value" }),
});

Storage Adapters

The package includes four built-in adapters and supports custom ones.

SplitStorageAdapter (default)

The default adapter since v1.2.0. Routes refresh_token to localStorage (survives browser close for the 24h refresh token TTL) and all other tokens to sessionStorage (cleared when the tab closes, limiting XSS exposure to the short-lived access token).

// This is the default — you do not need to specify it explicitly
import { AntzAuthClient, SplitStorageAdapter } from "@antzsoft/wso2-auth-web";

const client = new AntzAuthClient({
  // ...
  storage: new SplitStorageAdapter(), // this is the default
});

SessionStorageAdapter

All tokens stored in sessionStorage — cleared when the tab or browser is closed. Use this if you want sessions that never survive browser close (e.g. high-security apps where persistent sessions are undesirable).

import { AntzAuthClient, SessionStorageAdapter } from "@antzsoft/wso2-auth-web";

const client = new AntzAuthClient({
  // ...
  storage: new SessionStorageAdapter(),
});

LocalStorageAdapter

All tokens stored in localStorage — survive tab close and page reload. Higher XSS exposure than SplitStorageAdapter since the access token is also persisted.

import { AntzAuthClient, LocalStorageAdapter } from "@antzsoft/wso2-auth-web";

const client = new AntzAuthClient({
  // ...
  storage: new LocalStorageAdapter(),
});

MemoryStorageAdapter

Tokens stored in memory — lost on page reload. Useful for SSR environments where sessionStorage/localStorage are not available.

import { AntzAuthClient, MemoryStorageAdapter } from "@antzsoft/wso2-auth-web";

const client = new AntzAuthClient({
  // ...
  storage: new MemoryStorageAdapter(),
});

Custom Adapter

Implement the StorageAdapter interface to use cookies, Redis, or any other store:

import type { StorageAdapter } from "@antzsoft/wso2-auth-web";

class CookieStorageAdapter implements StorageAdapter {
  get(key: string): string | null { /* ... */ }
  set(key: string, value: string): void { /* ... */ }
  remove(key: string): void { /* ... */ }
  clear(): void { /* ... */ }
}

Error Types

All errors extend AntzAuthError which extends Error.

| Class | When thrown | |-------|-------------| | AntzAuthError | Base class — generic auth errors, CSRF, config errors | | AntzTokenError | Token exchange or refresh failed | | AntzApiError | WSO2 API returned a non-2xx response. Has .status (number) and .body (string) | | AntzSessionExpiredError | Refresh token expired — user must log in again | | AntzInvalidCredentialsError | Wrong current password in changePassword() | | AntzInvalidOtpError | Wrong OTP code | | AntzOtpExpiredError | OTP TTL (5 min) elapsed | | AntzOtpRequiredError | OTP enabled but not provided | | AntzOtpMaxAttemptsError | 3 consecutive wrong OTP codes | | AntzPasswordPolicyError | New password fails WSO2 complexity/history policy |

import {
  AntzInvalidCredentialsError,
  AntzInvalidOtpError,
  AntzOtpExpiredError,
  AntzOtpRequiredError,
  AntzOtpMaxAttemptsError,
  AntzPasswordPolicyError,
  AntzSessionExpiredError,
  AntzApiError,
} from "@antzsoft/wso2-auth-web";

try {
  await client.changePassword(current, newPassword, otp);
} catch (err) {
  if (err instanceof AntzInvalidCredentialsError) { /* wrong current password */ }
  if (err instanceof AntzInvalidOtpError)         { /* wrong OTP */ }
  if (err instanceof AntzOtpExpiredError)         { /* OTP expired */ }
  if (err instanceof AntzOtpRequiredError)        { /* need to call sendOtp() first */ }
  if (err instanceof AntzOtpMaxAttemptsError)     { /* too many wrong attempts */ }
  if (err instanceof AntzPasswordPolicyError)     { /* err.message has policy detail */ }
  if (err instanceof AntzSessionExpiredError)     { /* redirect to login */ }
  if (err instanceof AntzApiError)                { /* err.status, err.body */ }
}

Framework Adapters

React Adapter

Import from @antzsoft/wso2-auth-web/react.

useAntzAuth(client, options?)

import { useAntzAuth } from "@antzsoft/wso2-auth-web/react";

const {
  status,           // 'idle' | 'loading' | 'authenticated' | 'unauthenticated'
  isAuthenticated,  // boolean — shorthand for status === 'authenticated'
  isLoading,        // boolean — true while status is 'idle' or 'loading'
  user,             // UserClaims | null
  accessToken,      // string | null — updates automatically after every background refresh
  error,            // Error | null
  login,            // (scopes?: string[]) => Promise<void>
  logout,           // () => Promise<void>
  getAccessToken,   // () => Promise<string | null>
  sendOtp,          // () => Promise<{ otpRequired: boolean; message?: string }>
  changePassword,   // (current, newPwd, otp?) => Promise<void>
  forgotPassword,   // (newTab?: boolean) => void
} = useAntzAuth(client);

Options:

| Option | Type | Description | |--------|------|-------------| | onSessionExpired | () => void | Fires when the refresh token is dead (proactive timer or tab-focus check). Alternative to watching status === "unauthenticated". | | onDailyExpiryWarning | () => void | Fires once per day at the configured time when the refresh token is expiring soon. Only active when enableDailyExpiryCheck: true is set in AntzAuthClient config. Register here (not in the client config) to get access to the hook's logout(), router, toasts, etc. |

Token refresh — fully automatic

The hook manages token refresh with three mechanisms (matching the RN package):

  1. Session restore on mount — checks stored tokens on load, silently refreshes if expiring soon
  2. Proactive timer — fires refreshBufferSeconds before access token expiry, silently refreshes in the background
  3. Tab visibility check — fires when the user switches back to the tab after it was hidden, in case the token expired while the tab was in the background

Reacting to session expiry — recommended: watch status

The cleanest way to handle session expiry is to watch the reactive status field in your router or a root layout component. When the refresh token expires, status automatically transitions to 'unauthenticated':

// Root layout or router guard
import { useAntzAuth } from "@antzsoft/wso2-auth-web/react";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import client from "../auth";

export function AuthGuard({ children }: { children: React.ReactNode }) {
  const { status } = useAntzAuth(client);
  const navigate = useNavigate();

  // Show loading screen while session is being restored
  if (status === "idle" || status === "loading") {
    return <div>Loading…</div>;
  }

  // Redirect to login if not authenticated
  if (status === "unauthenticated") {
    navigate("/login", { replace: true });
    return null;
  }

  return <>{children}</>;
}

This handles all four cases automatically:

| status | Cause | What to show | |---|---|---| | idle | Before session restore runs | Loading screen | | loading | Session restore in progress | Loading screen | | authenticated | Valid tokens | App content | | unauthenticated | Not logged in, logout, or refresh token expired/revoked | Login page |

Alternative: onSessionExpired callback

If you prefer a callback over watching status (e.g. in a deeply nested component), you can pass onSessionExpired:

useAntzAuth(client, {
  onSessionExpired: () => router.replace("/login"),
});

Both approaches work — use whichever fits your routing setup.

useAntzCallback(client, onSuccess, onError?)

Use this on your /callback page component.

import { useAntzCallback } from "@antzsoft/wso2-auth-web/react";

const { isLoading, error } = useAntzCallback(
  client,
  () => router.push("/dashboard"),        // onSuccess
  (err) => router.push(`/?error=${err.message}`) // onError (optional)
);

React 18 Strict Mode: In development, React mounts components twice. useAntzCallback is protected by a useRef run-once guard — the token exchange fires only once per navigation, even in Strict Mode. Similarly, useAntzAuth's session-restore useEffect uses the same guard to prevent double refresh calls on mount.


Vue 3 Adapter

Import from @antzsoft/wso2-auth-web/vue.

useAntzAuth(client, options?)

import { useAntzAuth } from "@antzsoft/wso2-auth-web/vue";

const {
  status,           // Readonly<Ref<'idle' | 'loading' | 'authenticated' | 'unauthenticated'>>
  isAuthenticated,  // Readonly<Ref<boolean>>
  isLoading,        // Readonly<Ref<boolean>>
  user,             // Readonly<Ref<UserClaims | null>>
  accessToken,      // Readonly<Ref<string | null>> — updates after every background refresh
  error,            // Readonly<Ref<Error | null>>
  login,            // (scopes?: string[]) => Promise<void>
  logout,           // () => Promise<void>
  getAccessToken,   // () => Promise<string | null>
  sendOtp,          // () => Promise<{ otpRequired: boolean; message?: string }>
  changePassword,   // (current, newPwd, otp?) => Promise<void>
  forgotPassword,   // (newTab?: boolean) => void
} = useAntzAuth(client, {
  onSessionExpired: () => router.replace("/login"),
});

All reactive state is returned as readonly refs — use .value to access them in <script setup> or the Options API, and they are automatically unwrapped in templates.

useAntzCallback(client, onSuccess, onError?)

import { useAntzCallback } from "@antzsoft/wso2-auth-web/vue";

const { isLoading, error } = useAntzCallback(
  client,
  () => router.push("/dashboard"),
  (err) => router.push(`/?error=${err.message}`)
);

Integration Guides

React / Vite SPA

1. Create the client singleton (src/auth.ts):

import { AntzAuthClient } from "@antzsoft/wso2-auth-web";

const client = new AntzAuthClient({
  baseUrl:     import.meta.env.VITE_WSO2_BASE_URL,
  clientId:    import.meta.env.VITE_WSO2_CLIENT_ID,
  redirectUri: import.meta.env.VITE_WSO2_REDIRECT_URI,
  tenant:      import.meta.env.VITE_WSO2_TENANT || undefined,
  scopes:      ["openid", "profile", "email", "roles"],
});

export default client;

2. .env:

VITE_WSO2_BASE_URL=https://auth.antzsystems.com
VITE_WSO2_TENANT=dev
VITE_WSO2_CLIENT_ID=your-client-id
VITE_WSO2_REDIRECT_URI=http://localhost:5173/callback

3. Login page (src/pages/LoginPage.tsx):

import { useAntzAuth } from "@antzsoft/wso2-auth-web/react";
import client from "../auth";

export default function LoginPage() {
  const { login } = useAntzAuth(client);

  return <button onClick={() => login()}>Sign in</button>;
}

4. Callback page (src/pages/CallbackPage.tsx):

import { useAntzCallback } from "@antzsoft/wso2-auth-web/react";
import { useNavigate } from "react-router-dom";
import client from "../auth";

export default function CallbackPage() {
  const navigate = useNavigate();
  const { isLoading, error } = useAntzCallback(
    client,
    () => navigate("/dashboard"),
    (err) => navigate(`/?error=${err.message}`)
  );

  if (isLoading) return <p>Signing in…</p>;
  if (error)     return <p>Error: {error.message}</p>;
  return null;
}

5. Protected page (src/pages/DashboardPage.tsx):

import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAntzAuth } from "@antzsoft/wso2-auth-web/react";
import client from "../auth";

export default function DashboardPage() {
  const navigate = useNavigate();
  const { isAuthenticated, isLoading, user, logout } = useAntzAuth(client, {
    onSessionExpired: () => navigate("/?error=Session+expired"),
  });

  useEffect(() => {
    if (!isLoading && !isAuthenticated) navigate("/");
  }, [isLoading, isAuthenticated, navigate]);

  if (isLoading || !user) return null;

  return (
    <div>
      <p>Welcome, {user.name}</p>
      <button onClick={logout}>Sign out</button>
    </div>
  );
}

6. vite.config.ts — add a dev proxy if WSO2 CORS is not configured:

export default {
  server: {
    proxy: {
      "/api/auth": {
        target: "https://auth.antzsystems.com",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api\/auth/, ""),
      },
    },
  },
};

Vue 3 / Vite SPA

1. Client singleton (src/auth.ts):

import { AntzAuthClient } from "@antzsoft/wso2-auth-web";

export const client = new AntzAuthClient({
  baseUrl:     import.meta.env.VITE_WSO2_BASE_URL,
  clientId:    import.meta.env.VITE_WSO2_CLIENT_ID,
  redirectUri: import.meta.env.VITE_WSO2_REDIRECT_URI,
  tenant:      import.meta.env.VITE_WSO2_TENANT || undefined,
  scopes:      ["openid", "profile", "email", "roles"],
});

2. Login page (src/pages/LoginPage.vue):

<script setup lang="ts">
import { useAntzAuth } from "@antzsoft/wso2-auth-web/vue";
import { client } from "../auth";

const { login } = useAntzAuth(client);
</script>

<template>
  <button @click="login()">Sign in</button>
</template>

3. Callback page (src/pages/CallbackPage.vue):

<script setup lang="ts">
import { useAntzCallback } from "@antzsoft/wso2-auth-web/vue";
import { useRouter } from "vue-router";
import { client } from "../auth";

const router = useRouter();
const { isLoading, error } = useAntzCallback(
  client,
  () => router.push("/dashboard"),
  (err) => router.push(`/?error=${err.message}`)
);
</script>

<template>
  <p v-if="isLoading">Signing in…</p>
  <p v-else-if="error">Error: {{ error.message }}</p>
</template>

4. Protected page (src/pages/DashboardPage.vue):

<script setup lang="ts">
import { watchEffect } from "vue";
import { useRouter } from "vue-router";
import { useAntzAuth } from "@antzsoft/wso2-auth-web/vue";
import { client } from "../auth";

const router = useRouter();
const { isAuthenticated, isLoading, user, logout } = useAntzAuth(client, {
  onSessionExpired: () => router.replace("/?error=Session+expired"),
});

watchEffect(() => {
  if (!isLoading.value && !isAuthenticated.value) router.replace("/");
});
</script>

<template>
  <div v-if="user">
    <p>Welcome, {{ user.name }}</p>
    <button @click="logout()">Sign out</button>
  </div>
</template>

Next.js with Proxy Routes (recommended)

Use this when WSO2 CORS is not configured for your app's origin. The proxy routes run server-side (Node.js → WSO2), bypassing CORS entirely.

1. Client singleton (src/lib/auth.ts):

import { AntzAuthClient } from "@antzsoft/wso2-auth-web";

const client = new AntzAuthClient({
  baseUrl:     process.env.NEXT_PUBLIC_WSO2_BASE_URL!,
  clientId:    process.env.NEXT_PUBLIC_WSO2_CLIENT_ID!,
  redirectUri: process.env.NEXT_PUBLIC_WSO2_REDIRECT_URI!,
  tenant:      process.env.NEXT_PUBLIC_WSO2_TENANT || undefined,
  scopes:      ["openid", "profile", "email", "roles"],
  proxyUrl:    "/api/auth/change-password",
});

export default client;

2. .env.local:

NEXT_PUBLIC_WSO2_BASE_URL=https://auth.antzsystems.com
NEXT_PUBLIC_WSO2_TENANT=dev
NEXT_PUBLIC_WSO2_CLIENT_ID=your-client-id
NEXT_PUBLIC_WSO2_REDIRECT_URI=http://localhost:3000/callback

3. WSO2 callback relay (src/app/api/auth/callback/route.ts):

import { NextRequest, NextResponse } from "next/server";

export async function GET(req: NextRequest) {
  const { searchParams, origin } = new URL(req.url);
  const error = searchParams.get("error");
  if (error) {
    return NextResponse.redirect(`${origin}/?error=${encodeURIComponent(error)}`);
  }
  return NextResponse.redirect(`${origin}/callback?${searchParams.toString()}`);
}

4. Proxy route — send OTP (src/app/api/auth/change-password/send-otp/route.ts):

import { NextRequest, NextResponse } from "next/server";

const WSO2_BASE = process.env.NEXT_PUBLIC_WSO2_BASE_URL!;
const TENANT    = process.env.NEXT_PUBLIC_WSO2_TENANT ?? "";

function sendOtpUrl() {
  const base = TENANT ? `${WSO2_BASE}/t/${TENANT}` : WSO2_BASE;
  return `${base}/api/users/v1/me/change-password/send-otp`;
}

function extractAlbCookies(cookieHeader: string): string {
  return cookieHeader.split(";").map(c => c.trim())
    .filter(c => c.startsWith("AWSALB=") || c.startsWith("AWSALBCORS="))
    .join("; ");
}

export async function POST(req: NextRequest) {
  try {
    const authHeader = req.headers.get("authorization") ?? "";
    if (!authHeader.startsWith("Bearer ")) {
      return NextResponse.json({ error: "Missing Bearer token" }, { status: 401 });
    }

    const albCookie = extractAlbCookies(req.headers.get("cookie") ?? "");
    const doFetch = (withCookie: boolean) => fetch(sendOtpUrl(), {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: authHeader,
        ...(withCookie && albCookie ? { Cookie: albCookie } : {}),
      },
    });

    let wso2Res = await doFetch(true);
    // Retry without stale ALB sticky cookie on 401
    if (wso2Res.status === 401 && albCookie) {
      wso2Res = await doFetch(false);
    }

    if (wso2Res.ok) {
      const body = await wso2Res.json().catch(() => ({}));
      const res = NextResponse.json({ otpRequired: true, message: body.message ?? undefined });
      // Propagate new ALB sticky cookie to browser
      for (const [k, v] of wso2Res.headers.entries()) {
        if (k.toLowerCase() === "set-cookie") res.headers.append("set-cookie", v);
      }
      return res;
    }

    const rawBody = await wso2Res.text();
    const parsed = JSON.parse(rawBody).catch?.(() => ({})) ?? {};
    if (wso2Res.status === 400 && parsed.code === "OTP_NOT_ENABLED") {
      return NextResponse.json({ otpRequired: false });
    }
    return NextResponse.json(parsed, { status: wso2Res.status });
  } catch (err) {
    console.error("[send-otp]", err);
    return NextResponse.json({ error: "Internal server error" }, { status: 500 });
  }
}

5. Proxy route — change password (src/app/api/auth/change-password/route.ts):

import { NextRequest, NextResponse } from "next/server";

const WSO2_BASE = process.env.NEXT_PUBLIC_WSO2_BASE_URL!;
const TENANT    = process.env.NEXT_PUBLIC_WSO2_TENANT ?? "";

function changePasswordUrl() {
  const base = TENANT ? `${WSO2_BASE}/t/${TENANT}` : WSO2_BASE;
  return `${base}/api/users/v1/me/change-password`;
}

function extractAlbCookies(cookieHeader: string): string {
  return cookieHeader.split(";").map(c => c.trim())
    .filter(c => c.startsWith("AWSALB=") || c.startsWith("AWSALBCORS="))
    .join("; ");
}

export async function POST(req: NextRequest) {
  try {
    const authHeader = req.headers.get("authorization") ?? "";
    if (!authHeader.startsWith("Bearer ")) {
      return NextResponse.json({ error: "Missing Bearer token" }, { status: 401 });
    }

    let body: { currentPassword?: string; newPassword?: string; otp?: string };
    try { body = await req.json(); }
    catch { return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); }

    const { currentPassword, newPassword, otp } = body;
    if (!currentPassword || !newPassword) {
      return NextResponse.json({ error: "currentPassword and newPassword are required" }, { status: 400 });
    }

    const wso2Payload: Record<string, string> = { currentPassword, newPassword };
    if (otp) wso2Payload.otp = otp;

    const albCookie = extractAlbCookies(req.headers.get("cookie") ?? "");

    const wso2Res = await fetch(changePasswordUrl(), {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: authHeader,
        ...(albCookie ? { Cookie: albCookie } : {}),
      },
      body: JSON.stringify(wso2Payload),
    });

    if (wso2Res.ok) return NextResponse.json({ message: "Password changed successfully" });

    const rawBody = await wso2Res.text();
    let parsed: { code?: string; message?: string } = {};
    try { parsed = JSON.parse(rawBody); } catch { /* not JSON */ }

    return NextResponse.json(parsed, { status: wso2Res.status });
  } catch (err) {
    console.error("[change-password]", err);
    return NextResponse.json({ error: "Internal server error" }, { status: 500 });
  }
}

6. next.config.mjs:

/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ["@antzsoft/wso2-auth-web"],
};

export default nextConfig;

Proxy URL routing summary:

| Package method | Proxy route called | |----------------|-------------------| | client.sendOtp() | POST /api/auth/change-password/send-otp | | client.changePassword() | POST /api/auth/change-password |


Next.js Client-Only

Use this when WSO2 CORS is configured to allow your app's origin. No proxy routes needed — all calls go directly from the browser to WSO2.

1. Client singleton (src/lib/auth.ts):

import { AntzAuthClient } from "@antzsoft/wso2-auth-web";

const client = new AntzAuthClient({
  baseUrl:     process.env.NEXT_PUBLIC_WSO2_BASE_URL!,
  clientId:    process.env.NEXT_PUBLIC_WSO2_CLIENT_ID!,
  redirectUri: process.env.NEXT_PUBLIC_WSO2_REDIRECT_URI!,
  tenant:      process.env.NEXT_PUBLIC_WSO2_TENANT || undefined,
  scopes:      ["openid", "profile", "email", "roles"],
  // No proxyUrl — direct browser → WSO2 calls
});

export default client;

2. Callback page (src/app/callback/page.tsx):

Since there is no server-side /api/auth/callback route, register the client page URL directly as the redirectUri in WSO2 Console (http://localhost:3000/callback). The client.handleCallback() call reads the code from window.location.href directly.

"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import client from "@/lib/auth";

export default function CallbackPage() {
  const router = useRouter();

  useEffect(() => {
    client.handleCallback()
      .then(() => router.replace("/dashboard"))
      .catch((err) => router.replace(`/?error=${encodeURIComponent(err.message)}`));
  }, []);

  return <p>Signing in…</p>;
}

3. All pages are "use client" — the package uses localStorage, sessionStorage, and window which are browser-only APIs.


Vanilla JS / TypeScript

import { AntzAuthClient } from "@antzsoft/wso2-auth-web";

const client = new AntzAuthClient({
  baseUrl:     "https://auth.antzsystems.com",
  clientId:    "your-client-id",
  redirectUri: "http://localhost:5173/callback.html",
  tenant:      "dev",
  scopes:      ["openid", "profile", "email"],
});

// Login
document.getElementById("login-btn")?.addEventListener("click", () => {
  client.login();
});

// Callback page — call once on callback.html
if (window.location.pathname === "/callback.html") {
  client.handleCallback()
    .then(() => window.location.replace("/dashboard.html"))
    .catch((err) => window.location.replace(`/?error=${err.message}`));
}

// Dashboard — check auth
if (!client.isAuthenticated()) {
  window.location.replace("/");
}

const user = client.getUser();
console.log("Logged in as:", user?.name);

// Auto-logout when refresh token expires — same logout() as manual button
setInterval(async () => {
  if (!client.isAuthenticated()) return;
  const token = await client.getAccessToken();
  if (!token) client.logout();
}, 60_000);

// Also check on tab focus (catches expiry while tab was hidden)
document.addEventListener("visibilitychange", async () => {
  if (document.visibilityState !== "visible") return;
  if (!client.isAuthenticated()) return;
  const token = await client.getAccessToken();
  if (!token) client.logout();
});

// Logout
document.getElementById("logout-btn")?.addEventListener("click", () => {
  client.logout();
});

CORS and Proxy Explained

WSO2's internal REST APIs (/api/users/v1/me/...) block direct cross-origin browser requests. This affects sendOtp() and changePassword().

Without proxy (browser → WSO2 directly):
  Browser fetches https://auth.antzsystems.com/api/users/v1/me/change-password
  → Browser sends CORS preflight (OPTIONS)
  → WSO2 returns no CORS headers for your origin
  → Browser blocks the request

Option A — Configure WSO2 CORS (React, Vue, client-only Next.js)

Add to WSO2 deployment.toml:

[cors]
allow_generic_http_requests = true
allow_any_origin = false
allowed_origins = ["http://localhost:5173", "https://yourapp.com"]
support_any_header = true
supports_credentials = true

Then omit proxyUrl from the client config.

Option B — Use proxy routes (Next.js with server)

Set proxyUrl in the client config. Add the two API routes shown in the Next.js with Proxy Routes section. The browser calls your own origin → your server forwards to WSO2 server-side — no CORS.

With proxy (browser → Next.js server → WSO2):
  Browser fetches http://localhost:3000/api/auth/change-password/send-otp  (same origin, no CORS)
  → Next.js API route fetches https://auth.antzsystems.com/...             (server→server, no CORS)

Token Refresh & Session Expiry

Token refresh is fully automatic in the React and Vue adapters — you do not need to call anything yourself.

How it works

Three mechanisms fire automatically (all configurable via refreshBufferSeconds):

| Mechanism | When it fires | |---|---| | Session restore | On mount — restores tokens from storage, refreshes if expiring soon | | Proactive timer | refreshBufferSeconds (default 60s) before access token expires — silent background refresh | | Tab visibility | When user switches back to the tab — catches tokens that expired while tab was hidden |

Configuring the refresh buffer

By default the proactive timer fires 60 seconds before the access token expires. Override via refreshBufferSeconds:

const client = new AntzAuthClient({
  baseUrl:              "https://auth.antzsystems.com",
  clientId:             "your-client-id",
  redirectUri:          "https://yourapp.com/callback",
  refreshBufferSeconds: 30,  // refresh 30s before expiry instead of 60s
});

Important: refreshBufferSeconds must be less than your WSO2 refresh token expiry time.

When the refresh token expires

If the refresh token is expired or revoked (e.g. server-side session invalidated), the package:

  1. Sets status = 'unauthenticated' (React) / status.value = 'unauthenticated' (Vue)
  2. Does not clear tokens — logout() must be called so it can read id_token to terminate the WSO2 SSO session

The recommended pattern is to mount an AuthSessionGuard in your root layout that watches for the loading → unauthenticated transition and calls logout(). This covers both the "browser reopened after expiry" scenario (main scenario) and mid-session expiry on protected pages. See the [AuthSessionGuard](#authsessionguard--required-root-