@cloudwick/acams-auth-client
v1.1.4
Published
ACAMS authentication client library for React applications with Context API and localStorage persistence
Readme
@cloudwick/acams-auth-client — Integration Guide
Comprehensive guide for integrating the ACAMS authentication provider into your React application.
Table of Contents
- Overview
- Installation
- Quick Start
- Architecture
- Authentication Flows
- Step-by-Step Integration
- API Reference
- Types
- Configuration
- Utility Functions
- Features
- Security Considerations
- Exports Summary
- Migration Guide (AWS Amplify)
- Troubleshooting
Overview
@cloudwick/acams-auth-client is a self-contained React authentication library for ACAMS (Amorphic Central Authentication Management System) that provides:
- OAuth 2.0 Authorization Code Flow with PKCE support
- Session Persistence via localStorage with cross-tab synchronization
- Automatic Token Renewal on 401/403 responses
- Session Validation at configurable intervals
- Personal Access Token (PAT) Management with caching
- Silent SSO via iframe-based auth bridge
Installation
npm install @cloudwick/acams-auth-client
# or
yarn add @cloudwick/acams-auth-clientQuick Start
import { AcamsAuthProvider, AuthGuard } from "@cloudwick/acams-auth-client";
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>
);
}The provider handles everything automatically. See Step-by-Step Integration for detailed setup.
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ AcamsAuthProvider │
│ ┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ useReducer │◄──►│ LocalStore │◄──►│ localStorage │ │
│ │ (authReducer) │ │ (persist + │ │ (acams_auth_ │ │
│ │ │ │ cross-tab) │ │ state) │ │
│ └────────┬────────┘ └──────────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ createHttpClient │ │
│ │ • Bearer token injection │ │
│ │ • Auto-renewal on 401/403 + JWT expiry │ │
│ │ • Request timeout handling │ │
│ └────────────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ createAuthApi │ │ createPatApi │ │
│ │ • signUp │ │ • createToken │ │
│ │ • prepare │ │ • listTokens │ │
│ │ • exchange │ │ • getDetails │ │
│ │ • renew │ │ • rotateToken │ │
│ │ • validate │ └─────────────────┘ │
│ └─────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AcamsAuthContext │
│ { state, dispatch, authApi, patApi, config } │
└─────────────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌────────────────┐ ┌─────────────────┐
│ useAcamsAuth │ │ useSession │ │ usePatApi │
│ • startLogin │ │ Validator │ │ • createToken │
│ • startSignUp │ │ • validateNow │ │ • listTokens │
│ • logout │ │ │ │ (cached) │
│ • forceRenewal │ └────────────────┘ └─────────────────┘
└──────────────────┘
│
▼
┌──────────────────┐
│ AuthGuard │
│ (conditional │
│ rendering) │
└──────────────────┘Directory Structure
acams-auth-provider/
├── src/
│ ├── index.ts # Public API barrel export
│ ├── AcamsAuthProvider.tsx # Root React provider
│ ├── AcamsAuthContext.ts # Context definition + hook
│ │
│ ├── api/
│ │ ├── httpClient.ts # Fetch wrapper with auth
│ │ ├── authApi.ts # Auth endpoints
│ │ ├── patApi.ts # PAT CRUD endpoints
│ │ └── index.ts
│ │
│ ├── components/
│ │ ├── AuthGuard.tsx # Conditional render component
│ │ └── index.ts
│ │
│ ├── hooks/
│ │ ├── useAcamsAuth.ts # Primary consumer hook
│ │ ├── useSessionValidator.ts # Manual validation trigger
│ │ ├── usePatApi.ts # PAT operations hook
│ │ └── index.ts
│ │
│ ├── services/
│ │ ├── loginService.ts # Login flow orchestration
│ │ └── index.ts # Iframe auth bridge
│ │
│ ├── store/
│ │ ├── authReducer.ts # State reducer
│ │ ├── localStore.ts # Persistence layer
│ │ └── index.ts
│ │
│ ├── types/
│ │ ├── auth.ts # Core auth types
│ │ ├── api.ts # API DTOs
│ │ ├── config.ts # Configuration types
│ │ └── index.ts
│ │
│ ├── utils/
│ │ ├── tokenDecoder.ts # JWT decode/expiry
│ │ ├── urlBuilder.ts # URL construction
│ │ ├── errorExtractor.ts # Error normalization
│ │ ├── patCache.ts # PAT list caching
│ │ └── index.ts
│ │
│ └── constants/
│ └── index.ts # Origin, login URL helpers
│
└── tests/ # Unit testsAuthentication Flows
High-Level Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────────────────────────┘Standard Login Flow (Sequence Diagram)
┌─────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────────┐
│ User │ │ App │ │ AuthBridge │ │ ACAMS │
│ │ │ │ │ (iframe) │ │ Server │
└────┬────┘ └──────┬──────┘ └──────┬──────┘ └─────┬──────┘
│ │ │ │
│ Click Login │ │ │
│────────────────►│ │ │
│ │ │ │
│ │ createAuthBridge │ │
│ │──────────────────►│ │
│ │ │ │
│ │ postMessage │ │
│ │◄──────────────────│ │
│ │ { idp_hint } │ │
│ │ │ │
│ │ POST /auth/prepare │
│ │──────────────────────────────────────►│
│ │ │ │
│ │ { authorization_url } │
│ │◄──────────────────────────────────────│
│ │ │ │
│ Redirect to ACAMS login │ │
│◄────────────────│ │ │
│ │ │ │
│ User authenticates with ACAMS │ │
│────────────────────────────────────────────────────────►│
│ │ │ │
│ Redirect back with code + state │ │
│◄────────────────────────────────────────────────────────│
│ │ │ │
│ App receives callback │ │
│────────────────►│ │ │
│ │ │ │
│ │ POST /auth/callback │
│ │──────────────────────────────────────►│
│ │ │ │
│ │ { tokens } │
│ │◄──────────────────────────────────────│
│ │ │ │
│ │ GET /me (user info) │
│ │──────────────────────────────────────►│
│ │ │ │
│ │ { user } │
│ │◄──────────────────────────────────────│
│ │ │ │
│ Authenticated │ │ │
│◄────────────────│ │ │Auto-Login Flow (?auto_auth=true)
When the app loads with ?auto_auth=true query param:
- Provider detects query param
- Initiates
startLoginFlowautomatically - Auth bridge attempts silent SSO
- If SSO succeeds → user authenticated
- If SSO fails → redirects to ACAMS login
Token Renewal Flow
┌──────────────┐ ┌──────────────┐ ┌────────────┐
│ HttpClient │ │ AuthApi │ │ ACAMS │
└──────┬───────┘ └──────┬───────┘ └─────┬──────┘
│ │ │
│ API Request (401/403) │
│◄──────────────────────────────────────►│
│ │ │
│ Check: isTokenExpired(opaqueToken)? │
│────────────────────┤ │
│ │ │
│ POST /auth/renew │ │
│───────────────────►│──────────────────►│
│ │ │
│ { new tokens } │◄──────────────────│
│◄───────────────────│ │
│ │ │
│ Retry original request │
│───────────────────────────────────────►│
│ │ │
│ { response } │
│◄──────────────────────────────────────│Session Validation Flow
Periodic validation ensures session is still active server-side:
- Provider schedules interval (default: 10 minutes)
- Calls
authApi.validateSession(appId, idToken) - If invalid → dispatches
INVALIDATE_SESSION, clears storage, firesonSessionExpired - If valid → updates
lastValidatedAt, firesonSessionValidated
Step-by-Step Integration
Step 1: Install Package
npm install @cloudwick/acams-auth-client
# or
yarn add @cloudwick/acams-auth-clientStep 2: Configure Provider
// src/config/auth.ts
import { AcamsAuthConfig } from '@cloudwick/acams-auth-client';
export const authConfig: AcamsAuthConfig = {
// Required
acamsApiGateway: 'https://api.acams.example.com',
appId: 'your-application-id',
redirectUri: 'https://your-app.com/auth/callback',
acamsLoginUrl: 'https://login.acams.example.com',
// Optional
validationInterval: 600000, // 10 minutes (default)
enableAutoValidation: true, // default: true
// Lifecycle callbacks
onLoginSuccess: (user) => {
console.log('Login success:', user);
analytics.track('login', { userId: user.id });
},
onLoginError: (error) => {
console.error('Login failed:', error);
},
onSessionExpired: () => {
toast.error('Session expired. Please log in again.');
},
onTokenRenewed: (tokens) => {
console.log('Tokens renewed');
},
onStateChange: (state) => {
// State changed (useful for debugging)
},
};Step 3: Wrap Application
// src/App.tsx
import { AcamsAuthProvider } from '@cloudwick/acams-auth-client';
import { authConfig } from './config/auth';
function App() {
return (
<AcamsAuthProvider config={authConfig}>
<Router>
<Routes />
</Router>
</AcamsAuthProvider>
);
}Step 4: Protect Routes
// src/components/ProtectedRoute.tsx
import { AuthGuard } from '@cloudwick/acams-auth-client';
function ProtectedRoute({ children }) {
return (
<AuthGuard
fallback={<Navigate to="/login" />}
loader={<LoadingSpinner />}
>
{children}
</AuthGuard>
);
}Step 5: Use Auth Hook
// src/components/Header.tsx
import { useAcamsAuth } from '@cloudwick/acams-auth-client';
function Header() {
const { isAuthenticated, user, startLogin, logout } = useAcamsAuth();
return (
<header>
{isAuthenticated ? (
<>
<span>Welcome, {user?.name}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<button onClick={startLogin}>Login</button>
)}
</header>
);
}Step 6: Manual Session Validation (Optional)
import { useSessionValidator } from '@cloudwick/acams-auth-client';
function Dashboard() {
const { validateNow, lastValidatedAt } = useSessionValidator();
return (
<div>
<p>Last validated: {lastValidatedAt}</p>
<button onClick={validateNow}>Validate Now</button>
</div>
);
}Step 7: PAT Management (Optional)
// src/components/TokenManager.tsx
import { usePatApi } from '@cloudwick/acams-auth-client';
function TokenManager() {
const { createToken, listTokens, invalidateCache } = usePatApi();
const handleCreate = async () => {
const result = await createToken({
name: 'My API Token',
expiry: '2025-12-31',
});
invalidateCache(); // Clear cache after mutation
};
const tokens = await listTokens(); // Cached for 15 minutes
return (/* ... */);
}API Reference
AcamsAuthProvider Props
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| acamsApiGateway | string | Yes | - | Base URL of the ACAMS API gateway |
| appId | string | Yes | - | Your application ID registered with ACAMS |
| redirectUri | string | Yes | - | OAuth callback URL |
| acamsBridgeUrl | string | No | - | URL of ACAMS iframe bridge for session detection |
| acamsLoginUrl | string | No | - | URL to redirect when no session exists |
| bridgeTimeout | number | No | 3000 | Max wait time for bridge response (ms) |
| validationInterval | number | No | 600000 | Session validation interval (ms) |
| enableAutoValidation | boolean | No | true | Auto-validate session periodically |
| onLoginStart | (email) => void | No | - | Fired when auth flow begins |
| onLoginSuccess | (user, tokens) => void | No | - | Fired on successful authentication |
| onLoginError | (error) => void | No | - | Fired on auth failure |
| onLogout | () => void | No | - | Fired after logout |
| onTokenRenewed | (tokens) => void | No | - | Fired when tokens refreshed |
| onTokenRenewalError | (error) => void | No | - | Fired on token refresh failure |
| onSessionExpired | () => void | No | - | Fired when session expires |
| onSessionValidated | (isValid) => void | No | - | Fired after validation check |
| onStateHydrated | (state) => void | No | - | Fired after localStorage hydration |
| onStateChange | (state, prev) => void | No | - | Fired on any state change |
| onError | (error, context) => void | No | - | General error handler |
| onAuthBridgeFailed | () => void | No | - | Fired when bridge times out |
AuthApi Methods
| Method | Endpoint | Description |
|--------|----------|-------------|
| signUp(params) | POST /auth/signup | Register new user |
| prepare(params) | POST /auth/prepare | Get authorization URL |
| exchangeAuthCode(code, state) | POST /auth/callback | Exchange code for tokens |
| renewToken(params) | POST /auth/renew | Refresh expired tokens |
| validateSession(appId, token) | GET /auth/validate | Check session validity |
PatApi Methods
| Method | Endpoint | Description |
|--------|----------|-------------|
| createToken(params) | POST /me/pats | Create new PAT |
| listTokens(params?) | GET /me/pats | List all PATs |
useAcamsAuth Hook
const {
isAuthenticated, // boolean - true if user has valid session
isLoading, // boolean - true during auth operations
validSession, // boolean - session validity flag
sessionActive, // boolean - session active state
user, // UserInfo - { userId, email, sessionId }
tokens, // TokenSet | null - { idToken, opaqueToken, accessToken }
startLogin, // (email: string, idpHint?: string) => Promise<PrepareAuthResponse>
startSignUp, // () => void
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();usePatApi Hook
const {
createToken, // (params) => Promise<CreateTokenResponse>
listTokens, // (params?) => Promise<TokenList> - Cached (15 min TTL)
invalidateCache, // () => void - Clear PAT cache
} = usePatApi();AuthGuard Component
<AuthGuard
fallback={<LoginPage />} // Shown when not authenticated
loader={<LoadingSpinner />} // Shown while loading (optional)
>
<ProtectedContent />
</AuthGuard>| Prop | Type | Description |
|------|------|-------------|
| children | ReactNode | Rendered when authenticated |
| fallback | ReactNode | Rendered when not authenticated |
| loader | ReactNode? | Rendered during loading state |
Types
AuthState
interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
validSession: boolean;
sessionActive: boolean;
user: UserInfo | null;
tokens: TokenSet | null;
lastValidatedAt: number | null;
error: string | null;
}TokenSet
interface TokenSet {
idToken: string;
opaqueToken: string;
accessToken?: string;
refreshToken?: string;
expiresAt?: number;
}UserInfo
interface UserInfo {
userId: string | null;
email: string | null;
}Reducer Actions
| Action | Payload | Effect |
|--------|---------|--------|
| SET_TOKENS | TokenSet | Store tokens, set authenticated |
| SET_SESSION | boolean | Update session validity |
| SET_USER_INFO | UserInfo | Store user data |
| SET_LOADING | boolean | Toggle loading state |
| INVALIDATE_SESSION | — | Clear auth state |
| UPDATE_VALIDATION | number | Update lastValidatedAt |
| HYDRATE | AuthState | Restore from storage |
| RESET | — | Full state reset |
Configuration
AcamsAuthConfig Interface
interface AcamsAuthConfig {
// Required
acamsApiGateway: string; // API base URL
appId: string; // Application identifier
redirectUri: string; // OAuth callback URL
acamsLoginUrl: string; // ACAMS login page URL
// Optional
validationInterval?: number; // Session check interval (ms), default: 600000
enableAutoValidation?: boolean; // Auto-validate session, default: true
// Lifecycle Callbacks
onLoginStart?: () => void;
onLoginSuccess?: (user: UserInfo) => void;
onLoginError?: (error: Error) => void;
onSignUpStart?: () => void;
onSignUpError?: (error: Error) => void;
onLogout?: () => void;
onTokenRenewed?: (tokens: TokenSet) => void;
onTokenRenewalError?: (error: Error) => void;
onSessionExpired?: () => void;
onSessionValidated?: () => void;
onStateHydrated?: (state: AuthState) => void;
onStateChange?: (state: AuthState) => void;
onError?: (error: Error) => void;
}Defaults
const DEFAULT_VALIDATION_INTERVAL = 600000; // 10 minutes
const DEFAULT_REQUEST_TIMEOUT = 35000; // 35 seconds
const DEFAULT_PAT_CACHE_TTL = 900000; // 15 minutesUtility Functions
Token Utilities
// Decode JWT payload
decodeToken<T>(token: string): T | null;
// Check if JWT is expired
isTokenExpired(token: string): boolean;
// Get expiry timestamp from JWT
getTokenExpiry(token: string): number | null;URL Utilities
// Build URL with path segments
urlBuilder(base: string, ...paths: string[]): string;
// Format query parameters
formatQueryParams(params: Record<string, string>): string;Error Utilities
// Extract error message from various error shapes
extractMessage(error: unknown): string;
// Extract structured error object
extractError(error: unknown): { message: string; code?: string };Constants
// Current window origin
currentOrigin: string;
// Build unauthenticated redirect URL
unAuthenticatedAcamsUri(baseUrl: string): string;
// Returns: baseUrl + ?auto_auth=trueFeatures
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 {domain_origin}:acams_auth_state.
Cross-Tab Sync: The storage event listener ensures state changes in one tab propagate to others.
// localStore.ts
const STORAGE_KEY = `${window.location.hostname.split(".").join("_")}:acams_auth_state`;
interface LocalStore {
getState(): AuthState | null;
setState(state: AuthState): void;
subscribe(listener: (state: AuthState) => void): () => void;
clear(): void;
hydrate(): AuthState | null;
}Security Considerations
Token Storage
- Tokens stored in
localStorage(accessible to JavaScript) - For enhanced security, consider:
- HttpOnly cookies for refresh tokens
- Shorter access token lifetimes
- Secure/SameSite cookie attributes
Cross-Tab Communication
- Uses native
storageevents (no broadcast channels) - State sync may have slight delay between tabs
- Logout in one tab triggers logout across all tabs
Session Validation
- Validates against server at regular intervals
- Uses
idToken(not access token) for validation - Clears local state if server reports invalid session
Auth Bridge (iframe SSO)
- Hidden iframe communicates via
postMessage - Targets ACAMS
/authBridge.htmlendpoint - Returns
idp_hintfor silent authentication - Falls back to redirect flow if bridge fails
Error Handling
- All API errors normalized via
extractError - Token renewal failures trigger
onTokenRenewalError - Session invalidation clears all local state
- Network errors surface through
onErrorcallback
Exports Summary
// Provider & Context
export { AcamsAuthProvider, AcamsAuthProviderProps };
export { AcamsAuthContext, useAcamsAuthContext, AcamsAuthContextValue };
// Hooks
export { useAcamsAuth, useSessionValidator, usePatApi };
// Components
export { AuthGuard, AuthGuardProps };
// Services
export { createAuthBridgeWithAcams, startLoginFlow, StartLoginParams };
// Store
export { createLocalStore, LocalStore, authReducer, AuthAction };
// Types
export type {
AuthState, TokenSet, UserInfo,
SessionInvalidError, TokenRenewalError,
AcamsAuthConfig, HttpClientConfig, RequestOptions,
// ... all API DTOs
};
// Utils
export { decodeToken, isTokenExpired, getTokenExpiry };
export { urlBuilder, formatQueryParams };
export { extractError, extractMessage };
// Constants
export { currentOrigin, unAuthenticatedAcamsUri };
export { DEFAULT_VALIDATION_INTERVAL, DEFAULT_REQUEST_TIMEOUT };Migration Guide (AWS Amplify)
This section covers migrating from aws-amplify SDK to @cloudwick/acams-auth-client.
Package Changes
Remove:
npm uninstall aws-amplify @aws-amplify/auth @aws-amplify/core
# or
yarn remove aws-amplify @aws-amplify/auth @aws-amplify/coreAdd:
npm install @cloudwick/acams-auth-client
# or
yarn add @cloudwick/acams-auth-clientCode Migration Patterns
| AWS Amplify | ACAMS Auth Client |
|-------------|-------------------|
| Amplify.configure({ Auth: {...} }) | <AcamsAuthProvider config={...}> |
| Auth.signIn(email, password) | startLogin(email) |
| Auth.signOut() | logout() |
| Auth.currentAuthenticatedUser() | useAcamsAuth().user |
| Auth.currentSession() | useAcamsAuth().tokens |
| Auth.fetchAuthSession() | useAcamsAuth().tokens |
| Hub.listen('auth', callback) | Provider callbacks: onLoginSuccess, onSessionExpired |
| Auth.forgotPassword(email) | Redirect to ACAMS forgot password URL |
| Auth.completeNewPassword() | Handled by ACAMS login flow |
Step-by-Step Migration
1. Remove Amplify Configuration
Before (Amplify):
// src/main.tsx or src/App.tsx
import { Amplify } from 'aws-amplify';
Amplify.configure({
Auth: {
Cognito: {
userPoolId: 'us-west-2_xxxxx',
userPoolClientId: 'xxxxxx',
signUpVerificationMethod: 'code',
}
}
});
function App() {
return <YourApp />;
}After (ACAMS):
// src/main.tsx or src/App.tsx
import { AcamsAuthProvider } from '@cloudwick/acams-auth-client';
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`}
acamsLoginUrl={import.meta.env.VITE_ACAMS_LOGIN_URL}
onLoginSuccess={(user) => console.log('Logged in:', user)}
onSessionExpired={() => console.log('Session expired')}
>
<YourApp />
</AcamsAuthProvider>
);
}2. Replace Auth Imports
Before (Amplify):
import { signIn, signOut, getCurrentUser, fetchAuthSession } from 'aws-amplify/auth';
import { Hub } from 'aws-amplify/utils';After (ACAMS):
import { useAcamsAuth } from '@cloudwick/acams-auth-client';3. Replace Auth Calls
Before (Amplify):
function LoginButton() {
const handleLogin = async () => {
try {
await signIn({ username: email, password });
const user = await getCurrentUser();
console.log('Logged in:', user);
} catch (error) {
console.error('Login failed:', error);
}
};
const handleLogout = async () => {
await signOut();
};
return (
<>
<button onClick={handleLogin}>Login</button>
<button onClick={handleLogout}>Logout</button>
</>
);
}After (ACAMS):
function LoginButton() {
const { startLogin, logout, isAuthenticated, user } = useAcamsAuth();
return (
<>
{isAuthenticated ? (
<>
<span>Welcome, {user?.email}</span>
<button onClick={logout}>Logout</button>
</>
) : (
<button onClick={() => startLogin(email)}>Login</button>
)}
</>
);
}4. Replace Hub Listeners
Before (Amplify):
useEffect(() => {
const unsubscribe = Hub.listen('auth', ({ payload }) => {
switch (payload.event) {
case 'signedIn':
console.log('User signed in');
break;
case 'signedOut':
console.log('User signed out');
break;
case 'tokenRefresh':
console.log('Token refreshed');
break;
}
});
return unsubscribe;
}, []);After (ACAMS):
// Configure callbacks in provider props
<AcamsAuthProvider
onLoginSuccess={(user) => console.log('User signed in:', user)}
onLogout={() => console.log('User signed out')}
onTokenRenewed={(tokens) => console.log('Token refreshed')}
onSessionExpired={() => console.log('Session expired')}
>5. Replace Protected Routes
Before (Amplify with custom hook):
function ProtectedRoute({ children }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
getCurrentUser()
.then(() => setIsAuthenticated(true))
.catch(() => setIsAuthenticated(false))
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
if (!isAuthenticated) return <Navigate to="/login" />;
return children;
}After (ACAMS):
import { AuthGuard } from '@cloudwick/acams-auth-client';
function ProtectedRoute({ children }) {
return (
<AuthGuard
fallback={<Navigate to="/login" />}
loader={<Spinner />}
>
{children}
</AuthGuard>
);
}6. Replace Token Access
Before (Amplify):
const session = await fetchAuthSession();
const idToken = session.tokens?.idToken?.toString();
const accessToken = session.tokens?.accessToken?.toString();
// In API calls
fetch('/api/data', {
headers: {
Authorization: `Bearer ${accessToken}`
}
});After (ACAMS):
const { tokens } = useAcamsAuth();
const { idToken, accessToken, opaqueToken } = tokens || {};
// Token injection handled automatically by httpClient
// Or use tokens directly if neededFiles to Check During Migration
- App entry point (
main.tsx,App.tsx) - RemoveAmplify.configure() - Auth configuration files - Delete Amplify config files
- Components using auth - Replace
signIn/signOutcalls - Protected route wrappers - Use
AuthGuardcomponent - API interceptors - Remove manual token injection (handled by provider)
- Event listeners - Replace
Hub.listenwith provider callbacks
Environment Variables
Before (Amplify):
VITE_COGNITO_USER_POOL_ID=us-west-2_xxxxx
VITE_COGNITO_CLIENT_ID=xxxxxx
VITE_COGNITO_REGION=us-west-2After (ACAMS):
VITE_ACAMS_API_URL=https://api.acams.example.com
VITE_ACAMS_APP_ID=your-app-id
VITE_ACAMS_LOGIN_URL=https://acams.example.com/login
VITE_ACAMS_BRIDGE_URL=https://acams.example.com/bridgeTroubleshooting
Common Issues
1. "useAcamsAuthContext must be used within AcamsAuthProvider"
- Ensure
AcamsAuthProviderwraps your component tree
2. Infinite redirect loop on callback
- Verify
redirectUrimatches registered OAuth callback - Check
stateparameter consistency
3. Session expires immediately after login
- Verify
appIdmatches server configuration - Check server-side session timeout settings
4. Cross-tab logout not working
- Ensure
localStorageaccess isn't blocked - Verify same origin policy compliance
5. Token renewal fails
- Check
refreshTokenpresence in token set - Verify
/auth/renewendpoint availability
6. Auth bridge timeout
- Verify
acamsBridgeUrlis correct - Check CORS configuration on bridge endpoint
- Increase
bridgeTimeoutif network is slow
7. Silent SSO not working
- Ensure cookies are enabled
- Check third-party cookie policies
- Verify ACAMS session exists in parent domain
Complete Example
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { AcamsAuthProvider, AuthGuard, useAcamsAuth } from "@cloudwick/acams-auth-client";
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 />);Created with ❤️ by @UXCOE @CLOUDWICK
