@witsauth/react-client 1.0.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 +3 -1
- package/dist/core/storage.d.ts +4 -0
- package/dist/index.es.js +99 -26
- 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,11 +4,13 @@ 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>;
|
|
10
11
|
private saveTokenInfo;
|
|
11
12
|
getAccessToken(): string | null;
|
|
13
|
+
getExpiresAt(): number | null;
|
|
12
14
|
isAuthenticated(): boolean;
|
|
13
|
-
logout(): void
|
|
15
|
+
logout(): Promise<void>;
|
|
14
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
|
|
@@ -144,18 +159,33 @@ var f = class {
|
|
|
144
159
|
getAccessToken() {
|
|
145
160
|
return this.storage.getItem(c.ACCESS_TOKEN);
|
|
146
161
|
}
|
|
147
|
-
|
|
162
|
+
getExpiresAt() {
|
|
148
163
|
let e = this.storage.getItem(c.TOKEN_EXPIRES_AT);
|
|
149
|
-
|
|
164
|
+
if (!e) return null;
|
|
165
|
+
let t = Number(e);
|
|
166
|
+
return Number.isFinite(t) ? t : null;
|
|
167
|
+
}
|
|
168
|
+
isAuthenticated() {
|
|
169
|
+
let e = this.getExpiresAt();
|
|
170
|
+
return e !== null && Date.now() < e - 3e4 && !!this.getAccessToken();
|
|
150
171
|
}
|
|
151
|
-
logout() {
|
|
152
|
-
|
|
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
|
+
}
|
|
153
183
|
}
|
|
154
184
|
}, m = e(null), h = ({ config: e, children: t }) => {
|
|
155
185
|
let [i, o] = a({
|
|
156
186
|
isAuthenticated: !1,
|
|
157
187
|
isLoading: !0
|
|
158
|
-
}), c = r(() => new p(e
|
|
188
|
+
}), c = r(() => new p(e), [e]);
|
|
159
189
|
n(() => {
|
|
160
190
|
(async () => {
|
|
161
191
|
try {
|
|
@@ -182,29 +212,72 @@ var f = class {
|
|
|
182
212
|
});
|
|
183
213
|
}
|
|
184
214
|
})();
|
|
185
|
-
}, [c])
|
|
215
|
+
}, [c]), n(() => {
|
|
216
|
+
if (e.autoRefresh === !1 || !i.isAuthenticated) return;
|
|
217
|
+
let t = !1, n = setInterval(async () => {
|
|
218
|
+
if (!t) try {
|
|
219
|
+
let n = c.getExpiresAt();
|
|
220
|
+
if (!n) return;
|
|
221
|
+
let r = (e.refreshThreshold ?? 60) * 1e3;
|
|
222
|
+
if (Date.now() >= n - r) {
|
|
223
|
+
t = !0;
|
|
224
|
+
try {
|
|
225
|
+
let e = await c.refreshAccessToken();
|
|
226
|
+
o((t) => ({
|
|
227
|
+
...t,
|
|
228
|
+
tokenInfo: e
|
|
229
|
+
}));
|
|
230
|
+
} catch {
|
|
231
|
+
u();
|
|
232
|
+
} finally {
|
|
233
|
+
t = !1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch (e) {
|
|
237
|
+
c.log("error", "Auto-refresh error:", e);
|
|
238
|
+
}
|
|
239
|
+
}, 1e4);
|
|
240
|
+
return () => clearInterval(n);
|
|
241
|
+
}, [
|
|
242
|
+
e.autoRefresh,
|
|
243
|
+
e.refreshThreshold,
|
|
244
|
+
i.isAuthenticated,
|
|
245
|
+
c
|
|
246
|
+
]);
|
|
186
247
|
let l = async () => {
|
|
187
248
|
o((e) => ({
|
|
188
249
|
...e,
|
|
189
250
|
isLoading: !0
|
|
190
251
|
})), await c.startAuthorization();
|
|
191
|
-
}, u = () => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
isLoading: !
|
|
195
|
-
});
|
|
196
|
-
|
|
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) => {
|
|
197
268
|
o((e) => ({
|
|
198
269
|
...e,
|
|
199
270
|
isLoading: !0
|
|
200
271
|
}));
|
|
201
272
|
try {
|
|
202
|
-
let
|
|
273
|
+
let r = await c.exchangeCodeForTokens(t, n);
|
|
203
274
|
return o({
|
|
204
275
|
isAuthenticated: !0,
|
|
205
276
|
isLoading: !1,
|
|
206
|
-
tokenInfo:
|
|
207
|
-
}),
|
|
277
|
+
tokenInfo: r
|
|
278
|
+
}), e.redirectRoute && typeof window < "u" && setTimeout(() => {
|
|
279
|
+
window.location.assign(e.redirectRoute);
|
|
280
|
+
}, 0), r;
|
|
208
281
|
} catch (e) {
|
|
209
282
|
throw o({
|
|
210
283
|
isAuthenticated: !1,
|
|
@@ -212,7 +285,7 @@ var f = class {
|
|
|
212
285
|
error: e instanceof Error ? e.message : String(e)
|
|
213
286
|
}), e;
|
|
214
287
|
}
|
|
215
|
-
},
|
|
288
|
+
}, f = {
|
|
216
289
|
...i,
|
|
217
290
|
login: l,
|
|
218
291
|
logout: u,
|
|
@@ -220,7 +293,7 @@ var f = class {
|
|
|
220
293
|
client: c
|
|
221
294
|
};
|
|
222
295
|
return /* @__PURE__ */ s(m.Provider, {
|
|
223
|
-
value:
|
|
296
|
+
value: f,
|
|
224
297
|
children: t
|
|
225
298
|
});
|
|
226
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",
|