Skip to content

@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

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. On mount it does a cookie-only GET /api/auth/sso/me against apiUrl with x-auth-app-id: authAppId and transitions the auth state machine. Pass jaduSSOUrl (the jadu-account login host) so the login button knows where to redirect.
  2. Render a "Login using JaduAccount" button that calls the arg-less startJaduSSO() returned by useJaduAuth().
  3. Use useJaduAuth() for user, 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.