Skip to content

@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

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 jose and axios (included).

Quick start

React (frontend)

  1. Wrap your app with JaduAuthProvider (auth API URL + app ID).
  2. Use useJaduAuth() in any component for user, login, logout, authenticatedAxios, and optionally jaduSpineToken (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)

  1. Call JaduAuth.init() once at startup with your auth server URL and default appId.
  2. Use JaduAuth.authorizeRequest([], appId?) as middleware on protected routes. The validated user 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/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: jaduSpineToken from useJaduAuth() — it is string | null and updates on login, signup, verify-email, token refresh, and logout.
  • React to changes: Pass onJaduSpineTokenChange to JaduAuthProvider so 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.