@witsauth/react-client 1.1.0 → 1.3.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 CHANGED
@@ -17,6 +17,9 @@ pnpm add @witsauth/react-client
17
17
  - Hooks (`useAuth`) for accessing user session state securely.
18
18
  - Built-in Axios interceptors for automatic Bearer token injection.
19
19
  - Seamless integration with Witsauth Console.
20
+ - Automatic Token Refresh with configurable thresholds.
21
+ - Configurable Storage (localStorage, sessionStorage, or custom).
22
+ - Flexible Routing Redirects for login and logout.
20
23
 
21
24
  ## Setup
22
25
 
@@ -49,6 +52,32 @@ function App() {
49
52
  }
50
53
  ```
51
54
 
55
+ ## Configuration Options
56
+
57
+ ```typescript
58
+ interface OAuth2Config {
59
+ clientId: string; // OAuth2 Client ID
60
+ authorizationEndpoint: string; // Authorization endpoint URL
61
+ tokenEndpoint: string; // Token endpoint URL
62
+ redirectUri: string; // Redirect URI for callback
63
+ revokeEndpoint?: string; // Token revocation endpoint (optional)
64
+ userInfoEndpoint?: string; // OIDC UserInfo endpoint (optional)
65
+ audience?: string; // OAuth2 audience parameter (optional)
66
+ scope?: string; // OAuth2 scope parameter
67
+ responseType?: string; // Response type (default: 'code')
68
+ codeChallengeMethod?: 'S256'; // PKCE method (default: 'S256')
69
+ storage?: 'localStorage' | 'sessionStorage' | 'custom'; // Storage strategy
70
+ customStorage?: OAuth2Storage; // Custom storage implementation
71
+ autoRefresh?: boolean; // Enable auto token refresh (default: true)
72
+ refreshThreshold?: number; // Seconds before expiry to refresh (default: 60)
73
+ logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug'; // Logging level
74
+ nonce?: string; // Custom nonce (optional)
75
+ redirectRoute?: string; // Route to redirect to after login (optional)
76
+ logoutRedirectRoute?: string; // Route to redirect to after logout (optional)
77
+ oAuthProvider?: 'witsauth' | string; // OAuth provider identifier
78
+ }
79
+ ```
80
+
52
81
  ## Usage
53
82
 
54
83
  Use the `useAuth` hook to access user state and login/logout functions.
@@ -76,6 +105,68 @@ function Profile() {
76
105
  }
77
106
  ```
78
107
 
108
+ ### Account Management
109
+
110
+ You can seamlessly redirect the user to the Witsauth Account Management console (to update their profile, change password, etc.) by invoking the `navigateToAccountManagement()` method. This securely POSTs to the IAM backend to preserve the user's active session and safely redirects them back to your application when they are done.
111
+
112
+ ```tsx
113
+ import { useAuth } from '@witsauth/react-client';
114
+
115
+ function Settings() {
116
+ const { navigateToAccountManagement } = useAuth();
117
+
118
+ return (
119
+ <button onClick={navigateToAccountManagement}>
120
+ Manage Account
121
+ </button>
122
+ );
123
+ }
124
+ ```
125
+
126
+ ## Advanced Usage
127
+
128
+ ### Custom Storage
129
+
130
+ You can configure the library to use `sessionStorage` or provide your own custom storage implementation (e.g., for React Native).
131
+
132
+ ```typescript
133
+ import { OAuth2Storage } from '@witsauth/react-client';
134
+
135
+ class CustomStorage implements OAuth2Storage {
136
+ getItem(key: string): string | null {
137
+ // Your custom storage logic
138
+ return null;
139
+ }
140
+
141
+ setItem(key: string, value: string): void {
142
+ // Your custom storage logic
143
+ }
144
+
145
+ removeItem(key: string): void {
146
+ // Your custom storage logic
147
+ }
148
+ }
149
+
150
+ // Use in configuration
151
+ const config = {
152
+ // ... other config
153
+ storage: 'custom',
154
+ customStorage: new CustomStorage()
155
+ };
156
+ ```
157
+
158
+ ### Custom Redirect Routes For Login And Logout
159
+
160
+ If you want the library to automatically redirect the user after a successful login or logout, you can configure the redirect routes. The library will use `window.location.assign` to perform a full page navigation.
161
+
162
+ ```typescript
163
+ const authConfig = {
164
+ // ... other config
165
+ redirectRoute: '/dashboard',
166
+ logoutRedirectRoute: '/login'
167
+ };
168
+ ```
169
+
79
170
  ## API Interceptors
80
171
 
81
172
  To automatically attach the Bearer token to your secure API requests, use the `setupAxiosInterceptors` helper.
