Skip to content

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 service
  • package/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

  1. The caller must have the IMPERSONATOR or SUPER_ADMIN role for the target auth app.
  2. 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