@tidecloak/js 0.13.1 → 0.13.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 -5
- package/dist/cjs/lib/tidecloak.js +1759 -1629
- package/dist/cjs/lib/tidecloak.js.map +1 -1
- package/dist/cjs/src/IAMService.js +526 -32
- package/dist/cjs/src/IAMService.js.map +1 -1
- package/dist/cjs/src/index.js +4 -21
- package/dist/cjs/src/index.js.map +1 -1
- package/dist/cjs/src/policy-react.js +3 -0
- package/dist/cjs/src/policy-react.js.map +1 -0
- package/dist/cjs/src/policy.css +1 -0
- package/dist/cjs/src/utils/fetch.js +36 -0
- package/dist/cjs/src/utils/fetch.js.map +1 -0
- package/dist/cjs/src/utils/index.js +3 -0
- package/dist/cjs/src/utils/index.js.map +1 -0
- package/dist/cjs/src/utils/pkce.js +43 -0
- package/dist/cjs/src/utils/pkce.js.map +1 -0
- package/dist/esm/lib/tidecloak.js +1760 -1619
- package/dist/esm/lib/tidecloak.js.map +1 -1
- package/dist/esm/src/IAMService.js +523 -23
- package/dist/esm/src/IAMService.js.map +1 -1
- package/dist/esm/src/index.js +2 -6
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/policy-react.js +3 -0
- package/dist/esm/src/policy-react.js.map +1 -0
- package/dist/esm/src/policy.css +1 -0
- package/dist/esm/src/utils/fetch.js +36 -0
- package/dist/esm/src/utils/fetch.js.map +1 -0
- package/dist/esm/src/utils/index.js +3 -0
- package/dist/esm/src/utils/index.js.map +1 -0
- package/dist/esm/src/utils/pkce.js +43 -0
- package/dist/esm/src/utils/pkce.js.map +1 -0
- package/dist/types/IAMService.d.ts +328 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/lib/tidecloak.d.ts +325 -31
- package/dist/types/policy-react.d.ts +1 -0
- package/dist/types/src/IAMService.d.ts +245 -23
- package/dist/types/src/index.d.ts +2 -2
- package/dist/types/src/policy-react.d.ts +1 -0
- package/dist/types/src/utils/fetch.d.ts +11 -0
- package/dist/types/src/utils/index.d.ts +2 -0
- package/dist/types/src/utils/pkce.d.ts +24 -0
- package/dist/types/utils/fetch.d.ts +11 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/pkce.d.ts +24 -0
- package/package.json +18 -29
- package/scripts/postinstall.cjs +36 -0
- package/silent-check-sso.html +1 -0
- package/scripts/postinstall.js +0 -43
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.IAMService = void 0;
|
|
7
|
-
const tidecloak_1 = __importDefault(require("../lib/tidecloak"));
|
|
1
|
+
import { makePkce, fetchJson } from "./utils/index.js";
|
|
2
|
+
import TideCloak from "../lib/tidecloak.js";
|
|
8
3
|
/**
|
|
9
4
|
* Singleton IAMService wrapping the TideCloak client.
|
|
10
5
|
*
|
|
6
|
+
* Supports two modes:
|
|
7
|
+
* - **Front-channel mode**: Browser handles all token operations (standard OIDC)
|
|
8
|
+
* - **Hybrid/BFF mode**: Browser handles PKCE, backend exchanges code for tokens (more secure)
|
|
9
|
+
*
|
|
10
|
+
* ---
|
|
11
|
+
* ## Front-channel Mode
|
|
12
|
+
*
|
|
11
13
|
* Usage A: pass an onReady callback directly
|
|
12
14
|
* ```js
|
|
13
|
-
* import { IAMService } from 'tidecloak
|
|
15
|
+
* import { IAMService } from '@tidecloak/js';
|
|
14
16
|
* import tidecloakConfig from './tidecloakAdapter.json';
|
|
15
17
|
*
|
|
16
18
|
* IAMService.initIAM(tidecloakConfig, authenticated => {
|
|
@@ -26,12 +28,83 @@ const tidecloak_1 = __importDefault(require("../lib/tidecloak"));
|
|
|
26
28
|
*
|
|
27
29
|
* await IAMService.initIAM(tidecloakConfig);
|
|
28
30
|
* ```
|
|
31
|
+
*
|
|
32
|
+
* ---
|
|
33
|
+
* ## Hybrid/BFF Mode (Backend-For-Frontend)
|
|
34
|
+
*
|
|
35
|
+
* In hybrid mode, the browser generates PKCE and redirects to the IdP, but the
|
|
36
|
+
* backend exchanges the authorization code for tokens. This keeps tokens server-side
|
|
37
|
+
* for improved security.
|
|
38
|
+
*
|
|
39
|
+
* ### Config shape:
|
|
40
|
+
* ```js
|
|
41
|
+
* const hybridConfig = {
|
|
42
|
+
* authMode: "hybrid",
|
|
43
|
+
* oidc: {
|
|
44
|
+
* authorizationEndpoint: "https://auth.example.com/realms/myrealm/protocol/openid-connect/auth",
|
|
45
|
+
* clientId: "my-client",
|
|
46
|
+
* redirectUri: "https://app.example.com/auth/callback",
|
|
47
|
+
* scope: "openid profile email", // optional, defaults to "openid profile email"
|
|
48
|
+
* prompt: "login" // optional
|
|
49
|
+
* },
|
|
50
|
+
* tokenExchange: {
|
|
51
|
+
* endpoint: "/api/authenticate", // Backend endpoint that exchanges code for tokens
|
|
52
|
+
* provider: "tidecloak-auth", // optional, defaults to "tidecloak-auth"
|
|
53
|
+
* headers: () => ({ // optional, custom headers (e.g., CSRF token)
|
|
54
|
+
* "anti-csrf-token": getCSRFToken()
|
|
55
|
+
* })
|
|
56
|
+
* }
|
|
57
|
+
* };
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* ### Login Page:
|
|
61
|
+
* ```js
|
|
62
|
+
* // Load config and trigger login
|
|
63
|
+
* await IAMService.loadConfig(hybridConfig);
|
|
64
|
+
* IAMService.doLogin("/dashboard"); // returnUrl after successful auth
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* ### Redirect/Callback Page:
|
|
68
|
+
* ```js
|
|
69
|
+
* // initIAM handles the callback automatically - exchanges code for tokens via backend
|
|
70
|
+
* const authenticated = await IAMService.initIAM(hybridConfig);
|
|
71
|
+
* if (authenticated) {
|
|
72
|
+
* const returnUrl = IAMService.getReturnUrl() || "/";
|
|
73
|
+
* window.location.assign(returnUrl);
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* ### Token Exchange Request Format:
|
|
78
|
+
* The backend endpoint receives a POST request with:
|
|
79
|
+
* ```json
|
|
80
|
+
* {
|
|
81
|
+
* "accessToken": "{\"code\":\"...\",\"code_verifier\":\"...\",\"redirect_uri\":\"...\"}",
|
|
82
|
+
* "provider": "tidecloak-auth"
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* ---
|
|
87
|
+
* ## Events
|
|
88
|
+
* - `ready` - Emitted when initialization completes (with authenticated boolean)
|
|
89
|
+
* - `initError` - Emitted when initialization fails
|
|
90
|
+
* - `authSuccess` - Emitted on successful authentication
|
|
91
|
+
* - `authError` - Emitted on authentication failure
|
|
92
|
+
* - `authRefreshSuccess` - Emitted when token refresh succeeds (front-channel only)
|
|
93
|
+
* - `authRefreshError` - Emitted when token refresh fails (front-channel only)
|
|
94
|
+
* - `logout` - Emitted on logout
|
|
95
|
+
* - `tokenExpired` - Emitted when token expires (front-channel only)
|
|
29
96
|
*/
|
|
30
97
|
class IAMService {
|
|
31
98
|
constructor() {
|
|
32
99
|
this._tc = null;
|
|
33
100
|
this._config = null;
|
|
34
101
|
this._listeners = {};
|
|
102
|
+
// --- Hybrid mode state ---
|
|
103
|
+
this._hybridAuthenticated = false;
|
|
104
|
+
this._hybridReturnUrl = null;
|
|
105
|
+
this._hybridCallbackHandled = false; // Guard against React StrictMode double-execution
|
|
106
|
+
this._hybridCallbackPromise = null; // Promise for pending token exchange (for StrictMode)
|
|
107
|
+
this._cachedCallbackData = null; // Cache for getHybridCallbackData to prevent data loss
|
|
35
108
|
}
|
|
36
109
|
/**
|
|
37
110
|
* Register an event listener.
|
|
@@ -68,21 +141,33 @@ class IAMService {
|
|
|
68
141
|
}
|
|
69
142
|
});
|
|
70
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Check if running in hybrid mode.
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
isHybridMode() {
|
|
149
|
+
var _a;
|
|
150
|
+
return (((_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode) || "frontchannel").toLowerCase() === "hybrid";
|
|
151
|
+
}
|
|
71
152
|
/**
|
|
72
153
|
* Load TideCloak configuration and instantiate the client once.
|
|
73
154
|
* @param {Object} config - TideCloak configuration object.
|
|
74
155
|
* @returns {Promise<Object|null>} The loaded config, or null on failure.
|
|
75
156
|
*/
|
|
76
157
|
async loadConfig(config) {
|
|
77
|
-
if (this.
|
|
158
|
+
if (this._config)
|
|
78
159
|
return this._config;
|
|
79
160
|
if (!config || Object.keys(config).length === 0) {
|
|
80
161
|
console.warn("[loadConfig] empty config");
|
|
81
162
|
return null;
|
|
82
163
|
}
|
|
83
164
|
this._config = config;
|
|
165
|
+
// Hybrid mode: do not construct TideCloak client (tokens are server-side)
|
|
166
|
+
if (this.isHybridMode()) {
|
|
167
|
+
return this._config;
|
|
168
|
+
}
|
|
84
169
|
try {
|
|
85
|
-
this._tc = new
|
|
170
|
+
this._tc = new TideCloak({
|
|
86
171
|
url: config["auth-server-url"],
|
|
87
172
|
realm: config.realm,
|
|
88
173
|
clientId: config.resource,
|
|
@@ -107,6 +192,7 @@ class IAMService {
|
|
|
107
192
|
}
|
|
108
193
|
/**
|
|
109
194
|
* Initialize the TideCloak SSO client with silent SSO check.
|
|
195
|
+
* In hybrid mode, handles the redirect callback if present.
|
|
110
196
|
* @param {Object} config - TideCloak configuration object.
|
|
111
197
|
* @param {Function} [onReady] - Optional callback for the 'ready' event.
|
|
112
198
|
* @returns {Promise<boolean>} true if authenticated, else false.
|
|
@@ -128,6 +214,41 @@ class IAMService {
|
|
|
128
214
|
this._emit("initError", new Error("Failed to load config"));
|
|
129
215
|
return false;
|
|
130
216
|
}
|
|
217
|
+
// --- Hybrid mode init: handle redirect callback if present ---
|
|
218
|
+
if (this.isHybridMode()) {
|
|
219
|
+
// Guard against React StrictMode double-execution
|
|
220
|
+
if (this._hybridCallbackHandled) {
|
|
221
|
+
console.debug("[IAMService] Hybrid callback already handled");
|
|
222
|
+
// If there's a pending token exchange, wait for it instead of returning stale state
|
|
223
|
+
if (this._hybridCallbackPromise) {
|
|
224
|
+
console.debug("[IAMService] Waiting for pending token exchange...");
|
|
225
|
+
return this._hybridCallbackPromise;
|
|
226
|
+
}
|
|
227
|
+
this._emit("ready", this._hybridAuthenticated);
|
|
228
|
+
return this._hybridAuthenticated;
|
|
229
|
+
}
|
|
230
|
+
// Check if this is a callback page (has code) - mark as handled BEFORE processing
|
|
231
|
+
const qs = new URLSearchParams(window.location.search);
|
|
232
|
+
if (qs.get("code")) {
|
|
233
|
+
this._hybridCallbackHandled = true;
|
|
234
|
+
}
|
|
235
|
+
// Store the promise so subsequent calls can wait for it (React StrictMode)
|
|
236
|
+
this._hybridCallbackPromise = (async () => {
|
|
237
|
+
const { handled, authenticated, returnUrl } = await this._handleHybridRedirectCallback({
|
|
238
|
+
onMissingVerifierRedirectTo: "/login",
|
|
239
|
+
});
|
|
240
|
+
this._hybridReturnUrl = returnUrl || null;
|
|
241
|
+
// If we weren't on a callback page, emit ready with current state (defaults false)
|
|
242
|
+
if (!handled) {
|
|
243
|
+
this._emit("ready", this._hybridAuthenticated);
|
|
244
|
+
}
|
|
245
|
+
// Clear the promise once complete
|
|
246
|
+
this._hybridCallbackPromise = null;
|
|
247
|
+
return authenticated || this._hybridAuthenticated;
|
|
248
|
+
})();
|
|
249
|
+
return this._hybridCallbackPromise;
|
|
250
|
+
}
|
|
251
|
+
// --- Front-channel mode ---
|
|
131
252
|
if (!this._tc) {
|
|
132
253
|
const err = new Error("TideCloak client not available");
|
|
133
254
|
this._emit("initError", err);
|
|
@@ -135,7 +256,7 @@ class IAMService {
|
|
|
135
256
|
}
|
|
136
257
|
if (this._tc.didInitialize) {
|
|
137
258
|
console.debug("[IAMService] IAM Already initialized once.");
|
|
138
|
-
return this._tc.
|
|
259
|
+
return !!this._tc.tokenParsed;
|
|
139
260
|
}
|
|
140
261
|
let authenticated = false;
|
|
141
262
|
try {
|
|
@@ -170,60 +291,128 @@ class IAMService {
|
|
|
170
291
|
}
|
|
171
292
|
return this._config;
|
|
172
293
|
}
|
|
173
|
-
/** @returns {boolean} Whether there's a valid token */
|
|
294
|
+
/** @returns {boolean} Whether there's a valid token (or session in hybrid mode) */
|
|
174
295
|
isLoggedIn() {
|
|
296
|
+
if (this.isHybridMode())
|
|
297
|
+
return this._hybridAuthenticated;
|
|
175
298
|
return !!this.getTideCloakClient().token;
|
|
176
299
|
}
|
|
177
|
-
/**
|
|
300
|
+
/**
|
|
301
|
+
* Get valid token (refreshing if needed).
|
|
302
|
+
* @returns {Promise<string>}
|
|
303
|
+
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
304
|
+
*/
|
|
178
305
|
async getToken() {
|
|
306
|
+
if (this.isHybridMode()) {
|
|
307
|
+
throw new Error("getToken() not available in hybrid mode - tokens are server-side");
|
|
308
|
+
}
|
|
179
309
|
const exp = this.getTokenExp();
|
|
180
310
|
if (exp < 3)
|
|
181
311
|
await this.updateIAMToken();
|
|
182
312
|
return this.getTideCloakClient().token;
|
|
183
313
|
}
|
|
184
|
-
/**
|
|
314
|
+
/**
|
|
315
|
+
* Seconds until token expiry.
|
|
316
|
+
* @returns {number}
|
|
317
|
+
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
318
|
+
*/
|
|
185
319
|
getTokenExp() {
|
|
320
|
+
if (this.isHybridMode()) {
|
|
321
|
+
throw new Error("getTokenExp() not available in hybrid mode - tokens are server-side");
|
|
322
|
+
}
|
|
186
323
|
const kc = this.getTideCloakClient();
|
|
187
324
|
return Math.round(kc.tokenParsed.exp + kc.timeSkew - Date.now() / 1000);
|
|
188
325
|
}
|
|
189
|
-
/**
|
|
326
|
+
/**
|
|
327
|
+
* Get ID token.
|
|
328
|
+
* @returns {string}
|
|
329
|
+
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
330
|
+
*/
|
|
190
331
|
getIDToken() {
|
|
332
|
+
if (this.isHybridMode()) {
|
|
333
|
+
throw new Error("getIDToken() not available in hybrid mode - tokens are server-side");
|
|
334
|
+
}
|
|
191
335
|
return this.getTideCloakClient().idToken;
|
|
192
336
|
}
|
|
193
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* Get username (preferred_username claim).
|
|
339
|
+
* @returns {string}
|
|
340
|
+
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
341
|
+
*/
|
|
194
342
|
getName() {
|
|
343
|
+
if (this.isHybridMode()) {
|
|
344
|
+
throw new Error("getName() not available in hybrid mode - tokens are server-side");
|
|
345
|
+
}
|
|
195
346
|
return this.getTideCloakClient().tokenParsed.preferred_username;
|
|
196
347
|
}
|
|
197
348
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
349
|
+
* Get the return URL after successful hybrid authentication.
|
|
350
|
+
* Only available in hybrid mode after successful auth.
|
|
351
|
+
* @returns {string|null}
|
|
352
|
+
*/
|
|
353
|
+
getReturnUrl() {
|
|
354
|
+
return this._hybridReturnUrl;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Check if user has a realm role.
|
|
358
|
+
* @param {string} role - the name of the role to check
|
|
359
|
+
* @returns {boolean} Whether the user has a given realm role
|
|
360
|
+
* @throws {Error} In hybrid mode (role checks not available client-side)
|
|
361
|
+
*/
|
|
200
362
|
hasRealmRole(role) {
|
|
363
|
+
if (this.isHybridMode()) {
|
|
364
|
+
throw new Error("hasRealmRole() not available in hybrid mode - tokens are server-side");
|
|
365
|
+
}
|
|
201
366
|
return this.getTideCloakClient().hasRealmRole(role);
|
|
202
367
|
}
|
|
203
368
|
/**
|
|
369
|
+
* Check if user has a client role.
|
|
204
370
|
* @param {string} role - the name of the role to check
|
|
205
371
|
* @param {string} [client] - optional client-ID (defaults to the configured adapter resource)
|
|
206
372
|
* @returns {boolean} - whether the user has that role
|
|
373
|
+
* @throws {Error} In hybrid mode (role checks not available client-side)
|
|
207
374
|
*/
|
|
208
375
|
hasClientRole(role, client) {
|
|
376
|
+
if (this.isHybridMode()) {
|
|
377
|
+
throw new Error("hasClientRole() not available in hybrid mode - tokens are server-side");
|
|
378
|
+
}
|
|
209
379
|
return this.getTideCloakClient().hasResourceRole(role, client);
|
|
210
380
|
}
|
|
211
381
|
/**
|
|
382
|
+
* Get custom claim from access token.
|
|
212
383
|
* @param {string} key - The name of the claim to retrieve from the Access token's payload.
|
|
213
|
-
* @returns {*} Custom claim from access token
|
|
384
|
+
* @returns {*} Custom claim from access token
|
|
385
|
+
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
386
|
+
*/
|
|
214
387
|
getValueFromToken(key) {
|
|
215
388
|
var _a;
|
|
389
|
+
if (this.isHybridMode()) {
|
|
390
|
+
throw new Error("getValueFromToken() not available in hybrid mode - tokens are server-side");
|
|
391
|
+
}
|
|
216
392
|
return (_a = this.getTideCloakClient().tokenParsed[key]) !== null && _a !== void 0 ? _a : null;
|
|
217
393
|
}
|
|
218
394
|
/**
|
|
395
|
+
* Get custom claim from ID token.
|
|
219
396
|
* @param {string} key - The name of the claim to retrieve from the ID token's payload.
|
|
220
|
-
* @returns {*} Custom claim from
|
|
397
|
+
* @returns {*} Custom claim from ID token
|
|
398
|
+
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
399
|
+
*/
|
|
221
400
|
getValueFromIDToken(key) {
|
|
222
401
|
var _a;
|
|
402
|
+
if (this.isHybridMode()) {
|
|
403
|
+
throw new Error("getValueFromIDToken() not available in hybrid mode - tokens are server-side");
|
|
404
|
+
}
|
|
223
405
|
return (_a = this.getTideCloakClient().idTokenParsed[key]) !== null && _a !== void 0 ? _a : null;
|
|
224
406
|
}
|
|
225
|
-
/**
|
|
407
|
+
/**
|
|
408
|
+
* Refreshes token if expired or about to expire.
|
|
409
|
+
* @returns {Promise<boolean>}
|
|
410
|
+
* @throws {Error} In hybrid mode (token refresh handled server-side)
|
|
411
|
+
*/
|
|
226
412
|
async updateIAMToken() {
|
|
413
|
+
if (this.isHybridMode()) {
|
|
414
|
+
throw new Error("updateIAMToken() not available in hybrid mode - tokens are server-side");
|
|
415
|
+
}
|
|
227
416
|
const kc = this.getTideCloakClient();
|
|
228
417
|
const refreshed = await kc.updateToken();
|
|
229
418
|
const expiresIn = this.getTokenExp();
|
|
@@ -233,8 +422,15 @@ class IAMService {
|
|
|
233
422
|
document.cookie = `kcToken=${kc.token}; path=/;`;
|
|
234
423
|
return refreshed;
|
|
235
424
|
}
|
|
236
|
-
/**
|
|
425
|
+
/**
|
|
426
|
+
* Force immediate refresh (min validity = -1).
|
|
427
|
+
* @returns {Promise<boolean>}
|
|
428
|
+
* @throws {Error} In hybrid mode (token refresh handled server-side)
|
|
429
|
+
*/
|
|
237
430
|
async forceUpdateToken() {
|
|
431
|
+
if (this.isHybridMode()) {
|
|
432
|
+
throw new Error("forceUpdateToken() not available in hybrid mode - tokens are server-side");
|
|
433
|
+
}
|
|
238
434
|
document.cookie = 'kcToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
239
435
|
const kc = this.getTideCloakClient();
|
|
240
436
|
const refreshed = await kc.updateToken(-1);
|
|
@@ -245,36 +441,334 @@ class IAMService {
|
|
|
245
441
|
document.cookie = `kcToken=${kc.token}; path=/;`;
|
|
246
442
|
return refreshed;
|
|
247
443
|
}
|
|
248
|
-
/**
|
|
249
|
-
|
|
250
|
-
|
|
444
|
+
/**
|
|
445
|
+
* Start login redirect.
|
|
446
|
+
* In hybrid mode, initiates PKCE flow and redirects to IdP.
|
|
447
|
+
* @param {string} [returnUrl] - URL to redirect to after successful auth (hybrid mode only)
|
|
448
|
+
*/
|
|
449
|
+
doLogin(returnUrl = "") {
|
|
450
|
+
var _a, _b;
|
|
451
|
+
console.debug("[IAMService.doLogin] Called with returnUrl:", returnUrl);
|
|
452
|
+
console.debug("[IAMService.doLogin] isHybridMode:", this.isHybridMode());
|
|
453
|
+
console.debug("[IAMService.doLogin] authMode config:", (_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode);
|
|
454
|
+
if (this.isHybridMode()) {
|
|
455
|
+
// Catch and log any errors from the async function
|
|
456
|
+
return this._startHybridLogin(returnUrl).catch(err => {
|
|
457
|
+
console.error("[IAMService.doLogin] Error in hybrid login:", err);
|
|
458
|
+
throw err;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
251
461
|
this.getTideCloakClient().login({
|
|
252
|
-
redirectUri: (
|
|
462
|
+
redirectUri: (_b = this._config["redirectUri"]) !== null && _b !== void 0 ? _b : `${window.location.origin}/auth/redirect`
|
|
253
463
|
});
|
|
254
464
|
}
|
|
255
|
-
/**
|
|
465
|
+
/**
|
|
466
|
+
* Encrypt data via adapter.
|
|
467
|
+
* Not available in hybrid mode (encryption requires client-side doken).
|
|
468
|
+
*/
|
|
256
469
|
async doEncrypt(data) {
|
|
470
|
+
if (this.isHybridMode()) {
|
|
471
|
+
throw new Error("Encrypt not supported in hybrid mode (tokens are server-side)");
|
|
472
|
+
}
|
|
257
473
|
return this.getTideCloakClient().encrypt(data);
|
|
258
474
|
}
|
|
259
|
-
/**
|
|
475
|
+
/**
|
|
476
|
+
* Decrypt data via adapter.
|
|
477
|
+
* Not available in hybrid mode (decryption requires client-side doken).
|
|
478
|
+
*/
|
|
260
479
|
async doDecrypt(data) {
|
|
480
|
+
if (this.isHybridMode()) {
|
|
481
|
+
throw new Error("Decrypt not supported in hybrid mode (tokens are server-side)");
|
|
482
|
+
}
|
|
261
483
|
return this.getTideCloakClient().decrypt(data);
|
|
262
484
|
}
|
|
263
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* Logout, clear cookie/session, then redirect.
|
|
487
|
+
* In hybrid mode, clears local state and emits logout event.
|
|
488
|
+
*/
|
|
264
489
|
doLogout() {
|
|
265
490
|
var _a;
|
|
491
|
+
if (this.isHybridMode()) {
|
|
492
|
+
this._hybridAuthenticated = false;
|
|
493
|
+
this._hybridReturnUrl = null;
|
|
494
|
+
this._emit("logout");
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
266
497
|
document.cookie = 'kcToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
267
498
|
this.getTideCloakClient().logout({
|
|
268
499
|
redirectUri: (_a = this._config["redirectUri"]) !== null && _a !== void 0 ? _a : `${window.location.origin}/auth/redirect`
|
|
269
500
|
});
|
|
270
501
|
}
|
|
271
|
-
/**
|
|
502
|
+
/**
|
|
503
|
+
* Base URL for Tidecloak realm (no trailing slash).
|
|
504
|
+
* In hybrid mode returns empty string.
|
|
505
|
+
*/
|
|
272
506
|
getBaseUrl() {
|
|
273
507
|
var _a, _b;
|
|
508
|
+
if (this.isHybridMode())
|
|
509
|
+
return "";
|
|
274
510
|
return ((_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a["auth-server-url"]) === null || _b === void 0 ? void 0 : _b.replace(/\/$/, "")) || "";
|
|
275
511
|
}
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// HYBRID MODE SUPPORT (private helpers)
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
/**
|
|
516
|
+
* Start hybrid login flow: generate PKCE, store verifier, redirect to IdP.
|
|
517
|
+
* @private
|
|
518
|
+
* @param {string} returnUrl - URL to redirect to after successful auth
|
|
519
|
+
*/
|
|
520
|
+
async _startHybridLogin(returnUrl = "") {
|
|
521
|
+
var _a, _b;
|
|
522
|
+
if (typeof window === "undefined") {
|
|
523
|
+
throw new Error("Cannot login in SSR context");
|
|
524
|
+
}
|
|
525
|
+
const oidc = (_a = this._config) === null || _a === void 0 ? void 0 : _a.oidc;
|
|
526
|
+
const tokenExchange = (_b = this._config) === null || _b === void 0 ? void 0 : _b.tokenExchange;
|
|
527
|
+
console.debug("[IAMService._startHybridLogin] Config:", {
|
|
528
|
+
authorizationEndpoint: oidc === null || oidc === void 0 ? void 0 : oidc.authorizationEndpoint,
|
|
529
|
+
clientId: oidc === null || oidc === void 0 ? void 0 : oidc.clientId,
|
|
530
|
+
redirectUri: oidc === null || oidc === void 0 ? void 0 : oidc.redirectUri,
|
|
531
|
+
tokenExchangeEndpoint: tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.endpoint,
|
|
532
|
+
});
|
|
533
|
+
if (!(oidc === null || oidc === void 0 ? void 0 : oidc.authorizationEndpoint) || !(oidc === null || oidc === void 0 ? void 0 : oidc.clientId) || !(oidc === null || oidc === void 0 ? void 0 : oidc.redirectUri)) {
|
|
534
|
+
throw new Error("Hybrid mode requires config.oidc.authorizationEndpoint, clientId, and redirectUri");
|
|
535
|
+
}
|
|
536
|
+
if (!(tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.endpoint)) {
|
|
537
|
+
throw new Error("Hybrid mode requires config.tokenExchange.endpoint");
|
|
538
|
+
}
|
|
539
|
+
console.debug("[IAMService] Generating PKCE...");
|
|
540
|
+
const { verifier, challenge, method } = await makePkce();
|
|
541
|
+
console.debug("[IAMService] PKCE generated, verifier length:", verifier.length);
|
|
542
|
+
// Store PKCE verifier and return URL in sessionStorage
|
|
543
|
+
sessionStorage.setItem("kc_pkce_verifier", verifier);
|
|
544
|
+
sessionStorage.setItem("kc_return_url", returnUrl || "");
|
|
545
|
+
// Verify storage worked
|
|
546
|
+
const storedVerifier = sessionStorage.getItem("kc_pkce_verifier");
|
|
547
|
+
console.debug("[IAMService] Stored verifier in sessionStorage, retrieved length:", storedVerifier === null || storedVerifier === void 0 ? void 0 : storedVerifier.length);
|
|
548
|
+
// Encode return URL in state parameter
|
|
549
|
+
const state = returnUrl ? `__url_${returnUrl}` : "";
|
|
550
|
+
const scope = oidc.scope || "openid profile email";
|
|
551
|
+
const authUrl = `${oidc.authorizationEndpoint}` +
|
|
552
|
+
`?client_id=${encodeURIComponent(oidc.clientId)}` +
|
|
553
|
+
`&redirect_uri=${encodeURIComponent(oidc.redirectUri)}` +
|
|
554
|
+
`&response_type=code` +
|
|
555
|
+
`&scope=${encodeURIComponent(scope)}` +
|
|
556
|
+
`&state=${encodeURIComponent(state)}` +
|
|
557
|
+
`&code_challenge=${encodeURIComponent(challenge)}` +
|
|
558
|
+
`&code_challenge_method=${encodeURIComponent(method)}` +
|
|
559
|
+
(oidc.prompt ? `&prompt=${encodeURIComponent(oidc.prompt)}` : "");
|
|
560
|
+
console.debug("[IAMService] Redirecting to:", authUrl.substring(0, 100) + "...");
|
|
561
|
+
window.location.assign(authUrl);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get hybrid callback data for custom token exchange.
|
|
565
|
+
* Use this when you need to handle token exchange with your own auth system
|
|
566
|
+
* (e.g., using a custom useLogin hook) instead of IAMService's built-in fetchJson.
|
|
567
|
+
*
|
|
568
|
+
* @param {Object} [opts] - Options
|
|
569
|
+
* @param {boolean} [opts.clearStorage=true] - Whether to clear sessionStorage after reading
|
|
570
|
+
* @param {string} [opts.redirectUri] - Override redirect URI (use when config isn't loaded)
|
|
571
|
+
* @param {string} [opts.provider] - Override provider (defaults to "tidecloak-auth")
|
|
572
|
+
* @returns {{
|
|
573
|
+
* isCallback: boolean,
|
|
574
|
+
* code: string,
|
|
575
|
+
* verifier: string,
|
|
576
|
+
* redirectUri: string,
|
|
577
|
+
* returnUrl: string,
|
|
578
|
+
* provider: string,
|
|
579
|
+
* error: string|null,
|
|
580
|
+
* errorDescription: string|null
|
|
581
|
+
* }}
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* // With redirectUri override (recommended for callback pages after full navigation)
|
|
585
|
+
* const data = IAMService.getHybridCallbackData({
|
|
586
|
+
* redirectUri: process.env.KEYCLOAK_REDIRECTURI,
|
|
587
|
+
* });
|
|
588
|
+
* if (data.isCallback && data.code && data.verifier) {
|
|
589
|
+
* login.execute({
|
|
590
|
+
* accessToken: JSON.stringify({
|
|
591
|
+
* code: data.code,
|
|
592
|
+
* code_verifier: data.verifier,
|
|
593
|
+
* redirect_uri: data.redirectUri,
|
|
594
|
+
* }),
|
|
595
|
+
* provider: data.provider,
|
|
596
|
+
* });
|
|
597
|
+
* }
|
|
598
|
+
*/
|
|
599
|
+
getHybridCallbackData(opts = {}) {
|
|
600
|
+
var _a, _b, _c, _d;
|
|
601
|
+
const { clearStorage = true, redirectUri: optsRedirectUri, provider: optsProvider } = opts;
|
|
602
|
+
if (typeof window === "undefined") {
|
|
603
|
+
return {
|
|
604
|
+
isCallback: false,
|
|
605
|
+
code: "",
|
|
606
|
+
verifier: "",
|
|
607
|
+
redirectUri: "",
|
|
608
|
+
returnUrl: "",
|
|
609
|
+
provider: "",
|
|
610
|
+
error: null,
|
|
611
|
+
errorDescription: null,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
// Return cached data if available (prevents data loss from multiple calls or initIAM clearing storage)
|
|
615
|
+
if (this._cachedCallbackData) {
|
|
616
|
+
console.debug("[IAMService.getHybridCallbackData] Returning cached data");
|
|
617
|
+
// Allow overriding redirectUri and provider even when returning cached data
|
|
618
|
+
return {
|
|
619
|
+
...this._cachedCallbackData,
|
|
620
|
+
redirectUri: optsRedirectUri || this._cachedCallbackData.redirectUri,
|
|
621
|
+
provider: optsProvider || this._cachedCallbackData.provider,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const qs = new URLSearchParams(window.location.search);
|
|
625
|
+
const error = qs.get("error");
|
|
626
|
+
const errorDescription = qs.get("error_description");
|
|
627
|
+
const code = qs.get("code") || "";
|
|
628
|
+
const state = qs.get("state") || "";
|
|
629
|
+
// Decode return URL from state, fallback to sessionStorage
|
|
630
|
+
const stateReturnUrl = state.startsWith("__url_") ? state.substring(6) : "";
|
|
631
|
+
const returnUrl = stateReturnUrl || sessionStorage.getItem("kc_return_url") || "";
|
|
632
|
+
const verifier = sessionStorage.getItem("kc_pkce_verifier") || "";
|
|
633
|
+
// Use opts override, then config, then empty string
|
|
634
|
+
const redirectUri = optsRedirectUri || ((_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a.oidc) === null || _b === void 0 ? void 0 : _b.redirectUri) || "";
|
|
635
|
+
const provider = optsProvider || ((_d = (_c = this._config) === null || _c === void 0 ? void 0 : _c.tokenExchange) === null || _d === void 0 ? void 0 : _d.provider) || "tidecloak-auth";
|
|
636
|
+
const isCallback = !!(code || error);
|
|
637
|
+
console.debug("[IAMService.getHybridCallbackData] code:", code ? code.substring(0, 20) + "..." : "(empty)");
|
|
638
|
+
console.debug("[IAMService.getHybridCallbackData] verifier:", verifier ? `(length: ${verifier.length})` : "(empty)");
|
|
639
|
+
console.debug("[IAMService.getHybridCallbackData] redirectUri:", redirectUri);
|
|
640
|
+
console.debug("[IAMService.getHybridCallbackData] returnUrl:", returnUrl);
|
|
641
|
+
console.debug("[IAMService.getHybridCallbackData] clearStorage:", clearStorage, "isCallback:", isCallback);
|
|
642
|
+
const data = {
|
|
643
|
+
isCallback,
|
|
644
|
+
code,
|
|
645
|
+
verifier,
|
|
646
|
+
redirectUri,
|
|
647
|
+
returnUrl,
|
|
648
|
+
provider,
|
|
649
|
+
error,
|
|
650
|
+
errorDescription,
|
|
651
|
+
};
|
|
652
|
+
// Cache the data before clearing storage
|
|
653
|
+
if (isCallback && verifier) {
|
|
654
|
+
this._cachedCallbackData = data;
|
|
655
|
+
console.debug("[IAMService.getHybridCallbackData] Cached callback data");
|
|
656
|
+
}
|
|
657
|
+
if (clearStorage && isCallback) {
|
|
658
|
+
sessionStorage.removeItem("kc_pkce_verifier");
|
|
659
|
+
sessionStorage.removeItem("kc_return_url");
|
|
660
|
+
console.debug("[IAMService.getHybridCallbackData] Cleared sessionStorage");
|
|
661
|
+
}
|
|
662
|
+
return data;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Handle hybrid redirect callback: exchange code for tokens via backend endpoint.
|
|
666
|
+
* @private
|
|
667
|
+
* @param {Object} opts - Options
|
|
668
|
+
* @param {string} [opts.onMissingVerifierRedirectTo] - URL to redirect if verifier is missing
|
|
669
|
+
* @returns {Promise<{handled: boolean, authenticated: boolean, returnUrl: string}>}
|
|
670
|
+
*/
|
|
671
|
+
async _handleHybridRedirectCallback(opts = {}) {
|
|
672
|
+
var _a, _b, _c, _d, _e;
|
|
673
|
+
if (typeof window === "undefined") {
|
|
674
|
+
return { handled: false, authenticated: false, returnUrl: "" };
|
|
675
|
+
}
|
|
676
|
+
const qs = new URLSearchParams(window.location.search);
|
|
677
|
+
const error = qs.get("error");
|
|
678
|
+
const errorDescription = qs.get("error_description") || "An error occurred";
|
|
679
|
+
const code = qs.get("code") || "";
|
|
680
|
+
const state = qs.get("state") || "";
|
|
681
|
+
// Decode return URL from state, fallback to sessionStorage (Keycloak broker modifies state)
|
|
682
|
+
const stateReturnUrl = state.startsWith("__url_") ? state.substring(6) : "";
|
|
683
|
+
const returnUrl = stateReturnUrl || sessionStorage.getItem("kc_return_url") || "";
|
|
684
|
+
// Handle error response from IdP
|
|
685
|
+
if (error) {
|
|
686
|
+
this._emit("authError", new Error(`${error}: ${errorDescription}`));
|
|
687
|
+
this._emit("ready", false);
|
|
688
|
+
return { handled: true, authenticated: false, returnUrl };
|
|
689
|
+
}
|
|
690
|
+
// No code = not a callback page
|
|
691
|
+
if (!code) {
|
|
692
|
+
console.debug("[IAMService] No code in URL, not a callback page");
|
|
693
|
+
return { handled: false, authenticated: false, returnUrl: "" };
|
|
694
|
+
}
|
|
695
|
+
console.debug("[IAMService] Code found in URL, checking for PKCE verifier...");
|
|
696
|
+
const verifier = sessionStorage.getItem("kc_pkce_verifier") || "";
|
|
697
|
+
const redirectUri = ((_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a.oidc) === null || _b === void 0 ? void 0 : _b.redirectUri) || "";
|
|
698
|
+
console.debug("[IAMService] Retrieved verifier from sessionStorage, length:", verifier.length);
|
|
699
|
+
console.debug("[IAMService] Current origin:", window.location.origin);
|
|
700
|
+
// Cache the callback data so getHybridCallbackData() can access it even after we clear storage
|
|
701
|
+
if (code && verifier) {
|
|
702
|
+
const provider = ((_d = (_c = this._config) === null || _c === void 0 ? void 0 : _c.tokenExchange) === null || _d === void 0 ? void 0 : _d.provider) || "tidecloak-auth";
|
|
703
|
+
this._cachedCallbackData = {
|
|
704
|
+
isCallback: true,
|
|
705
|
+
code,
|
|
706
|
+
verifier,
|
|
707
|
+
redirectUri,
|
|
708
|
+
returnUrl,
|
|
709
|
+
provider,
|
|
710
|
+
error: null,
|
|
711
|
+
errorDescription: null,
|
|
712
|
+
};
|
|
713
|
+
console.debug("[IAMService] Cached callback data from _handleHybridRedirectCallback");
|
|
714
|
+
}
|
|
715
|
+
// Code present but verifier missing (e.g., page refresh after verifier consumed)
|
|
716
|
+
if (code.length > 0 && verifier.length === 0) {
|
|
717
|
+
console.error("[IAMService] PKCE verifier missing! Code present but no verifier in sessionStorage.");
|
|
718
|
+
console.debug("[IAMService] All sessionStorage keys:", Object.keys(sessionStorage));
|
|
719
|
+
if (opts.onMissingVerifierRedirectTo) {
|
|
720
|
+
window.location.assign(opts.onMissingVerifierRedirectTo);
|
|
721
|
+
}
|
|
722
|
+
this._emit("authError", new Error("Missing PKCE verifier (likely page refresh after it was consumed)"));
|
|
723
|
+
this._emit("ready", false);
|
|
724
|
+
return { handled: true, authenticated: false, returnUrl };
|
|
725
|
+
}
|
|
726
|
+
// Clear session storage
|
|
727
|
+
sessionStorage.removeItem("kc_pkce_verifier");
|
|
728
|
+
sessionStorage.removeItem("kc_return_url");
|
|
729
|
+
const tokenExchange = (_e = this._config) === null || _e === void 0 ? void 0 : _e.tokenExchange;
|
|
730
|
+
const exchangeEndpoint = tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.endpoint;
|
|
731
|
+
const provider = (tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.provider) || "tidecloak-auth";
|
|
732
|
+
// Support custom headers (static object or dynamic function)
|
|
733
|
+
const customHeaders = typeof (tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.headers) === "function"
|
|
734
|
+
? tokenExchange.headers()
|
|
735
|
+
: ((tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.headers) || {});
|
|
736
|
+
console.debug("[IAMService] Token exchange endpoint:", exchangeEndpoint);
|
|
737
|
+
console.debug("[IAMService] Custom headers:", customHeaders);
|
|
738
|
+
try {
|
|
739
|
+
// Exchange code for tokens via backend endpoint
|
|
740
|
+
// Payload format matches existing backend expectation
|
|
741
|
+
await fetchJson(exchangeEndpoint, {
|
|
742
|
+
method: "POST",
|
|
743
|
+
headers: customHeaders,
|
|
744
|
+
body: JSON.stringify({
|
|
745
|
+
accessToken: JSON.stringify({
|
|
746
|
+
code: code,
|
|
747
|
+
code_verifier: verifier,
|
|
748
|
+
redirect_uri: redirectUri,
|
|
749
|
+
}),
|
|
750
|
+
provider,
|
|
751
|
+
}),
|
|
752
|
+
});
|
|
753
|
+
this._hybridAuthenticated = true;
|
|
754
|
+
this._hybridReturnUrl = returnUrl || null; // Set BEFORE emitting so getReturnUrl() works in handler
|
|
755
|
+
this._emit("authSuccess");
|
|
756
|
+
// Clean URL (remove code, state, etc.)
|
|
757
|
+
const url = new URL(window.location.href);
|
|
758
|
+
["code", "state", "session_state", "iss", "error", "error_description"].forEach(k => url.searchParams.delete(k));
|
|
759
|
+
window.history.replaceState({}, document.title, url.toString());
|
|
760
|
+
this._emit("ready", true);
|
|
761
|
+
return { handled: true, authenticated: true, returnUrl };
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
this._hybridAuthenticated = false;
|
|
765
|
+
this._emit("authError", err);
|
|
766
|
+
this._emit("ready", false);
|
|
767
|
+
return { handled: true, authenticated: false, returnUrl };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
276
770
|
}
|
|
277
771
|
const IAMServiceInstance = new IAMService();
|
|
278
|
-
|
|
279
|
-
|
|
772
|
+
export { IAMServiceInstance as IAMService };
|
|
773
|
+
export default IAMServiceInstance;
|
|
280
774
|
//# sourceMappingURL=IAMService.js.map
|