@antzsoft/wso2-auth-thingsboard-js
v1.0.1
Published
Minimal WSO2 IS login-redirect + token-storage connector for ThingsBoard CE's Angular frontend — a standard (non-SPA) confidential-client web app whose backend does its own OAuth2 code exchange.
Readme
@antzsoft/wso2-auth-thingsboard-js
Minimal login-redirect + token-capture connector for WSO2 Identity Server 7.x, built for ThingsBoard CE's Angular frontend — a standard confidential-client web app (separate Java backend + separate frontend), not a PKCE/SPA client.
Unlike @antzsoft/wso2-auth-web (which owns the whole OAuth2 code exchange in the browser), this
SDK never talks to WSO2 directly and never holds a client secret. The backend does its own
authorization-code exchange server-side (via Spring Security's stock OAuth2 login) and attaches
the WSO2 access/refresh token to the post-login redirect as query params — this package's whole
job is to build the login-redirect URL, capture those query params, store them, and (optionally)
keep them fresh.
Contents
- What's in the package
- Installation
- Configuration
- Core API
- Backend Contract
- Integration Guide (ThingsBoard)
What's in the package
| Export | Description |
|--------|-------------|
| AntzAuthClient | Core client class — login/logout navigation, token capture/storage, proactive refresh |
| withLoginHint | Stateless helper for apps that build their own per-provider login URLs |
| AntzWso2RefreshError | Thrown by refresh()/getSessionInfo(); carries an HTTP status when available, so callers can distinguish a confirmed-dead token from a transient failure |
| AntzAuthConfig, AntzTokenSet, AntzLoginOptions, AntzSessionInfo | TypeScript types |
There is no framework adapter (no React/Vue/Angular hook) — the class is plain TypeScript with
only browser globals (localStorage, window, fetch, URL). ThingsBoard's AuthService
(Angular) wraps it directly; any other backend-mediated-redirect app can do the same.
Installation
npm install @antzsoft/wso2-auth-thingsboard-jsFor local SDK development, build/publish it to a local Verdaccio registry the same way as the
web/reactnative SDKs — see ../PUBLISH.md (the outer sdks/ publishing tooling covers all
four packages: web, rn, thingsboard-js, and spring).
Configuration
import { AntzAuthClient } from "@antzsoft/wso2-auth-thingsboard-js";
const auth = new AntzAuthClient({
loginUrl: "/oauth2/authorization/wso2", // optional — see below
logoutUrl: "/logout", // optional — see below
refreshUrl: "/api/auth/wso2/refresh", // optional — enables refresh()/startAutoRefresh()
headersProvider: () => ({ Authorization: `Bearer ${myAppToken}` }), // optional
});Config options
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| loginUrl | string | only for login()/getLoginUrl() | — | The app's own OAuth2 login-initiation URL (e.g. Spring Security's stock /oauth2/authorization/{registrationId}). Omit if the app builds its own per-provider URLs and only uses this SDK for token capture — withLoginHint() still works standalone. |
| logoutUrl | string | only for logout() | — | The app's own logout URL. |
| accessTokenParam | string | no | wso2AccessToken | Query param the backend attaches the WSO2 access token as. |
| refreshTokenParam | string | no | wso2RefreshToken | Query param the backend attaches the WSO2 refresh token as. |
| idTokenParam | string | no | wso2IdToken | Query param for the captured OIDC id_token (requires openid scope on the WSO2 app). |
| logoutUrlParam | string | no | wso2LogoutUrl | Query param for WSO2's end-session URL — used by ssoLogout(). |
| oauth2ClientIdParam | string | no | wso2OAuth2ClientId | Query param identifying which registered WSO2 app this session came from. |
| expiresInParam | string | no | wso2ExpiresIn | Query param for the access token's remaining lifetime in seconds, as of the redirect. |
| storageKeyPrefix | string | no | antz_wso2_ | localStorage key prefix. |
| refreshUrl | string | only for refresh()/startAutoRefresh() | — | The app's own backend endpoint that refreshes the WSO2 token pair server-side (this SDK never holds the confidential client's secret). |
| refreshBufferSeconds | number | no | 60 | How many seconds before actual expiry to proactively refresh. |
| headersProvider | () => Record<string, string> | no | — | Called once per refresh()/getSessionInfo() request to supply extra headers (e.g. the host app's own bearer token, if the backend endpoint requires authentication). |
| onSessionExpired | () => void | no | — | Fires when the proactive refresh loop confirms the WSO2 refresh token itself is dead (a 4xx response from refreshUrl, e.g. invalid_grant) — never for a transient network/5xx failure, which is retried silently instead. |
| sessionInfoUrl | string | only for getSessionInfo()/startSessionPoll() | — | The app's own backend endpoint that returns the current WSO2 token's expiry, read server-side from WSO2's token DB (the wso2-session-info bundle). |
| sessionPollIntervalSeconds | number | no | 0 (off) | How often startSessionPoll() calls getSessionInfo() to detect server-side session revocation ahead of the access token's own natural expiry. |
Core API
login() / getLoginUrl()
Navigates the browser to the app's own OAuth2 login-initiation URL (loginUrl), with an optional
login_hint to prefill WSO2's hosted login form.
auth.login({ loginHint: "[email protected]" }); // navigates immediately
const href = auth.getLoginUrl(); // for <a href> bindings, doesn't navigatelogin_hint is a pure UX convenience — it is never a user-existence check. WSO2 still owns
all credential/existence validation.
Multi-account / session-mismatch enforcement — expectedUser/autoSwitchAccount guard
against WSO2's single-session-per-browser behavior (see docs/multi-account-login-behavior.md):
auth.login({ loginHint: "[email protected]", expectedUser: "[email protected]" });
// default (autoSwitchAccount unset/false): if the browser's live WSO2 SSO session belongs to a
// DIFFERENT user, the backend redirects to /login?loginError=... instead of completing the
// login as the wrong user.
auth.login({ loginHint: "[email protected]", expectedUser: "[email protected]", autoSwitchAccount: true });
// opt-in: same user → silent reuse (no login page shown); different user → backend forces a
// fresh WSO2 credential prompt (prompt=login) for [email protected] instead of erroring.Unlike login_hint, this SDK never checks the result itself — it never sees the OAuth2 callback,
the backend does (see ../docs/INTEGRATION_GUIDE.md's Oauth2AuthenticationSuccessHandler section). A
mismatch surfaces as the app's existing loginError query param, the same mechanism any other
OAuth2 login failure already uses — there is no thrown JS exception to catch here.
withLoginHint()
Stateless helper for apps that build their own per-provider login URLs (e.g. ThingsBoard, which
can have several configured OAuth2 clients) rather than using a single fixed loginUrl. Carries
the same multi-account enforcement as login()/getLoginUrl() above: since there's only ever the
one username the user typed into the form, it sets both login_hint and expected_user to
loginHint (there's no separate expectedUser to pass).
import { withLoginHint } from "@antzsoft/wso2-auth-thingsboard-js";
const url = withLoginHint(oauth2Client.url, username); // appends login_hint AND expected_user
const urlAutoSwitch = withLoginHint(oauth2Client.url, username, /* autoSwitchAccount */ true);
// also appends auto_switch_account=true — see the `login()` example above for the semanticscaptureTokenFromRedirect()
Reads the WSO2 access/refresh/id token, end-session URL, OAuth2 client id, and expiry (whichever are present) off the current URL's query params — attached by the backend's OAuth2 success handler after login — stores them, and strips them from the URL. Call this once on app bootstrap.
const tokens = auth.captureTokenFromRedirect();
// { accessToken, refreshToken?, idToken?, logoutUrl?, expiresAt? } | nullReturns null if no access token query param was present (e.g. a non-SSO page load).
getStoredAccessToken() / getStoredTokens()
const token = auth.getStoredAccessToken(); // string | null — for later API calls (e.g. change-password)
const tokens = auth.getStoredTokens(); // AntzTokenSet | nullgetStoredIdToken() / getStoredOAuth2ClientId()
const idToken = auth.getStoredIdToken(); // string | null — present only if `openid` scope was granted
const oauth2ClientId = auth.getStoredOAuth2ClientId(); // string | null — which registered WSO2 app this session came fromlogout()
Navigates the browser to the app's own logoutUrl and clears stored WSO2 tokens. This is a
local, app-only logout — it does not touch WSO2's shared SSO session. Use ssoLogout()
instead when you need to actually kill the commonAuthId cookie.
auth.logout();ssoLogout()
Real WSO2 SSO logout: a top-level browser navigation (not fetch, not an iframe) to WSO2's
end-session endpoint with the id_token captured at login, killing the shared commonAuthId
cookie so a later SSO redirect — in this app or any other — can't silently re-authenticate the
user.
const navigated = auth.ssoLogout("https://your-app.example.com/login"); // post_logout_redirect_uri
if (!navigated) {
auth.clearStoredTokens(); // local-only fallback
}A real navigation is required here, not an iframe/fetch:
- WSO2's
/oidc/logoutdoes not sendAccess-Control-Allow-Credentials, so a cross-originfetchwould never carry thecommonAuthIdcookie even withcredentials: 'include'. - WSO2 commonly sends
X-Frame-Options/a restrictiveframe-ancestorsCSP on its login/logout pages (clickjacking protection), which silently blocks an iframe from loading them at all.
Returns false (and does nothing) if no id_token/logout URL was captured at login — e.g. the
WSO2 app wasn't registered with openid scope, or this session didn't go through a real SSO
redirect at all (a local-login-only session). Callers should fall back to logout() in that case.
refresh() / startAutoRefresh() / stopAutoRefresh()
Renews the stored WSO2 access/refresh token pair via the app's own refreshUrl — the backend
calls WSO2's refresh_token grant server-side, since this SDK never holds the confidential
client's secret.
const tokens = await auth.refresh(); // throws if refreshUrl unset, no refresh token stored, or the request fails
auth.startAutoRefresh(); // self-re-arming proactive refresh, fires `refreshBufferSeconds` before expiry
auth.stopAutoRefresh(); // cancel a pending scheduled refreshstartAutoRefresh() is a no-op if no WSO2 token is currently stored (e.g. a local-login-only
session that never went through WSO2 SSO) or expiresAt isn't known yet. A failed refresh either:
- retries after a short delay, for a transient network/5xx failure — never treated as "session ended," or
- stops and calls
onSessionExpired, for a confirmed-dead refresh token (a 4xx response — WSO2 itself rejected it, e.g.invalid_grant).
startAutoRefresh() also installs a visibilitychange listener: if the tab was backgrounded (or
suspended) long enough that the scheduled timer didn't fire on time, regaining focus triggers an
immediate catch-up refresh instead of waiting for the next scheduled attempt or the next API call.
stopAutoRefresh() removes both the timer and this listener.
Concurrent refresh() calls (e.g. the scheduled timer and a visibilitychange catch-up landing
around the same time) share a single in-flight request rather than each independently POSTing —
WSO2 rotates the refresh token on use, so two parallel requests would otherwise race and one would
get rejected with an already-stale refresh token. The underlying fetch() is also guarded by a
15s timeout, so a stuck request against an unreachable backend can't stall the refresh loop
forever — a timeout is treated the same as any other network failure (retried, never treated as
"session ended").
getSessionInfo() / startSessionPoll() / stopSessionPoll()
Fetches the current WSO2 access/refresh token's expiry via the app's own sessionInfoUrl — server-side
truth read from WSO2's token DB by the wso2-session-info bundle, rather than trusting the
locally-stored expiresAt's clock or assuming the token is still valid. This is how a session that
was revoked server-side (e.g. an admin-forced logout, or a password changed on another device) gets
detected — something the proactive refresh timer alone can't catch, since it only reacts to the
locally-known expiresAt.
const info = await auth.getSessionInfo(); // throws if sessionInfoUrl unset, no access token stored, or the request fails
auth.startSessionPoll(() => {
// Called once, and the poll stops itself, when the session is confirmed dead server-side (4xx).
console.log("Session expired — redirect to login");
});
auth.stopSessionPoll(); // cancel a running pollstartSessionPoll() is a no-op unless both sessionInfoUrl and sessionPollIntervalSeconds are
configured (the latter defaults to 0/off), or if a poll is already running. Paused while the tab
is hidden (document.visibilityState === 'hidden') — a network/5xx failure on a tick is swallowed
and retried on the next tick, never treated as "session ended." clearStoredTokens() and
ssoLogout() both stop a running poll automatically, same as they do for auto-refresh.
clearStoredTokens()
Clears any stored WSO2 tokens without navigating anywhere. Also stops auto-refresh and the session poll, if running.
auth.clearStoredTokens(); // e.g. after a successful password change (WSO2 revokes the old token anyway)Backend Contract
This SDK is one half of a pair — the other half is the backend's own OAuth2 success handler,
which must attach these query params to the post-login redirect (all optional except the access
token; names configurable via the *Param config options above):
| Query param | Purpose |
|---|---|
| wso2AccessToken | The WSO2 access token (required — its absence means captureTokenFromRedirect() returns null) |
| wso2RefreshToken | The WSO2 refresh token |
| wso2IdToken | The OIDC id_token, if the WSO2 app was logged in via an openid-scoped flow |
| wso2LogoutUrl | WSO2's end-session (/oidc/logout) URL for this tenant, if an id_token was captured |
| wso2OAuth2ClientId | Identifier for which registered WSO2 app this session came from |
| wso2ExpiresIn | Seconds until the access token expires, as of the redirect |
And, for refresh()/startAutoRefresh() to work, an endpoint at the configured refreshUrl
accepting POST { refreshToken, oauth2ClientId } and returning
{ accessToken, refreshToken?, idToken?, expiresIn? }.
And, for getSessionInfo()/startSessionPoll() to work, an endpoint at the configured
sessionInfoUrl accepting GET with an X-Wso2-Access-Token header (the caller's own stored WSO2
access token) and returning
{ accessTokenExpiresAt, accessTokenExpiresInSeconds, refreshTokenExpiresAt, refreshTokenExpiresInSeconds }
(the first/third as Unix epoch seconds — this SDK converts to epoch milliseconds itself).
See com.antzsoft.wso2auth:wso2-auth-spring (the companion Spring Boot connector) for a
ready-made server-side implementation of this contract.
Integration Guide (ThingsBoard)
See ../docs/INTEGRATION_GUIDE.md in this repo for the full end-to-end wiring into ThingsBoard's
Angular AuthService/LoginComponent/AuthController (Spring), including the global logout
flow, JWKS validation, change-password, and M2M SCIM2 user administration on the backend side.
