@cloudwick/acams-auth-client
v1.0.0
Published
ACAMS authentication client library for React applications with Context API and localStorage persistence
Readme
ACAMS Auth Provider
React authentication provider for ACAMS (Amorphic Central Authentication Management System). Handles silent SSO via iframe bridge and redirect-based login.
Installation
npm install @acams/auth-provider
# or
yarn add @acams/auth-providerQuick Start
import { AcamsAuthProvider, AuthGuard } from "@acams/auth-provider";
function App() {
return (
<AcamsAuthProvider
acamsApiGateway="https://api.acams.example.com"
appId="your-app-id"
redirectUri={window.location.origin + "/auth/callback"}
acamsBridgeUrl="https://acams.example.com/bridge"
acamsLoginUrl="https://acams.example.com/login"
onLoginSuccess={(user) => console.log("Logged in:", user.email)}
onSessionExpired={() => console.log("Session expired")}
>
<AuthGuard fallback={<div>Authenticating...</div>} loader={<Spinner />}>
<Dashboard />
</AuthGuard>
</AcamsAuthProvider>
);
}That's it. The provider handles everything automatically.
Authentication Flow
How It Works
- App loads → Provider hydrates state from localStorage
- If valid tokens exist → User is authenticated immediately
- If no tokens → Provider checks iframe bridge for existing ACAMS session
- If ACAMS session found → Silent login via
POST auth/preparewithsilent: true - If no session / bridge timeout → Redirect to
acamsLoginUrl - After ACAMS login → ACAMS redirects back with
codeandstateparams - Provider auto-handles callback → Exchanges code for tokens, stores them
- Session validated → User is authenticated, session auto-validates every 10 minutes
Flow Diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ App Loads │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Provider hydrates state from localStorage │
│ - If valid tokens exist → isAuthenticated = true │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼ (no tokens)
┌─────────────────────────────────────────────────────────────────────────────┐
│ Check iframe bridge for existing ACAMS session │
│ - Bridge responds → startSilentAuth() → redirect to ACAMS │
│ - Bridge timeout → redirect to acamsLoginUrl │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ACAMS redirects back with ?code=...&state=... │
│ Provider auto-detects and exchanges for tokens │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ User is authenticated │
│ - Tokens stored in localStorage │
│ - Session auto-validates every 10 minutes (configurable) │
└─────────────────────────────────────────────────────────────────────────────┘Integration Guide
Step 1: Wrap Your App
// src/main.tsx
import { AcamsAuthProvider, AuthGuard } from "@acams/auth-provider";
function App() {
return (
<AcamsAuthProvider
acamsApiGateway={import.meta.env.VITE_ACAMS_API_URL}
appId={import.meta.env.VITE_ACAMS_APP_ID}
redirectUri={`${window.location.origin}/auth/callback`}
acamsBridgeUrl={import.meta.env.VITE_ACAMS_BRIDGE_URL}
acamsLoginUrl={import.meta.env.VITE_ACAMS_LOGIN_URL}
onLoginSuccess={(user, tokens) => {
console.log("Login success:", user.email);
}}
onSessionExpired={() => {
console.log("Session expired");
}}
onAuthBridgeFailed={() => {
console.log("No ACAMS session, redirecting to login...");
}}
onError={(error, context) => {
console.error(`Auth error in ${context}:`, error);
}}
>
<AuthGuard fallback={<LoadingScreen />}>
<YourApp />
</AuthGuard>
</AcamsAuthProvider>
);
}Step 2: Access Auth State (Optional)
Use useAcamsAuth hook anywhere inside the provider to access auth state.
import { useAcamsAuth } from "@acams/auth-provider";
function Header() {
const { isAuthenticated, user, logout } = useAcamsAuth();
if (!isAuthenticated) return null;
return (
<header>
<span>Welcome, {user.email}</span>
<button onClick={logout}>Logout</button>
</header>
);
}Step 3: Manual Session Validation (Optional)
Use useSessionValidator if you need to manually trigger validation.
import { useSessionValidator } from "@acams/auth-provider";
function Dashboard() {
const { validateNow, lastValidatedAt } = useSessionValidator();
return (
<div>
<p>Last validated: {lastValidatedAt}</p>
<button onClick={validateNow}>Validate Now</button>
</div>
);
}API Reference
AcamsAuthProvider Props
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| acamsApiGateway | string | ✅ | - | Base URL of the ACAMS API gateway |
| appId | string | ✅ | - | Your application ID registered with ACAMS |
| redirectUri | string | ✅ | - | OAuth callback URL |
| acamsBridgeUrl | string | ❌ | - | URL of ACAMS iframe bridge for session detection |
| acamsLoginUrl | string | ❌ | - | URL to redirect when no session exists |
| bridgeTimeout | number | ❌ | 3000 | Max wait time for bridge response (ms) |
| enableAuthBridge | boolean | ❌ | true | Enable iframe bridge SSO |
| validationInterval | number | ❌ | 600000 | Session validation interval (ms) |
| enableAutoValidation | boolean | ❌ | true | Auto-validate session periodically |
| onLoginStart | (email) => void | ❌ | - | Fired when auth flow begins |
| onLoginSuccess | (user, tokens) => void | ❌ | - | Fired on successful authentication |
| onLoginError | (error) => void | ❌ | - | Fired on auth failure |
| onLogout | () => void | ❌ | - | Fired after logout |
| onTokenRenewed | (tokens) => void | ❌ | - | Fired when tokens refreshed |
| onTokenRenewalError | (error) => void | ❌ | - | Fired on token refresh failure |
| onSessionExpired | () => void | ❌ | - | Fired when session expires |
| onSessionValidated | (isValid) => void | ❌ | - | Fired after validation check |
| onStateHydrated | (state) => void | ❌ | - | Fired after localStorage hydration |
| onStateChange | (state, prev) => void | ❌ | - | Fired on any state change |
| onError | (error, context) => void | ❌ | - | General error handler |
| onAuthBridgeFailed | () => void | ❌ | - | Fired when bridge times out |
useAcamsAuth Hook
const {
isAuthenticated, // boolean - true if user has valid session
isLoading, // boolean - true during auth operations
validSession, // boolean - session validity flag
user, // UserInfo - { userId, email, sessionId }
tokens, // TokenSet | null - { idToken, opaqueToken, accessToken }
startLogin, // (email: string, idpHint?: string) => Promise<PrepareAuthResponse>
startSilentLogin, // (email?: string) => Promise<void>
handleCallback, // (code: string, state: string) => Promise<void>
logout, // () => void
forceTokenRenewal // () => Promise<boolean>
} = useAcamsAuth();useSessionValidator Hook
const {
validateNow, // () => Promise<boolean> - manually validate session
lastValidatedAt, // string | null - ISO timestamp of last validation
isValidating // boolean - true during validation
} = useSessionValidator();AuthGuard Component
<AuthGuard
fallback={<LoginPage />} // Shown when not authenticated
loader={<LoadingSpinner />} // Shown while loading (optional)
>
<ProtectedContent />
</AuthGuard>Types
AuthState
interface AuthState {
idToken: string | null;
opaqueToken: string | null;
accessToken: string | null;
sessionActive: boolean;
validSession: boolean;
sessionId: string | null;
userId: string | null;
email: string | null;
lastValidatedAt: string | null;
isLoading: boolean;
}TokenSet
interface TokenSet {
idToken: string;
opaqueToken: string;
accessToken?: string;
}UserInfo
interface UserInfo {
userId: string | null;
email: string | null;
sessionId: string | null;
}Features
Auto OAuth Callback Handling
Provider automatically detects code and state URL params and exchanges them for tokens. No need to create a separate callback route.
Cross-Tab Sync
Auth state syncs across browser tabs via localStorage events. Login/logout in one tab updates all others instantly.
Token Renewal
HTTP client automatically renews tokens on 401/403 responses. Failed renewal triggers onSessionExpired.
Storage
Auth state persists in localStorage under key acams_auth_state.
Complete Example
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { AcamsAuthProvider, AuthGuard, useAcamsAuth } from "@acams/auth-provider";
function Dashboard() {
const { user, logout } = useAcamsAuth();
return (
<div>
<h1>Welcome, {user.email}</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
function LoadingScreen() {
return <div>Authenticating with ACAMS...</div>;
}
function App() {
return (
<AcamsAuthProvider
acamsApiGateway={import.meta.env.VITE_ACAMS_API_URL}
appId={import.meta.env.VITE_ACAMS_APP_ID}
redirectUri={`${window.location.origin}/auth/callback`}
acamsBridgeUrl={import.meta.env.VITE_ACAMS_BRIDGE_URL}
acamsLoginUrl={import.meta.env.VITE_ACAMS_LOGIN_URL}
onLoginSuccess={(user) => console.log("Welcome:", user.email)}
onSessionExpired={() => console.log("Session expired")}
onError={(err, ctx) => console.error(`[${ctx}]`, err)}
>
<AuthGuard fallback={<LoadingScreen />}>
<Dashboard />
</AuthGuard>
</AcamsAuthProvider>
);
}
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);License
MIT
