@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 +73 -0
- package/dist/core/AuthClient.d.ts +2 -1
- package/dist/core/storage.d.ts +4 -0
- package/dist/index.es.js +61 -25
- package/dist/index.umd.js +1 -1
- package/dist/models/types.d.ts +14 -0
- package/dist/react/AuthContext.d.ts +1 -1
- package/package.json +1 -1
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
|
}
|
package/dist/core/storage.d.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
isLoading: !
|
|
232
|
-
});
|
|
233
|
-
|
|
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
|
|
273
|
+
let r = await c.exchangeCodeForTokens(t, n);
|
|
240
274
|
return o({
|
|
241
275
|
isAuthenticated: !0,
|
|
242
276
|
isLoading: !1,
|
|
243
|
-
tokenInfo:
|
|
244
|
-
}),
|
|
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
|
-
},
|
|
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:
|
|
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
|
|
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});
|
package/dist/models/types.d.ts
CHANGED
|
@@ -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.
|
|
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",
|