@scenarix/jadu-auth¶
JaduAuth SDK — Authentication for React apps and Node backends. Handles login, signup, JWT access/session tokens, automatic token refresh, and server-side JWT verification (JWKS).
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(auth API URL + app ID). - Use
useJaduAuth()in any component foruser,login,logout,authenticatedAxios, and optionallyjaduSpineToken(for real-time / Centrifugo).
// 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" appId="my-app-id">
{children}
</JaduAuthProvider>
);
}
// components/Dashboard.tsx
import { useJaduAuth, AuthState } from "@scenarix/jadu-auth/react";
export function Dashboard() {
const { user, authState, login, logout, authenticatedAxios } = useJaduAuth();
if (authState !== AuthState.AUTHENTICATED) {
return (
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.currentTarget;
login(form.email.value, form.password.value);
}}
>
<input name="email" type="email" />
<input name="password" type="password" />
<button type="submit">Log in</button>
</form>
);
}
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>
);
}
Backend (Node / Express)¶
- Call
JaduAuth.init()once at startup with your auth server URL and defaultappId. - Use
JaduAuth.authorizeRequest([], appId?)as middleware on protected routes. The validated user is onreq.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/public", (req, res) => {
res.json({ message: "Hello" });
});
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);
Package entry points¶
| Import | Use case |
|---|---|
@scenarix/jadu-auth |
Core: AuthClient, TokenStorage, createAuthenticatedAxios, types, errors |
@scenarix/jadu-auth/react |
React: JaduAuthProvider, useJaduAuth, auth state + helpers |
@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 token storage without React.
Scenarios & examples¶
1. Login and call a protected API (React)¶
Use authenticatedAxios from useJaduAuth(). It attaches Authorization: Bearer <accessToken> and, on 401, refreshes the token and retries once.
const { authenticatedAxios } = useJaduAuth();
const { data } = await authenticatedAxios.get("/api/protected");
await authenticatedAxios.post("/api/items", { name: "New item" });
2. Signup and optional email verification¶
signup returns either a normal auth result or a “requires email verification” result. Handle both:
const { signup, verifyEmail, sendVerificationEmail } = useJaduAuth();
const result = await signup(email, password, name);
if (result.requiresEmailVerification) {
// Show “Check your email” and a form for the 6-digit code
setMessage(result.message);
setStep("verify");
} else {
// Logged in; result.user is set
}
// When user submits the code from email:
await verifyEmail(code, email);
// Resend code
await sendVerificationEmail(email);
3. Forgot / reset password¶
Same OTP flow as email verification: request code, then reset with code + new password.
const { forgotPassword, resetPassword } = useJaduAuth();
// Step 1: request code
await forgotPassword(email);
// Step 2: after user receives code
await resetPassword(email, code, newPassword);
4. 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 <LoginForm />;
return <div>Hello, {user?.name}</div>;
authState: AuthState.INITIALIZING | AuthState.AUTHENTICATED | AuthState.UNAUTHENTICATED.
5. Logout and redirect¶
Clear session and optionally redirect:
<JaduAuthProvider
apiUrl={authUrl}
appId={appId}
onLogout={() => window.location.assign("/login")}
>
{children}
</JaduAuthProvider>
Calling logout() from useJaduAuth() clears tokens and state; onLogout also runs when the session is invalid (e.g. refresh failed).
6. JaduSpine token (real-time / Centrifugo)¶
The auth backend returns a JaduSpine token (JWT) with the same expiry as the access token. Use it to authenticate with JaduSpine or Centrifugo for real-time features.
- Read the token:
jaduSpineTokenfromuseJaduAuth()— it isstring | nulland updates on login, signup, verify-email, token refresh, and logout. - React to changes: Pass
onJaduSpineTokenChangetoJaduAuthProviderso you can pass the token to your real-time client whenever it changes.
import {
JaduAuthProvider,
useJaduAuth,
AuthState,
} from "@scenarix/jadu-auth/react";
import { Centrifuge } from "centrifuge";
// In your root layout: notify Centrifugo when the token changes
function App() {
const centrifugeRef = useRef<Centrifuge | null>(null);
return (
<JaduAuthProvider
apiUrl="https://auth.example.com"
appId="my-app"
onJaduSpineTokenChange={(token) => {
if (centrifugeRef.current) {
if (token) centrifugeRef.current.setToken(token);
else centrifugeRef.current.disconnect();
}
}}
>
<RealTimeApp centrifugeRef={centrifugeRef} />
</JaduAuthProvider>
);
}
// In any component: use the current token if needed
function RealTimePanel() {
const { jaduSpineToken, authState } = useJaduAuth();
if (authState !== AuthState.AUTHENTICATED || !jaduSpineToken) return null;
return (
<div>Connected with JaduSpine token (same expiry as access token)</div>
);
}
The token is stored in localStorage (under the same prefix as other auth keys) and is refreshed whenever the access token is refreshed.
7. Error handling in the UI¶
The hook exposes error (and clearError). Errors are instances of AuthError with a code you can branch on:
import { useJaduAuth } from "@scenarix/jadu-auth/react";
import { AuthErrorCode } from "@scenarix/jadu-auth";
const { login, error, clearError } = useJaduAuth();
const handleLogin = async () => {
clearError();
try {
await login(email, password);
} catch (e) {
// error state is updated; also in catch if you want to handle here
}
};
if (error) {
if (error.code === AuthErrorCode.INVALID_CREDENTIALS) {
return <p>Wrong email or password.</p>;
}
if (error.code === AuthErrorCode.USER_ALREADY_EXISTS) {
return <p>An account with this email already exists.</p>;
}
return <p>{error.message}</p>;
}
See Error handling for all codes and types.
8. Backend: protected route with req.jaduAuth¶
After JaduAuth.authorizeRequest([], "my-app-id"), req.jaduAuth is set with the verified token data:
app.get("/api/profile", JaduAuth.authorizeRequest([], "my-app-id"), (req, res) => {
const { userId, email, name, appId, claims } = req.jaduAuth!;
// Use userId for DB lookups, etc.
res.json({ userId, email, name });
});
9. Backend: verify token without Express (e.g. WebSockets)¶
Use JaduAuth.verifyToken() when you’re not using the Express middleware:
import { JaduAuth } from "@scenarix/jadu-auth/server";
await JaduAuth.init({ authServerUrl: "...", appId: "my-app" });
const token = req.headers.authorization?.replace(/^Bearer\s+/i, "") ?? "";
const userData = await JaduAuth.verifyToken(token);
console.log(userData.userId, userData.email);
10. Impersonation (React)¶
Users with the IMPERSONATOR or SUPER_ADMIN role can view-as another user. The SDK provides a built-in modal for selecting a target user and a draggable banner while impersonation is active.
import { useJaduAuth } from "@scenarix/jadu-auth/react";
function AdminPanel() {
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;
}
How it works:
startImpersonation()opens a built-in modal to search and select a target user.- The SDK issues new access/jaduSpine tokens for the target user (the original session is preserved).
- A draggable red banner appears with a "Stop" button. Banner position persists in
localStorage. - Token auto-refresh is paused during impersonation.
endImpersonation()re-issues tokens for the original user and resumes refresh.- The original user info survives page reloads via
sessionStorage.
Backend detection: On the server side, req.jaduAuth.isImpersonation is true and req.jaduAuth.originalUserId contains the real user's ID when a request is made with an impersonation token.
app.get("/api/data", JaduAuth.authorizeRequest([], "my-app-id"), (req, res) => {
if (req.jaduAuth!.isImpersonation) {
console.log(`Impersonated by ${req.jaduAuth!.originalUserId}`);
}
res.json({ userId: req.jaduAuth!.userId });
});
11. Vanilla JS/TS or Node (no React)¶
Use the main entry: create an AuthClient, TokenStorage, and createAuthenticatedAxios. You handle login and storing tokens yourself; then use the authenticated axios instance for API calls.
import {
AuthClient,
TokenStorage,
createAuthenticatedAxios,
} from "@scenarix/jadu-auth";
const tokenStorage = new TokenStorage("my_app");
const authClient = new AuthClient({
apiUrl: "https://auth.example.com",
appId: "my-app-id",
});
const authAxios = createAuthenticatedAxios({
tokenStorage,
authClient,
onAuthFailure: () => {
console.log("Session expired");
},
});
// After login (e.g. with authClient.login(...) and tokenStorage.setTokens(...)):
const { data } = await authAxios.get("https://api.example.com/me");
Configuration¶
JaduAuthProvider (React)¶
| Prop | Type | Description |
|---|---|---|
apiUrl |
string |
Auth API base URL (e.g. https://auth.example.com) |
appId |
string |
Application ID for RBAC / token scope |
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 (e.g. login failure, refresh failure) |
onJaduSpineTokenChange |
(token) => void |
Called when the JaduSpine token changes (login, signup, verify, refresh, logout). token is the new JWT or null when logged out. Use to pass the token to JaduSpine/Centrifugo. |
JaduAuth.init (server)¶
| Option | Type | Description |
|---|---|---|
authServerUrl |
string |
Auth server base URL |
appId |
string |
App ID; only tokens with this appId are accepted |
jwksPath |
string |
JWKS path (default: "/api/better-auth/jwks") |
JaduAuth.authorizeRequest (server)¶
| Param | Type | Description |
|---|---|---|
permissions |
string[] |
Reserved for future RBAC route checks (currently no-op) |
appId |
string |
Optional per-route app override. When provided, token authAppId falls back to this value if missing |
Error handling¶
Errors thrown by the SDK are subclasses of AuthError and include a code from AuthErrorCode:
| Code | Typical cause |
|---|---|
INVALID_CREDENTIALS |
Wrong email/password |
USER_ALREADY_EXISTS |
Signup with existing email |
SESSION_EXPIRED |
Session no longer valid |
REFRESH_FAILED |
Token refresh failed (e.g. session revoked) |
VALIDATION_ERROR |
Invalid input (e.g. email format) |
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 React hook also sets error so you can show it in the UI and use clearError() when starting a new action.
Development & playground¶
From the repo root:
# Build the SDK
npm run build
# Typecheck
npm run typecheck
# Tests
npm run test
Playground: A small React frontend and Express backend demonstrate login, signup, email verification, and protected API calls.
npm run playground:install # install playground deps
npm run dev:all # SDK watch + playground BE + playground FE
Then open the frontend URL (see terminal) and use the dev UI to log in and hit the backend’s protected endpoints. Backend and frontend both point at the same auth server URL and app ID (see playground/be/src/constants.ts and playground/fe config).
License¶
See repository license.