Jadu Auth¶
Authentication for React apps and Node backends: login, signup, JWT access/session tokens, automatic refresh, and server-side verification (JWKS).
Package: @scenarix/jadu-auth
Docs: Onboarding · Infrastructure
Repo layout¶
package/— SDK source and full package docs (config, error handling, JaduSpine token, etc.)be/— Auth backend (email/password, email OTP, SMS OTP login, RBAC)fe/— Admin frontend for the auth servicepackage/playground/— Example React app and Express API using the SDK
npm run dev # run fe + be + package + playground
npm run dev:package # build SDK in watch mode
npm run be:lock # after changing be/package.json — updates be/package-lock.json for Docker build
Install¶
npm install @scenarix/jadu-auth
Use the Scenarix GitHub Packages registry and set GITHUB_PKG_TOKEN if needed.
Usage¶
React¶
Wrap the app with JaduAuthProvider, then use useJaduAuth() for user state and auth methods.
import {
JaduAuthProvider,
useJaduAuth,
AuthState,
} from "@scenarix/jadu-auth/react";
// 1. Provider at the root
export default function App() {
return (
<JaduAuthProvider apiUrl="https://auth.example.com" authAppId="my-app">
<MyApp />
</JaduAuthProvider>
);
}
// 2. Hook in any component
function Dashboard() {
const {
user,
authState,
loginWithEmailPassword,
logout,
authenticatedAxios,
} = useJaduAuth();
if (authState !== AuthState.AUTHENTICATED) {
return (
<button
onClick={() =>
loginWithEmailPassword({ email: "user@example.com", password: "***" })
}
>
Log in
</button>
);
}
const fetchData = async () => {
const { data } = await authenticatedAxios.get("/api/data");
return data;
};
return (
<div>
Welcome, {user?.name}! <button onClick={() => logout()}>Log out</button>
</div>
);
}
Backend (Express)¶
Call JaduAuth.init() at startup, then protect routes with JaduAuth.authorizeRequest([], appId?). Validated user data is on req.jaduAuth.
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-id",
});
app.get("/api/me", JaduAuth.authorizeRequest([], "my-app-id"), (req, res) => {
const { userId, email, name } = req.jaduAuth!;
res.json({ userId, email, name });
});
app.listen(3000);
Entry points¶
| Import | Use |
|---|---|
@scenarix/jadu-auth |
Core: AuthClient, TokenStorage, createAuthenticatedAxios, types, errors |
@scenarix/jadu-auth/react |
React: JaduAuthProvider, useJaduAuth |
@scenarix/jadu-auth/server |
Server: JaduAuth, canUserDo |
Frontend API (@scenarix/jadu-auth/react)¶
JaduAuthProvider (props)¶
| Prop | Type | Description |
|---|---|---|
apiUrl |
string |
Auth API base URL |
authAppId |
string |
Application ID for RBAC / token scope |
children |
ReactNode |
App tree |
autoRefresh |
boolean |
Enable automatic token refresh (default: true) |
refreshBuffer |
number |
Ms before expiry to trigger refresh (default: 60000) |
debug |
boolean |
Debug logging (default: false) |
storageKeyPrefix |
string |
localStorage key prefix (default: "jadu_auth") |
onLogout |
() => void |
Called on logout and when session becomes invalid |
onError |
(error: Error) => void |
Called on auth errors |
onJaduSpineTokenChange |
(token: string \| null) => void |
Called when JaduSpine token changes (e.g. for Centrifugo) |
useJaduAuth() — state¶
| Field | Type | Description |
|---|---|---|
authState |
AuthState |
AuthState.INITIALIZING | AuthState.AUTHENTICATED | AuthState.UNAUTHENTICATED |
user |
User \| null |
Current user (id, email, name, etc.) |
jaduSpineToken |
string \| null |
JWT for JaduSpine / real-time (same expiry as access token) |
error |
AuthError \| null |
Last auth error (use clearError() to reset) |
useJaduAuth() — methods¶
| Method | Description |
|---|---|
loginWithEmailPassword({ email, password }) |
Log in with email/password |
signupWithEmailPassword({ email, password, name }) |
Sign up; returns SignupResult (may require email verification) |
forgotPasswordInit(email) |
Send OTP for password reset |
forgotPasswordVerify({ email, code, newPassword }) |
Complete password reset with OTP |
sendVerificationEmail(email) |
Send or resend verification email |
verifyEmail({ email, code }) |
Verify email with OTP and get tokens |
signupInitWithEmailOtp({ email }) |
Email-OTP signup step 1: send OTP |
signupVerifyWithEmailOtp({ email, code, name }) |
Email-OTP signup step 2: complete signup |
loginInitWithEmailOtp({ email }) |
Email-OTP login step 1: send OTP |
loginVerifyWithEmailOtp({ email, code }) |
Email-OTP login step 2: complete login |
logout() |
Clear session and tokens |
refreshToken() |
Manually trigger access token refresh |
clearError() |
Clear the current error state |
canUserDo(permission) |
Returns whether the current user has the given permission (from token) |
useJaduAuth() — impersonation¶
Impersonation lets privileged users (with IMPERSONATOR or SUPER_ADMIN role) view-as another user. The original session is preserved so you can always switch back.
| Field / Method | Type | Description |
|---|---|---|
canImpersonate |
boolean |
true when the current token includes the impersonate permission (IMPERSONATOR or SUPER_ADMIN) |
isImpersonating |
boolean |
true when currently viewing as another user |
originalUser |
{ id: string; name: string } \| null |
The real user's info while impersonating (persisted across reloads via sessionStorage) |
startImpersonation() |
() => void |
Opens the built-in user selection modal |
endImpersonation() |
() => Promise<void> |
Ends impersonation and restores the original user's tokens |
useJaduAuth() — other¶
| Field | Type | Description |
|---|---|---|
authenticatedAxios |
AuthenticatedAxios |
Axios instance that sends Authorization: Bearer and retries on 401 after refresh |
simulateAccessTokenExpiry() |
() => void |
Dev helper: clear access token to test refresh flow |
Backend API (@scenarix/jadu-auth/server)¶
JaduAuth (static methods)¶
| Method | Description |
|---|---|
JaduAuth.init(config) |
Initialize once at startup. Fetches and caches JWKS. config: { authServerUrl, appId, jwksPath? }. |
JaduAuth.isInitialized() |
Returns whether init() has been called. |
JaduAuth.authorizeRequest(permissions?, appId?) |
Express middleware. Validates JWT from Authorization: Bearer, sets req.jaduAuth (userId, email, name, appId, claims). Optional appId overrides init app matching for that route. |
JaduAuth.verifyToken(token) |
Verify a JWT string without Express. Returns { userId, email, name, appId, claims }. Use for WebSockets or custom flows. |
req.jaduAuth (after authorizeRequest)¶
| Field | Type | Description |
|---|---|---|
userId |
string |
User ID from token |
email |
string |
User email |
name |
string |
User name |
appId |
string |
Token's auth app ID |
isImpersonation |
boolean |
true when the request is from an impersonation token |
originalUserId |
string \| undefined |
The real user's ID when isImpersonation is true |
claims |
object |
Full JWT payload (e.g. permissions) |
canUserDo (server)¶
| Function | Description |
|---|---|
canUserDo(permission) |
Call only inside a request that used JaduAuth.authorizeRequest(). Returns whether the request user's token includes the given permission (e.g. "admin:read"). |
Impersonation¶
Impersonation allows admin users to "view-as" another user without knowing their credentials. The original user's session is untouched — only short-lived access tokens are issued for the target user.
Prerequisites¶
- The caller must have the
IMPERSONATORorSUPER_ADMINrole for the target auth app. - The target user must have access to the same auth app.
React usage¶
The useJaduAuth() hook exposes everything needed:
import { useJaduAuth, AuthState } from "@scenarix/jadu-auth/react";
function AdminToolbar() {
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>
);
}
if (canImpersonate) {
return <button onClick={startImpersonation}>Impersonate user</button>;
}
return null;
}
Calling startImpersonation() opens a built-in modal where the admin can search and select a target user. A draggable banner appears while impersonation is active, with a "Stop" button to end it.
Key behaviors:
- Token auto-refresh is paused during impersonation (impersonation tokens are not refreshed).
- The original user info is stored in
sessionStorage, so the banner persists across page reloads. endImpersonation()re-issues tokens for the original user and resumes normal token refresh.
Backend detection¶
On the server side, req.jaduAuth includes impersonation metadata so your API can detect and handle impersonated requests:
app.get("/api/data", JaduAuth.authorizeRequest([]), (req, res) => {
const { userId, isImpersonation, originalUserId } = req.jaduAuth!;
if (isImpersonation) {
console.log(`User ${originalUserId} is viewing as ${userId}`);
}
res.json({ data: "..." });
});
API endpoints¶
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/auth/impersonate/users |
Session + IMPERSONATOR/SUPER_ADMIN | List users available for impersonation (paginated, searchable) |
| POST | /api/auth/impersonate |
Session + IMPERSONATOR/SUPER_ADMIN | Start impersonation — returns target user tokens |
| POST | /api/auth/endImpersonation |
Session only | End impersonation — returns original user tokens |
System roles¶
Three system roles are code-defined and cannot be created, edited, or deleted via the API:
| Role ID | Description |
|---|---|
USER |
Default role assigned on signup (no permissions) |
SUPER_ADMIN |
All permissions (*) for all apps |
IMPERSONATOR |
Can impersonate users in the assigned app |