@arow-software/auth-client 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,342 @@
1
+ # @arow-software/auth-client
2
+
3
+ Reusable authentication package for ArowAuth SSO integration. Provides React context, hooks, and utilities for seamless SSO authentication with automatic token refresh.
4
+
5
+ ## Features
6
+
7
+ - 🔐 **Token Management** - Secure storage, automatic refresh, JWT decoding
8
+ - 🔄 **API Interceptors** - Axios interceptors with 401 handling and request queuing
9
+ - ⚛️ **React Integration** - Context provider and hooks for easy auth state management
10
+ - 🎯 **TypeScript** - Full type definitions included
11
+ - 📦 **Lightweight** - Peer dependencies only (React, Axios)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # npm
17
+ npm install @arow-software/auth-client
18
+
19
+ # yarn
20
+ yarn add @arow-software/auth-client
21
+
22
+ # pnpm
23
+ pnpm add @arow-software/auth-client
24
+ ```
25
+
26
+ ### Peer Dependencies
27
+
28
+ Make sure you have these installed:
29
+
30
+ ```bash
31
+ npm install react axios
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Wrap your app with AuthProvider
37
+
38
+ ```tsx
39
+ // App.tsx
40
+ import { AuthProvider } from '@arow-software/auth-client';
41
+
42
+ function App() {
43
+ return (
44
+ <AuthProvider
45
+ ssoBaseUrl="https://sso.arowsoftware.co.uk"
46
+ clientId="arowtrades"
47
+ apiBaseUrl="http://localhost:5001"
48
+ >
49
+ <YourApp />
50
+ </AuthProvider>
51
+ );
52
+ }
53
+ ```
54
+
55
+ ### 2. Use the useAuth hook
56
+
57
+ ```tsx
58
+ // LoginButton.tsx
59
+ import { useAuth } from '@arow-software/auth-client';
60
+
61
+ function LoginButton() {
62
+ const { isAuthenticated, isLoading, user, login, logout } = useAuth();
63
+
64
+ if (isLoading) {
65
+ return <div>Loading...</div>;
66
+ }
67
+
68
+ if (isAuthenticated) {
69
+ return (
70
+ <div>
71
+ <span>Welcome, {user?.displayName || user?.email}!</span>
72
+ <button onClick={logout}>Logout</button>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ return <button onClick={() => login()}>Login with SSO</button>;
78
+ }
79
+ ```
80
+
81
+ ### 3. Make authenticated API calls
82
+
83
+ ```tsx
84
+ // DataComponent.tsx
85
+ import { useAuthClient } from '@arow-software/auth-client';
86
+ import { useState, useEffect } from 'react';
87
+
88
+ function DataComponent() {
89
+ const client = useAuthClient();
90
+ const [data, setData] = useState(null);
91
+
92
+ useEffect(() => {
93
+ const fetchData = async () => {
94
+ try {
95
+ const response = await client.get('/api/data');
96
+ setData(response.data);
97
+ } catch (error) {
98
+ console.error('Failed to fetch data', error);
99
+ }
100
+ };
101
+
102
+ fetchData();
103
+ }, [client]);
104
+
105
+ return <div>{JSON.stringify(data)}</div>;
106
+ }
107
+ ```
108
+
109
+ ## API Reference
110
+
111
+ ### AuthProvider Props
112
+
113
+ | Prop | Type | Required | Description |
114
+ |------|------|----------|-------------|
115
+ | `ssoBaseUrl` | `string` | ✅ | Base URL of ArowAuth SSO server |
116
+ | `clientId` | `string` | ✅ | Client ID registered with ArowAuth |
117
+ | `apiBaseUrl` | `string` | ❌ | Base URL for your API (for axios client) |
118
+ | `redirectUri` | `string` | ❌ | Custom redirect URI (defaults to origin + /callback) |
119
+ | `scopes` | `string[]` | ❌ | OAuth scopes (defaults to ['openid', 'email', 'profile']) |
120
+ | `storagePrefix` | `string` | ❌ | Storage key prefix (defaults to 'arowauth') |
121
+ | `useSessionStorage` | `boolean` | ❌ | Use sessionStorage instead of localStorage |
122
+ | `onTokenRefresh` | `function` | ❌ | Callback when tokens are refreshed |
123
+ | `onAuthError` | `function` | ❌ | Callback when auth error occurs |
124
+ | `onLogout` | `function` | ❌ | Callback when user is logged out |
125
+
126
+ ### useAuth Hook
127
+
128
+ Returns an object with:
129
+
130
+ ```typescript
131
+ {
132
+ user: User | null; // Current user object
133
+ isAuthenticated: boolean; // Whether user is logged in
134
+ isLoading: boolean; // Initial auth check in progress
135
+ error: string | null; // Any auth error message
136
+ login: (redirectPath?: string) => void; // Redirect to SSO login
137
+ logout: () => Promise<void>; // Clear tokens and logout
138
+ refreshUser: () => Promise<void>; // Refresh user from token
139
+ }
140
+ ```
141
+
142
+ ### User Object
143
+
144
+ ```typescript
145
+ interface User {
146
+ id: string;
147
+ email: string;
148
+ firstName?: string;
149
+ lastName?: string;
150
+ displayName?: string;
151
+ avatarUrl?: string;
152
+ emailVerified: boolean;
153
+ roles?: string[];
154
+ permissions?: string[];
155
+ }
156
+ ```
157
+
158
+ ### useAuthClient Hook
159
+
160
+ Returns a configured Axios instance that:
161
+ - Automatically attaches Bearer token to all requests
162
+ - Handles 401 responses by refreshing tokens and retrying
163
+ - Queues concurrent requests during token refresh
164
+
165
+ ### Token Manager Functions
166
+
167
+ For advanced use cases, you can import token management functions directly:
168
+
169
+ ```typescript
170
+ import {
171
+ getAccessToken,
172
+ getRefreshToken,
173
+ setTokens,
174
+ clearTokens,
175
+ isTokenExpired,
176
+ hasValidToken,
177
+ getUserFromToken,
178
+ refreshTokens,
179
+ getValidAccessToken,
180
+ decodeJwt,
181
+ } from '@arow-software/auth-client';
182
+
183
+ // Check if token exists and is valid
184
+ if (hasValidToken()) {
185
+ const user = getUserFromToken();
186
+ console.log('Current user:', user);
187
+ }
188
+
189
+ // Manually refresh tokens
190
+ const newTokens = await refreshTokens();
191
+
192
+ // Decode JWT without verification
193
+ const payload = decodeJwt(token);
194
+ ```
195
+
196
+ ### createApiClient Function
197
+
198
+ Create a standalone axios instance with auth interceptors:
199
+
200
+ ```typescript
201
+ import axios from 'axios';
202
+ import { createAuthClient, initTokenManager } from '@arow-software/auth-client';
203
+
204
+ // Initialize token manager
205
+ initTokenManager({
206
+ ssoBaseUrl: 'https://sso.arowsoftware.co.uk',
207
+ clientId: 'myapp',
208
+ });
209
+
210
+ // Add auth to existing axios instance
211
+ const myAxios = axios.create({ baseURL: 'http://api.example.com' });
212
+ const authAxios = createAuthClient(myAxios, {
213
+ ssoBaseUrl: 'https://sso.arowsoftware.co.uk',
214
+ clientId: 'myapp',
215
+ });
216
+ ```
217
+
218
+ ## SSO Callback Setup
219
+
220
+ Create a callback route in your app to handle the SSO redirect:
221
+
222
+ ```tsx
223
+ // pages/callback.tsx (or wherever your callback route is)
224
+ import { useEffect } from 'react';
225
+ import { useAuth } from '@arow-software/auth-client';
226
+ import { useNavigate } from 'react-router-dom';
227
+
228
+ function CallbackPage() {
229
+ const { isAuthenticated, isLoading } = useAuth();
230
+ const navigate = useNavigate();
231
+
232
+ useEffect(() => {
233
+ // AuthProvider automatically handles token extraction from URL hash
234
+ // Just wait for auth to complete and redirect
235
+ if (!isLoading) {
236
+ if (isAuthenticated) {
237
+ navigate('/dashboard');
238
+ } else {
239
+ navigate('/login?error=auth_failed');
240
+ }
241
+ }
242
+ }, [isAuthenticated, isLoading, navigate]);
243
+
244
+ return <div>Completing login...</div>;
245
+ }
246
+ ```
247
+
248
+ ## Protected Routes
249
+
250
+ Example with React Router:
251
+
252
+ ```tsx
253
+ import { useAuth } from '@arow-software/auth-client';
254
+ import { Navigate, useLocation } from 'react-router-dom';
255
+
256
+ function ProtectedRoute({ children }: { children: React.ReactNode }) {
257
+ const { isAuthenticated, isLoading } = useAuth();
258
+ const location = useLocation();
259
+
260
+ if (isLoading) {
261
+ return <div>Loading...</div>;
262
+ }
263
+
264
+ if (!isAuthenticated) {
265
+ // Save the attempted location for redirect after login
266
+ return <Navigate to="/login" state={{ from: location }} replace />;
267
+ }
268
+
269
+ return <>{children}</>;
270
+ }
271
+
272
+ // Usage
273
+ <Route
274
+ path="/dashboard"
275
+ element={
276
+ <ProtectedRoute>
277
+ <Dashboard />
278
+ </ProtectedRoute>
279
+ }
280
+ />
281
+ ```
282
+
283
+ ## Role-Based Access
284
+
285
+ ```tsx
286
+ import { useAuth } from '@arow-software/auth-client';
287
+
288
+ function AdminPanel() {
289
+ const { user, isAuthenticated } = useAuth();
290
+
291
+ if (!isAuthenticated) return null;
292
+
293
+ const isAdmin = user?.roles?.includes('admin');
294
+
295
+ if (!isAdmin) {
296
+ return <div>Access denied. Admin role required.</div>;
297
+ }
298
+
299
+ return <div>Admin Panel Content</div>;
300
+ }
301
+ ```
302
+
303
+ ## Error Handling
304
+
305
+ ```tsx
306
+ <AuthProvider
307
+ ssoBaseUrl="https://sso.arowsoftware.co.uk"
308
+ clientId="arowtrades"
309
+ onAuthError={(error) => {
310
+ console.error('Auth error:', error);
311
+ // Show notification, redirect to login, etc.
312
+ }}
313
+ onLogout={() => {
314
+ // Redirect to home, clear app state, etc.
315
+ window.location.href = '/';
316
+ }}
317
+ >
318
+ <App />
319
+ </AuthProvider>
320
+ ```
321
+
322
+ ## Environment Variables
323
+
324
+ For Vite projects:
325
+
326
+ ```env
327
+ VITE_SSO_BASE_URL=https://sso.arowsoftware.co.uk
328
+ VITE_SSO_CLIENT_ID=arowtrades
329
+ VITE_API_BASE_URL=http://localhost:5001
330
+ ```
331
+
332
+ ```tsx
333
+ <AuthProvider
334
+ ssoBaseUrl={import.meta.env.VITE_SSO_BASE_URL}
335
+ clientId={import.meta.env.VITE_SSO_CLIENT_ID}
336
+ apiBaseUrl={import.meta.env.VITE_API_BASE_URL}
337
+ >
338
+ ```
339
+
340
+ ## License
341
+
342
+ MIT © ArowSoftware
@@ -0,0 +1,251 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { AxiosInstance } from 'axios';
3
+
4
+ /**
5
+ * User object returned from ArowAuth
6
+ */
7
+ interface User {
8
+ id: string;
9
+ email: string;
10
+ firstName?: string;
11
+ lastName?: string;
12
+ displayName?: string;
13
+ avatarUrl?: string;
14
+ emailVerified: boolean;
15
+ roles?: string[];
16
+ permissions?: string[];
17
+ createdAt?: string;
18
+ updatedAt?: string;
19
+ }
20
+ /**
21
+ * JWT token payload structure
22
+ */
23
+ interface JwtPayload {
24
+ sub: string;
25
+ email: string;
26
+ given_name?: string;
27
+ family_name?: string;
28
+ name?: string;
29
+ picture?: string;
30
+ email_verified?: boolean;
31
+ roles?: string[];
32
+ permissions?: string[];
33
+ iat: number;
34
+ exp: number;
35
+ iss: string;
36
+ aud: string | string[];
37
+ }
38
+ /**
39
+ * Token pair returned from auth endpoints
40
+ */
41
+ interface TokenPair {
42
+ accessToken: string;
43
+ refreshToken: string;
44
+ expiresIn?: number;
45
+ tokenType?: string;
46
+ }
47
+ /**
48
+ * Auth context state
49
+ */
50
+ interface AuthState {
51
+ user: User | null;
52
+ isAuthenticated: boolean;
53
+ isLoading: boolean;
54
+ error: string | null;
55
+ }
56
+ /**
57
+ * Auth context value including actions
58
+ */
59
+ interface AuthContextValue extends AuthState {
60
+ login: (redirectPath?: string) => void;
61
+ logout: () => Promise<void>;
62
+ refreshUser: () => Promise<void>;
63
+ }
64
+ /**
65
+ * Configuration for the auth client
66
+ */
67
+ interface AuthConfig {
68
+ /**
69
+ * Base URL of the ArowAuth SSO server
70
+ * @example "https://sso.arowsoftware.co.uk"
71
+ */
72
+ ssoBaseUrl: string;
73
+ /**
74
+ * Client ID registered with ArowAuth
75
+ * @example "arowtrades"
76
+ */
77
+ clientId: string;
78
+ /**
79
+ * Base URL of your API that will validate tokens
80
+ * @example "http://localhost:5001"
81
+ */
82
+ apiBaseUrl?: string;
83
+ /**
84
+ * Custom redirect URI after SSO login (defaults to current origin + /callback)
85
+ */
86
+ redirectUri?: string;
87
+ /**
88
+ * Scopes to request from SSO (defaults to ['openid', 'email', 'profile'])
89
+ */
90
+ scopes?: string[];
91
+ /**
92
+ * Storage key prefix (defaults to 'arowauth')
93
+ */
94
+ storagePrefix?: string;
95
+ /**
96
+ * Use sessionStorage instead of localStorage (defaults to false)
97
+ */
98
+ useSessionStorage?: boolean;
99
+ /**
100
+ * Callback when tokens are refreshed
101
+ */
102
+ onTokenRefresh?: (tokens: TokenPair) => void;
103
+ /**
104
+ * Callback when auth error occurs
105
+ */
106
+ onAuthError?: (error: Error) => void;
107
+ /**
108
+ * Callback when user is logged out
109
+ */
110
+ onLogout?: () => void;
111
+ }
112
+ /**
113
+ * Props for AuthProvider component
114
+ */
115
+ interface AuthProviderProps extends AuthConfig {
116
+ children: ReactNode;
117
+ }
118
+
119
+ /**
120
+ * Initialize token manager with configuration
121
+ */
122
+ declare function initTokenManager(authConfig: AuthConfig): void;
123
+ /**
124
+ * Decode a JWT token without verification
125
+ */
126
+ declare function decodeJwt(token: string): JwtPayload | null;
127
+ /**
128
+ * Get the access token from storage
129
+ */
130
+ declare function getAccessToken(): string | null;
131
+ /**
132
+ * Get the refresh token from storage
133
+ */
134
+ declare function getRefreshToken(): string | null;
135
+ /**
136
+ * Store tokens in storage
137
+ */
138
+ declare function setTokens(accessToken: string, refreshToken: string): void;
139
+ /**
140
+ * Clear all tokens from storage
141
+ */
142
+ declare function clearTokens(): void;
143
+ /**
144
+ * Check if the access token is expired or about to expire
145
+ */
146
+ declare function isTokenExpired(token?: string | null): boolean;
147
+ /**
148
+ * Check if we have a valid (non-expired) access token
149
+ */
150
+ declare function hasValidToken(): boolean;
151
+ /**
152
+ * Get user info from the current access token
153
+ */
154
+ declare function getUserFromToken(): JwtPayload | null;
155
+ /**
156
+ * Refresh tokens using the refresh token
157
+ * Handles concurrent refresh requests by returning the same promise
158
+ */
159
+ declare function refreshTokens(): Promise<TokenPair | null>;
160
+ /**
161
+ * Get a valid access token, refreshing if necessary
162
+ */
163
+ declare function getValidAccessToken(): Promise<string | null>;
164
+
165
+ /**
166
+ * Create and configure axios instance with auth interceptors
167
+ */
168
+ declare function createAuthClient(axiosInstance: AxiosInstance, config: AuthConfig): AxiosInstance;
169
+ /**
170
+ * Create a new axios instance with auth interceptors
171
+ */
172
+ declare function createApiClient(config: AuthConfig): AxiosInstance;
173
+
174
+ declare const AuthContext: React.Context<AuthContextValue | undefined>;
175
+ /**
176
+ * AuthProvider component - wraps app with auth context
177
+ */
178
+ declare function AuthProvider(props: AuthProviderProps): React.ReactElement;
179
+
180
+ /**
181
+ * Hook to access auth state and actions
182
+ *
183
+ * @returns Auth context value with user, isAuthenticated, login, logout, etc.
184
+ * @throws Error if used outside of AuthProvider
185
+ *
186
+ * @example
187
+ * ```tsx
188
+ * function MyComponent() {
189
+ * const { user, isAuthenticated, login, logout, isLoading } = useAuth();
190
+ *
191
+ * if (isLoading) return <div>Loading...</div>;
192
+ *
193
+ * if (!isAuthenticated) {
194
+ * return <button onClick={() => login()}>Login</button>;
195
+ * }
196
+ *
197
+ * return (
198
+ * <div>
199
+ * <p>Welcome, {user?.displayName || user?.email}!</p>
200
+ * <button onClick={logout}>Logout</button>
201
+ * </div>
202
+ * );
203
+ * }
204
+ * ```
205
+ */
206
+ declare function useAuth(): AuthContextValue;
207
+ /**
208
+ * Hook to check if user is authenticated (convenience wrapper)
209
+ *
210
+ * @returns Boolean indicating if user is authenticated
211
+ */
212
+ declare function useIsAuthenticated(): boolean;
213
+ /**
214
+ * Hook to get current user (convenience wrapper)
215
+ *
216
+ * @returns Current user or null
217
+ */
218
+ declare function useUser(): User | null;
219
+
220
+ /**
221
+ * Hook to get a configured axios instance with auth interceptors
222
+ *
223
+ * The returned axios instance will:
224
+ * - Automatically attach Bearer token to requests
225
+ * - Handle 401 responses by refreshing tokens and retrying
226
+ * - Queue concurrent requests during token refresh
227
+ *
228
+ * @returns Configured axios instance
229
+ * @throws Error if used outside of AuthProvider
230
+ *
231
+ * @example
232
+ * ```tsx
233
+ * function MyComponent() {
234
+ * const client = useAuthClient();
235
+ *
236
+ * const fetchData = async () => {
237
+ * try {
238
+ * const response = await client.get('/api/data');
239
+ * console.log(response.data);
240
+ * } catch (error) {
241
+ * console.error('Failed to fetch data', error);
242
+ * }
243
+ * };
244
+ *
245
+ * return <button onClick={fetchData}>Fetch Data</button>;
246
+ * }
247
+ * ```
248
+ */
249
+ declare function useAuthClient(): AxiosInstance;
250
+
251
+ export { type AuthConfig, AuthContext, type AuthContextValue, AuthProvider, type AuthProviderProps, type AuthState, type JwtPayload, type TokenPair, type User, clearTokens, createApiClient, createAuthClient, decodeJwt, getAccessToken, getRefreshToken, getUserFromToken, getValidAccessToken, hasValidToken, initTokenManager, isTokenExpired, refreshTokens, setTokens, useAuth, useAuthClient, useIsAuthenticated, useUser };