@witsauth/react-client 1.0.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 +94 -0
- package/dist/axios/interceptor.d.ts +7 -0
- package/dist/core/AuthClient.d.ts +14 -0
- package/dist/core/pkce.d.ts +13 -0
- package/dist/core/storage.d.ts +9 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.es.js +273 -0
- package/dist/index.umd.js +1 -0
- package/dist/models/types.d.ts +43 -0
- package/dist/react/AuthCallback.d.ts +8 -0
- package/dist/react/AuthContext.d.ts +15 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @witsauth/react-client
|
|
2
|
+
|
|
3
|
+
Official React client library for integrating Witsauth Single Sign-On (SSO) authentication or similar services into your React applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @witsauth/react-client
|
|
9
|
+
# or
|
|
10
|
+
yarn add @witsauth/react-client
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @witsauth/react-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
- Complete OAuth2 Authorization Code Flow with PKCE.
|
|
17
|
+
- Hooks (`useAuth`) for accessing user session state securely.
|
|
18
|
+
- Built-in Axios interceptors for automatic Bearer token injection.
|
|
19
|
+
- Seamless integration with Witsauth Console.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
Wrap your application with the `AuthProvider` and pass your configuration.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { AuthProvider, AuthCallback } from '@witsauth/react-client';
|
|
27
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
28
|
+
|
|
29
|
+
const authConfig = {
|
|
30
|
+
clientId: 'your-client-id',
|
|
31
|
+
authorizationEndpoint: 'https://api.yourdomain.com/auth/authorize',
|
|
32
|
+
tokenEndpoint: 'https://api.yourdomain.com/auth/oauth/token',
|
|
33
|
+
redirectUri: window.location.origin + '/auth/callback',
|
|
34
|
+
scope: 'openid profile email'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function App() {
|
|
38
|
+
return (
|
|
39
|
+
<BrowserRouter>
|
|
40
|
+
<AuthProvider config={authConfig}>
|
|
41
|
+
<Routes>
|
|
42
|
+
<Route path="/" element={<Home />} />
|
|
43
|
+
{/* Ensure you map a route for the OAuth callback */}
|
|
44
|
+
<Route path="/auth/callback" element={<AuthCallback />} />
|
|
45
|
+
</Routes>
|
|
46
|
+
</AuthProvider>
|
|
47
|
+
</BrowserRouter>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Use the `useAuth` hook to access user state and login/logout functions.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { useAuth } from '@witsauth/react-client';
|
|
58
|
+
|
|
59
|
+
function Profile() {
|
|
60
|
+
const { isAuthenticated, login, logout, isLoading } = useAuth();
|
|
61
|
+
|
|
62
|
+
if (isLoading) return <div>Loading...</div>;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
{isAuthenticated ? (
|
|
67
|
+
<>
|
|
68
|
+
<p>Welcome!</p>
|
|
69
|
+
<button onClick={logout}>Logout</button>
|
|
70
|
+
</>
|
|
71
|
+
) : (
|
|
72
|
+
<button onClick={login}>Login</button>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API Interceptors
|
|
80
|
+
|
|
81
|
+
To automatically attach the Bearer token to your secure API requests, use the `setupAxiosInterceptors` helper.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import axios from 'axios';
|
|
85
|
+
import { setupAxiosInterceptors } from '@witsauth/react-client';
|
|
86
|
+
|
|
87
|
+
export const apiClient = axios.create({
|
|
88
|
+
baseURL: 'https://api.yourdomain.com'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// The interceptor will automatically inject Authorization: Bearer <token>
|
|
92
|
+
// Note: You must pass the AuthClient instance, for example from useAuth().client
|
|
93
|
+
// setupAxiosInterceptors(apiClient, authClient);
|
|
94
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AxiosInstance } from 'axios';
|
|
2
|
+
import { AuthClient } from '../core/AuthClient';
|
|
3
|
+
/**
|
|
4
|
+
* Attaches a Request Interceptor to the provided Axios instance to inject the Bearer token,
|
|
5
|
+
* and a Response Interceptor to automatically handle 401s by refreshing the token.
|
|
6
|
+
*/
|
|
7
|
+
export declare function setupAxiosInterceptors(axiosInstance: AxiosInstance, authClient: AuthClient): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OAuth2Config, TokenInfo } from '../models/types';
|
|
2
|
+
import { TokenStorage } from './storage';
|
|
3
|
+
export declare class AuthClient {
|
|
4
|
+
private config;
|
|
5
|
+
private storage;
|
|
6
|
+
constructor(config: OAuth2Config, storage?: TokenStorage);
|
|
7
|
+
startAuthorization(): Promise<void>;
|
|
8
|
+
exchangeCodeForTokens(code: string, state: string): Promise<TokenInfo>;
|
|
9
|
+
refreshAccessToken(): Promise<TokenInfo>;
|
|
10
|
+
private saveTokenInfo;
|
|
11
|
+
getAccessToken(): string | null;
|
|
12
|
+
isAuthenticated(): boolean;
|
|
13
|
+
logout(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a cryptographically secure code verifier for PKCE
|
|
3
|
+
* RFC 7636 specifies it must be 43-128 characters long
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateCodeVerifier(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Generates a state parameter for CSRF protection
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateState(): string;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a SHA-256 code challenge from the code verifier
|
|
12
|
+
*/
|
|
13
|
+
export declare function createCodeChallenge(verifier: string): Promise<string>;
|
package/dist/index.d.ts
ADDED
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { createContext as e, useContext as t, useEffect as n, useMemo as r, useRef as i, useState as a } from "react";
|
|
2
|
+
import { Fragment as o, jsx as s } from "react/jsx-runtime";
|
|
3
|
+
//#region src/models/types.ts
|
|
4
|
+
var c = {
|
|
5
|
+
ACCESS_TOKEN: "witsauth_access_token",
|
|
6
|
+
REFRESH_TOKEN: "witsauth_refresh_token",
|
|
7
|
+
TOKEN_EXPIRES_AT: "witsauth_expires_at",
|
|
8
|
+
TOKEN_TYPE: "witsauth_token_type",
|
|
9
|
+
SCOPE: "witsauth_scope",
|
|
10
|
+
CODE_VERIFIER: "witsauth_code_verifier",
|
|
11
|
+
STATE: "witsauth_state",
|
|
12
|
+
NONCE: "witsauth_nonce"
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/core/pkce.ts
|
|
16
|
+
function l() {
|
|
17
|
+
let e = /* @__PURE__ */ new Uint8Array(32);
|
|
18
|
+
return crypto.getRandomValues(e), btoa(String.fromCharCode(...e)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
19
|
+
}
|
|
20
|
+
function u() {
|
|
21
|
+
let e = /* @__PURE__ */ new Uint8Array(32);
|
|
22
|
+
return crypto.getRandomValues(e), btoa(String.fromCharCode(...e)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
23
|
+
}
|
|
24
|
+
async function d(e) {
|
|
25
|
+
let t = new TextEncoder().encode(e), n = await crypto.subtle.digest("SHA-256", t), r = new Uint8Array(n);
|
|
26
|
+
return btoa(String.fromCharCode(...r)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/core/storage.ts
|
|
30
|
+
var f = class {
|
|
31
|
+
memoryStorage = /* @__PURE__ */ new Map();
|
|
32
|
+
getStorage() {
|
|
33
|
+
if (typeof window < "u" && window.localStorage) return window.localStorage;
|
|
34
|
+
let e = this.memoryStorage;
|
|
35
|
+
return {
|
|
36
|
+
getItem: (t) => e.has(t) ? e.get(t) : null,
|
|
37
|
+
setItem: (t, n) => {
|
|
38
|
+
e.set(t, n);
|
|
39
|
+
},
|
|
40
|
+
removeItem: (t) => {
|
|
41
|
+
e.delete(t);
|
|
42
|
+
},
|
|
43
|
+
clear: () => {
|
|
44
|
+
e.clear();
|
|
45
|
+
},
|
|
46
|
+
get length() {
|
|
47
|
+
return e.size;
|
|
48
|
+
},
|
|
49
|
+
key: (t) => Array.from(e.keys())[t] || null
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
setItem(e, t) {
|
|
53
|
+
this.getStorage().setItem(e, t);
|
|
54
|
+
}
|
|
55
|
+
getItem(e) {
|
|
56
|
+
return this.getStorage().getItem(e);
|
|
57
|
+
}
|
|
58
|
+
removeItem(e) {
|
|
59
|
+
this.getStorage().removeItem(e);
|
|
60
|
+
}
|
|
61
|
+
clearTokens() {
|
|
62
|
+
let e = this.getStorage();
|
|
63
|
+
e.removeItem(c.ACCESS_TOKEN), e.removeItem(c.REFRESH_TOKEN), e.removeItem(c.TOKEN_EXPIRES_AT), e.removeItem(c.TOKEN_TYPE), e.removeItem(c.SCOPE);
|
|
64
|
+
}
|
|
65
|
+
clearTemporary() {
|
|
66
|
+
let e = this.getStorage();
|
|
67
|
+
e.removeItem(c.CODE_VERIFIER), e.removeItem(c.STATE), e.removeItem(c.NONCE);
|
|
68
|
+
}
|
|
69
|
+
}, p = class {
|
|
70
|
+
config;
|
|
71
|
+
storage;
|
|
72
|
+
constructor(e, t) {
|
|
73
|
+
this.config = e, this.storage = t || new f();
|
|
74
|
+
}
|
|
75
|
+
async startAuthorization() {
|
|
76
|
+
let e = l(), t = await d(e), n = u(), r = u();
|
|
77
|
+
this.storage.setItem(c.CODE_VERIFIER, e), this.storage.setItem(c.STATE, n), this.storage.setItem(c.NONCE, r);
|
|
78
|
+
let i = new URLSearchParams({
|
|
79
|
+
client_id: this.config.clientId,
|
|
80
|
+
redirect_uri: this.config.redirectUri,
|
|
81
|
+
response_type: this.config.responseType || "code",
|
|
82
|
+
code_challenge_method: "S256",
|
|
83
|
+
code_challenge: t,
|
|
84
|
+
state: n,
|
|
85
|
+
nonce: r
|
|
86
|
+
});
|
|
87
|
+
this.config.scope && i.append("scope", this.config.scope), this.config.audience && i.append("audience", this.config.audience);
|
|
88
|
+
let a = `${this.config.authorizationEndpoint}?${i.toString()}`;
|
|
89
|
+
if (typeof window > "u" || !window.location) throw Error("startAuthorization requires a browser environment to redirect the user.");
|
|
90
|
+
window.location.href = a;
|
|
91
|
+
}
|
|
92
|
+
async exchangeCodeForTokens(e, t) {
|
|
93
|
+
let n = this.storage.getItem(c.STATE), r = this.storage.getItem(c.CODE_VERIFIER);
|
|
94
|
+
if (t !== n) throw Error("Invalid state parameter. Possible CSRF attack.");
|
|
95
|
+
if (!r) throw Error("Code verifier missing from storage.");
|
|
96
|
+
let i = new URLSearchParams({
|
|
97
|
+
grant_type: "authorization_code",
|
|
98
|
+
client_id: this.config.clientId,
|
|
99
|
+
redirect_uri: this.config.redirectUri,
|
|
100
|
+
code: e,
|
|
101
|
+
code_verifier: r
|
|
102
|
+
}), a = await fetch(this.config.tokenEndpoint, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
105
|
+
body: i.toString()
|
|
106
|
+
});
|
|
107
|
+
if (!a.ok) throw Error("Failed to exchange code for tokens");
|
|
108
|
+
let o = await a.json(), s = {
|
|
109
|
+
accessToken: o.access_token,
|
|
110
|
+
refreshToken: o.refresh_token,
|
|
111
|
+
tokenType: o.token_type || "Bearer",
|
|
112
|
+
expiresAt: Date.now() + o.expires_in * 1e3,
|
|
113
|
+
scope: o.scope,
|
|
114
|
+
idToken: o.id_token
|
|
115
|
+
};
|
|
116
|
+
return this.saveTokenInfo(s), this.storage.clearTemporary(), s;
|
|
117
|
+
}
|
|
118
|
+
async refreshAccessToken() {
|
|
119
|
+
let e = this.storage.getItem(c.REFRESH_TOKEN);
|
|
120
|
+
if (!e) throw Error("No refresh token available");
|
|
121
|
+
let t = new URLSearchParams({
|
|
122
|
+
grant_type: "refresh_token",
|
|
123
|
+
client_id: this.config.clientId,
|
|
124
|
+
refresh_token: e
|
|
125
|
+
}), n = await fetch(this.config.tokenEndpoint, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
128
|
+
body: t.toString()
|
|
129
|
+
});
|
|
130
|
+
if (!n.ok) throw this.storage.clearTokens(), Error("Failed to refresh token");
|
|
131
|
+
let r = await n.json(), i = {
|
|
132
|
+
accessToken: r.access_token,
|
|
133
|
+
refreshToken: r.refresh_token || e,
|
|
134
|
+
tokenType: r.token_type || "Bearer",
|
|
135
|
+
expiresAt: Date.now() + r.expires_in * 1e3,
|
|
136
|
+
scope: r.scope,
|
|
137
|
+
idToken: r.id_token
|
|
138
|
+
};
|
|
139
|
+
return this.saveTokenInfo(i), i;
|
|
140
|
+
}
|
|
141
|
+
saveTokenInfo(e) {
|
|
142
|
+
this.storage.setItem(c.ACCESS_TOKEN, e.accessToken), this.storage.setItem(c.TOKEN_EXPIRES_AT, e.expiresAt.toString()), this.storage.setItem(c.TOKEN_TYPE, e.tokenType), e.refreshToken && this.storage.setItem(c.REFRESH_TOKEN, e.refreshToken), e.scope && this.storage.setItem(c.SCOPE, e.scope);
|
|
143
|
+
}
|
|
144
|
+
getAccessToken() {
|
|
145
|
+
return this.storage.getItem(c.ACCESS_TOKEN);
|
|
146
|
+
}
|
|
147
|
+
isAuthenticated() {
|
|
148
|
+
let e = this.storage.getItem(c.TOKEN_EXPIRES_AT);
|
|
149
|
+
return e ? Date.now() < parseInt(e, 10) - 3e4 && !!this.getAccessToken() : !1;
|
|
150
|
+
}
|
|
151
|
+
logout() {
|
|
152
|
+
this.storage.clearTokens(), this.storage.clearTemporary();
|
|
153
|
+
}
|
|
154
|
+
}, m = e(null), h = ({ config: e, children: t }) => {
|
|
155
|
+
let [i, o] = a({
|
|
156
|
+
isAuthenticated: !1,
|
|
157
|
+
isLoading: !0
|
|
158
|
+
}), c = r(() => new p(e, new f()), [e]);
|
|
159
|
+
n(() => {
|
|
160
|
+
(async () => {
|
|
161
|
+
try {
|
|
162
|
+
if (c.isAuthenticated()) o({
|
|
163
|
+
isAuthenticated: !0,
|
|
164
|
+
isLoading: !1
|
|
165
|
+
});
|
|
166
|
+
else try {
|
|
167
|
+
await c.refreshAccessToken(), o({
|
|
168
|
+
isAuthenticated: !0,
|
|
169
|
+
isLoading: !1
|
|
170
|
+
});
|
|
171
|
+
} catch {
|
|
172
|
+
o({
|
|
173
|
+
isAuthenticated: !1,
|
|
174
|
+
isLoading: !1
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
} catch (e) {
|
|
178
|
+
o({
|
|
179
|
+
isAuthenticated: !1,
|
|
180
|
+
isLoading: !1,
|
|
181
|
+
error: e instanceof Error ? e.message : String(e)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
}, [c]);
|
|
186
|
+
let l = async () => {
|
|
187
|
+
o((e) => ({
|
|
188
|
+
...e,
|
|
189
|
+
isLoading: !0
|
|
190
|
+
})), await c.startAuthorization();
|
|
191
|
+
}, u = () => {
|
|
192
|
+
c.logout(), o({
|
|
193
|
+
isAuthenticated: !1,
|
|
194
|
+
isLoading: !1
|
|
195
|
+
});
|
|
196
|
+
}, d = async (e, t) => {
|
|
197
|
+
o((e) => ({
|
|
198
|
+
...e,
|
|
199
|
+
isLoading: !0
|
|
200
|
+
}));
|
|
201
|
+
try {
|
|
202
|
+
let n = await c.exchangeCodeForTokens(e, t);
|
|
203
|
+
return o({
|
|
204
|
+
isAuthenticated: !0,
|
|
205
|
+
isLoading: !1,
|
|
206
|
+
tokenInfo: n
|
|
207
|
+
}), n;
|
|
208
|
+
} catch (e) {
|
|
209
|
+
throw o({
|
|
210
|
+
isAuthenticated: !1,
|
|
211
|
+
isLoading: !1,
|
|
212
|
+
error: e instanceof Error ? e.message : String(e)
|
|
213
|
+
}), e;
|
|
214
|
+
}
|
|
215
|
+
}, h = {
|
|
216
|
+
...i,
|
|
217
|
+
login: l,
|
|
218
|
+
logout: u,
|
|
219
|
+
handleCallback: d,
|
|
220
|
+
client: c
|
|
221
|
+
};
|
|
222
|
+
return /* @__PURE__ */ s(m.Provider, {
|
|
223
|
+
value: h,
|
|
224
|
+
children: t
|
|
225
|
+
});
|
|
226
|
+
}, g = () => {
|
|
227
|
+
let e = t(m);
|
|
228
|
+
if (!e) throw Error("useAuth must be used within an AuthProvider");
|
|
229
|
+
return e;
|
|
230
|
+
}, _ = ({ onSuccess: e, onError: t, loadingComponent: r = /* @__PURE__ */ s("div", { children: "Processing authentication..." }) }) => {
|
|
231
|
+
let { handleCallback: a } = g(), c = i(!1);
|
|
232
|
+
return n(() => {
|
|
233
|
+
c.current || (c.current = !0, (async () => {
|
|
234
|
+
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");
|
|
235
|
+
if (s) {
|
|
236
|
+
let e = /* @__PURE__ */ Error(`OAuth Error: ${s}`);
|
|
237
|
+
t && t(e);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (i && o) try {
|
|
241
|
+
await a(i, o), e && e();
|
|
242
|
+
} catch (e) {
|
|
243
|
+
t && t(e instanceof Error ? e : Error(String(e)));
|
|
244
|
+
}
|
|
245
|
+
})());
|
|
246
|
+
}, [
|
|
247
|
+
a,
|
|
248
|
+
e,
|
|
249
|
+
t
|
|
250
|
+
]), /* @__PURE__ */ s(o, { children: r });
|
|
251
|
+
};
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/axios/interceptor.ts
|
|
254
|
+
function v(e, t) {
|
|
255
|
+
e.interceptors.request.use((e) => {
|
|
256
|
+
let n = t.getAccessToken();
|
|
257
|
+
return n && e.headers && (e.headers.Authorization = `Bearer ${n}`), e;
|
|
258
|
+
}, (e) => Promise.reject(e)), e.interceptors.response.use((e) => e, async (n) => {
|
|
259
|
+
let r = n.config;
|
|
260
|
+
if (n.response?.status === 401 && r && !r._retry) {
|
|
261
|
+
r._retry = !0;
|
|
262
|
+
try {
|
|
263
|
+
let n = await t.refreshAccessToken();
|
|
264
|
+
return r.headers && (r.headers.Authorization = `Bearer ${n.accessToken}`), e(r);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
return t.logout(), Promise.reject(e);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return Promise.reject(n);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
export { _ as AuthCallback, p as AuthClient, h as AuthProvider, c as STORAGE_KEYS, f as TokenStorage, d as createCodeChallenge, l as generateCodeVerifier, u as generateState, v as setupAxiosInterceptors, g as useAuth };
|
|
@@ -0,0 +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)}isAuthenticated(){let e=this.storage.getItem(u.TOKEN_EXPIRES_AT);return e?Date.now()<parseInt(e,10)-3e4&&!!this.getAccessToken():!1}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]);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});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface OAuth2Config {
|
|
2
|
+
clientId: string;
|
|
3
|
+
authorizationEndpoint: string;
|
|
4
|
+
tokenEndpoint: string;
|
|
5
|
+
redirectUri: string;
|
|
6
|
+
userInfoEndpoint?: string;
|
|
7
|
+
scope?: string;
|
|
8
|
+
audience?: string;
|
|
9
|
+
responseType?: string;
|
|
10
|
+
autoRefresh?: boolean;
|
|
11
|
+
refreshThreshold?: number;
|
|
12
|
+
oAuthProvider?: 'witsauth' | string;
|
|
13
|
+
}
|
|
14
|
+
export interface TokenInfo {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken?: string;
|
|
17
|
+
tokenType: string;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
scope?: string;
|
|
20
|
+
idToken?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AuthState {
|
|
23
|
+
isAuthenticated: boolean;
|
|
24
|
+
isLoading: boolean;
|
|
25
|
+
tokenInfo?: TokenInfo;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface UserInfo {
|
|
29
|
+
sub?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
[key: string]: any;
|
|
33
|
+
}
|
|
34
|
+
export declare const STORAGE_KEYS: {
|
|
35
|
+
ACCESS_TOKEN: string;
|
|
36
|
+
REFRESH_TOKEN: string;
|
|
37
|
+
TOKEN_EXPIRES_AT: string;
|
|
38
|
+
TOKEN_TYPE: string;
|
|
39
|
+
SCOPE: string;
|
|
40
|
+
CODE_VERIFIER: string;
|
|
41
|
+
STATE: string;
|
|
42
|
+
NONCE: string;
|
|
43
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { OAuth2Config, AuthState, TokenInfo } from '../models/types';
|
|
3
|
+
import { AuthClient } from '../core/AuthClient';
|
|
4
|
+
interface AuthContextValue extends AuthState {
|
|
5
|
+
login: () => Promise<void>;
|
|
6
|
+
logout: () => void;
|
|
7
|
+
client: AuthClient;
|
|
8
|
+
handleCallback: (code: string, state: string) => Promise<TokenInfo>;
|
|
9
|
+
}
|
|
10
|
+
export declare const AuthProvider: React.FC<{
|
|
11
|
+
config: OAuth2Config;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}>;
|
|
14
|
+
export declare const useAuth: () => AuthContextValue;
|
|
15
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@witsauth/react-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official React client library for integrating Witsauth Single Sign-On (SSO) authentication or similar services into your React applications.",
|
|
5
|
+
"main": "./dist/index.umd.js",
|
|
6
|
+
"module": "./dist/index.es.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.es.js",
|
|
11
|
+
"require": "./dist/index.umd.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react",
|
|
20
|
+
"auth",
|
|
21
|
+
"oauth2",
|
|
22
|
+
"pkce"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"devEngines": {
|
|
27
|
+
"packageManager": {
|
|
28
|
+
"name": "pnpm",
|
|
29
|
+
"version": "^11.15.1",
|
|
30
|
+
"onFail": "download"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"type": "module",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"axios": "^1.18.1",
|
|
36
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
37
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@testing-library/jest-dom": "^7.0.0",
|
|
41
|
+
"@testing-library/react": "^16.3.2",
|
|
42
|
+
"@types/node": "^26.1.1",
|
|
43
|
+
"@types/react": "^19.2.17",
|
|
44
|
+
"@types/react-dom": "^19.2.3",
|
|
45
|
+
"@typescript/typescript6": "^6.0.2",
|
|
46
|
+
"jsdom": "^29.1.1",
|
|
47
|
+
"typescript": "^7.0.2",
|
|
48
|
+
"vite": "^8.1.5",
|
|
49
|
+
"vite-plugin-dts": "^5.0.3",
|
|
50
|
+
"vitest": "^4.1.10"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"dev": "vite",
|
|
54
|
+
"build": "tsc && vite build",
|
|
55
|
+
"test": "vitest run"
|
|
56
|
+
}
|
|
57
|
+
}
|