@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 +91 -0
- package/dist/core/AuthClient.d.ts +3 -1
- package/dist/core/storage.d.ts +4 -0
- package/dist/index.es.js +81 -27
- package/dist/index.umd.js +1 -1
- package/dist/models/types.d.ts +14 -0
- package/dist/react/AuthContext.d.ts +2 -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,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
|
-
|
|
15
|
+
navigateToAccountManagement(): void;
|
|
16
|
+
logout(): Promise<void>;
|
|
15
17
|
}
|
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,35 @@ var f = class {
|
|
|
154
169
|
let e = this.getExpiresAt();
|
|
155
170
|
return e !== null && Date.now() < e - 3e4 && !!this.getAccessToken();
|
|
156
171
|
}
|
|
157
|
-
|
|
158
|
-
this.storage.
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
244
|
-
}),
|
|
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
|
-
},
|
|
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:
|
|
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(),
|
|
317
|
+
let { handleCallback: a, client: c } = g(), l = i(!1);
|
|
269
318
|
return n(() => {
|
|
270
|
-
|
|
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.
|
|
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});
|
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,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.
|
|
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",
|