@scenarix/jadu-auth¶
JaduAuth SDK (JaduSSO mode) — Central-login client for React apps and Node backends. Login lives on jadu-account; this SDK reads the shared session cookie, decodes the access-token JWT for permission checks, manages impersonation, and ships an authenticated axios instance for tenant-BE calls.
Table of contents¶
- Installation
- Quick start
- Package entry points
- Scenarios & examples
- Configuration
- Error handling
- Development & playground
Installation¶
Requirements: Node.js ≥ 18. React ≥ 18 is optional (only needed for the React entry point).
The package is published to GitHub Packages. Configure npm to use the Scenarix registry and auth:
npm install @scenarix/jadu-auth
If you're not already using the Scenarix registry, add a project-level .npmrc (or use your user npmrc):
@scenarix:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_PKG_TOKEN}
Set GITHUB_PKG_TOKEN to a GitHub Personal Access Token with read:packages (and write:packages if you publish).
Peer dependencies:
- For React usage: install
react(≥18) in your app. - For server usage: no extra peer deps; the SDK uses
joseandaxios(included).
Quick start¶
React (frontend)¶
- Wrap your app with
JaduAuthProvider. On mount it does a cookie-onlyGET /api/auth/sso/meagainstapiUrlwithx-auth-app-id: authAppIdand transitions the auth state machine. PassjaduSSOUrl(the jadu-account login host) so the login button knows where to redirect. - Render a "Login using JaduAccount" button that calls the arg-less
startJaduSSO()returned byuseJaduAuth(). - Use
useJaduAuth()foruser,authState,logout,authenticatedAxios, and the impersonation helpers.
// app/layout.tsx or _app.tsx
import { JaduAuthProvider } from "@scenarix/jadu-auth/react";
export default function RootLayout({ children }) {
return (
<JaduAuthProvider
apiUrl="https://auth.example.com"
authAppId="my-app"
jaduSSOUrl="https://account.example.com"
>
{children}
</JaduAuthProvider>
);
}
// components/Dashboard.tsx
import { useJaduAuth, AuthState } from "@scenarix/jadu-auth/react";
export function Dashboard() {
const { user, authState, logout, authenticatedAxios, startJaduSSO } =
useJaduAuth();
if (authState === AuthState.INITIALIZING) return <p>Loading…</p>;
if (authState !== AuthState.AUTHENTICATED) {
return (
<button onClick={() => startJaduSSO()}>Login using JaduAccount</button>
);
}
const loadData = async () => {
const { data } = await authenticatedAxios.get("/api/data");
return data;
};
return (
<div>
<p>Welcome, {user?.name}!</p>
<button onClick={() => logout()}>Log out</button>
</div>
);
}
Auto-redirect to SSO (skip the button)¶
By default an unauthenticated user sees the "Login using JaduAccount" button and
clicks it. To auto-redirect instead — so a user who lands on the app
unauthenticated is sent straight to SSO login with no click — set
autoSSORedirect on the provider (requires jaduSSOUrl):
<JaduAuthProvider
apiUrl="https://auth.example.com"
authAppId="my-app"
jaduSSOUrl="https://account.example.com"
autoSSORedirect
>
<App />
</JaduAuthProvider>
The provider redirects on mount only when the session probe finds the user is
cleanly not logged in (401/403). It does not redirect on transient/network
failures, after an explicit logout(), or when a built-in loop guard trips —
those surface a fallback so the user can retry by hand. Render the two flags from
useJaduAuth():
const { authState, ssoRedirecting, ssoRedirectBlocked, startJaduSSO } =
useJaduAuth();
if (authState === AuthState.INITIALIZING || ssoRedirecting) {
return <p>Signing you in…</p>; // redirect in flight — don't flash the button
}
if (authState !== AuthState.AUTHENTICATED) {
// Fallback: auto-redirect was blocked (loop guard / transient failure / logout)
return (
<div>
{ssoRedirectBlocked && <p>We couldn't sign you in automatically.</p>}
<button onClick={() => startJaduSSO()}>Login using JaduAccount</button>
</div>
);
}
Loop guard. If the shared session cookie isn't readable on return (cookie-domain
misconfig, blocked cookies), jadu-account would bounce the user straight back and
the app would redirect forever. The guard caps consecutive auto-redirects within a
short window (one attempt by default), then sets ssoRedirectBlocked so the
fallback shows. A successful login, or clicking the manual button, resets it.
State lives in per-tab sessionStorage.
Backend (Node / Express)¶
The server SDK is unchanged: validate JWTs from the Authorization: Bearer …
header attached by authenticatedAxios.
import express from "express";
import { JaduAuth } from "@scenarix/jadu-auth/server";
const app = express();
await JaduAuth.init({
authServerUrl: "https://auth.example.com",
appId: "my-app",
});
app.get("/api/me", JaduAuth.authorizeRequest([], "my-app"), (req, res) => {
const { userId, email, name } = req.jaduAuth!;
res.json({ userId, email, name });
});
app.listen(3000);
Package entry points¶
| Import | Use case |
|---|---|
@scenarix/jadu-auth |
Core: AuthClient, TokenStorage, createAuthenticatedAxios, buildJaduSSOUrl, types, errors |
@scenarix/jadu-auth/react |
React: JaduAuthProvider, useJaduAuth (returns startJaduSSO), auth state |
@scenarix/jadu-auth/server |
Server: JaduAuth (init, authorizeRequest, verifyToken) |
Use the react entry in React apps so the provider and hook are available. Use the server entry in backend services to validate JWTs. Use the main entry when you need the low-level client or the buildJaduSSOUrl helper without React.
Scenarios & examples¶
1. Trigger SSO redirect¶
The everyday way is the arg-less startJaduSSO() from the hook. It reads
jaduSSOUrl and authAppId from the provider and defaults returnTo to the
current URL:
import { useJaduAuth } from "@scenarix/jadu-auth/react";
function LoginButton() {
const { startJaduSSO } = useJaduAuth();
return (
<button onClick={() => startJaduSSO()}>Login using JaduAccount</button>
);
}
Pass a partial override to change any field per call:
startJaduSSO({ returnTo: "/v2/projects" });
returnTo may be a path (absolutized against window.location.origin) or
an absolute URL. jadu-account validates it against its own host allowlist
before redirecting.
Outside React (SSR, anchor hrefs, tests), build the URL directly with
buildJaduSSOUrl({ jaduSSOUrl, appId, returnTo }) from
@scenarix/jadu-auth.
2. Call a protected API (React)¶
authenticatedAxios from useJaduAuth() attaches Authorization: Bearer
<accessToken> and, on 401/403, refreshes the token and retries once.
const { authenticatedAxios } = useJaduAuth();
const { data } = await authenticatedAxios.get("/api/protected");
await authenticatedAxios.post("/api/items", { name: "New item" });
3. Loading and auth state¶
Use authState for loading and authentication checks.
import { AuthState } from "@scenarix/jadu-auth/react";
const { authState, user } = useJaduAuth();
if (authState === AuthState.INITIALIZING) return <Spinner />;
if (authState !== AuthState.AUTHENTICATED) return <SSOButton />;
return <div>Hello, {user?.name}</div>;
4. Logout¶
logout() calls POST /api/auth/logout (clears the cookie), wipes local
storage, and fires onLogout. The provider also fires onLogout
implicitly when authenticatedAxios hits a non-recoverable 401.
<JaduAuthProvider
apiUrl={authUrl}
authAppId={appId}
onLogout={() => window.location.assign("/")}
>
{children}
</JaduAuthProvider>
5. JaduSpine token (real-time / Centrifugo)¶
The session probe and refresh both return a JaduSpine token (JWT) with the same expiry as the access token. Use it for JaduSpine or Centrifugo.
import {
JaduAuthProvider,
useJaduAuth,
AuthState,
} from "@scenarix/jadu-auth/react";
import { Centrifuge } from "centrifuge";
function App() {
return (
<JaduAuthProvider
apiUrl="https://auth.example.com"
authAppId="my-app"
onJaduSpineTokenChange={(token) => {
if (token) centrifuge.setToken(token);
else centrifuge.disconnect();
}}
>
<RealTimeApp />
</JaduAuthProvider>
);
}
function RealTimePanel() {
const { jaduSpineToken, authState } = useJaduAuth();
if (authState !== AuthState.AUTHENTICATED || !jaduSpineToken) return null;
return <div>Connected with JaduSpine token</div>;
}
6. Permissions: canUserDo¶
Permissions are encoded in the access-token JWT. canUserDo decodes the
token client-side and returns true when permissions include the value (or
the super-admin wildcard).
const { canUserDo } = useJaduAuth();
if (canUserDo("admin:read")) {
// show admin UI
}
7. Profile read / update¶
const { profile, getProfile, updateProfile } = useJaduAuth();
useEffect(() => {
void getProfile();
}, [getProfile]);
await updateProfile({
name: "Renamed",
applicationData: { theme: "dark" },
});
8. Backend: protected route¶
app.get(
"/api/profile",
JaduAuth.authorizeRequest([], "my-app"),
(req, res) => {
const { userId, email, name, isImpersonation, originalUserId } =
req.jaduAuth!;
res.json({ userId, email, name });
},
);
9. Impersonation (React)¶
Users with the IMPERSONATOR or SUPER_ADMIN role can view-as another
user. The SDK provides a built-in modal and a draggable banner.
const {
user,
canImpersonate,
isImpersonating,
originalUser,
startImpersonation,
endImpersonation,
} = useJaduAuth();
if (isImpersonating) {
return (
<div>
Viewing as {user?.name} (originally {originalUser?.name})
<button onClick={endImpersonation}>Stop impersonating</button>
</div>
);
}
return canImpersonate ? (
<button onClick={startImpersonation}>Impersonate user</button>
) : null;
startImpersonation() opens the modal, the SDK swaps the access/spine
tokens, the banner appears, and endImpersonation() restores the original
user. Token auto-refresh is paused while impersonating.
10. Vanilla JS/TS (no React)¶
import {
AuthClient,
TokenStorage,
createAuthenticatedAxios,
buildJaduSSOUrl,
} from "@scenarix/jadu-auth";
const tokenStorage = new TokenStorage("my_app");
const authClient = new AuthClient({
apiUrl: "https://auth.example.com",
authAppId: "my-app",
});
const authAxios = createAuthenticatedAxios({
tokenStorage,
authClient,
onAuthFailure: () => console.log("Session expired"),
});
// Send the user to jadu-account on sign-in
window.location.assign(
buildJaduSSOUrl({
jaduSSOUrl: "https://account.example.com",
appId: "my-app",
returnTo: window.location.href,
}),
);
Configuration¶
JaduAuthProvider (React)¶
| Prop | Type | Description |
|---|---|---|
apiUrl |
string |
Auth API base URL (e.g. https://auth.example.com) |
authAppId |
string |
Application ID for RBAC / token scope |
jaduSSOUrl |
string |
jadu-account login host (e.g. https://account.example.com). Required to use startJaduSSO(). |
autoSSORedirect |
boolean |
Auto-redirect to SSO login on mount when cleanly unauthenticated (default: false). Requires jaduSSOUrl. Has a built-in loop guard; exposes ssoRedirecting / ssoRedirectBlocked. |
autoRefresh |
boolean |
Enable automatic access token refresh (default: true) |
refreshBuffer |
number |
Ms before expiry to trigger refresh (default: 60000) |
debug |
boolean |
Log debug messages (default: false) |
storageKeyPrefix |
string |
Prefix for localStorage keys (default: "jadu_auth") |
onLogout |
() => void |
Called on logout and when session becomes invalid |
onError |
(error: Error) => void |
Called on auth errors |
onJaduSpineTokenChange |
(token) => void |
Called when the JaduSpine token changes. token is the new JWT or null when logged out. |
onAuthenticated |
(info) => void |
Called on session probe success. info.isFirstLoginForApp is currently false for SSO bootstraps. |
onImpersonationStart |
(user, original) => void |
Fired after a successful startImpersonation |
onImpersonationEnd |
(user) => void |
Fired after a successful endImpersonation |
startJaduSSO (from useJaduAuth()) and buildJaduSSOUrl¶
startJaduSSO(overrides?) is returned by useJaduAuth(). Called with no
arguments it reads jaduSSOUrl and authAppId from the provider and defaults
returnTo to the current URL; pass a partial override for any field.
buildJaduSSOUrl(opts) (from @scenarix/jadu-auth) builds the same URL without
React and takes all three fields explicitly.
| Option | Type | Description |
|---|---|---|
jaduSSOUrl |
string |
Base URL of the jadu-account login host. Defaults to the provider's jaduSSOUrl prop when using the hook. |
appId |
string |
Auth-app id this tenant is requesting access to. jadu-account uses this to gate post-login: if the user has no role for appId, jadu-account routes them to /no-access?app=<appId>. Defaults to the provider's authAppId when using the hook. |
returnTo |
string |
Where jadu-account should send the user after login. Path or absolute URL. Validated against jadu-account's host allowlist. Defaults to the current URL when using the hook. |
JaduAuth.init (server)¶
| Option | Type | Description |
|---|---|---|
authServerUrl |
string |
Auth server base URL |
appId |
string |
App ID; only tokens with this authAppId are accepted |
jwksPath |
string |
JWKS path (default: "/api/better-auth/jwks") |
Error handling¶
Errors thrown by the SDK are subclasses of AuthError and include a code
from AuthErrorCode:
| Code | Typical cause |
|---|---|
SESSION_EXPIRED |
Session no longer valid |
REFRESH_FAILED |
Token refresh failed (e.g. session revoked) |
VALIDATION_ERROR |
Invalid input |
USER_NOT_FOUND |
User doesn’t exist |
USER_BANNED |
Account banned |
NETWORK_ERROR |
Request failed (e.g. no network) |
SERVER_ERROR |
Auth server error |
Use error.code for branching and error.message (and optional
error.details) for display. The hook also exposes error so you can show
it in the UI and call clearError() when starting a new action.
Development & playground¶
From the package root:
npm run build
npm run typecheck
npm run test
Playground: A small React frontend and Express backend demonstrate the JaduSSO redirect, the cookie session probe, and protected API calls.
npm run playground:install
npm run dev:all
The playground also requires jadu-account to be running (default
http://localhost:3005); set NEXT_PUBLIC_JADU_ACCOUNT_URL to point
elsewhere.
License¶
See repository license.