@@ -4,6 +4,7 @@ export declare class AuthClient {
4
4
  private config;
5
5
  private storage;
6
6
  constructor(config: OAuth2Config, storage?: TokenStorage);
7
+ log(level: 'error' | 'warn' | 'info' | 'debug', ...args: any[]): void;
7
8
  startAuthorization(): Promise<void>;
8
9
  exchangeCodeForTokens(code: string, state: string): Promise<TokenInfo>;
9
10
  refreshAccessToken(): Promise<TokenInfo>;
@@ -11,5 +12,6 @@ export declare class AuthClient {
11
12
  getAccessToken(): string | null;
12
13
  getExpiresAt(): number | null;
13
14
  isAuthenticated(): boolean;
14
- logout(): void;
15
+ navigateToAccountManagement(): void;
16
+ logout(): Promise<void>;
15
17
  }
@@ -1,5 +1,9 @@
1
+ import { OAuth2Storage } from '../models/types';
1
2
  export declare class TokenStorage {
3
+ private storageType?;
4
+ private customStorage?;
2
5
  private memoryStorage;
6
+ constructor(storageType?: "localStorage" | "sessionStorage" | "custom" | undefined, customStorage?: OAuth2Storage | undefined);
3
7
  private getStorage;
4
8
  setItem(key: string, value: string): void;
5
9
  getItem(key: string): string | null;
package/dist/index.es.js CHANGED
@@ -28,9 +28,18 @@ async function d(e) {
28
28
  //#endregion
29
29
  //#region src/core/storage.ts
30
30
  var f = class {
31
+ storageType;
32
+ customStorage;
31
33
  memoryStorage = /* @__PURE__ */ new Map();
34
+ constructor(e, t) {
35
+ this.storageType = e, this.customStorage = t;
36
+ }
32
37
  getStorage() {
33
- if (typeof window < "u" && window.localStorage) return window.localStorage;
38
+ if (this.storageType === "custom" && this.customStorage) return this.customStorage;
39
+ if (typeof window < "u") {
40
+ if (this.storageType === "sessionStorage" && window.sessionStorage) return window.sessionStorage;
41
+ if ((!this.storageType || this.storageType === "localStorage") && window.localStorage) return window.localStorage;
42
+ }
34
43
  let e = this.memoryStorage;
35
44
  return {
36
45
  getItem: (t) => e.has(t) ? e.get(t) : null,
@@ -42,11 +51,7 @@ var f = class {
42
51
  },
43
52
  clear: () => {
44
53
  e.clear();
45
- },
46
- get length() {
47
- return e.size;
48
- },
49
- key: (t) => Array.from(e.keys())[t] || null
54
+ }
50
55
  };
51
56
  }
52
57
  setItem(e, t) {
@@ -70,16 +75,26 @@ var f = class {
70
75
  config;
71
76
  storage;
72
77
  constructor(e, t) {
73
- this.config = e, this.storage = t || new f();
78
+ this.config = e, this.storage = t || new f(e.storage, e.customStorage);
79
+ }
80
+ log(e, ...t) {
81
+ let n = {
82
+ none: 0,
83
+ error: 1,
84
+ warn: 2,
85
+ info: 3,
86
+ debug: 4
87
+ }, r = n[this.config.logLevel || "warn"] || 0, i = n[e] || 0;
88
+ i > 0 && r >= i && console[e](...t);
74
89
  }
75
90
  async startAuthorization() {
76
- let e = l(), t = await d(e), n = u(), r = u();
91
+ let e = l(), t = await d(e), n = u(), r = this.config.nonce || u();
77
92
  this.storage.setItem(c.CODE_VERIFIER, e), this.storage.setItem(c.STATE, n), this.storage.setItem(c.NONCE, r);
78
93
  let i = new URLSearchParams({
79
94
  client_id: this.config.clientId,
80
95
  redirect_uri: this.config.redirectUri,
81
96
  response_type: this.config.responseType || "code",
82
- code_challenge_method: "S256",
97
+ code_challenge_method: this.config.codeChallengeMethod || "S256",
83
98
  code_challenge: t,
84
99
  state: n,
85
100
  nonce: r
@@ -154,14 +169,35 @@ var f = class {
154
169
  let e = this.getExpiresAt();
155
170
  return e !== null && Date.now() < e - 3e4 && !!this.getAccessToken();
156
171
  }
157
- logout() {
158
- this.storage.clearTokens(), this.storage.clearTemporary();
172
+ navigateToAccountManagement() {
173
+ let e = this.storage.getItem(c.REFRESH_TOKEN);
174
+ if (!e) throw Error("No refresh token available to access account management.");
175
+ if (typeof window > "u" || !document) throw Error("navigateToAccountManagement requires a browser environment.");
176
+ let t = document.createElement("form");
177
+ t.method = "POST", t.action = `${this.config.authorizationEndpoint.replace(/\/authorize\/?$/, "")}/account/session`;
178
+ let n = (e, n) => {
179
+ let r = document.createElement("input");
180
+ r.type = "hidden", r.name = e, r.value = n, t.appendChild(r);
181
+ };
182
+ n("refreshToken", e), n("clientId", this.config.clientId), n("redirectUri", this.config.redirectUri), document.body.appendChild(t), t.submit();
183
+ }
184
+ async logout() {
185
+ let e = this.getAccessToken();
186
+ if (this.storage.clearTokens(), this.storage.clearTemporary(), this.config.revokeEndpoint && e) try {
187
+ await fetch(this.config.revokeEndpoint, {
188
+ method: "POST",
189
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
190
+ body: new URLSearchParams({ token: e }).toString()
191
+ });
192
+ } catch (e) {
193
+ this.log("error", "Failed to revoke token during logout", e);
194
+ }
159
195
  }
160
196
  }, m = e(null), h = ({ config: e, children: t }) => {
161
197
  let [i, o] = a({
162
198
  isAuthenticated: !1,
163
199
  isLoading: !0
164
- }), c = r(() => new p(e, new f()), [e]);
200
+ }), c = r(() => new p(e), [e]);
165
201
  n(() => {
166
202
  (async () => {
167
203
  try {
@@ -189,7 +225,7 @@ var f = class {
189
225
  }
190
226
  })();
191
227
  }, [c]), n(() => {
192
- if (!e.autoRefresh || !i.isAuthenticated) return;
228
+ if (e.autoRefresh === !1 || !i.isAuthenticated) return;
193
229
  let t = !1, n = setInterval(async () => {
194
230
  if (!t) try {
195
231
  let n = c.getExpiresAt();
@@ -210,7 +246,7 @@ var f = class {
210
246
  }
211
247
  }
212
248
  } catch (e) {
213
- console.error("Auto-refresh error:", e);
249
+ c.log("error", "Auto-refresh error:", e);
214
250
  }
215
251
  }, 1e4);
216
252
  return () => clearInterval(n);
@@ -225,23 +261,35 @@ var f = class {
225
261
  ...e,
226
262
  isLoading: !0
227
263
  })), await c.startAuthorization();
228
- }, u = () => {
229
- c.logout(), o({
230
- isAuthenticated: !1,
231
- isLoading: !1
232
- });
233
- }, d = async (e, t) => {
264
+ }, u = async () => {
234
265
  o((e) => ({
235
266
  ...e,
236
267
  isLoading: !0
237
268
  }));
238
269
  try {
239
- let n = await c.exchangeCodeForTokens(e, t);
270
+ await c.logout();
271
+ } catch (e) {
272
+ c.log("error", "Logout error:", e);
273
+ } finally {
274
+ o({
275
+ isAuthenticated: !1,
276
+ isLoading: !1
277
+ }), e.logoutRedirectRoute && typeof window < "u" && window.location.assign(e.logoutRedirectRoute);
278
+ }
279
+ }, d = async (t, n) => {
280
+ o((e) => ({
281
+ ...e,
282
+ isLoading: !0
283
+ }));
284
+ try {
285
+ let r = await c.exchangeCodeForTokens(t, n);
240
286
  return o({
241
287
  isAuthenticated: !0,
242
288
  isLoading: !1,
243
- tokenInfo: n
244
- }), n;
289
+ tokenInfo: r
290
+ }), e.redirectRoute && typeof window < "u" && setTimeout(() => {
291
+ window.location.assign(e.redirectRoute);
292
+ }, 0), r;
245
293
  } catch (e) {
246
294
  throw o({
247
295
  isAuthenticated: !1,
@@ -249,15 +297,16 @@ var f = class {
249
297
  error: e instanceof Error ? e.message : String(e)
250
298
  }), e;
251
299
  }
252
- }, h = {
300
+ }, f = {
253
301
  ...i,
254
302
  login: l,
255
303
  logout: u,
304
+ navigateToAccountManagement: () => c.navigateToAccountManagement(),
256
305
  handleCallback: d,
257
306
  client: c
258
307
  };
259
308
  return /* @__PURE__ */ s(m.Provider, {
260
- value: h,
309
+ value: f,
261
310
  children: t
262
311
  });
263
312
  }, g = () => {
@@ -265,9 +314,9 @@ var f = class {
265
314
  if (!e) throw Error("useAuth must be used within an AuthProvider");
266
315
  return e;
267
316
  }, _ = ({ onSuccess: e, onError: t, loadingComponent: r = /* @__PURE__ */ s("div", { children: "Processing authentication..." }) }) => {
268
- let { handleCallback: a } = g(), c = i(!1);
317
+ let { handleCallback: a, client: c } = g(), l = i(!1);
269
318
  return n(() => {
270
- c.current || (c.current = !0, (async () => {
319
+ l.current || (l.current = !0, (async () => {
271
320
  let n = new URLSearchParams(window.location.search), r = new URLSearchParams(window.location.hash.substring(1)), i = n.get("code") || r.get("code"), o = n.get("state") || r.get("state"), s = n.get("error") || r.get("error");
272
321
  if (s) {
273
322
  let e = /* @__PURE__ */ Error(`OAuth Error: ${s}`);
@@ -279,9 +328,14 @@ var f = class {
279
328
  } catch (e) {
280
329
  t && t(e instanceof Error ? e : Error(String(e)));
281
330
  }
331
+ else if (!i) {
332
+ let e = c.config;
333
+ window.location.assign(e.redirectRoute || "/");
334
+ }
282
335
  })());
283
336
  }, [
284
337
  a,
338
+ c,
285
339
  e,
286
340
  t
287
341
  ]), /* @__PURE__ */ s(o, { children: r });
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("react"),require("react/jsx-runtime")):typeof define==`function`&&define.amd?define([`exports`,`react`,`react/jsx-runtime`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.WitsAuthReactClient={},e.React,e.react_jsx_runtime))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),l(t||!e||!e.__esModule?i(n,`default`,{value:e,enumerable:!0}):n,e)))(t,1);var u={ACCESS_TOKEN:`witsauth_access_token`,REFRESH_TOKEN:`witsauth_refresh_token`,TOKEN_EXPIRES_AT:`witsauth_expires_at`,TOKEN_TYPE:`witsauth_token_type`,SCOPE:`witsauth_scope`,CODE_VERIFIER:`witsauth_code_verifier`,STATE:`witsauth_state`,NONCE:`witsauth_nonce`};function d(){let e=new Uint8Array(32);return crypto.getRandomValues(e),btoa(String.fromCharCode(...e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=/g,``)}function f(){let e=new Uint8Array(32);return crypto.getRandomValues(e),btoa(String.fromCharCode(...e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=/g,``)}async function p(e){let t=new TextEncoder().encode(e),n=await crypto.subtle.digest(`SHA-256`,t),r=new Uint8Array(n);return btoa(String.fromCharCode(...r)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=/g,``)}var m=class{memoryStorage=new Map;getStorage(){if(typeof window<`u`&&window.localStorage)return window.localStorage;let e=this.memoryStorage;return{getItem:t=>e.has(t)?e.get(t):null,setItem:(t,n)=>{e.set(t,n)},removeItem:t=>{e.delete(t)},clear:()=>{e.clear()},get length(){return e.size},key:t=>Array.from(e.keys())[t]||null}}setItem(e,t){this.getStorage().setItem(e,t)}getItem(e){return this.getStorage().getItem(e)}removeItem(e){this.getStorage().removeItem(e)}clearTokens(){let e=this.getStorage();e.removeItem(u.ACCESS_TOKEN),e.removeItem(u.REFRESH_TOKEN),e.removeItem(u.TOKEN_EXPIRES_AT),e.removeItem(u.TOKEN_TYPE),e.removeItem(u.SCOPE)}clearTemporary(){let e=this.getStorage();e.removeItem(u.CODE_VERIFIER),e.removeItem(u.STATE),e.removeItem(u.NONCE)}},h=class{config;storage;constructor(e,t){this.config=e,this.storage=t||new m}async startAuthorization(){let e=d(),t=await p(e),n=f(),r=f();this.storage.setItem(u.CODE_VERIFIER,e),this.storage.setItem(u.STATE,n),this.storage.setItem(u.NONCE,r);let i=new URLSearchParams({client_id:this.config.clientId,redirect_uri:this.config.redirectUri,response_type:this.config.responseType||`code`,code_challenge_method:`S256`,code_challenge:t,state:n,nonce:r});this.config.scope&&i.append(`scope`,this.config.scope),this.config.audience&&i.append(`audience`,this.config.audience);let a=`${this.config.authorizationEndpoint}?${i.toString()}`;if(typeof window>`u`||!window.location)throw Error(`startAuthorization requires a browser environment to redirect the user.`);window.location.href=a}async exchangeCodeForTokens(e,t){let n=this.storage.getItem(u.STATE),r=this.storage.getItem(u.CODE_VERIFIER);if(t!==n)throw Error(`Invalid state parameter. Possible CSRF attack.`);if(!r)throw Error(`Code verifier missing from storage.`);let i=new URLSearchParams({grant_type:`authorization_code`,client_id:this.config.clientId,redirect_uri:this.config.redirectUri,code:e,code_verifier:r}),a=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:i.toString()});if(!a.ok)throw Error(`Failed to exchange code for tokens`);let o=await a.json(),s={accessToken:o.access_token,refreshToken:o.refresh_token,tokenType:o.token_type||`Bearer`,expiresAt:Date.now()+o.expires_in*1e3,scope:o.scope,idToken:o.id_token};return this.saveTokenInfo(s),this.storage.clearTemporary(),s}async refreshAccessToken(){let e=this.storage.getItem(u.REFRESH_TOKEN);if(!e)throw Error(`No refresh token available`);let t=new URLSearchParams({grant_type:`refresh_token`,client_id:this.config.clientId,refresh_token:e}),n=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:t.toString()});if(!n.ok)throw this.storage.clearTokens(),Error(`Failed to refresh token`);let r=await n.json(),i={accessToken:r.access_token,refreshToken:r.refresh_token||e,tokenType:r.token_type||`Bearer`,expiresAt:Date.now()+r.expires_in*1e3,scope:r.scope,idToken:r.id_token};return this.saveTokenInfo(i),i}saveTokenInfo(e){this.storage.setItem(u.ACCESS_TOKEN,e.accessToken),this.storage.setItem(u.TOKEN_EXPIRES_AT,e.expiresAt.toString()),this.storage.setItem(u.TOKEN_TYPE,e.tokenType),e.refreshToken&&this.storage.setItem(u.REFRESH_TOKEN,e.refreshToken),e.scope&&this.storage.setItem(u.SCOPE,e.scope)}getAccessToken(){return this.storage.getItem(u.ACCESS_TOKEN)}getExpiresAt(){let e=this.storage.getItem(u.TOKEN_EXPIRES_AT);if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}isAuthenticated(){let e=this.getExpiresAt();return e!==null&&Date.now()<e-3e4&&!!this.getAccessToken()}logout(){this.storage.clearTokens(),this.storage.clearTemporary()}},g=(0,t.createContext)(null),_=({config:e,children:r})=>{let[i,a]=(0,t.useState)({isAuthenticated:!1,isLoading:!0}),o=(0,t.useMemo)(()=>new h(e,new m),[e]);(0,t.useEffect)(()=>{(async()=>{try{if(o.isAuthenticated())a({isAuthenticated:!0,isLoading:!1});else try{await o.refreshAccessToken(),a({isAuthenticated:!0,isLoading:!1})}catch{a({isAuthenticated:!1,isLoading:!1})}}catch(e){a({isAuthenticated:!1,isLoading:!1,error:e instanceof Error?e.message:String(e)})}})()},[o]),(0,t.useEffect)(()=>{if(!e.autoRefresh||!i.isAuthenticated)return;let t=!1,n=setInterval(async()=>{if(!t)try{let n=o.getExpiresAt();if(!n)return;let r=(e.refreshThreshold??60)*1e3;if(Date.now()>=n-r){t=!0;try{let e=await o.refreshAccessToken();a(t=>({...t,tokenInfo:e}))}catch{c()}finally{t=!1}}}catch(e){console.error(`Auto-refresh error:`,e)}},1e4);return()=>clearInterval(n)},[e.autoRefresh,e.refreshThreshold,i.isAuthenticated,o]);let s=async()=>{a(e=>({...e,isLoading:!0})),await o.startAuthorization()},c=()=>{o.logout(),a({isAuthenticated:!1,isLoading:!1})},l=async(e,t)=>{a(e=>({...e,isLoading:!0}));try{let n=await o.exchangeCodeForTokens(e,t);return a({isAuthenticated:!0,isLoading:!1,tokenInfo:n}),n}catch(e){throw a({isAuthenticated:!1,isLoading:!1,error:e instanceof Error?e.message:String(e)}),e}},u={...i,login:s,logout:c,handleCallback:l,client:o};return(0,n.jsx)(g.Provider,{value:u,children:r})},v=()=>{let e=(0,t.useContext)(g);if(!e)throw Error(`useAuth must be used within an AuthProvider`);return e},y=({onSuccess:e,onError:r,loadingComponent:i=(0,n.jsx)(`div`,{children:`Processing authentication...`})})=>{let{handleCallback:a}=v(),o=(0,t.useRef)(!1);return(0,t.useEffect)(()=>{o.current||(o.current=!0,(async()=>{let t=new URLSearchParams(window.location.search),n=new URLSearchParams(window.location.hash.substring(1)),i=t.get(`code`)||n.get(`code`),o=t.get(`state`)||n.get(`state`),s=t.get(`error`)||n.get(`error`);if(s){let e=Error(`OAuth Error: ${s}`);r&&r(e);return}if(i&&o)try{await a(i,o),e&&e()}catch(e){r&&r(e instanceof Error?e:Error(String(e)))}})())},[a,e,r]),(0,n.jsx)(n.Fragment,{children:i})};function b(e,t){e.interceptors.request.use(e=>{let n=t.getAccessToken();return n&&e.headers&&(e.headers.Authorization=`Bearer ${n}`),e},e=>Promise.reject(e)),e.interceptors.response.use(e=>e,async n=>{let r=n.config;if(n.response?.status===401&&r&&!r._retry){r._retry=!0;try{let n=await t.refreshAccessToken();return r.headers&&(r.headers.Authorization=`Bearer ${n.accessToken}`),e(r)}catch(e){return t.logout(),Promise.reject(e)}}return Promise.reject(n)})}e.AuthCallback=y,e.AuthClient=h,e.AuthProvider=_,e.STORAGE_KEYS=u,e.TokenStorage=m,e.createCodeChallenge=p,e.generateCodeVerifier=d,e.generateState=f,e.setupAxiosInterceptors=b,e.useAuth=v});
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("react"),require("react/jsx-runtime")):typeof define==`function`&&define.amd?define([`exports`,`react`,`react/jsx-runtime`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.WitsAuthReactClient={},e.React,e.jsxRuntime))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),l(t||!e||!e.__esModule?i(n,`default`,{value:e,enumerable:!0}):n,e)))(t,1);var u={ACCESS_TOKEN:`witsauth_access_token`,REFRESH_TOKEN:`witsauth_refresh_token`,TOKEN_EXPIRES_AT:`witsauth_expires_at`,TOKEN_TYPE:`witsauth_token_type`,SCOPE:`witsauth_scope`,CODE_VERIFIER:`witsauth_code_verifier`,STATE:`witsauth_state`,NONCE:`witsauth_nonce`};function d(){let e=new Uint8Array(32);return crypto.getRandomValues(e),btoa(String.fromCharCode(...e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=/g,``)}function f(){let e=new Uint8Array(32);return crypto.getRandomValues(e),btoa(String.fromCharCode(...e)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=/g,``)}async function p(e){let t=new TextEncoder().encode(e),n=await crypto.subtle.digest(`SHA-256`,t),r=new Uint8Array(n);return btoa(String.fromCharCode(...r)).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=/g,``)}var m=class{storageType;customStorage;memoryStorage=new Map;constructor(e,t){this.storageType=e,this.customStorage=t}getStorage(){if(this.storageType===`custom`&&this.customStorage)return this.customStorage;if(typeof window<`u`){if(this.storageType===`sessionStorage`&&window.sessionStorage)return window.sessionStorage;if((!this.storageType||this.storageType===`localStorage`)&&window.localStorage)return window.localStorage}let e=this.memoryStorage;return{getItem:t=>e.has(t)?e.get(t):null,setItem:(t,n)=>{e.set(t,n)},removeItem:t=>{e.delete(t)},clear:()=>{e.clear()}}}setItem(e,t){this.getStorage().setItem(e,t)}getItem(e){return this.getStorage().getItem(e)}removeItem(e){this.getStorage().removeItem(e)}clearTokens(){let e=this.getStorage();e.removeItem(u.ACCESS_TOKEN),e.removeItem(u.REFRESH_TOKEN),e.removeItem(u.TOKEN_EXPIRES_AT),e.removeItem(u.TOKEN_TYPE),e.removeItem(u.SCOPE)}clearTemporary(){let e=this.getStorage();e.removeItem(u.CODE_VERIFIER),e.removeItem(u.STATE),e.removeItem(u.NONCE)}},h=class{config;storage;constructor(e,t){this.config=e,this.storage=t||new m(e.storage,e.customStorage)}log(e,...t){let n={none:0,error:1,warn:2,info:3,debug:4},r=n[this.config.logLevel||`warn`]||0,i=n[e]||0;i>0&&r>=i&&console[e](...t)}async startAuthorization(){let e=d(),t=await p(e),n=f(),r=this.config.nonce||f();this.storage.setItem(u.CODE_VERIFIER,e),this.storage.setItem(u.STATE,n),this.storage.setItem(u.NONCE,r);let i=new URLSearchParams({client_id:this.config.clientId,redirect_uri:this.config.redirectUri,response_type:this.config.responseType||`code`,code_challenge_method:this.config.codeChallengeMethod||`S256`,code_challenge:t,state:n,nonce:r});this.config.scope&&i.append(`scope`,this.config.scope),this.config.audience&&i.append(`audience`,this.config.audience);let a=`${this.config.authorizationEndpoint}?${i.toString()}`;if(typeof window>`u`||!window.location)throw Error(`startAuthorization requires a browser environment to redirect the user.`);window.location.href=a}async exchangeCodeForTokens(e,t){let n=this.storage.getItem(u.STATE),r=this.storage.getItem(u.CODE_VERIFIER);if(t!==n)throw Error(`Invalid state parameter. Possible CSRF attack.`);if(!r)throw Error(`Code verifier missing from storage.`);let i=new URLSearchParams({grant_type:`authorization_code`,client_id:this.config.clientId,redirect_uri:this.config.redirectUri,code:e,code_verifier:r}),a=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:i.toString()});if(!a.ok)throw Error(`Failed to exchange code for tokens`);let o=await a.json(),s={accessToken:o.access_token,refreshToken:o.refresh_token,tokenType:o.token_type||`Bearer`,expiresAt:Date.now()+o.expires_in*1e3,scope:o.scope,idToken:o.id_token};return this.saveTokenInfo(s),this.storage.clearTemporary(),s}async refreshAccessToken(){let e=this.storage.getItem(u.REFRESH_TOKEN);if(!e)throw Error(`No refresh token available`);let t=new URLSearchParams({grant_type:`refresh_token`,client_id:this.config.clientId,refresh_token:e}),n=await fetch(this.config.tokenEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:t.toString()});if(!n.ok)throw this.storage.clearTokens(),Error(`Failed to refresh token`);let r=await n.json(),i={accessToken:r.access_token,refreshToken:r.refresh_token||e,tokenType:r.token_type||`Bearer`,expiresAt:Date.now()+r.expires_in*1e3,scope:r.scope,idToken:r.id_token};return this.saveTokenInfo(i),i}saveTokenInfo(e){this.storage.setItem(u.ACCESS_TOKEN,e.accessToken),this.storage.setItem(u.TOKEN_EXPIRES_AT,e.expiresAt.toString()),this.storage.setItem(u.TOKEN_TYPE,e.tokenType),e.refreshToken&&this.storage.setItem(u.REFRESH_TOKEN,e.refreshToken),e.scope&&this.storage.setItem(u.SCOPE,e.scope)}getAccessToken(){return this.storage.getItem(u.ACCESS_TOKEN)}getExpiresAt(){let e=this.storage.getItem(u.TOKEN_EXPIRES_AT);if(!e)return null;let t=Number(e);return Number.isFinite(t)?t:null}isAuthenticated(){let e=this.getExpiresAt();return e!==null&&Date.now()<e-3e4&&!!this.getAccessToken()}navigateToAccountManagement(){let e=this.storage.getItem(u.REFRESH_TOKEN);if(!e)throw Error(`No refresh token available to access account management.`);if(typeof window>`u`||!document)throw Error(`navigateToAccountManagement requires a browser environment.`);let t=document.createElement(`form`);t.method=`POST`,t.action=`${this.config.authorizationEndpoint.replace(/\/authorize\/?$/,``)}/account/session`;let n=(e,n)=>{let r=document.createElement(`input`);r.type=`hidden`,r.name=e,r.value=n,t.appendChild(r)};n(`refreshToken`,e),n(`clientId`,this.config.clientId),n(`redirectUri`,this.config.redirectUri),document.body.appendChild(t),t.submit()}async logout(){let e=this.getAccessToken();if(this.storage.clearTokens(),this.storage.clearTemporary(),this.config.revokeEndpoint&&e)try{await fetch(this.config.revokeEndpoint,{method:`POST`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:new URLSearchParams({token:e}).toString()})}catch(e){this.log(`error`,`Failed to revoke token during logout`,e)}}},g=(0,t.createContext)(null),_=({config:e,children:r})=>{let[i,a]=(0,t.useState)({isAuthenticated:!1,isLoading:!0}),o=(0,t.useMemo)(()=>new h(e),[e]);(0,t.useEffect)(()=>{(async()=>{try{if(o.isAuthenticated())a({isAuthenticated:!0,isLoading:!1});else try{await o.refreshAccessToken(),a({isAuthenticated:!0,isLoading:!1})}catch{a({isAuthenticated:!1,isLoading:!1})}}catch(e){a({isAuthenticated:!1,isLoading:!1,error:e instanceof Error?e.message:String(e)})}})()},[o]),(0,t.useEffect)(()=>{if(e.autoRefresh===!1||!i.isAuthenticated)return;let t=!1,n=setInterval(async()=>{if(!t)try{let n=o.getExpiresAt();if(!n)return;let r=(e.refreshThreshold??60)*1e3;if(Date.now()>=n-r){t=!0;try{let e=await o.refreshAccessToken();a(t=>({...t,tokenInfo:e}))}catch{c()}finally{t=!1}}}catch(e){o.log(`error`,`Auto-refresh error:`,e)}},1e4);return()=>clearInterval(n)},[e.autoRefresh,e.refreshThreshold,i.isAuthenticated,o]);let s=async()=>{a(e=>({...e,isLoading:!0})),await o.startAuthorization()},c=async()=>{a(e=>({...e,isLoading:!0}));try{await o.logout()}catch(e){o.log(`error`,`Logout error:`,e)}finally{a({isAuthenticated:!1,isLoading:!1}),e.logoutRedirectRoute&&typeof window<`u`&&window.location.assign(e.logoutRedirectRoute)}},l=async(t,n)=>{a(e=>({...e,isLoading:!0}));try{let r=await o.exchangeCodeForTokens(t,n);return a({isAuthenticated:!0,isLoading:!1,tokenInfo:r}),e.redirectRoute&&typeof window<`u`&&setTimeout(()=>{window.location.assign(e.redirectRoute)},0),r}catch(e){throw a({isAuthenticated:!1,isLoading:!1,error:e instanceof Error?e.message:String(e)}),e}},u={...i,login:s,logout:c,navigateToAccountManagement:()=>o.navigateToAccountManagement(),handleCallback:l,client:o};return(0,n.jsx)(g.Provider,{value:u,children:r})},v=()=>{let e=(0,t.useContext)(g);if(!e)throw Error(`useAuth must be used within an AuthProvider`);return e},y=({onSuccess:e,onError:r,loadingComponent:i=(0,n.jsx)(`div`,{children:`Processing authentication...`})})=>{let{handleCallback:a,client:o}=v(),s=(0,t.useRef)(!1);return(0,t.useEffect)(()=>{s.current||(s.current=!0,(async()=>{let t=new URLSearchParams(window.location.search),n=new URLSearchParams(window.location.hash.substring(1)),i=t.get(`code`)||n.get(`code`),s=t.get(`state`)||n.get(`state`),c=t.get(`error`)||n.get(`error`);if(c){let e=Error(`OAuth Error: ${c}`);r&&r(e);return}if(i&&s)try{await a(i,s),e&&e()}catch(e){r&&r(e instanceof Error?e:Error(String(e)))}else if(!i){let e=o.config;window.location.assign(e.redirectRoute||`/`)}})())},[a,o,e,r]),(0,n.jsx)(n.Fragment,{children:i})};function b(e,t){e.interceptors.request.use(e=>{let n=t.getAccessToken();return n&&e.headers&&(e.headers.Authorization=`Bearer ${n}`),e},e=>Promise.reject(e)),e.interceptors.response.use(e=>e,async n=>{let r=n.config;if(n.response?.status===401&&r&&!r._retry){r._retry=!0;try{let n=await t.refreshAccessToken();return r.headers&&(r.headers.Authorization=`Bearer ${n.accessToken}`),e(r)}catch(e){return t.logout(),Promise.reject(e)}}return Promise.reject(n)})}e.AuthCallback=y,e.AuthClient=h,e.AuthProvider=_,e.STORAGE_KEYS=u,e.TokenStorage=m,e.createCodeChallenge=p,e.generateCodeVerifier=d,e.generateState=f,e.setupAxiosInterceptors=b,e.useAuth=v});
@@ -1,14 +1,28 @@
1
+ export interface OAuth2Storage {
2
+ getItem(key: string): string | null;
3
+ setItem(key: string, value: string): void;
4
+ removeItem(key: string): void;
5
+ clear?(): void;
6
+ }
1
7
  export interface OAuth2Config {
2
8
  clientId: string;
3
9
  authorizationEndpoint: string;
4
10
  tokenEndpoint: string;
5
11
  redirectUri: string;
12
+ revokeEndpoint?: string;
6
13
  userInfoEndpoint?: string;
7
14
  scope?: string;
8
15
  audience?: string;
9
16
  responseType?: string;
17
+ codeChallengeMethod?: 'S256';
18
+ storage?: 'localStorage' | 'sessionStorage' | 'custom';
19
+ customStorage?: OAuth2Storage;
10
20
  autoRefresh?: boolean;
11
21
  refreshThreshold?: number;
22
+ logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
23
+ nonce?: string;
24
+ redirectRoute?: string;
25
+ logoutRedirectRoute?: string;
12
26
  oAuthProvider?: 'witsauth' | string;
13
27
  }
14
28
  export interface TokenInfo {
@@ -3,7 +3,8 @@ import { OAuth2Config, AuthState, TokenInfo } from '../models/types';
3
3
  import { AuthClient } from '../core/AuthClient';
4
4
  interface AuthContextValue extends AuthState {
5
5
  login: () => Promise<void>;
6
- logout: () => void;
6
+ logout: () => Promise<void>;
7
+ navigateToAccountManagement: () => void;
7
8
  client: AuthClient;
8
9
  handleCallback: (code: string, state: string) => Promise<TokenInfo>;
9
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witsauth/react-client",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Official React client library for integrating Witsauth Single Sign-On (SSO) authentication or similar services into your React applications.",
5
5
  "main": "./dist/index.umd.js",
6
6
  "module": "./dist/index.es.js",