tokenidp-react 0.3.5
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 +170 -0
- package/dist/index.css +50 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +691 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
// src/AuthProvider.jsx
|
|
2
|
+
import React, {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useReducer,
|
|
8
|
+
useRef
|
|
9
|
+
} from "react";
|
|
10
|
+
|
|
11
|
+
// src/config.js
|
|
12
|
+
var defaultAuthConfig = {
|
|
13
|
+
authority: "",
|
|
14
|
+
// e.g. https://idp.tokentresor.com
|
|
15
|
+
clientId: "",
|
|
16
|
+
tenantKey: "",
|
|
17
|
+
tenantPropagationMode: "all",
|
|
18
|
+
// all | api | none
|
|
19
|
+
tenantQueryParameter: "tenant",
|
|
20
|
+
tenantHeaderName: "X-Tenant-Key",
|
|
21
|
+
tenantKeyStorageKey: "idp_tenant_key",
|
|
22
|
+
redirectUri: "",
|
|
23
|
+
// e.g. https://app.com/auth/callback
|
|
24
|
+
postLoginRedirectUri: "/",
|
|
25
|
+
// where to go after success
|
|
26
|
+
postLogoutRedirectUri: "/login",
|
|
27
|
+
// where to go after logout
|
|
28
|
+
scope: "openid profile offline_access",
|
|
29
|
+
audience: "",
|
|
30
|
+
// optional
|
|
31
|
+
// endpoints (default paths)
|
|
32
|
+
authorizePath: "/authorize",
|
|
33
|
+
tokenPath: "/token",
|
|
34
|
+
revokePath: "/revoke",
|
|
35
|
+
logoutPath: "/logout",
|
|
36
|
+
// storage: "memory" | "sessionStorage" | "localStorage"
|
|
37
|
+
storage: "sessionStorage",
|
|
38
|
+
// keys
|
|
39
|
+
storageKey: "idp_user",
|
|
40
|
+
pkceVerifierKey: "idp_pkce_verifier",
|
|
41
|
+
oauthStateKey: "idp_oauth_state",
|
|
42
|
+
// refresh behavior
|
|
43
|
+
autoRefresh: true,
|
|
44
|
+
// refresh skew in seconds (refresh token slightly before expiry)
|
|
45
|
+
refreshSkewSeconds: 180
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/storage.js
|
|
49
|
+
function createStorage(mode) {
|
|
50
|
+
if (mode === "localStorage") return window.localStorage;
|
|
51
|
+
if (mode === "sessionStorage") return window.sessionStorage;
|
|
52
|
+
let mem = {};
|
|
53
|
+
return {
|
|
54
|
+
getItem: (k) => k in mem ? mem[k] : null,
|
|
55
|
+
setItem: (k, v) => {
|
|
56
|
+
mem[k] = String(v);
|
|
57
|
+
},
|
|
58
|
+
removeItem: (k) => {
|
|
59
|
+
delete mem[k];
|
|
60
|
+
},
|
|
61
|
+
clear: () => {
|
|
62
|
+
mem = {};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/pkce.js
|
|
68
|
+
function base64UrlEncode(arrayBuffer) {
|
|
69
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
70
|
+
let str = "";
|
|
71
|
+
for (let i = 0; i < bytes.byteLength; i++)
|
|
72
|
+
str += String.fromCharCode(bytes[i]);
|
|
73
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
74
|
+
}
|
|
75
|
+
function generateCodeVerifier(length = 64) {
|
|
76
|
+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
77
|
+
const randomValues = new Uint8Array(length);
|
|
78
|
+
crypto.getRandomValues(randomValues);
|
|
79
|
+
let verifier = "";
|
|
80
|
+
for (let i = 0; i < randomValues.length; i++) {
|
|
81
|
+
verifier += charset[randomValues[i] % charset.length];
|
|
82
|
+
}
|
|
83
|
+
return verifier;
|
|
84
|
+
}
|
|
85
|
+
async function generateCodeChallenge(verifier) {
|
|
86
|
+
const enc = new TextEncoder();
|
|
87
|
+
const data = enc.encode(verifier);
|
|
88
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
89
|
+
return base64UrlEncode(digest);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/tenant.js
|
|
93
|
+
function normalizeTenantPropagationMode(value) {
|
|
94
|
+
const normalized = String(value || "all").trim().toLowerCase();
|
|
95
|
+
if (normalized === "none" || normalized === "api") {
|
|
96
|
+
return normalized;
|
|
97
|
+
}
|
|
98
|
+
return "all";
|
|
99
|
+
}
|
|
100
|
+
function getAuthTenantKey(config) {
|
|
101
|
+
return normalizeTenantPropagationMode(config == null ? void 0 : config.tenantPropagationMode) === "all" ? String((config == null ? void 0 : config.tenantKey) || "").trim() : "";
|
|
102
|
+
}
|
|
103
|
+
function getApiTenantKey(config) {
|
|
104
|
+
const mode = normalizeTenantPropagationMode(config == null ? void 0 : config.tenantPropagationMode);
|
|
105
|
+
return mode === "all" || mode === "api" ? String((config == null ? void 0 : config.tenantKey) || "").trim() : "";
|
|
106
|
+
}
|
|
107
|
+
function resolveRawTenantKey(config, overrides = {}) {
|
|
108
|
+
const explicitTenantKey = String(
|
|
109
|
+
(overrides == null ? void 0 : overrides.tenantKey) || (config == null ? void 0 : config.tenantKey) || ""
|
|
110
|
+
).trim();
|
|
111
|
+
if (explicitTenantKey) {
|
|
112
|
+
return explicitTenantKey;
|
|
113
|
+
}
|
|
114
|
+
if (typeof window !== "undefined") {
|
|
115
|
+
const tenantFromQuery = new URLSearchParams(window.location.search).get(
|
|
116
|
+
(config == null ? void 0 : config.tenantQueryParameter) || "tenant"
|
|
117
|
+
);
|
|
118
|
+
if (tenantFromQuery) {
|
|
119
|
+
return tenantFromQuery.trim();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (typeof sessionStorage !== "undefined") {
|
|
123
|
+
const tenantFromStorage = sessionStorage.getItem(
|
|
124
|
+
(config == null ? void 0 : config.tenantKeyStorageKey) || "idp_tenant_key"
|
|
125
|
+
);
|
|
126
|
+
if (tenantFromStorage) {
|
|
127
|
+
return tenantFromStorage.trim();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
function resolveAuthTenantKey(config, overrides = {}) {
|
|
133
|
+
return normalizeTenantPropagationMode(config == null ? void 0 : config.tenantPropagationMode) === "all" ? resolveRawTenantKey(config, overrides) : "";
|
|
134
|
+
}
|
|
135
|
+
function resolveApiTenantKey(config, overrides = {}) {
|
|
136
|
+
const mode = normalizeTenantPropagationMode(config == null ? void 0 : config.tenantPropagationMode);
|
|
137
|
+
return mode === "all" || mode === "api" ? resolveRawTenantKey(config, overrides) : "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/oauth.js
|
|
141
|
+
function randomState(length = 32) {
|
|
142
|
+
const bytes = new Uint8Array(length);
|
|
143
|
+
crypto.getRandomValues(bytes);
|
|
144
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
145
|
+
}
|
|
146
|
+
function buildAuthorizeUrl(config, params) {
|
|
147
|
+
const url = new URL(config.authority + config.authorizePath);
|
|
148
|
+
const tenantKey = getAuthTenantKey({
|
|
149
|
+
...config,
|
|
150
|
+
tenantPropagationMode: normalizeTenantPropagationMode(
|
|
151
|
+
config == null ? void 0 : config.tenantPropagationMode
|
|
152
|
+
),
|
|
153
|
+
tenantKey: params.tenantKey || config.tenantKey
|
|
154
|
+
});
|
|
155
|
+
url.searchParams.set("response_type", "code");
|
|
156
|
+
url.searchParams.set("client_id", config.clientId);
|
|
157
|
+
url.searchParams.set("redirect_uri", config.redirectUri);
|
|
158
|
+
url.searchParams.set("scope", config.scope);
|
|
159
|
+
url.searchParams.set("code_challenge", params.codeChallenge);
|
|
160
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
161
|
+
if (params.state) url.searchParams.set("state", params.state);
|
|
162
|
+
if (params.audience || config.audience)
|
|
163
|
+
url.searchParams.set("audience", params.audience || config.audience);
|
|
164
|
+
if (tenantKey) {
|
|
165
|
+
url.searchParams.set(
|
|
166
|
+
config.tenantQueryParameter || "tenant",
|
|
167
|
+
tenantKey
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (params.prompt) url.searchParams.set("prompt", params.prompt);
|
|
171
|
+
if (params.loginHint) url.searchParams.set("login_hint", params.loginHint);
|
|
172
|
+
return url.toString();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/authApi.js
|
|
176
|
+
async function httpPostJson(url, body, extraHeaders = {}) {
|
|
177
|
+
const res = await fetch(url, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "Content-Type": "application/json", ...extraHeaders },
|
|
180
|
+
body: JSON.stringify(body)
|
|
181
|
+
});
|
|
182
|
+
const text = await res.text();
|
|
183
|
+
let data = null;
|
|
184
|
+
try {
|
|
185
|
+
data = text ? JSON.parse(text) : null;
|
|
186
|
+
} catch {
|
|
187
|
+
data = text;
|
|
188
|
+
}
|
|
189
|
+
if (!res.ok) {
|
|
190
|
+
const msg = getErrorMessage(data, res.status);
|
|
191
|
+
const err = new Error(msg);
|
|
192
|
+
err.status = res.status;
|
|
193
|
+
err.data = data;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
return data;
|
|
197
|
+
}
|
|
198
|
+
function getErrorMessage(data, status) {
|
|
199
|
+
var _a;
|
|
200
|
+
if (!data) return `HTTP ${status}`;
|
|
201
|
+
const wrappedError = data.error && typeof data.error === "object" ? data.error : ((_a = data.value) == null ? void 0 : _a.error) && typeof data.value.error === "object" ? data.value.error : null;
|
|
202
|
+
return data.error_description || (wrappedError == null ? void 0 : wrappedError.error) || (wrappedError == null ? void 0 : wrappedError.Error) || (wrappedError == null ? void 0 : wrappedError.message) || (wrappedError == null ? void 0 : wrappedError.Message) || (typeof data.error === "string" ? data.error : "") || data.message || `HTTP ${status}`;
|
|
203
|
+
}
|
|
204
|
+
function withTenant(url, config, target = "api") {
|
|
205
|
+
const tenantKey = target === "auth" ? getAuthTenantKey(config) : getApiTenantKey(config);
|
|
206
|
+
if (!tenantKey) {
|
|
207
|
+
return url;
|
|
208
|
+
}
|
|
209
|
+
const tenantUrl = new URL(url, typeof window !== "undefined" ? window.location.origin : void 0);
|
|
210
|
+
tenantUrl.searchParams.set(config.tenantQueryParameter || "tenant", tenantKey);
|
|
211
|
+
return tenantUrl.toString();
|
|
212
|
+
}
|
|
213
|
+
function extractToken(tokenPayload) {
|
|
214
|
+
if (!tokenPayload)
|
|
215
|
+
return { accessToken: "", refreshToken: "", expiresIn: 0, idToken: "" };
|
|
216
|
+
const accessToken = tokenPayload.value.accessToken || tokenPayload.value.access_token || "";
|
|
217
|
+
const refreshToken = tokenPayload.value.refreshToken || tokenPayload.value.refresh_token || "";
|
|
218
|
+
const expiresIn = Number(
|
|
219
|
+
tokenPayload.value.expiresIn || tokenPayload.value.expires_in || 0
|
|
220
|
+
) || 0;
|
|
221
|
+
const idToken = tokenPayload.idToken || tokenPayload.id_token || "";
|
|
222
|
+
return { accessToken, refreshToken, expiresIn, idToken };
|
|
223
|
+
}
|
|
224
|
+
async function exchangeAuthorizationCode(config, payload) {
|
|
225
|
+
const url = withTenant(config.authority + config.tokenPath, config, "auth");
|
|
226
|
+
return await httpPostJson(url, payload);
|
|
227
|
+
}
|
|
228
|
+
async function refreshWithToken(config, payload) {
|
|
229
|
+
const url = withTenant(config.authority + config.tokenPath, config, "auth");
|
|
230
|
+
return await httpPostJson(url, payload);
|
|
231
|
+
}
|
|
232
|
+
async function revokeToken(config, { accessToken, token, reasonRevoked }) {
|
|
233
|
+
if (!(config == null ? void 0 : config.authority) || !accessToken || !token) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const url = withTenant(
|
|
237
|
+
new URL(config.revokePath || "/revoke", config.authority).toString(),
|
|
238
|
+
config,
|
|
239
|
+
"auth"
|
|
240
|
+
);
|
|
241
|
+
const res = await fetch(url, {
|
|
242
|
+
method: "DELETE",
|
|
243
|
+
headers: {
|
|
244
|
+
Authorization: `Bearer ${accessToken}`,
|
|
245
|
+
"Content-Type": "application/json"
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({
|
|
248
|
+
token,
|
|
249
|
+
reasonRevoked: reasonRevoked || "logout"
|
|
250
|
+
})
|
|
251
|
+
});
|
|
252
|
+
const text = await res.text();
|
|
253
|
+
let data = null;
|
|
254
|
+
try {
|
|
255
|
+
data = text ? JSON.parse(text) : null;
|
|
256
|
+
} catch {
|
|
257
|
+
data = text;
|
|
258
|
+
}
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
const msg = getErrorMessage(data, res.status);
|
|
261
|
+
const err = new Error(msg);
|
|
262
|
+
err.status = res.status;
|
|
263
|
+
err.data = data;
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
return data;
|
|
267
|
+
}
|
|
268
|
+
function buildLogoutUrl(config) {
|
|
269
|
+
if (!(config == null ? void 0 : config.authority)) return "";
|
|
270
|
+
const url = new URL(
|
|
271
|
+
withTenant(
|
|
272
|
+
new URL(config.logoutPath || "/logout", config.authority).toString(),
|
|
273
|
+
config,
|
|
274
|
+
"auth"
|
|
275
|
+
)
|
|
276
|
+
);
|
|
277
|
+
if (config.clientId) {
|
|
278
|
+
url.searchParams.set("client_id", config.clientId);
|
|
279
|
+
}
|
|
280
|
+
const postLogoutRedirectUri = resolvePostLogoutRedirectUri(config);
|
|
281
|
+
if (postLogoutRedirectUri) {
|
|
282
|
+
url.searchParams.set("post_logout_redirect_uri", postLogoutRedirectUri);
|
|
283
|
+
}
|
|
284
|
+
return url.toString();
|
|
285
|
+
}
|
|
286
|
+
function resolvePostLogoutRedirectUri(config) {
|
|
287
|
+
var _a;
|
|
288
|
+
const candidate = config == null ? void 0 : config.postLogoutRedirectUri;
|
|
289
|
+
if (!candidate) {
|
|
290
|
+
return "";
|
|
291
|
+
}
|
|
292
|
+
if (typeof window !== "undefined" && ((_a = window.location) == null ? void 0 : _a.origin)) {
|
|
293
|
+
return new URL(candidate, window.location.origin).toString();
|
|
294
|
+
}
|
|
295
|
+
if (config == null ? void 0 : config.redirectUri) {
|
|
296
|
+
return new URL(candidate, config.redirectUri).toString();
|
|
297
|
+
}
|
|
298
|
+
return String(candidate);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/AuthProvider.jsx
|
|
302
|
+
var AuthContext = createContext(null);
|
|
303
|
+
var initialState = {
|
|
304
|
+
isAuthenticated: false,
|
|
305
|
+
tenantKey: "",
|
|
306
|
+
landingPage: "",
|
|
307
|
+
accessToken: "",
|
|
308
|
+
refreshToken: "",
|
|
309
|
+
idToken: "",
|
|
310
|
+
expiresAt: 0,
|
|
311
|
+
error: ""
|
|
312
|
+
};
|
|
313
|
+
function reducer(state, action) {
|
|
314
|
+
switch (action.type) {
|
|
315
|
+
case "LOGIN_SUCCESS":
|
|
316
|
+
return {
|
|
317
|
+
...state,
|
|
318
|
+
...action.payload,
|
|
319
|
+
isAuthenticated: true,
|
|
320
|
+
error: ""
|
|
321
|
+
};
|
|
322
|
+
case "SET_ERROR":
|
|
323
|
+
return { ...state, error: action.payload || "Unknown error" };
|
|
324
|
+
case "LOGOUT":
|
|
325
|
+
return { ...initialState };
|
|
326
|
+
case "TOKENS_UPDATED":
|
|
327
|
+
return { ...state, ...action.payload };
|
|
328
|
+
default:
|
|
329
|
+
return state;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function IdpAuthProvider({ children, config }) {
|
|
333
|
+
const baseConfig = useMemo(
|
|
334
|
+
() => ({ ...defaultAuthConfig, ...config || {} }),
|
|
335
|
+
[config]
|
|
336
|
+
);
|
|
337
|
+
const storage = useMemo(
|
|
338
|
+
() => createStorage(baseConfig.storage),
|
|
339
|
+
[baseConfig.storage]
|
|
340
|
+
);
|
|
341
|
+
const persistedRaw = storage.getItem(baseConfig.storageKey);
|
|
342
|
+
const persisted = persistedRaw ? safeJsonParse(persistedRaw) : null;
|
|
343
|
+
const mergedConfig = useMemo(() => {
|
|
344
|
+
const normalizedConfig = {
|
|
345
|
+
...baseConfig,
|
|
346
|
+
tenantPropagationMode: normalizeTenantPropagationMode(
|
|
347
|
+
baseConfig == null ? void 0 : baseConfig.tenantPropagationMode
|
|
348
|
+
)
|
|
349
|
+
};
|
|
350
|
+
const resolvedTenantKey = resolveApiTenantKey(normalizedConfig) || getApiTenantKey({
|
|
351
|
+
...normalizedConfig,
|
|
352
|
+
tenantKey: persisted == null ? void 0 : persisted.tenantKey
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
...normalizedConfig,
|
|
356
|
+
tenantKey: resolvedTenantKey
|
|
357
|
+
};
|
|
358
|
+
}, [baseConfig, persisted == null ? void 0 : persisted.tenantKey]);
|
|
359
|
+
const [state, dispatch] = useReducer(
|
|
360
|
+
reducer,
|
|
361
|
+
buildInitialState(persisted, mergedConfig)
|
|
362
|
+
);
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
storage.setItem(mergedConfig.storageKey, JSON.stringify(state));
|
|
365
|
+
}, [state, storage, mergedConfig.storageKey]);
|
|
366
|
+
const refreshTimerRef = useRef(null);
|
|
367
|
+
const refreshInFlightRef = useRef(false);
|
|
368
|
+
function clearRefreshTimer() {
|
|
369
|
+
if (refreshTimerRef.current) {
|
|
370
|
+
clearTimeout(refreshTimerRef.current);
|
|
371
|
+
refreshTimerRef.current = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function clearLocalSession() {
|
|
375
|
+
storage.removeItem(mergedConfig.storageKey);
|
|
376
|
+
sessionStorage.removeItem(mergedConfig.pkceVerifierKey);
|
|
377
|
+
sessionStorage.removeItem(mergedConfig.oauthStateKey);
|
|
378
|
+
sessionStorage.removeItem(mergedConfig.tenantKeyStorageKey);
|
|
379
|
+
dispatch({ type: "LOGOUT" });
|
|
380
|
+
}
|
|
381
|
+
async function tryRefreshWithRetry(retries, retryDelayMs) {
|
|
382
|
+
try {
|
|
383
|
+
await api.refresh();
|
|
384
|
+
return true;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (retries > 0) {
|
|
387
|
+
await new Promise((res) => setTimeout(res, retryDelayMs));
|
|
388
|
+
return tryRefreshWithRetry(retries - 1, retryDelayMs);
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function scheduleAutoRefresh(nextExpiresAtMs) {
|
|
394
|
+
clearRefreshTimer();
|
|
395
|
+
if (!mergedConfig.autoRefresh || !nextExpiresAtMs) return;
|
|
396
|
+
const skewMs = (mergedConfig.refreshSkewSeconds || 60) * 1e3;
|
|
397
|
+
const delay = Math.max(0, nextExpiresAtMs - Date.now() - skewMs);
|
|
398
|
+
refreshTimerRef.current = setTimeout(async () => {
|
|
399
|
+
if (refreshInFlightRef.current) return;
|
|
400
|
+
refreshInFlightRef.current = true;
|
|
401
|
+
const ok = await tryRefreshWithRetry(1, 5e3);
|
|
402
|
+
refreshInFlightRef.current = false;
|
|
403
|
+
if (!ok) {
|
|
404
|
+
api.logout();
|
|
405
|
+
}
|
|
406
|
+
}, delay);
|
|
407
|
+
}
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
if (!state.isAuthenticated || !state.expiresAt) return;
|
|
410
|
+
scheduleAutoRefresh(state.expiresAt);
|
|
411
|
+
return () => {
|
|
412
|
+
clearRefreshTimer();
|
|
413
|
+
};
|
|
414
|
+
}, [state.isAuthenticated, state.expiresAt]);
|
|
415
|
+
const api = useMemo(() => {
|
|
416
|
+
return {
|
|
417
|
+
...state,
|
|
418
|
+
login: async (options = {}) => {
|
|
419
|
+
if (!mergedConfig.authority || !mergedConfig.clientId || !mergedConfig.redirectUri) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
"Missing authority/clientId/redirectUri in IdpAuthProvider config."
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const verifier = generateCodeVerifier();
|
|
425
|
+
const challenge = await generateCodeChallenge(verifier);
|
|
426
|
+
const stateVal = randomState();
|
|
427
|
+
const authorizeTenantKey = resolveAuthTenantKey(mergedConfig, options);
|
|
428
|
+
const apiTenantKey = resolveApiTenantKey(mergedConfig, options);
|
|
429
|
+
sessionStorage.setItem(mergedConfig.pkceVerifierKey, verifier);
|
|
430
|
+
sessionStorage.setItem(mergedConfig.oauthStateKey, stateVal);
|
|
431
|
+
if (apiTenantKey) {
|
|
432
|
+
sessionStorage.setItem(mergedConfig.tenantKeyStorageKey, apiTenantKey);
|
|
433
|
+
} else {
|
|
434
|
+
sessionStorage.removeItem(mergedConfig.tenantKeyStorageKey);
|
|
435
|
+
}
|
|
436
|
+
const authorizeUrl = buildAuthorizeUrl(mergedConfig, {
|
|
437
|
+
codeChallenge: challenge,
|
|
438
|
+
state: stateVal,
|
|
439
|
+
prompt: options.prompt,
|
|
440
|
+
loginHint: options.loginHint,
|
|
441
|
+
audience: options.audience,
|
|
442
|
+
tenantKey: authorizeTenantKey
|
|
443
|
+
});
|
|
444
|
+
window.location.assign(authorizeUrl);
|
|
445
|
+
},
|
|
446
|
+
logout: async () => {
|
|
447
|
+
const logoutUrl = buildLogoutUrl(mergedConfig);
|
|
448
|
+
clearRefreshTimer();
|
|
449
|
+
try {
|
|
450
|
+
await revokeToken(mergedConfig, {
|
|
451
|
+
accessToken: state.accessToken,
|
|
452
|
+
token: state.refreshToken,
|
|
453
|
+
reasonRevoked: "logout"
|
|
454
|
+
});
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.warn("Token revocation during logout failed.", error);
|
|
457
|
+
}
|
|
458
|
+
if (typeof window !== "undefined" && logoutUrl) {
|
|
459
|
+
window.addEventListener("pagehide", clearLocalSession, { once: true });
|
|
460
|
+
window.location.assign(logoutUrl);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
clearLocalSession();
|
|
464
|
+
},
|
|
465
|
+
// exchanges code->tokens and stores the OAuth session
|
|
466
|
+
handleCallback: async ({ code, state: returnedState }) => {
|
|
467
|
+
const verifier = sessionStorage.getItem(mergedConfig.pkceVerifierKey);
|
|
468
|
+
if (!verifier) throw new Error("Missing code verifier (PKCE).");
|
|
469
|
+
const tenantKey = resolveApiTenantKey(mergedConfig);
|
|
470
|
+
const expectedState = sessionStorage.getItem(
|
|
471
|
+
mergedConfig.oauthStateKey
|
|
472
|
+
);
|
|
473
|
+
if (expectedState && returnedState && expectedState !== returnedState) {
|
|
474
|
+
throw new Error("Invalid OAuth state. Possible CSRF.");
|
|
475
|
+
}
|
|
476
|
+
var tokenPayload = {};
|
|
477
|
+
try {
|
|
478
|
+
tokenPayload = await exchangeAuthorizationCode(mergedConfig, {
|
|
479
|
+
grantType: "authorization_code",
|
|
480
|
+
clientId: mergedConfig.clientId,
|
|
481
|
+
redirectUri: mergedConfig.redirectUri,
|
|
482
|
+
code,
|
|
483
|
+
codeVerifier: verifier,
|
|
484
|
+
scope: mergedConfig.scope
|
|
485
|
+
});
|
|
486
|
+
} catch (e) {
|
|
487
|
+
console.error("exchangeAuthorizationCode failed:", e);
|
|
488
|
+
console.error("Status:", e == null ? void 0 : e.status);
|
|
489
|
+
console.error("Data:", e == null ? void 0 : e.data);
|
|
490
|
+
throw e;
|
|
491
|
+
}
|
|
492
|
+
const { accessToken, refreshToken, expiresIn, idToken } = extractToken(tokenPayload);
|
|
493
|
+
if (!accessToken)
|
|
494
|
+
throw new Error("Token response did not include an access token.");
|
|
495
|
+
const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : 0;
|
|
496
|
+
dispatch({
|
|
497
|
+
type: "LOGIN_SUCCESS",
|
|
498
|
+
payload: {
|
|
499
|
+
tenantKey,
|
|
500
|
+
accessToken,
|
|
501
|
+
refreshToken: refreshToken || "",
|
|
502
|
+
idToken: idToken || "",
|
|
503
|
+
expiresAt,
|
|
504
|
+
landingPage: mergedConfig.postLoginRedirectUri || "/"
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
sessionStorage.removeItem(mergedConfig.pkceVerifierKey);
|
|
508
|
+
sessionStorage.removeItem(mergedConfig.oauthStateKey);
|
|
509
|
+
return {
|
|
510
|
+
tenantKey,
|
|
511
|
+
accessToken,
|
|
512
|
+
refreshToken: refreshToken || "",
|
|
513
|
+
idToken: idToken || "",
|
|
514
|
+
expiresAt
|
|
515
|
+
};
|
|
516
|
+
},
|
|
517
|
+
refresh: async () => {
|
|
518
|
+
if (!state.refreshToken) throw new Error("No refresh token available.");
|
|
519
|
+
const tokenPayload = await refreshWithToken(mergedConfig, {
|
|
520
|
+
grantType: "refresh_token",
|
|
521
|
+
clientId: mergedConfig.clientId,
|
|
522
|
+
refreshToken: state.refreshToken,
|
|
523
|
+
scope: mergedConfig.scope
|
|
524
|
+
});
|
|
525
|
+
const { accessToken, refreshToken, expiresIn, idToken } = extractToken(tokenPayload);
|
|
526
|
+
if (!accessToken)
|
|
527
|
+
throw new Error("Refresh response did not include an access token.");
|
|
528
|
+
const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : 0;
|
|
529
|
+
dispatch({
|
|
530
|
+
type: "TOKENS_UPDATED",
|
|
531
|
+
payload: {
|
|
532
|
+
accessToken,
|
|
533
|
+
// if rotation: use new refresh token if provided
|
|
534
|
+
refreshToken: refreshToken || state.refreshToken,
|
|
535
|
+
idToken: idToken || state.idToken,
|
|
536
|
+
expiresAt
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
return {
|
|
540
|
+
accessToken,
|
|
541
|
+
refreshToken: refreshToken || state.refreshToken,
|
|
542
|
+
idToken,
|
|
543
|
+
expiresAt
|
|
544
|
+
};
|
|
545
|
+
},
|
|
546
|
+
setError: (message) => dispatch({ type: "SET_ERROR", payload: message })
|
|
547
|
+
};
|
|
548
|
+
}, [state, mergedConfig, storage]);
|
|
549
|
+
return /* @__PURE__ */ React.createElement(AuthContext.Provider, { value: api }, children);
|
|
550
|
+
}
|
|
551
|
+
function useAuth() {
|
|
552
|
+
const ctx = useContext(AuthContext);
|
|
553
|
+
if (!ctx) throw new Error("useAuth must be used inside IdpAuthProvider");
|
|
554
|
+
return ctx;
|
|
555
|
+
}
|
|
556
|
+
function safeJsonParse(raw) {
|
|
557
|
+
try {
|
|
558
|
+
return JSON.parse(raw);
|
|
559
|
+
} catch {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function buildInitialState(persistedState, config) {
|
|
564
|
+
if (!persistedState) {
|
|
565
|
+
return {
|
|
566
|
+
...initialState,
|
|
567
|
+
tenantKey: config.tenantKey
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
...initialState,
|
|
572
|
+
isAuthenticated: !!persistedState.isAuthenticated,
|
|
573
|
+
landingPage: persistedState.landingPage || "",
|
|
574
|
+
accessToken: persistedState.accessToken || "",
|
|
575
|
+
refreshToken: persistedState.refreshToken || "",
|
|
576
|
+
idToken: persistedState.idToken || "",
|
|
577
|
+
expiresAt: persistedState.expiresAt || 0,
|
|
578
|
+
error: persistedState.error || "",
|
|
579
|
+
tenantKey: config.tenantKey
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/AuthCallback.jsx
|
|
584
|
+
import React2, { useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
585
|
+
import { useNavigate } from "react-router-dom";
|
|
586
|
+
function renderLogoContent(logo, logoAlt, fallback) {
|
|
587
|
+
if (!logo) {
|
|
588
|
+
return /* @__PURE__ */ React2.createElement("div", { className: "logo mb-3" }, fallback);
|
|
589
|
+
}
|
|
590
|
+
if (typeof logo === "string") {
|
|
591
|
+
return /* @__PURE__ */ React2.createElement("div", { className: "mb-3" }, /* @__PURE__ */ React2.createElement("img", { src: logo, alt: logoAlt, width: "250" }));
|
|
592
|
+
}
|
|
593
|
+
return /* @__PURE__ */ React2.createElement("div", { className: "mb-3" }, logo);
|
|
594
|
+
}
|
|
595
|
+
function AuthCallback({
|
|
596
|
+
redirectTo,
|
|
597
|
+
logo = null,
|
|
598
|
+
logoAlt = "Application logo",
|
|
599
|
+
fallbackBadge = "ID"
|
|
600
|
+
}) {
|
|
601
|
+
const navigate = useNavigate();
|
|
602
|
+
const auth = useAuth();
|
|
603
|
+
const [error, setError] = useState(null);
|
|
604
|
+
const ranRef = useRef2(false);
|
|
605
|
+
useEffect2(() => {
|
|
606
|
+
const run = async () => {
|
|
607
|
+
if (ranRef.current) return;
|
|
608
|
+
ranRef.current = true;
|
|
609
|
+
const qs = new URLSearchParams(window.location.search);
|
|
610
|
+
const code = qs.get("code");
|
|
611
|
+
const state = qs.get("state");
|
|
612
|
+
const err = qs.get("error");
|
|
613
|
+
const errDesc = qs.get("error_description");
|
|
614
|
+
if (err) {
|
|
615
|
+
const msg = errDesc || err || "Authentication error.";
|
|
616
|
+
setError(msg);
|
|
617
|
+
auth.setError(msg);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (!code || !state) {
|
|
621
|
+
const msg = "Missing authorization code or state.";
|
|
622
|
+
setError(msg);
|
|
623
|
+
auth.setError(msg);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
await auth.handleCallback({ code, state });
|
|
628
|
+
const target = redirectTo || auth.landingPage || "/";
|
|
629
|
+
navigate(target, { replace: true });
|
|
630
|
+
} catch (e) {
|
|
631
|
+
const msg = (e == null ? void 0 : e.message) || "Login callback failed.";
|
|
632
|
+
setError(msg);
|
|
633
|
+
auth.setError(msg);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
run();
|
|
637
|
+
}, [auth, navigate, redirectTo]);
|
|
638
|
+
return /* @__PURE__ */ React2.createElement("div", { className: "login-page" }, /* @__PURE__ */ React2.createElement("section", { className: "login-section-container" }, /* @__PURE__ */ React2.createElement("div", { className: "login-section redirect-card" }, /* @__PURE__ */ React2.createElement("div", { className: "col-12 p-4 text-center" }, renderLogoContent(logo, logoAlt, fallbackBadge), /* @__PURE__ */ React2.createElement("h1", null, "Completing sign-in"), /* @__PURE__ */ React2.createElement("p", { className: "text-muted" }, error ? error : "Please wait while we finish authentication.")))));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/LoginPage.jsx
|
|
642
|
+
import React3, { useEffect as useEffect3 } from "react";
|
|
643
|
+
function renderLogoContent2(logo, logoAlt, fallback) {
|
|
644
|
+
if (!logo) {
|
|
645
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "logo mb-3" }, fallback);
|
|
646
|
+
}
|
|
647
|
+
if (typeof logo === "string") {
|
|
648
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "mb-3" }, /* @__PURE__ */ React3.createElement("img", { src: logo, alt: logoAlt, width: "250" }));
|
|
649
|
+
}
|
|
650
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "mb-3" }, logo);
|
|
651
|
+
}
|
|
652
|
+
function LoginPage({
|
|
653
|
+
logo = null,
|
|
654
|
+
logoAlt = "Application logo",
|
|
655
|
+
title = "Redirecting to sign-in...",
|
|
656
|
+
subtitle = "Please wait while we securely connect to Identity.",
|
|
657
|
+
signedOutBadge = "Signed out",
|
|
658
|
+
signedOutTitle = "You have been signed out",
|
|
659
|
+
signedOutSubtitle = "Start a new session when you are ready.",
|
|
660
|
+
signInAgainLabel = "Sign in again",
|
|
661
|
+
fallbackBadge = "ID",
|
|
662
|
+
loginOptions
|
|
663
|
+
}) {
|
|
664
|
+
const auth = useAuth();
|
|
665
|
+
const isLoggedOut = new URLSearchParams(window.location.search).get("logged_out") === "1";
|
|
666
|
+
useEffect3(() => {
|
|
667
|
+
if (isLoggedOut) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
auth.login(loginOptions);
|
|
671
|
+
}, [auth, isLoggedOut, loginOptions]);
|
|
672
|
+
if (isLoggedOut) {
|
|
673
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "login-page" }, /* @__PURE__ */ React3.createElement("section", { className: "login-section-container" }, /* @__PURE__ */ React3.createElement("div", { className: "login-section redirect-card" }, /* @__PURE__ */ React3.createElement("div", { className: "p-4 text-center" }, renderLogoContent2(logo, logoAlt, fallbackBadge), /* @__PURE__ */ React3.createElement("h1", null, signedOutTitle), /* @__PURE__ */ React3.createElement("p", { className: "text-muted" }, signedOutSubtitle), /* @__PURE__ */ React3.createElement(
|
|
674
|
+
"button",
|
|
675
|
+
{
|
|
676
|
+
className: "btn btn-primary mt-3",
|
|
677
|
+
onClick: () => auth.login(loginOptions)
|
|
678
|
+
},
|
|
679
|
+
signInAgainLabel
|
|
680
|
+
)))));
|
|
681
|
+
}
|
|
682
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "login-page" }, /* @__PURE__ */ React3.createElement("section", { className: "login-section-container" }, /* @__PURE__ */ React3.createElement("div", { className: "login-section redirect-card" }, /* @__PURE__ */ React3.createElement("div", { className: "p-4 text-center" }, renderLogoContent2(logo, logoAlt, fallbackBadge), /* @__PURE__ */ React3.createElement("h1", null, title), /* @__PURE__ */ React3.createElement("p", { className: "text-muted" }, subtitle), /* @__PURE__ */ React3.createElement("div", { className: "spinner mt-3" })))));
|
|
683
|
+
}
|
|
684
|
+
export {
|
|
685
|
+
AuthCallback,
|
|
686
|
+
IdpAuthProvider,
|
|
687
|
+
LoginPage,
|
|
688
|
+
defaultAuthConfig,
|
|
689
|
+
useAuth
|
|
690
|
+
};
|
|
691
|
+
//# sourceMappingURL=index.mjs.map
|