@witsauth/react-client 1.1.0 → 1.2.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,50 @@ function Profile() {
76
105
  }
77
106
  ```
78
107
 
108
+ ## Advanced Usage
109
+
110
+ ### Custom Storage
111
+
112
+ You can configure the library to use `sessionStorage` or provide your own custom storage implementation (e.g., for React Native).
113
+
114
+ ```typescript
115
+ import { OAuth2Storage } from '@witsauth/react-client';
116
+
117
+ class CustomStorage implements OAuth2Storage {
118
+ getItem(key: string): string | null {
119
+ // Your custom storage logic
120
+ return null;
121
+ }
122
+
123
+ setItem(key: string, value: string): void {
124
+ // Your custom storage logic
125
+ }
126
+
127
+ removeItem(key: string): void {
128
+ // Your custom storage logic
129
+ }
130
+ }
131
+
132
+ // Use in configuration
133
+ const config = {
134
+ // ... other config
135
+ storage: 'custom',
136
+ customStorage: new CustomStorage()
137
+ };
138
+ ```
139
+
140
+ ### Custom Redirect Routes For Login And Logout
141
+
142
+ 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.
143
+
144
+ ```typescript
145
+ const authConfig = {
146
+ // ... other config
147
+ redirectRoute: '/dashboard',
148
+ logoutRedirectRoute: '/login'
149
+ };
150
+ ```
151
+
79
152
  ## API Interceptors
80
153
 
81
154
  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,5 @@ export declare class AuthClient {
11
12
  getAccessToken(): string | null;
12
13
  getExpiresAt(): number | null;
13
14
  isAuthenticated(): boolean;
14
- logout(): void;
15
+ logout(): Promise<void>;
15
16
  }
@@ -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,23 @@ 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
+ async logout() {
173
+ let e = this.getAccessToken();
174
+ if (this.storage.clearTokens(), this.storage.clearTemporary(), this.config.revokeEndpoint && e) try {
175
+ await fetch(this.config.revokeEndpoint, {
176
+ method: "POST",
177
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
178
+ body: new URLSearchParams({ token: e }).toString()
179
+ });
180
+ } catch (e) {
181
+ this.log("error", "Failed to revoke token during logout", e);
182
+ }
159
183
  }
160
184
  }, m = e(null), h = ({ config: e, children: t }) => {
161
185
  let [i, o] = a({
162
186
  isAuthenticated: !1,
163
187
  isLoading: !0
164
- }), c = r(() => new p(e, new f()), [e]);
188
+ }), c = r(() => new p(e), [e]);
165
189
  n(() => {
166
190
  (async () => {
167
191
  try {
@@ -189,7 +213,7 @@ var f = class {
189
213
  }
190
214
  })();
191
215
  }, [c]), n(() => {
192
- if (!e.autoRefresh || !i.isAuthenticated) return;
216
+ if (e.autoRefresh === !1 || !i.isAuthenticated) return;
193
217
  let t = !1, n = setInterval(async () => {
194
218
  if (!t) try {
195
219
  let n = c.getExpiresAt();
@@ -210,7 +234,7 @@ var f = class {
210
234
  }
211
235
  }
212
236
  } catch (e) {
213
- console.error("Auto-refresh error:", e);
237
+ c.log("error", "Auto-refresh error:", e);
214
238
  }
215
239
  }, 1e4);
216
240
  return () => clearInterval(n);
@@ -225,23 +249,35 @@ var f = class {
225
249
  ...e,
226
250
  isLoading: !0
227
251
  })), await c.startAuthorization();
228
- }, u = () => {
229
- c.logout(), o({
230
- isAuthenticated: !1,
231
- isLoading: !1
232
- });
233
- }, d = async (e, t) => {
252
+ }, u = async () => {
253
+ o((e) => ({
254
+ ...e,
255
+ isLoading: !0
256
+ }));
257
+ try {
258
+ await c.logout();
259
+ } catch (e) {
260
+ c.log("error", "Logout error:", e);
261
+ } finally {
262
+ o({
263
+ isAuthenticated: !1,
264
+ isLoading: !1
265
+ }), e.logoutRedirectRoute && typeof window < "u" && window.location.assign(e.logoutRedirectRoute);
266
+ }
267
+ }, d = async (t, n) => {
234
268
  o((e) => ({
235
269
  ...e,
236
270
  isLoading: !0
237
271
  }));
238
272
  try {
239
- let n = await c.exchangeCodeForTokens(e, t);
273
+ let r = await c.exchangeCodeForTokens(t, n);
240
274
  return o({
241
275
  isAuthenticated: !0,
242
276
  isLoading: !1,
243
- tokenInfo: n
244
- }), n;
277
+ tokenInfo: r
278
+ }), e.redirectRoute && typeof window < "u" && setTimeout(() => {
279
+ window.location.assign(e.redirectRoute);
280
+ }, 0), r;
245
281
  } catch (e) {
246
282
  throw o({
247
283
  isAuthenticated: !1,
@@ -249,7 +285,7 @@ var f = class {
249
285
  error: e instanceof Error ? e.message : String(e)
250
286
  }), e;
251
287
  }
252
- }, h = {
288
+ }, f = {
253
289
  ...i,
254
290
  login: l,
255
291
  logout: u,
@@ -257,7 +293,7 @@ var f = class {
257
293
  client: c
258
294
  };
259
295
  return /* @__PURE__ */ s(m.Provider, {
260
- value: h,
296
+ value: f,
261
297
  children: t
262
298
  });
263
299
  }, g = () => {
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.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{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()}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,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,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,7 @@ 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
7
  client: AuthClient;
8
8
  handleCallback: (code: string, state: string) => Promise<TokenInfo>;
9
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witsauth/react-client",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",