MonoCloud Web SDK (@monocloud/auth-web-js)
Browser-side authentication SDK for single-page applications and any vanilla JavaScript / TypeScript app running in the browser. Implements OAuth 2.0 / OpenID Connect with PKCE, handles redirect / popup / silent (iframe) flows, manages sessions and tokens, and acts as the foundation for higher-level framework SDKs (React, Vue, Angular, Svelte, etc.).
Package identity — read this first
Use: @monocloud/auth-web-js (this skill). The exported class is MonoCloudWebJSClient.
This is not the same as:
@monocloud/auth-nextjs— server-side session auth for Next.js (skill:monocloud-auth-nextjs).@monocloud/auth-react— React wrapper around this SDK:<MonoCloudAuthProvider>,useAuth(),useClient(),<SignIn>/<Protected>/<ProcessCallback>(skill:monocloud-auth-react). Use that one for React SPAs.@monocloud/backend-node— API token validation on Express / Fastify (skills:monocloud-auth-express,monocloud-auth-fastify).@monocloud/management— server-only Management API client (skill:monocloud-management-js).@monocloud/auth-core— internal core used by this SDK. App code should import from@monocloud/auth-web-js; types and error classes (MonoCloudOPError,MonoCloudValidationError, etc.) are re-exported from the public package.
Use this SDK when you are building a browser app without a server-rendered framework — pure HTML/JS, Vite, Parcel, webpack-bundled SPA, or a custom framework integration on top of MonoCloud. If your project ships with Next.js, prefer @monocloud/auth-nextjs instead — it gives you cookie-based sessions and server-side helpers that this SDK does not. If it is a React SPA (Vite / CRA / custom React with no server framework), prefer @monocloud/auth-react — it wraps this exact client with a provider, hooks, and components.
Installation
npm install @monocloud/auth-web-js
No environment variables. All configuration is passed to the constructor.
Prerequisites (in the MonoCloud dashboard)
The client in your MonoCloud tenant must be configured as a Single Page Application with:
- Allowed Callback URLs: the full URL matching
appUrl + callbackPath(e.g.http://localhost:5173/callback). - Allowed Sign-out URLs: the full URL matching
appUrl + signOutPath(e.g.http://localhost:5173/logout). - Allowed Origins (CORS): the bare origin of
appUrl(e.g.http://localhost:5173). - Scopes: at minimum
openid,profile,email. Addoffline_accessif you want refresh tokens.
Browser SPAs are public clients — never ship a clientSecret. The clientSecret option on MonoCloudWebJSClient exists only for advanced confidential-client setups; for a normal SPA, omit it.
Quick start
Create one shared client and reuse it across the app.
// src/auth.ts
import { MonoCloudWebJSClient } from '@monocloud/auth-web-js';
export const client = new MonoCloudWebJSClient({
tenantDomain: 'https://<your-tenant>.us.monocloud.com',
clientId: '<your-client-id>',
appUrl: 'http://localhost:5173',
callbackPath: '/callback',
signOutPath: '/logout',
defaultAuthParams: {
scopes: 'openid profile email offline_access', // offline_access => refresh tokens
},
});
Bootstrap the app by completing any in-flight callback before rendering:
// src/main.ts
import { client } from './auth';
async function init() {
await client.processCallback(); // no-op when the current URL is not a callback
// ...mount your UI
}
init();
Sign in / sign out:
await client.signIn(); // redirect (default)
await client.signIn({ mode: 'popup' }); // popup window
await client.signIn({ signUp: true }); // open sign-up instead of sign-in
await client.signOut(); // federated sign-out by default
Read the session:
const session = await client.getSession();
if (session) {
console.log(session.user, session.accessTokens, session.idToken);
}
Constructor options
new MonoCloudWebJSClient(options: MonoCloudWebJSClientOptions).
Required:
| Option | Purpose |
|---|---|
tenantDomain |
Your MonoCloud tenant URL, e.g. https://acme.us.monocloud.com |
clientId |
OIDC client id (SPA application registered in the dashboard) |
Common optional:
| Option | Default | Purpose |
|---|---|---|
appUrl |
window.location.origin |
Public origin of the app, e.g. http://localhost:5173. Used to build redirect URIs and to validate cross-origin postMessages from popups / iframes. Provide it explicitly if the app is served from multiple origins or behind a reverse proxy where window.location.origin does not match the registered callback. |
callbackPath |
/ |
Relative path where MonoCloud redirects after sign-in. Must be in Allowed Callback URLs. |
signOutPath |
/ |
Relative path where MonoCloud redirects after sign-out. Must be in Allowed Sign-out URLs. |
defaultAuthParams |
— | Pre-set scopes / resource / responseType / prompt etc. for every auth request. |
resources |
— | Additional Indicator[] (resource + scopes) requestable via getTokens(). |
storage |
new LocalStorage() |
Session persistence — LocalStorage, SessionStorage, MemoryStorage, or your own IStorage. |
postCallback |
full-page reload to returnUrl |
Hook to integrate with a client-side router instead of a hard navigation. |
onSessionCreating |
— | Async hook to mutate the session as it is being created (e.g. attach app-specific claims). |
federatedSignOut |
true |
When false, signOut() only clears the local session without sending the user to MonoCloud. |
fetchUserinfo |
true |
When false, skip the UserInfo call after authentication (user object will only carry ID-token claims). |
validateIdToken |
true |
When false, skip ID token signature/claims validation. Not recommended. |
authWindowTimeout |
600 (sec) |
Timeout for popup / iframe auth windows. |
popupWindowWidth |
375 |
Popup width in pixels. |
popupWindowHeight |
600 |
Popup height in pixels. |
clockSkew |
0 (sec) |
Maximum allowed clock skew, applied to all time-based ID-token claim validations. Must be >= 0. |
clockTolerance |
60 (sec) |
Additional tolerance applied to all time-based ID-token claim validations (exp, nbf, auth_time + maxAge) — not just nbf/auth_time. Must be >= 0. |
idTokenSigningAlgorithm |
'RS256' |
Expected signing algorithm for ID-token validation. Also selects the SHA digest used for at_hash / s_hash checks in implicit flows — so it applies to public SPAs, not just confidential clients. |
sessionKey |
— | Extra suffix on the internal storage key. Use when you need multiple MonoCloudWebJSClient instances with the same clientId in the same tab. |
filteredIdTokenClaims |
protocol claims set | Override the list of claims stripped from the user object. |
clientSecret, clientAuthMethod |
— | Confidential-client extras. Do not use in a normal SPA — secrets cannot be safely shipped to a browser. |
Pre-configurable subset of authorization params (via defaultAuthParams): scopes, resource, responseType, prompt, display, uiLocales, acrValues, maxAge, loginHint, authenticatorHint, audience, idTokenHint. Per-request values (state, nonce, codeChallenge, codeChallengeMethod, redirectUri) are managed internally and cannot be overridden.
processCallback() — wire this once at startup
processCallback() inspects the current URL plus the persisted callback state (mc.state.<clientId> in sessionStorage) and automatically finishes a pending sign-in or sign-out flow. It is a no-op when the URL is not a callback — so it is safe (and recommended) to call unconditionally during app bootstrap.
await client.processCallback();
Do not dispatch on the route yourself — the SDK matches the URL against appUrl + callbackPath / appUrl + signOutPath internally and knows which side of the flow it is on. There is no need to mount a special "callback" page or route component — the same entrypoint works for every page.
If the app is loaded inside a popup or iframe (because the SDK opened it), processCallback() forwards the callback URL back to the main window via postMessage and returns; the main window's pending signIn() / signOut() promise resolves there.
Sign-in modes
signIn(options?: SignInOptions).
await client.signIn(); // redirect (default)
await client.signIn({ mode: 'popup' }); // popup window
await client.signIn({ signUp: true }); // open sign-up (prompt=create)
await client.signIn({
returnUrl: '/dashboard', // where to go after callback
loginHint: 'alice@example.com',
scopes: 'openid profile email offline_access', // overrides defaults for this call
resource: 'https://api.example.com',
appState: { from: 'pricing-page' }, // arbitrary state — surfaced to onSessionCreating
});
| Mode | When to use it |
|---|---|
redirect |
Default. Full-page redirect to MonoCloud, comes back via callbackPath. |
popup |
Keep the user on the current page. Requires the call to happen in a user-initiated event (click handler) — otherwise the popup is blocked. |
Silent sign-in (SSO restore at bootstrap, no UI):
import { MonoCloudOPError } from '@monocloud/auth-web-js';
try {
const session = await client.signInSilent();
console.log('Restored session for:', session.user);
} catch (e) {
if (e instanceof MonoCloudOPError && e.error === 'login_required') {
// MonoCloud has no active IdP session — user needs to sign in interactively.
} else {
throw e;
}
}
signInSilent() runs a full prompt=none authorization round-trip through a hidden iframe. If MonoCloud has a valid session it resolves with the new MonoCloudSession; otherwise it rejects with MonoCloudOPError (login_required, interaction_required, etc.).
Sign out
signOut(options?: SignOutOptions).
await client.signOut(); // federated (clears local + MonoCloud session)
await client.signOut({ mode: 'popup' });
await client.signOut({ federatedSignOut: false }); // local-only — keeps the MonoCloud session
await client.signOut({ postLogoutRedirectUri: 'https://example.com/bye' });
await client.signOut({ idTokenHint: '<id-token>' }); // override the session's id_token_hint
When federatedSignOut is true (the default) the SDK sends the user to MonoCloud's end-session endpoint, then back to signOutPath. When false the local session is cleared without contacting MonoCloud — useful when you want to log the user out of this app while leaving any other apps signed in.
Reading tokens
const tokens = await client.getTokens();
// { accessToken, scopes, requestedScopes, resource, accessTokenExpiration,
// idToken, refreshToken, isExpired }
await client.getTokens({ forceRefresh: true });
await client.getTokens({
resource: 'https://api.example.com',
scopes: 'read:things write:things',
});
getTokens() returns the matching access token from the current session. If it is missing or about to expire (30-second buffer) it transparently runs the OAuth 2.0 Refresh Token Grant first and stores the new tokens. Throws MonoCloudValidationError when there is no session.
Refresh-token flow needs offline_access in scopes at sign-in time — the authorization server only issues a refresh token when that scope is granted.
refreshSession() is the imperative refresh; getTokens() is the right call for "give me a usable access token now."
await client.refreshSession(); // refresh with the default audience/scope
await client.refreshSession({
refreshGrantOptions: {
resource: 'https://api.example.com',
scopes: 'read:data',
},
});
refetchUserInfo() re-calls the UserInfo endpoint with the default access token and updates session.user:
await client.refetchUserInfo();
const session = await client.getSession();
All four methods (signInSilent, refreshSession, refetchUserInfo, getTokens) are wrapped in cross-tab + in-flight dedupe locks so concurrent callers in the same tab — or across tabs — collapse onto a single network round-trip.
Storage adapters
Sessions persist via an IStorage implementation.
| Class | Backed by | When to use |
|---|---|---|
LocalStorage |
window.localStorage |
Default. Survives tab closes; shared across tabs on the same origin. |
SessionStorage |
window.sessionStorage |
Cleared when the tab closes; not shared between tabs. |
MemoryStorage |
in-memory object | Lost on reload. Useful for testing or strict-privacy modes. |
| Custom | implements IStorage |
Encrypted store, IndexedDB wrapper, custom secure-cookie helper, etc. |
import { MonoCloudWebJSClient, MemoryStorage } from '@monocloud/auth-web-js';
export const client = new MonoCloudWebJSClient({
tenantDomain: 'https://<your-tenant>',
clientId: '<your-client-id>',
appUrl: 'http://localhost:5173',
storage: new MemoryStorage(),
});
Custom adapter (note: methods are async — return promises):
import type { IStorage } from '@monocloud/auth-web-js';
class IndexedDbStorage implements IStorage {
async getItem(key: string): Promise<string | null> { /* ... */ }
async setItem(key: string, value: string): Promise<void> { /* ... */ }
async removeItem(key: string): Promise<void> { /* ... */ }
}
If you swap to MemoryStorage, the default postCallback performs a full-page reload to returnUrl and wipes the session — provide a custom postCallback that uses your router instead (see next section).
Integrating with a client-side router
The default postCallback performs window.location.href = returnUrl (a full page reload). That works in most setups but throws away in-memory state and disqualifies MemoryStorage. If you use a client-side router, override it:
import { MonoCloudWebJSClient } from '@monocloud/auth-web-js';
import { router } from './router';
export const client = new MonoCloudWebJSClient({
tenantDomain: 'https://<your-tenant>',
clientId: '<your-client-id>',
appUrl: 'http://localhost:5173',
callbackPath: '/callback',
postCallback: state => {
// state.returnUrl, state.appState, state.signOut, state.mode are available.
router.push(state.returnUrl ?? '/dashboard');
},
});
postCallback runs after both sign-in and sign-out callbacks (state.signOut distinguishes them) and only on the main window (not inside the popup/iframe).
Errors
All errors extend MonoCloudAuthBaseError (which extends Error). Use instanceof to branch. Errors raised from an unsuccessful HTTP response also carry a raw ({ status, statusText, headers, body }), and MonoCloudHttpError exposes status / statusText getters.
| Class | Thrown for |
|---|---|
MonoCloudOPError |
OAuth/OIDC error from the authorization server. Exposes .error (code) and .errorDescription. Examples: login_required, interaction_required, access_denied, invalid_grant. |
MonoCloudValidationError |
Bad state — no session when one is required, missing parameters in a callback, scopes/response_type mismatch. Also thrown by processCallback() in implicit flows on hash validation failures: Invalid 'at_hash' in id token (when responseType is 'id_token token' and the SDK can compute at_hash from the access token but the id token's claim does not match) and Invalid 's_hash' in id token (any implicit flow where the id token's s_hash does not match the callback state). |
MonoCloudTokenError |
Token operation failed (e.g. validation of an ID token). |
MonoCloudHttpError |
Network / unexpected HTTP response talking to MonoCloud. |
MonoCloudJsError |
Browser-environment failure (popup blocked, iframe in cross-origin-isolated context, window timeout, redirect attempted from inside an iframe, etc.). |
MonoCloudAuthBaseError |
Base class — catch this when you want to handle any SDK error generically. |
import {
MonoCloudOPError,
MonoCloudValidationError,
MonoCloudJsError,
} from '@monocloud/auth-web-js';
try {
await client.signInSilent();
} catch (e) {
if (e instanceof MonoCloudOPError && e.error === 'login_required') return;
if (e instanceof MonoCloudJsError) { /* popup blocked, iframe issue */ }
throw e;
}
Multiple clients in the same app
Each MonoCloudWebJSClient keys its persisted state by clientId. If you have two clients with the same clientId (rare — e.g. switching audiences/tenants from one app), pass a unique sessionKey so they don't trample each other's session in storage. Note sessionKey namespaces the session and lock keys only — the in-flight callback state always lives at mc.state.<clientId>, so never have two same-clientId instances with a redirect flow pending at the same time:
const adminClient = new MonoCloudWebJSClient({ /* ... */ clientId: 'app', sessionKey: 'admin' });
const userClient = new MonoCloudWebJSClient({ /* ... */ clientId: 'app', sessionKey: 'user' });
For different clientIds, this is unnecessary — the SDK already namespaces by clientId.
Common pitfalls
- Calling
processCallback()only on a specific route. Don't gate it on the URL — the SDK does that itself. Call it once at app bootstrap. - Forgetting
offline_access. No refresh token is issued.refreshSession()and the auto-refresh ingetTokens()will throwMonoCloudValidationError. Addoffline_accesstodefaultAuthParams.scopes(or to the per-callsignIn({ scopes })). - Calling
signIn({ mode: 'popup' })outside a user gesture. Browsers block the popup. Call it inside a click handler. - Mismatched callback URLs in the dashboard.
appUrl + callbackPathmust exactly match an entry in the client's Allowed Callback URLs — including scheme, host, port, and path. Same for sign-out. - Missing CORS origin. MonoCloud rejects token / userinfo requests from origins that aren't in Allowed Origins (CORS).
- Shipping a
clientSecretin a SPA. Browser bundles are public. Use a public client — leaveclientSecretunset. MemoryStorage+ defaultpostCallback. The defaultpostCallbackdoes a full page reload, which empties memory and drops the just-created session. Either keepLocalStorage/SessionStorageor pass apostCallbackthat hands control to your router instead.- Redirect sign-in from inside an iframe. Throws
MonoCloudJsError— MonoCloud's sign-in page can't render framed. Switch tomode: 'popup'or perform the redirect from the top window. - Wrong
returnUrlorigin. The defaultpostCallbackignoresreturnUrlvalues that resolve to a different origin thanappUrl(and logs a warning) — that's intentional, not a bug. - Custom
IStoragereturning sync values. All three methods (getItem,setItem,removeItem) must return promises. Wrap synchronous backends withPromise.resolve(...).
Onboarding checklist for a fresh integration
npm install @monocloud/auth-web-js.- In the MonoCloud dashboard, configure the client as a Single Page Application and register Callback URL, Sign-out URL, and Allowed Origin matching your local dev origin (e.g.
http://localhost:5173). - Create a
src/auth.ts(or similar) that constructs and exports a singleMonoCloudWebJSClientinstance — passtenantDomain,clientId,appUrl,callbackPath,signOutPath, anddefaultAuthParams.scopes = 'openid profile email offline_access'. - In your app entry (
main.ts/index.ts),await client.processCallback()before mounting the UI. - Wire
signIn()/signOut()to buttons. Usemode: 'popup'if you want to avoid full-page redirects. - Optionally call
signInSilent()at bootstrap to restore SSO without prompting; catchMonoCloudOPErrorforlogin_required. - For protected fetches, call
getTokens()and forwardaccessTokenin theAuthorizationheader. Refresh is automatic; pass{ resource, scopes }for audience-specific tokens. - Run
node skills/monocloud-web-js/scripts/verify.jsto sanity-check that the package is installed and constructor options look plausible.
Deeper reference
references/api-surface.md— every export, with constructor option shapes, method signatures, and storage adapter contracts.references/troubleshooting.md— extended symptom → cause → fix index covering popup blockers, iframe / cross-origin issues, callback URL mismatches,login_requiredon silent, refresh-token gotchas, custom storage pitfalls, and training-data SDK ghosts.