@tidecloak/js 0.14.41 → 0.14.42-staging
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 +32 -47
- package/dist/cjs/lib/tidecloak-dpop.js +151 -42
- package/dist/cjs/lib/tidecloak-dpop.js.map +1 -1
- package/dist/cjs/lib/tidecloak.js +300 -378
- package/dist/cjs/lib/tidecloak.js.map +1 -1
- package/dist/cjs/src/AdminAPI.js +12 -19
- package/dist/cjs/src/AdminAPI.js.map +1 -1
- package/dist/cjs/src/IAMService.js +155 -2098
- package/dist/cjs/src/IAMService.js.map +1 -1
- package/dist/cjs/src/index.js +1 -7
- package/dist/cjs/src/index.js.map +1 -1
- package/dist/esm/lib/tidecloak-dpop.js +151 -42
- package/dist/esm/lib/tidecloak-dpop.js.map +1 -1
- package/dist/esm/lib/tidecloak.js +300 -377
- package/dist/esm/lib/tidecloak.js.map +1 -1
- package/dist/esm/src/AdminAPI.js +3 -7
- package/dist/esm/src/AdminAPI.js.map +1 -1
- package/dist/esm/src/IAMService.js +152 -2065
- package/dist/esm/src/IAMService.js.map +1 -1
- package/dist/esm/src/index.js +1 -7
- package/dist/esm/src/index.js.map +1 -1
- package/dist/types/AdminAPI.d.ts +1 -1
- package/dist/types/IAMService.d.ts +52 -603
- package/dist/types/index.d.ts +1 -11
- package/dist/types/lib/tidecloak-dpop.d.ts +19 -3
- package/dist/types/lib/tidecloak.d.ts +46 -38
- package/dist/types/src/AdminAPI.d.ts +1 -1
- package/dist/types/src/IAMService.d.ts +52 -603
- package/dist/types/src/index.d.ts +1 -11
- package/docs/FRONT_CHANNEL.md +55 -55
- package/lib/README.md +10 -14
- package/package.json +4 -25
- package/dist/cjs/lib/secureFetchPolicy.js +0 -45
- package/dist/cjs/lib/secureFetchPolicy.js.map +0 -1
- package/dist/cjs/src/dpopRoute.js +0 -30
- package/dist/cjs/src/dpopRoute.js.map +0 -1
- package/dist/cjs/src/dpopServer.js +0 -77
- package/dist/cjs/src/dpopServer.js.map +0 -1
- package/dist/cjs/src/generated/dpopAuthAsset.js +0 -7
- package/dist/cjs/src/generated/dpopAuthAsset.js.map +0 -1
- package/dist/cjs/src/policy-react.js +0 -51
- package/dist/cjs/src/policy-react.js.map +0 -1
- package/dist/cjs/src/types.js +0 -6
- package/dist/cjs/src/types.js.map +0 -1
- package/dist/cjs/src/utils/dpopAuthPage.js +0 -69
- package/dist/cjs/src/utils/dpopAuthPage.js.map +0 -1
- package/dist/cjs/src/utils/fetch.js +0 -39
- package/dist/cjs/src/utils/fetch.js.map +0 -1
- package/dist/cjs/src/utils/index.js +0 -17
- package/dist/cjs/src/utils/index.js.map +0 -1
- package/dist/cjs/src/utils/initOptions.js +0 -78
- package/dist/cjs/src/utils/initOptions.js.map +0 -1
- package/dist/cjs/src/utils/pkce.js +0 -48
- package/dist/cjs/src/utils/pkce.js.map +0 -1
- package/dist/cjs/src/utils/silentCheckSso.js +0 -138
- package/dist/cjs/src/utils/silentCheckSso.js.map +0 -1
- package/dist/cjs/src/vite.js +0 -43
- package/dist/cjs/src/vite.js.map +0 -1
- package/dist/esm/lib/secureFetchPolicy.js +0 -42
- package/dist/esm/lib/secureFetchPolicy.js.map +0 -1
- package/dist/esm/src/dpopRoute.js +0 -27
- package/dist/esm/src/dpopRoute.js.map +0 -1
- package/dist/esm/src/dpopServer.js +0 -68
- package/dist/esm/src/dpopServer.js.map +0 -1
- package/dist/esm/src/generated/dpopAuthAsset.js +0 -4
- package/dist/esm/src/generated/dpopAuthAsset.js.map +0 -1
- package/dist/esm/src/policy-react.js +0 -45
- package/dist/esm/src/policy-react.js.map +0 -1
- package/dist/esm/src/types.js +0 -5
- package/dist/esm/src/types.js.map +0 -1
- package/dist/esm/src/utils/dpopAuthPage.js +0 -63
- package/dist/esm/src/utils/dpopAuthPage.js.map +0 -1
- package/dist/esm/src/utils/fetch.js +0 -36
- package/dist/esm/src/utils/fetch.js.map +0 -1
- package/dist/esm/src/utils/index.js +0 -6
- package/dist/esm/src/utils/index.js.map +0 -1
- package/dist/esm/src/utils/initOptions.js +0 -75
- package/dist/esm/src/utils/initOptions.js.map +0 -1
- package/dist/esm/src/utils/pkce.js +0 -43
- package/dist/esm/src/utils/pkce.js.map +0 -1
- package/dist/esm/src/utils/silentCheckSso.js +0 -134
- package/dist/esm/src/utils/silentCheckSso.js.map +0 -1
- package/dist/esm/src/vite.js +0 -40
- package/dist/esm/src/vite.js.map +0 -1
- package/dist/types/dpopRoute.d.ts +0 -11
- package/dist/types/dpopServer.d.ts +0 -35
- package/dist/types/generated/dpopAuthAsset.d.ts +0 -2
- package/dist/types/lib/secureFetchPolicy.d.ts +0 -37
- package/dist/types/policy-react.d.ts +0 -40
- package/dist/types/src/dpopRoute.d.ts +0 -11
- package/dist/types/src/dpopServer.d.ts +0 -35
- package/dist/types/src/generated/dpopAuthAsset.d.ts +0 -2
- package/dist/types/src/policy-react.d.ts +0 -40
- package/dist/types/src/types.d.ts +0 -114
- package/dist/types/src/utils/dpopAuthPage.d.ts +0 -22
- package/dist/types/src/utils/fetch.d.ts +0 -11
- package/dist/types/src/utils/index.d.ts +0 -5
- package/dist/types/src/utils/initOptions.d.ts +0 -69
- package/dist/types/src/utils/pkce.d.ts +0 -24
- package/dist/types/src/utils/silentCheckSso.d.ts +0 -82
- package/dist/types/src/vite.d.ts +0 -13
- package/dist/types/types.d.ts +0 -114
- package/dist/types/utils/dpopAuthPage.d.ts +0 -22
- package/dist/types/utils/fetch.d.ts +0 -11
- package/dist/types/utils/index.d.ts +0 -5
- package/dist/types/utils/initOptions.d.ts +0 -69
- package/dist/types/utils/pkce.d.ts +0 -24
- package/dist/types/utils/silentCheckSso.d.ts +0 -82
- package/dist/types/vite.d.ts +0 -13
- package/docs/HYBRID_MODE.md +0 -185
- package/docs/NATIVE_MODE.md +0 -284
|
@@ -1,2142 +1,199 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
if (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
2
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
5
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
8
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
9
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
10
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
11
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
12
|
+
};
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
var _IAMServiceImplementation_instances, _IAMServiceImplementation_core, _IAMServiceImplementation_listeners, _IAMServiceImplementation_initPromise, _IAMServiceImplementation_tide, _IAMServiceImplementation_emit, _IAMServiceImplementation_registerEvents, _IAMServiceImplementation_setTokenCookie, _IAMServiceImplementation_defaultInitOptions, _IAMServiceImplementation_getCore;
|
|
35
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
18
|
exports.IAMService = void 0;
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Singleton IAMService wrapping the TideCloak client.
|
|
41
|
-
*
|
|
42
|
-
* Supports three modes:
|
|
43
|
-
* - **Front-channel mode**: Browser handles all token operations (standard OIDC)
|
|
44
|
-
* - **Hybrid/BFF mode**: Browser handles PKCE, backend exchanges code for tokens (more secure)
|
|
45
|
-
* - **Native mode**: External browser for login, app handles tokens via adapter (Electron, Tauri, React Native)
|
|
46
|
-
*
|
|
47
|
-
* ---
|
|
48
|
-
* ## Front-channel Mode
|
|
49
|
-
*
|
|
50
|
-
* Handlers are called with the event name first, then the event's arguments.
|
|
51
|
-
*
|
|
52
|
-
* Usage A: pass an onReady callback directly
|
|
53
|
-
* ```js
|
|
54
|
-
* import { IAMService } from '@tidecloak/js';
|
|
55
|
-
* import tidecloakConfig from './tidecloak.json';
|
|
56
|
-
*
|
|
57
|
-
* IAMService.initIAM(tidecloakConfig, (_event, authenticated) => {
|
|
58
|
-
* if (!authenticated) IAMService.doLogin();
|
|
59
|
-
* }).catch(console.error);
|
|
60
|
-
* ```
|
|
61
|
-
*
|
|
62
|
-
* Usage B: attach multiple listeners, then init
|
|
63
|
-
* ```js
|
|
64
|
-
* IAMService
|
|
65
|
-
* .on('ready', (_event, auth) => console.log('ready', auth))
|
|
66
|
-
* .on('authError', (_event, err) => console.error('Auth failed', err));
|
|
67
|
-
*
|
|
68
|
-
* await IAMService.initIAM(tidecloakConfig);
|
|
69
|
-
* ```
|
|
70
|
-
*
|
|
71
|
-
* ---
|
|
72
|
-
* ## Hybrid/BFF Mode (Backend-For-Frontend)
|
|
73
|
-
*
|
|
74
|
-
* In hybrid mode, the browser generates PKCE and redirects to the IdP, but the
|
|
75
|
-
* backend exchanges the authorization code for tokens. This keeps tokens server-side
|
|
76
|
-
* for improved security.
|
|
77
|
-
*
|
|
78
|
-
* ### Config shape:
|
|
79
|
-
* ```js
|
|
80
|
-
* const hybridConfig = {
|
|
81
|
-
* authMode: "hybrid",
|
|
82
|
-
* oidc: {
|
|
83
|
-
* authorizationEndpoint: "https://auth.example.com/realms/myrealm/protocol/openid-connect/auth",
|
|
84
|
-
* clientId: "my-client",
|
|
85
|
-
* redirectUri: "https://app.example.com/auth/callback",
|
|
86
|
-
* scope: "openid profile email", // optional, defaults to "openid profile email"
|
|
87
|
-
* prompt: "login" // optional
|
|
88
|
-
* },
|
|
89
|
-
* tokenExchange: {
|
|
90
|
-
* endpoint: "/api/authenticate", // Backend endpoint that exchanges code for tokens
|
|
91
|
-
* provider: "tidecloak-auth", // optional, defaults to "tidecloak-auth"
|
|
92
|
-
* headers: () => ({ // optional, custom headers (e.g., CSRF token)
|
|
93
|
-
* "anti-csrf-token": getCSRFToken()
|
|
94
|
-
* })
|
|
95
|
-
* }
|
|
96
|
-
* };
|
|
97
|
-
* ```
|
|
98
|
-
*
|
|
99
|
-
* ### Login Page:
|
|
100
|
-
* ```js
|
|
101
|
-
* // Load config and trigger login
|
|
102
|
-
* await IAMService.loadConfig(hybridConfig);
|
|
103
|
-
* IAMService.doLogin("/dashboard"); // returnUrl after successful auth
|
|
104
|
-
* ```
|
|
105
|
-
*
|
|
106
|
-
* ### Redirect/Callback Page:
|
|
107
|
-
* ```js
|
|
108
|
-
* // initIAM handles the callback automatically - exchanges code for tokens via backend
|
|
109
|
-
* const authenticated = await IAMService.initIAM(hybridConfig);
|
|
110
|
-
* if (authenticated) {
|
|
111
|
-
* const returnUrl = IAMService.getReturnUrl() || "/";
|
|
112
|
-
* window.location.assign(returnUrl);
|
|
113
|
-
* }
|
|
114
|
-
* ```
|
|
115
|
-
*
|
|
116
|
-
* ### Token Exchange Request Format:
|
|
117
|
-
* The backend endpoint receives a POST request with:
|
|
118
|
-
* ```json
|
|
119
|
-
* {
|
|
120
|
-
* "accessToken": "{\"code\":\"...\",\"code_verifier\":\"...\",\"redirect_uri\":\"...\"}",
|
|
121
|
-
* "provider": "tidecloak-auth"
|
|
122
|
-
* }
|
|
123
|
-
* ```
|
|
124
|
-
*
|
|
125
|
-
* ---
|
|
126
|
-
* ## Events
|
|
127
|
-
* Every handler receives `(eventName, ...args)`.
|
|
128
|
-
* - `ready` - Emitted when initialization completes (with authenticated boolean)
|
|
129
|
-
* - `initError` - Emitted when initialization fails
|
|
130
|
-
* - `authSuccess` - Emitted on successful authentication
|
|
131
|
-
* - `authError` - Emitted on authentication failure
|
|
132
|
-
* - `authRefreshSuccess` - Emitted when token refresh succeeds (front-channel only)
|
|
133
|
-
* - `authRefreshError` - Emitted when token refresh fails (front-channel only)
|
|
134
|
-
* - `logout` - Emitted on logout
|
|
135
|
-
* - `tokenExpired` - Emitted when token expires (front-channel only)
|
|
136
|
-
*/
|
|
137
|
-
class IAMService {
|
|
19
|
+
const tidecloak_js_1 = __importDefault(require("../lib/tidecloak.js"));
|
|
20
|
+
class IAMServiceImplementation {
|
|
138
21
|
constructor() {
|
|
139
|
-
this
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this
|
|
144
|
-
this
|
|
145
|
-
this
|
|
146
|
-
this.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// effect that re-runs before the first init resolves) join the in-flight init
|
|
169
|
-
// instead of short-circuiting past it and reporting a not-yet-populated state.
|
|
170
|
-
this._frontchannelInitPromise = null;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Register an event listener. The handler is called as `handler(event, ...args)`,
|
|
174
|
-
* so the event name comes first, e.g. `on('ready', (_event, authenticated) => ...)`.
|
|
175
|
-
* @param {'ready'|'initError'|'authSuccess'|'authError'|'authRefreshSuccess'|'authRefreshError'|'logout'|'tokenExpired'} event
|
|
176
|
-
* @param {Function} handler
|
|
177
|
-
* @returns {this}
|
|
178
|
-
*/
|
|
179
|
-
on(event, handler) {
|
|
180
|
-
if (!this._listeners[event])
|
|
181
|
-
this._listeners[event] = [];
|
|
182
|
-
this._listeners[event].push(handler);
|
|
183
|
-
return this;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Unregister an event listener.
|
|
187
|
-
* @param {string} event
|
|
188
|
-
* @param {Function} handler
|
|
189
|
-
* @returns {this}
|
|
190
|
-
*/
|
|
191
|
-
off(event, handler) {
|
|
192
|
-
if (this._listeners[event]) {
|
|
193
|
-
this._listeners[event] = this._listeners[event].filter(fn => fn !== handler);
|
|
194
|
-
}
|
|
195
|
-
return this;
|
|
196
|
-
}
|
|
197
|
-
/** @private */
|
|
198
|
-
_emit(event, ...args) {
|
|
199
|
-
(this._listeners[event] || []).forEach(fn => {
|
|
200
|
-
try {
|
|
201
|
-
fn(event, ...args);
|
|
202
|
-
}
|
|
203
|
-
catch (e) {
|
|
204
|
-
console.error(`Error in "${event}" handler:`, e);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Check if running in hybrid mode.
|
|
210
|
-
* @returns {boolean}
|
|
211
|
-
*/
|
|
212
|
-
isHybridMode() {
|
|
213
|
-
var _a;
|
|
214
|
-
return (((_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode) || "frontchannel").toLowerCase() === "hybrid";
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Check if running in native mode.
|
|
218
|
-
* @returns {boolean}
|
|
219
|
-
*/
|
|
220
|
-
isNativeMode() {
|
|
221
|
-
var _a;
|
|
222
|
-
return (((_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode) || "frontchannel").toLowerCase() === "native";
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Load TideCloak configuration and instantiate the client once.
|
|
226
|
-
* @param {Object} config - TideCloak configuration object.
|
|
227
|
-
* @returns {Promise<Object|null>} The loaded config, or null on failure.
|
|
228
|
-
*/
|
|
229
|
-
async loadConfig(config) {
|
|
230
|
-
var _a;
|
|
231
|
-
if (this._config)
|
|
232
|
-
return this._config;
|
|
233
|
-
if (!config || Object.keys(config).length === 0) {
|
|
234
|
-
console.warn("[loadConfig] empty config");
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
// Shallow-copy so we never mutate the caller's object.
|
|
238
|
-
this._config = { ...config };
|
|
239
|
-
// ---------------------------------------------------------------------
|
|
240
|
-
// DPoP is OPT-IN. Do NOT default it on. Ever.
|
|
241
|
-
//
|
|
242
|
-
// `useDPoP` is passed through to `TideCloak.init()` VERBATIM and only when
|
|
243
|
-
// the caller actually set it (see ./utils/initOptions.js). If it is absent,
|
|
244
|
-
// no DPoP provider is constructed, no `dpop_jkt` is appended to the
|
|
245
|
-
// authorization request, and the realm issues a PLAIN access token.
|
|
246
|
-
//
|
|
247
|
-
// A previous revision defaulted this to `{ mode: "strict" }` whenever the
|
|
248
|
-
// caller omitted it ("DPoP on by default"). That silently broke every
|
|
249
|
-
// consumer that runs a deliberately non-DPoP flow: the authorization
|
|
250
|
-
// request grew a `dpop_jkt`, Keycloak issued a `typ: DPoP` /
|
|
251
|
-
// `cnf.jkt`-bound token, and the consumer's plain-Bearer fetch - correct
|
|
252
|
-
// for the flow it thought it was in - was rejected with a bare `401`
|
|
253
|
-
// (RFC 9449: a DPoP-bound token presented as a plain Bearer is invalid).
|
|
254
|
-
// The TideCloak admin console's bootstrap path is exactly this: it omits
|
|
255
|
-
// `useDPoP` on purpose and fetches with a plain Bearer.
|
|
256
|
-
//
|
|
257
|
-
// Enabling a sender-constraint the caller did not ask for changes the
|
|
258
|
-
// shape of the issued token. That is never a safe default to invent.
|
|
259
|
-
// ---------------------------------------------------------------------
|
|
260
|
-
// Hybrid mode: do not construct TideCloak client (tokens are server-side)
|
|
261
|
-
if (this.isHybridMode()) {
|
|
262
|
-
return this._config;
|
|
263
|
-
}
|
|
264
|
-
// Native mode: store adapter, do not construct TideCloak client
|
|
265
|
-
if (this.isNativeMode()) {
|
|
266
|
-
if (!config.adapter) {
|
|
267
|
-
console.error("[loadConfig] Native mode requires config.adapter with platform-specific functions");
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
this._nativeAdapter = config.adapter;
|
|
271
|
-
return this._config;
|
|
272
|
-
}
|
|
273
|
-
try {
|
|
274
|
-
this._tc = new tidecloak_js_1.default({
|
|
275
|
-
url: config["auth-server-url"],
|
|
276
|
-
realm: config.realm,
|
|
277
|
-
clientId: config.resource,
|
|
278
|
-
vendorId: config.vendorId,
|
|
279
|
-
homeOrkUrl: config.homeOrkUrl,
|
|
280
|
-
clientOriginAuth: config['client-origin-auth-' + window.location.origin],
|
|
281
|
-
backgroundUrl: config['backgroundUrl'],
|
|
282
|
-
logoUrl: config['logoUrl'],
|
|
283
|
-
setupRequestEnclave: (_a = config["setupRequestEnclave"]) !== null && _a !== void 0 ? _a : true // set true by default as its most likely for a client to need it
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
console.error("[loadConfig] Failed to initialize TideCloak client:", err);
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
// wire Tidecloak callbacks → our emitter
|
|
291
|
-
this._tc.onReady = auth => this._emit("ready", auth);
|
|
292
|
-
this._tc.onAuthSuccess = () => this._emit("authSuccess");
|
|
293
|
-
this._tc.onAuthError = err => this._emit("authError", err);
|
|
294
|
-
this._tc.onAuthRefreshSuccess = () => this._emit("authRefreshSuccess");
|
|
295
|
-
this._tc.onAuthRefreshError = err => this._emit("authRefreshError", err);
|
|
296
|
-
this._tc.onAuthLogout = () => this._emit("logout");
|
|
297
|
-
this._tc.onTokenExpired = () => this._emit("tokenExpired");
|
|
298
|
-
return this._config;
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Initialize the TideCloak SSO client with silent SSO check.
|
|
302
|
-
* In hybrid mode, handles the redirect callback if present.
|
|
303
|
-
* @param {Object} config - TideCloak configuration object.
|
|
304
|
-
* @param {Function} [onReady] - Optional 'ready' handler, called as `(event, authenticated)`.
|
|
305
|
-
* @returns {Promise<boolean>} true if authenticated, else false.
|
|
306
|
-
*/
|
|
307
|
-
async initIAM(config, onReady) {
|
|
308
|
-
console.debug("[IAMService] Initializing IAM...");
|
|
309
|
-
// register callback on "ready" if provided
|
|
310
|
-
if (typeof onReady === "function") {
|
|
311
|
-
this.on("ready", onReady);
|
|
312
|
-
}
|
|
313
|
-
// no-op on server
|
|
314
|
-
if (typeof window === "undefined") {
|
|
315
|
-
this._emit("initError", new Error("SSR context: cannot initIAM on server"));
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
// load IAM config
|
|
319
|
-
const loaded = await this.loadConfig(config);
|
|
320
|
-
if (!loaded) {
|
|
321
|
-
this._emit("initError", new Error("Failed to load config"));
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
// --- Hybrid mode init: handle redirect callback if present ---
|
|
325
|
-
if (this.isHybridMode()) {
|
|
326
|
-
// Guard against React StrictMode double-execution
|
|
327
|
-
if (this._hybridCallbackHandled) {
|
|
328
|
-
console.debug("[IAMService] Hybrid callback already handled");
|
|
329
|
-
// If there's a pending token exchange, wait for it instead of returning stale state
|
|
330
|
-
if (this._hybridCallbackPromise) {
|
|
331
|
-
console.debug("[IAMService] Waiting for pending token exchange...");
|
|
332
|
-
return this._hybridCallbackPromise;
|
|
333
|
-
}
|
|
334
|
-
this._emit("ready", this._hybridAuthenticated);
|
|
335
|
-
return this._hybridAuthenticated;
|
|
336
|
-
}
|
|
337
|
-
// Check if this is a callback page (has code) - mark as handled BEFORE processing
|
|
338
|
-
const qs = new URLSearchParams(window.location.search);
|
|
339
|
-
if (qs.get("code")) {
|
|
340
|
-
this._hybridCallbackHandled = true;
|
|
341
|
-
}
|
|
342
|
-
// Store the promise so subsequent calls can wait for it (React StrictMode)
|
|
343
|
-
this._hybridCallbackPromise = (async () => {
|
|
344
|
-
const { handled, authenticated, returnUrl } = await this._handleHybridRedirectCallback({
|
|
345
|
-
onMissingVerifierRedirectTo: "/login",
|
|
346
|
-
});
|
|
347
|
-
this._hybridReturnUrl = returnUrl || null;
|
|
348
|
-
// If we weren't on a callback page, emit ready with current state (defaults false)
|
|
349
|
-
if (!handled) {
|
|
350
|
-
this._emit("ready", this._hybridAuthenticated);
|
|
351
|
-
}
|
|
352
|
-
// Clear the promise once complete
|
|
353
|
-
this._hybridCallbackPromise = null;
|
|
354
|
-
return authenticated || this._hybridAuthenticated;
|
|
355
|
-
})();
|
|
356
|
-
return this._hybridCallbackPromise;
|
|
357
|
-
}
|
|
358
|
-
// --- Native mode init: check stored tokens and subscribe to callbacks ---
|
|
359
|
-
if (this.isNativeMode()) {
|
|
360
|
-
// Guard against React StrictMode double-execution
|
|
361
|
-
if (this._nativeCallbackHandled) {
|
|
362
|
-
console.debug("[IAMService] Native callback already handled");
|
|
363
|
-
if (this._nativeCallbackPromise) {
|
|
364
|
-
console.debug("[IAMService] Waiting for pending native token exchange...");
|
|
365
|
-
return this._nativeCallbackPromise;
|
|
366
|
-
}
|
|
367
|
-
this._emit("ready", this._nativeAuthenticated);
|
|
368
|
-
return this._nativeAuthenticated;
|
|
369
|
-
}
|
|
370
|
-
// Mark as handled BEFORE creating the promise to prevent
|
|
371
|
-
// React StrictMode double-execution from re-initializing
|
|
372
|
-
this._nativeCallbackHandled = true;
|
|
373
|
-
this._nativeCallbackPromise = (async () => {
|
|
374
|
-
var _a, _b;
|
|
375
|
-
// Initialize DPoP if configured
|
|
376
|
-
if (((_a = this._config) === null || _a === void 0 ? void 0 : _a.useDPoP) && !this._dpopProvider) {
|
|
377
|
-
try {
|
|
378
|
-
const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
|
|
379
|
-
const issuerUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}`;
|
|
380
|
-
const { DPoPSignatureProvider, BrowserSignatureAlgs } = await Promise.resolve().then(() => __importStar(require('../lib/tidecloak-dpop.js')));
|
|
381
|
-
const alg = this._config.useDPoP.alg || 'ES256';
|
|
382
|
-
this._dpopProvider = new DPoPSignatureProvider({
|
|
383
|
-
issuerUrl: new URL(issuerUrl),
|
|
384
|
-
clientId: clientId,
|
|
385
|
-
serverSupportedAlgorithms: [alg],
|
|
386
|
-
requestedAlgorithm: BrowserSignatureAlgs[alg] || BrowserSignatureAlgs.ES256,
|
|
387
|
-
});
|
|
388
|
-
await this._dpopProvider.init();
|
|
389
|
-
console.debug("[IAMService] DPoP initialized for native mode");
|
|
390
|
-
}
|
|
391
|
-
catch (err) {
|
|
392
|
-
console.warn("[IAMService] Failed to initialize DPoP:", err);
|
|
393
|
-
if (this._config.useDPoP.mode === 'strict') {
|
|
394
|
-
throw err;
|
|
395
|
-
}
|
|
396
|
-
// In 'auto' mode, continue without DPoP
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
// Check for stored tokens
|
|
400
|
-
const storedTokens = await this._nativeAdapter.getTokens();
|
|
401
|
-
// sessionMode: 'online' (default) = validate tokens, refresh if needed, require login if invalid
|
|
402
|
-
// sessionMode: 'offline' = accept stored tokens without validation (for offline-first apps)
|
|
403
|
-
const sessionMode = ((_b = this._config) === null || _b === void 0 ? void 0 : _b.sessionMode) || 'online';
|
|
404
|
-
if (storedTokens) {
|
|
405
|
-
console.debug("[IAMService] Found stored tokens in native mode, sessionMode:", sessionMode);
|
|
406
|
-
// Load doken if present
|
|
407
|
-
if (storedTokens.doken) {
|
|
408
|
-
this._nativeDoken = storedTokens.doken;
|
|
409
|
-
this._nativeDokenParsed = this._parseToken(storedTokens.doken);
|
|
410
|
-
console.debug("[IAMService] Loaded doken from stored tokens");
|
|
411
|
-
}
|
|
412
|
-
// Load voucher if present
|
|
413
|
-
if (storedTokens.voucher) {
|
|
414
|
-
this._nativeVoucher = storedTokens.voucher;
|
|
415
|
-
console.debug("[IAMService] Loaded voucher from stored tokens");
|
|
416
|
-
}
|
|
417
|
-
if (sessionMode === 'offline') {
|
|
418
|
-
// Offline mode: Accept stored tokens without validation
|
|
419
|
-
this._nativeTokens = storedTokens;
|
|
420
|
-
this._nativeAuthenticated = true;
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
// Online mode: Validate tokens before accepting
|
|
424
|
-
try {
|
|
425
|
-
const payload = JSON.parse(atob(storedTokens.accessToken.split('.')[1]));
|
|
426
|
-
const exp = payload.exp * 1000;
|
|
427
|
-
const now = Date.now();
|
|
428
|
-
if (exp > now) {
|
|
429
|
-
// Token still valid
|
|
430
|
-
console.debug("[IAMService] Online mode: token valid, authenticating");
|
|
431
|
-
this._nativeTokens = storedTokens;
|
|
432
|
-
this._nativeAuthenticated = true;
|
|
433
|
-
}
|
|
434
|
-
else if (storedTokens.refreshToken) {
|
|
435
|
-
// Token expired, try refresh
|
|
436
|
-
console.debug("[IAMService] Online mode: token expired, attempting refresh");
|
|
437
|
-
try {
|
|
438
|
-
const newTokens = await this._refreshNativeToken(storedTokens.refreshToken);
|
|
439
|
-
this._nativeTokens = newTokens;
|
|
440
|
-
this._nativeAuthenticated = true;
|
|
441
|
-
await this._nativeAdapter.saveTokens(newTokens);
|
|
442
|
-
console.debug("[IAMService] Online mode: token refreshed successfully");
|
|
443
|
-
}
|
|
444
|
-
catch (refreshErr) {
|
|
445
|
-
console.debug("[IAMService] Online mode: refresh failed, user must login", refreshErr);
|
|
446
|
-
await this._nativeAdapter.deleteTokens();
|
|
447
|
-
this._nativeAuthenticated = false;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
else {
|
|
451
|
-
// Token expired, no refresh token
|
|
452
|
-
console.debug("[IAMService] Online mode: token expired, no refresh token");
|
|
453
|
-
await this._nativeAdapter.deleteTokens();
|
|
454
|
-
this._nativeAuthenticated = false;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
catch (parseErr) {
|
|
458
|
-
console.error("[IAMService] Online mode: failed to parse token", parseErr);
|
|
459
|
-
await this._nativeAdapter.deleteTokens();
|
|
460
|
-
this._nativeAuthenticated = false;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
// Subscribe to auth callbacks from native app
|
|
465
|
-
this._nativeCallbackUnsubscribe = this._nativeAdapter.onAuthCallback(async ({ code, voucher, error, errorDescription }) => {
|
|
466
|
-
// Guard against duplicate callback processing
|
|
467
|
-
if (this._nativeCallbackProcessing || this._nativeAuthenticated) {
|
|
468
|
-
console.debug("[IAMService] Ignoring duplicate native callback");
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
if (error) {
|
|
472
|
-
console.error("[IAMService] Native auth error:", error, errorDescription);
|
|
473
|
-
this._emit("authError", new Error(`${error}: ${errorDescription || "Unknown error"}`));
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (code) {
|
|
477
|
-
await this._handleNativeCallback(code, voucher);
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
// Subscribe to encryption callbacks from native app (if adapter supports it)
|
|
481
|
-
if (this._nativeAdapter.onEncryptionCallback) {
|
|
482
|
-
this._nativeEncryptionCallbackUnsubscribe = this._nativeAdapter.onEncryptionCallback(({ operation, requestId, result, error }) => {
|
|
483
|
-
const pending = this._pendingEncryptionRequests.get(requestId);
|
|
484
|
-
if (pending) {
|
|
485
|
-
this._pendingEncryptionRequests.delete(requestId);
|
|
486
|
-
if (error) {
|
|
487
|
-
pending.reject(new Error(error));
|
|
488
|
-
}
|
|
489
|
-
else if (result) {
|
|
490
|
-
pending.resolve(result);
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
pending.reject(new Error("Empty result from encryption callback"));
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
else {
|
|
497
|
-
console.warn("[IAMService] Received encryption callback for unknown requestId:", requestId);
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
this._emit("ready", this._nativeAuthenticated);
|
|
502
|
-
this._nativeCallbackPromise = null;
|
|
503
|
-
return this._nativeAuthenticated;
|
|
504
|
-
})();
|
|
505
|
-
return this._nativeCallbackPromise;
|
|
506
|
-
}
|
|
507
|
-
// --- Front-channel mode ---
|
|
508
|
-
if (!this._tc) {
|
|
509
|
-
const err = new Error("TideCloak client not available");
|
|
510
|
-
this._emit("initError", err);
|
|
511
|
-
return false;
|
|
512
|
-
}
|
|
513
|
-
if (this._tc.didInitialize) {
|
|
514
|
-
console.debug("[IAMService] IAM Already initialized once.");
|
|
515
|
-
// An init started by an EARLIER call may still be in flight (React
|
|
516
|
-
// StrictMode double-mounts; a provider effect that re-runs before the first
|
|
517
|
-
// init settles). Its trailing `_emit("ready", ...)` has not happened yet, so
|
|
518
|
-
// it will still reach the handler we registered at the top of this call -
|
|
519
|
-
// just join it and let the event do the work.
|
|
520
|
-
if (this._frontchannelInitPromise) {
|
|
521
|
-
return this._frontchannelInitPromise;
|
|
522
|
-
}
|
|
523
|
-
// Init has already SETTLED, and its `ready` event has been emitted and is
|
|
524
|
-
// gone. Anyone registering a handler after the fact - a React provider
|
|
525
|
-
// re-running its effect and re-subscribing after a `reload()` /
|
|
526
|
-
// `approveTideRequests()` - would hear NOTHING, forever. It would go on
|
|
527
|
-
// serving whatever auth state it last observed while this singleton kept
|
|
528
|
-
// refreshing its own token underneath.
|
|
529
|
-
//
|
|
530
|
-
// That desync is exactly how a `401` is manufactured: the consumer sends a
|
|
531
|
-
// stale `Authorization: Bearer <old token>`, `TideCloak.secureFetch` no
|
|
532
|
-
// longer recognises it as the token it holds, silently falls back to a plain
|
|
533
|
-
// non-DPoP `fetch`, and a `dpop.bound.access.tokens` realm rejects a
|
|
534
|
-
// DPoP-bound token presented as a plain Bearer.
|
|
535
|
-
//
|
|
536
|
-
// `ready` is a STATE notification, not a one-shot lifecycle signal, so
|
|
537
|
-
// re-emitting the current state is safe and idempotent - every listener
|
|
538
|
-
// simply re-syncs to the truth.
|
|
539
|
-
const alreadyAuthenticated = !!this._tc.tokenParsed;
|
|
540
|
-
this._emit("ready", alreadyAuthenticated);
|
|
541
|
-
return alreadyAuthenticated;
|
|
542
|
-
}
|
|
543
|
-
this._frontchannelInitPromise = this._initFrontchannel(config);
|
|
544
|
-
try {
|
|
545
|
-
return await this._frontchannelInitPromise;
|
|
546
|
-
}
|
|
547
|
-
finally {
|
|
548
|
-
this._frontchannelInitPromise = null;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Drive the one-and-only `TideCloak.init()` for front-channel mode and emit the
|
|
553
|
-
* resulting `ready` state. Split out of {@link initIAM} so the in-flight promise
|
|
554
|
-
* can be shared with concurrent callers.
|
|
555
|
-
* @private
|
|
556
|
-
* @param {Object} config
|
|
22
|
+
_IAMServiceImplementation_instances.add(this);
|
|
23
|
+
/**
|
|
24
|
+
* @type {TideCloak | null}
|
|
25
|
+
*/
|
|
26
|
+
_IAMServiceImplementation_core.set(this, null);
|
|
27
|
+
_IAMServiceImplementation_listeners.set(this, {});
|
|
28
|
+
_IAMServiceImplementation_initPromise.set(this, null);
|
|
29
|
+
_IAMServiceImplementation_tide.set(this, Object.freeze({
|
|
30
|
+
encrypt: (toEncrypt, decryptionPolicy) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).encrypt(toEncrypt, decryptionPolicy),
|
|
31
|
+
decrypt: (toDecrypt, decryptionPolicy) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).decrypt(toDecrypt, decryptionPolicy),
|
|
32
|
+
draftEncryption: (toEncrypt) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).draftEncryption(toEncrypt),
|
|
33
|
+
commitEncryption: (request, decryptionPolicy) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).commitEncryption(request, decryptionPolicy),
|
|
34
|
+
draftDecryption: (toDecrypt) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).draftDecryption(toDecrypt),
|
|
35
|
+
commitDecryption: (request, decryptionPolicy) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).commitDecryption(request, decryptionPolicy),
|
|
36
|
+
createTideRequest: (encodedRequest) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).createTideRequest(encodedRequest),
|
|
37
|
+
requestTideOperatorApproval: (requests) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).requestTideOperatorApproval(requests),
|
|
38
|
+
executeSignRequest: (request, waitForAll) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).executeSignRequest(request, waitForAll),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
get tide() {
|
|
42
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_tide, "f");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Initialize from the TideCloak adapter config (tidecloak.json)
|
|
46
|
+
* @param {Record<string, any>} json The contents of tidecloak.json
|
|
47
|
+
* @param {import("../lib/tidecloak.js").TideCloakInitOptions | undefined} options Overrides the defaults
|
|
48
|
+
* (check-sso, S256 PKCE, request enclave on, dpopConfig / checkLoginIframe from tidecloak.json, and
|
|
49
|
+
* silentCheckSsoRedirectUri at `<origin>/silent-check-sso.html`). Apps that serve the silent SSO page
|
|
50
|
+
* elsewhere pass their own `silentCheckSsoRedirectUri`.
|
|
557
51
|
* @returns {Promise<boolean>}
|
|
558
52
|
*/
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
redirectUri: pick("redirectUri"),
|
|
583
|
-
});
|
|
584
|
-
if (!silentSso.uri) {
|
|
585
|
-
// FAIL LOUD. Passing `undefined` makes TideCloak skip the silent check
|
|
586
|
-
// entirely and fall back to a redirect-based `prompt=none` login, which
|
|
587
|
-
// still authenticates the user. That is strictly better than emitting a
|
|
588
|
-
// URI we know will 400 and wedge the app on its spinner.
|
|
589
|
-
console.error(`[IAMService] Cannot derive a silent-check-sso redirect URI: ${silentSso.reason}. ` +
|
|
590
|
-
"Skipping silent check-sso and falling back to a redirect-based login. " +
|
|
591
|
-
"Set `silentCheckSsoRedirectUri` in your TideCloak config to a URI that is " +
|
|
592
|
-
"both served by your app and registered on the OIDC client.");
|
|
593
|
-
}
|
|
594
|
-
else if (silentSso.source !== "config") {
|
|
595
|
-
console.debug(`[IAMService] silentCheckSsoRedirectUri derived from ${silentSso.source}: ${silentSso.uri}`);
|
|
596
|
-
}
|
|
597
|
-
// Not awaited: a diagnostic must not slow init.
|
|
598
|
-
if (pick("useDPoP")) {
|
|
599
|
-
const cfg = (_a = this._config) !== null && _a !== void 0 ? _a : config;
|
|
600
|
-
void (0, index_js_1.preflightDpopAuthPage)({
|
|
601
|
-
origin: window.location.origin,
|
|
602
|
-
authServerUrl: cfg === null || cfg === void 0 ? void 0 : cfg["auth-server-url"],
|
|
603
|
-
realm: cfg === null || cfg === void 0 ? void 0 : cfg.realm,
|
|
604
|
-
clientId: cfg === null || cfg === void 0 ? void 0 : cfg.resource,
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
let authenticated = false;
|
|
608
|
-
try {
|
|
609
|
-
authenticated = await this._tc.init((0, index_js_1.buildInitOptions)({
|
|
610
|
-
config: (_b = this._config) !== null && _b !== void 0 ? _b : config,
|
|
611
|
-
setupRequestEnclave: (_c = config.setupRequestEnclave) !== null && _c !== void 0 ? _c : true, // true by default because most clients that uses this will need it on
|
|
612
|
-
silentCheckSsoRedirectUri: silentSso.uri,
|
|
613
|
-
}));
|
|
614
|
-
// if successful, store token for middleware
|
|
615
|
-
if (authenticated && this._tc.token) {
|
|
616
|
-
this._setTokenCookie(this._tc.token);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
catch (err) {
|
|
620
|
-
console.error("[IAMService] TideCloak init error:", err);
|
|
621
|
-
this._emit("initError", err);
|
|
622
|
-
}
|
|
623
|
-
this._emit("ready", authenticated);
|
|
624
|
-
return authenticated;
|
|
625
|
-
}
|
|
626
|
-
/** @private */
|
|
627
|
-
getTideCloakClient() {
|
|
628
|
-
if (!this._tc) {
|
|
629
|
-
throw new Error("TideCloak client not initialized - call initIAM() first");
|
|
53
|
+
init(json, options) {
|
|
54
|
+
if (typeof window === 'undefined')
|
|
55
|
+
return Promise.resolve(false);
|
|
56
|
+
if (__classPrivateFieldGet(this, _IAMServiceImplementation_initPromise, "f"))
|
|
57
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_initPromise, "f");
|
|
58
|
+
const tidecloakConfig = {
|
|
59
|
+
...json,
|
|
60
|
+
url: json['auth-server-url'],
|
|
61
|
+
realm: json.realm,
|
|
62
|
+
clientId: json.resource,
|
|
63
|
+
clientOriginAuth: json[`client-origin-auth-${window.location.origin}`],
|
|
64
|
+
};
|
|
65
|
+
if (!__classPrivateFieldGet(this, _IAMServiceImplementation_core, "f"))
|
|
66
|
+
__classPrivateFieldSet(this, _IAMServiceImplementation_core, new tidecloak_js_1.default(tidecloakConfig), "f");
|
|
67
|
+
__classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_registerEvents).call(this);
|
|
68
|
+
if (!__classPrivateFieldGet(this, _IAMServiceImplementation_initPromise, "f")) {
|
|
69
|
+
__classPrivateFieldSet(this, _IAMServiceImplementation_initPromise, __classPrivateFieldGet(this, _IAMServiceImplementation_core, "f").init({ ...__classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_defaultInitOptions).call(this, json), ...options }).catch((err) => {
|
|
70
|
+
// A TideCloak instance can only be initialized once, so a retry needs a fresh one.
|
|
71
|
+
__classPrivateFieldSet(this, _IAMServiceImplementation_initPromise, null, "f");
|
|
72
|
+
__classPrivateFieldSet(this, _IAMServiceImplementation_core, null, "f");
|
|
73
|
+
throw err;
|
|
74
|
+
}), "f");
|
|
75
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_initPromise, "f");
|
|
630
76
|
}
|
|
631
|
-
return this._tc;
|
|
632
77
|
}
|
|
633
78
|
/**
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
* subrequests) while still allowing the post-login top-level redirect, and
|
|
637
|
-
* marks it `Secure` whenever the page is served over HTTPS.
|
|
638
|
-
* @private
|
|
639
|
-
*/
|
|
640
|
-
_setTokenCookie(token) {
|
|
641
|
-
var _a;
|
|
642
|
-
const secure = typeof window !== "undefined" && ((_a = window.location) === null || _a === void 0 ? void 0 : _a.protocol) === "https:" ? "; Secure" : "";
|
|
643
|
-
document.cookie = `kcToken=${token}; path=/; SameSite=Lax${secure}`;
|
|
644
|
-
}
|
|
645
|
-
/** @private */
|
|
646
|
-
_clearTokenCookie() {
|
|
647
|
-
document.cookie =
|
|
648
|
-
"kcToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax";
|
|
649
|
-
}
|
|
650
|
-
/**
|
|
651
|
-
* Restrict a post-login return URL to a same-origin, root-relative path to
|
|
652
|
-
* prevent open-redirect. The OAuth `state` (and the sessionStorage fallback)
|
|
653
|
-
* round-trips this value and is then handed to `window.location.assign`, so an
|
|
654
|
-
* attacker-seeded absolute/protocol-relative URL ("https://evil", "//evil",
|
|
655
|
-
* "/\\evil", "javascript:...") must be rejected. Falls back to "/".
|
|
656
|
-
* @private
|
|
79
|
+
*
|
|
80
|
+
* @param {import("../lib/tidecloak.js").TideCloakLoginOptions?} loginOpts
|
|
657
81
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return "";
|
|
661
|
-
// Must be a single-leading-slash path; reject protocol-relative "//host"
|
|
662
|
-
// and backslash variants browsers normalise to "//".
|
|
663
|
-
if (url[0] !== "/" || url[1] === "/" || url[1] === "\\")
|
|
664
|
-
return "/";
|
|
665
|
-
// Defensive: reject anything that still parses as having a URL scheme.
|
|
666
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(url))
|
|
667
|
-
return "/";
|
|
668
|
-
return url;
|
|
669
|
-
}
|
|
670
|
-
/** @returns {Object} Loaded config */
|
|
671
|
-
getConfig() {
|
|
672
|
-
if (!this._config) {
|
|
673
|
-
throw new Error("Config not loaded - call initIAM() first");
|
|
674
|
-
}
|
|
675
|
-
return this._config;
|
|
82
|
+
async doLogin(loginOpts) {
|
|
83
|
+
await __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).login(loginOpts);
|
|
676
84
|
}
|
|
677
85
|
/**
|
|
678
|
-
* Whether initialization has already run to completion for the current mode.
|
|
679
|
-
*
|
|
680
|
-
* "Settled", not "started": while an init is still in flight this returns
|
|
681
|
-
* `false`, because the auth state it will produce does not exist yet and reading
|
|
682
|
-
* it would report a spurious logged-out state.
|
|
683
|
-
*
|
|
684
|
-
* Consumers that subscribe to events LATE (a React provider re-running its
|
|
685
|
-
* effect and re-subscribing, say) must use this to re-read the current auth
|
|
686
|
-
* state synchronously rather than waiting on a `ready` event that has already
|
|
687
|
-
* been emitted and is gone.
|
|
688
86
|
*
|
|
689
|
-
* @
|
|
87
|
+
* @param {import("../lib/tidecloak.js").TideCloakLogoutOptions?} logoutOpts
|
|
690
88
|
*/
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
return this._hybridCallbackHandled && !this._hybridCallbackPromise;
|
|
695
|
-
}
|
|
696
|
-
if (this.isNativeMode()) {
|
|
697
|
-
return this._nativeCallbackHandled && !this._nativeCallbackPromise;
|
|
698
|
-
}
|
|
699
|
-
return !!((_a = this._tc) === null || _a === void 0 ? void 0 : _a.didInitialize) && !this._frontchannelInitPromise;
|
|
700
|
-
}
|
|
701
|
-
/** @returns {boolean} Whether there's a valid token (or session in hybrid/native mode) */
|
|
702
|
-
isLoggedIn() {
|
|
703
|
-
if (this.isHybridMode())
|
|
704
|
-
return this._hybridAuthenticated;
|
|
705
|
-
if (this.isNativeMode())
|
|
706
|
-
return this._nativeAuthenticated;
|
|
707
|
-
return !!this.getTideCloakClient().token;
|
|
89
|
+
async doLogout(logoutOpts) {
|
|
90
|
+
__classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_setTokenCookie).call(this);
|
|
91
|
+
await __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).logout(logoutOpts);
|
|
708
92
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
* when no tokens are stored or a refresh fails.
|
|
712
|
-
* @returns {Promise<string|null>}
|
|
713
|
-
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
714
|
-
*/
|
|
715
|
-
async getToken() {
|
|
716
|
-
var _a, _b;
|
|
717
|
-
if (this.isHybridMode()) {
|
|
718
|
-
throw new Error("getToken() not available in hybrid mode - tokens are server-side");
|
|
719
|
-
}
|
|
720
|
-
if (this.isNativeMode()) {
|
|
721
|
-
const tokens = await this._nativeAdapter.getTokens();
|
|
722
|
-
if (!tokens)
|
|
723
|
-
return null;
|
|
724
|
-
// Check if token is expired or about to expire (30 second buffer)
|
|
725
|
-
const now = Date.now();
|
|
726
|
-
const bufferMs = 30 * 1000;
|
|
727
|
-
if (now >= tokens.expiresAt - bufferMs) {
|
|
728
|
-
console.debug("[IAMService] Native token expired, refreshing...");
|
|
729
|
-
try {
|
|
730
|
-
const newTokens = await this._refreshNativeToken(tokens.refreshToken);
|
|
731
|
-
await this._nativeAdapter.saveTokens(newTokens);
|
|
732
|
-
this._nativeTokens = newTokens;
|
|
733
|
-
return newTokens.accessToken;
|
|
734
|
-
}
|
|
735
|
-
catch (err) {
|
|
736
|
-
console.error("[IAMService] Native token refresh failed:", err);
|
|
737
|
-
// The refresh token is no longer usable - drop the session so callers
|
|
738
|
-
// don't keep sending a stale/`null` bearer token, and surface the error.
|
|
739
|
-
this._nativeAuthenticated = false;
|
|
740
|
-
this._nativeTokens = null;
|
|
741
|
-
try {
|
|
742
|
-
await ((_b = (_a = this._nativeAdapter) === null || _a === void 0 ? void 0 : _a.deleteTokens) === null || _b === void 0 ? void 0 : _b.call(_a));
|
|
743
|
-
}
|
|
744
|
-
catch (_) { /* best-effort cleanup */ }
|
|
745
|
-
this._emit("authError", err);
|
|
746
|
-
return null;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return tokens.accessToken;
|
|
750
|
-
}
|
|
751
|
-
const exp = this.getTokenExp();
|
|
752
|
-
if (exp < 3)
|
|
753
|
-
await this.updateIAMToken();
|
|
754
|
-
return this.getTideCloakClient().token;
|
|
93
|
+
getToken() {
|
|
94
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).token;
|
|
755
95
|
}
|
|
756
96
|
/**
|
|
757
|
-
*
|
|
758
|
-
* @returns {
|
|
759
|
-
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
97
|
+
* When the access token expires, in browser time (adjusted for server clock skew).
|
|
98
|
+
* @returns {Date | undefined}
|
|
760
99
|
*/
|
|
761
100
|
getTokenExp() {
|
|
762
|
-
var _a;
|
|
763
|
-
if (this.isHybridMode()) {
|
|
764
|
-
throw new Error("getTokenExp() not available in hybrid mode - tokens are server-side");
|
|
765
|
-
}
|
|
766
|
-
if (this.isNativeMode()) {
|
|
767
|
-
if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
|
|
768
|
-
return 0;
|
|
769
|
-
try {
|
|
770
|
-
const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
|
|
771
|
-
return Math.round(payload.exp - Date.now() / 1000);
|
|
772
|
-
}
|
|
773
|
-
catch (e) {
|
|
774
|
-
console.error("[IAMService] Failed to parse token for expiry:", e);
|
|
775
|
-
return 0;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
const kc = this.getTideCloakClient();
|
|
779
|
-
return Math.round(kc.tokenParsed.exp + kc.timeSkew - Date.now() / 1000);
|
|
780
|
-
}
|
|
781
|
-
/**
|
|
782
|
-
* Get ID token.
|
|
783
|
-
* @returns {string|null}
|
|
784
|
-
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
785
|
-
*/
|
|
786
|
-
getIDToken() {
|
|
787
|
-
var _a;
|
|
788
|
-
if (this.isHybridMode()) {
|
|
789
|
-
throw new Error("getIDToken() not available in hybrid mode - tokens are server-side");
|
|
790
|
-
}
|
|
791
|
-
if (this.isNativeMode()) {
|
|
792
|
-
return ((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.idToken) || null;
|
|
793
|
-
}
|
|
794
|
-
return this.getTideCloakClient().idToken;
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* Get username (preferred_username claim).
|
|
798
|
-
* @returns {string}
|
|
799
|
-
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
800
|
-
*/
|
|
801
|
-
getName() {
|
|
802
|
-
if (this.isHybridMode()) {
|
|
803
|
-
throw new Error("getName() not available in hybrid mode - tokens are server-side");
|
|
804
|
-
}
|
|
805
|
-
if (this.isNativeMode()) {
|
|
806
|
-
return this.getValueFromToken('preferred_username');
|
|
807
|
-
}
|
|
808
|
-
return this.getTideCloakClient().tokenParsed.preferred_username;
|
|
809
|
-
}
|
|
810
|
-
/**
|
|
811
|
-
* Get the return URL after successful hybrid authentication.
|
|
812
|
-
* Only available in hybrid mode after successful auth.
|
|
813
|
-
* @returns {string|null}
|
|
814
|
-
*/
|
|
815
|
-
getReturnUrl() {
|
|
816
|
-
return this._hybridReturnUrl;
|
|
817
|
-
}
|
|
818
|
-
/**
|
|
819
|
-
* Check if user has a realm role.
|
|
820
|
-
* @param {string} role - the name of the role to check
|
|
821
|
-
* @returns {boolean} Whether the user has a given realm role
|
|
822
|
-
* @throws {Error} In hybrid mode (role checks not available client-side)
|
|
823
|
-
*/
|
|
824
|
-
hasRealmRole(role) {
|
|
825
|
-
var _a, _b;
|
|
826
|
-
if (this.isHybridMode()) {
|
|
827
|
-
throw new Error("hasRealmRole() not available in hybrid mode - tokens are server-side");
|
|
828
|
-
}
|
|
829
|
-
if (this.isNativeMode()) {
|
|
830
|
-
if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
|
|
831
|
-
return false;
|
|
832
|
-
try {
|
|
833
|
-
const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
|
|
834
|
-
const realmRoles = ((_b = payload.realm_access) === null || _b === void 0 ? void 0 : _b.roles) || [];
|
|
835
|
-
return realmRoles.includes(role);
|
|
836
|
-
}
|
|
837
|
-
catch (e) {
|
|
838
|
-
console.error("[IAMService] Failed to parse token for realm role check:", e);
|
|
839
|
-
return false;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
return this.getTideCloakClient().hasRealmRole(role);
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* Check if user has a client role.
|
|
846
|
-
* @param {string} role - the name of the role to check
|
|
847
|
-
* @param {string} [client] - optional client-ID (defaults to the configured adapter resource)
|
|
848
|
-
* @returns {boolean} - whether the user has that role
|
|
849
|
-
* @throws {Error} In hybrid mode (role checks not available client-side)
|
|
850
|
-
*/
|
|
851
|
-
hasClientRole(role, client) {
|
|
852
|
-
var _a, _b, _c, _d;
|
|
853
|
-
if (this.isHybridMode()) {
|
|
854
|
-
throw new Error("hasClientRole() not available in hybrid mode - tokens are server-side");
|
|
855
|
-
}
|
|
856
|
-
if (this.isNativeMode()) {
|
|
857
|
-
if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
|
|
858
|
-
return false;
|
|
859
|
-
try {
|
|
860
|
-
const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
|
|
861
|
-
const clientId = client || ((_b = this._config) === null || _b === void 0 ? void 0 : _b.resource);
|
|
862
|
-
const clientRoles = ((_d = (_c = payload.resource_access) === null || _c === void 0 ? void 0 : _c[clientId]) === null || _d === void 0 ? void 0 : _d.roles) || [];
|
|
863
|
-
return clientRoles.includes(role);
|
|
864
|
-
}
|
|
865
|
-
catch (e) {
|
|
866
|
-
console.error("[IAMService] Failed to parse token for client role check:", e);
|
|
867
|
-
return false;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
return this.getTideCloakClient().hasResourceRole(role, client);
|
|
871
|
-
}
|
|
872
|
-
/**
|
|
873
|
-
* Get custom claim from access token.
|
|
874
|
-
* @param {string} key - The name of the claim to retrieve from the Access token's payload.
|
|
875
|
-
* @returns {*} Custom claim from access token
|
|
876
|
-
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
877
|
-
*/
|
|
878
|
-
getValueFromToken(key) {
|
|
879
101
|
var _a;
|
|
880
102
|
var _b;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
|
|
886
|
-
return null;
|
|
887
|
-
try {
|
|
888
|
-
const payload = JSON.parse(atob(this._nativeTokens.accessToken.split('.')[1]));
|
|
889
|
-
return payload[key] !== undefined ? payload[key] : null;
|
|
890
|
-
}
|
|
891
|
-
catch (e) {
|
|
892
|
-
console.error("[IAMService] Failed to parse access token:", e);
|
|
893
|
-
return null;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
return (_b = this.getTideCloakClient().tokenParsed[key]) !== null && _b !== void 0 ? _b : null;
|
|
103
|
+
const exp = (_a = __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).tokenParsed) === null || _a === void 0 ? void 0 : _a.exp;
|
|
104
|
+
if (exp === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
return new Date((exp + ((_b = __classPrivateFieldGet(this, _IAMServiceImplementation_core, "f").timeSkew) !== null && _b !== void 0 ? _b : 0)) * 1000);
|
|
897
107
|
}
|
|
898
|
-
|
|
899
|
-
* Get custom claim from ID token.
|
|
900
|
-
* @param {string} key - The name of the claim to retrieve from the ID token's payload.
|
|
901
|
-
* @returns {*} Custom claim from ID token
|
|
902
|
-
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
903
|
-
*/
|
|
904
|
-
getValueFromIDToken(key) {
|
|
108
|
+
getClaim(name) {
|
|
905
109
|
var _a;
|
|
906
|
-
|
|
907
|
-
if (this.isHybridMode()) {
|
|
908
|
-
throw new Error("getValueFromIDToken() not available in hybrid mode - tokens are server-side");
|
|
909
|
-
}
|
|
910
|
-
if (this.isNativeMode()) {
|
|
911
|
-
if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.idToken))
|
|
912
|
-
return null;
|
|
913
|
-
try {
|
|
914
|
-
const payload = JSON.parse(atob(this._nativeTokens.idToken.split('.')[1]));
|
|
915
|
-
return payload[key] !== undefined ? payload[key] : null;
|
|
916
|
-
}
|
|
917
|
-
catch (e) {
|
|
918
|
-
console.error("[IAMService] Failed to parse ID token:", e);
|
|
919
|
-
return null;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
return (_b = this.getTideCloakClient().idTokenParsed[key]) !== null && _b !== void 0 ? _b : null;
|
|
110
|
+
return (_a = __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).tokenParsed) === null || _a === void 0 ? void 0 : _a[name];
|
|
923
111
|
}
|
|
924
|
-
|
|
925
|
-
* Alias of {@link IAMService#getValueFromIDToken}, with the same casing as the React context.
|
|
926
|
-
* @param {string} key - The name of the claim to retrieve from the ID token's payload.
|
|
927
|
-
* @returns {*} Custom claim from ID token
|
|
928
|
-
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
929
|
-
*/
|
|
930
|
-
getValueFromIdToken(key) {
|
|
931
|
-
return this.getValueFromIDToken(key);
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Refreshes token if expired or about to expire.
|
|
935
|
-
* @returns {Promise<boolean>}
|
|
936
|
-
* @throws {Error} In hybrid mode (token refresh handled server-side)
|
|
937
|
-
*/
|
|
938
|
-
async updateIAMToken() {
|
|
939
|
-
if (this.isHybridMode()) {
|
|
940
|
-
throw new Error("updateIAMToken() not available in hybrid mode - tokens are server-side");
|
|
941
|
-
}
|
|
942
|
-
if (this.isNativeMode()) {
|
|
943
|
-
// Native mode token refresh is handled in getToken(). Only report a refresh
|
|
944
|
-
// as performed when the token was actually expiring AND getToken() returned a
|
|
945
|
-
// fresh token (a failed refresh resolves to null and must not report success).
|
|
946
|
-
const exp = this.getTokenExp();
|
|
947
|
-
if (exp < 30) {
|
|
948
|
-
const refreshedToken = await this.getToken();
|
|
949
|
-
return refreshedToken != null;
|
|
950
|
-
}
|
|
951
|
-
return false;
|
|
952
|
-
}
|
|
953
|
-
const kc = this.getTideCloakClient();
|
|
954
|
-
const refreshed = await kc.updateToken();
|
|
955
|
-
const expiresIn = this.getTokenExp();
|
|
956
|
-
console.debug(refreshed
|
|
957
|
-
? `[updateIAMToken] Refreshed: ${expiresIn}s`
|
|
958
|
-
: `[updateIAMToken] Still valid: ${expiresIn}s`);
|
|
959
|
-
this._setTokenCookie(kc.token);
|
|
960
|
-
return refreshed;
|
|
961
|
-
}
|
|
962
|
-
/**
|
|
963
|
-
* Force immediate refresh (min validity = -1).
|
|
964
|
-
* @returns {Promise<boolean>}
|
|
965
|
-
* @throws {Error} In hybrid mode (token refresh handled server-side)
|
|
966
|
-
*/
|
|
967
|
-
async forceUpdateToken() {
|
|
968
|
-
var _a;
|
|
969
|
-
if (this.isHybridMode()) {
|
|
970
|
-
throw new Error("forceUpdateToken() not available in hybrid mode - tokens are server-side");
|
|
971
|
-
}
|
|
972
|
-
if (this.isNativeMode()) {
|
|
973
|
-
// Force refresh by clearing cached tokens and refreshing
|
|
974
|
-
if ((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.refreshToken) {
|
|
975
|
-
try {
|
|
976
|
-
const newTokens = await this._refreshNativeToken(this._nativeTokens.refreshToken);
|
|
977
|
-
this._nativeTokens = newTokens;
|
|
978
|
-
await this._nativeAdapter.saveTokens(newTokens);
|
|
979
|
-
return true;
|
|
980
|
-
}
|
|
981
|
-
catch (err) {
|
|
982
|
-
console.error("[IAMService] Native force refresh failed:", err);
|
|
983
|
-
return false;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
return false;
|
|
987
|
-
}
|
|
988
|
-
this._clearTokenCookie();
|
|
989
|
-
const kc = this.getTideCloakClient();
|
|
990
|
-
const refreshed = await kc.updateToken(-1);
|
|
991
|
-
const expiresIn = this.getTokenExp();
|
|
992
|
-
console.debug(refreshed
|
|
993
|
-
? `[updateToken] Immediately refreshed: ${expiresIn}s`
|
|
994
|
-
: `[updateToken] No refresh needed: ${expiresIn}s`);
|
|
995
|
-
this._setTokenCookie(kc.token);
|
|
996
|
-
return refreshed;
|
|
997
|
-
}
|
|
998
|
-
/**
|
|
999
|
-
* Start login redirect.
|
|
1000
|
-
* In hybrid mode, initiates PKCE flow and redirects to IdP.
|
|
1001
|
-
* In native mode, opens external browser with auth URL.
|
|
1002
|
-
* @param {string} [returnUrl] - URL to redirect to after successful auth (hybrid/native mode)
|
|
1003
|
-
* @returns {void|Promise<void>} Resolves once the redirect/external-browser flow is initiated (hybrid/native); returns void in front-channel mode.
|
|
1004
|
-
*/
|
|
1005
|
-
doLogin(returnUrl = "") {
|
|
112
|
+
getName() {
|
|
1006
113
|
var _a;
|
|
1007
|
-
|
|
1008
|
-
console.debug("[IAMService.doLogin] Called with returnUrl:", returnUrl);
|
|
1009
|
-
console.debug("[IAMService.doLogin] isHybridMode:", this.isHybridMode());
|
|
1010
|
-
console.debug("[IAMService.doLogin] isNativeMode:", this.isNativeMode());
|
|
1011
|
-
console.debug("[IAMService.doLogin] authMode config:", (_a = this._config) === null || _a === void 0 ? void 0 : _a.authMode);
|
|
1012
|
-
if (this.isHybridMode()) {
|
|
1013
|
-
// Catch and log any errors from the async function
|
|
1014
|
-
return this._startHybridLogin(returnUrl).catch(err => {
|
|
1015
|
-
console.error("[IAMService.doLogin] Error in hybrid login:", err);
|
|
1016
|
-
throw err;
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
if (this.isNativeMode()) {
|
|
1020
|
-
return this._startNativeLogin(returnUrl).catch(err => {
|
|
1021
|
-
console.error("[IAMService.doLogin] Error in native login:", err);
|
|
1022
|
-
throw err;
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
this.getTideCloakClient().login({
|
|
1026
|
-
redirectUri: (_b = this._config["redirectUri"]) !== null && _b !== void 0 ? _b : `${window.location.origin}/auth/redirect`
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
/**
|
|
1030
|
-
* Encrypt data via adapter.
|
|
1031
|
-
* Not available in hybrid mode (encryption requires client-side doken).
|
|
1032
|
-
* In native mode, passing a decryption policy throws.
|
|
1033
|
-
* @param {{ data: string | Uint8Array, tags: string[] }[]} data - Array of objects to encrypt
|
|
1034
|
-
* @param {Uint8Array} [decryption_policy] Optional policy to protect the encrypted data
|
|
1035
|
-
* @returns {Promise<(string | Uint8Array)[]>} Array of encrypted values
|
|
1036
|
-
*/
|
|
1037
|
-
async doEncrypt(data, decryption_policy = null) {
|
|
1038
|
-
if (this.isHybridMode()) {
|
|
1039
|
-
throw new Error("Encrypt not supported in hybrid mode (tokens are server-side)");
|
|
1040
|
-
}
|
|
1041
|
-
if (this.isNativeMode()) {
|
|
1042
|
-
// The native path doesn't forward policies, so fail rather than drop it.
|
|
1043
|
-
if (decryption_policy != null) {
|
|
1044
|
-
throw new Error("encrypt with a decryption policy not supported in native mode");
|
|
1045
|
-
}
|
|
1046
|
-
return this._nativeEncrypt(data);
|
|
1047
|
-
}
|
|
1048
|
-
return this.getTideCloakClient().encrypt(data, decryption_policy);
|
|
114
|
+
return (_a = __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).tokenParsed) === null || _a === void 0 ? void 0 : _a.preferred_username;
|
|
1049
115
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
* Not available in hybrid mode (decryption requires client-side doken).
|
|
1053
|
-
* In native mode, passing a decryption policy throws.
|
|
1054
|
-
* @param {{ encrypted: string | Uint8Array, tags: string[] }[]} data - Array of objects to decrypt
|
|
1055
|
-
* @param {Uint8Array} [decryption_policy] Optional policy the data was encrypted under
|
|
1056
|
-
* @returns {Promise<(string | Uint8Array)[]>} Array of decrypted values
|
|
1057
|
-
*/
|
|
1058
|
-
async doDecrypt(data, decryption_policy = null) {
|
|
1059
|
-
if (this.isHybridMode()) {
|
|
1060
|
-
throw new Error("Decrypt not supported in hybrid mode (tokens are server-side)");
|
|
1061
|
-
}
|
|
1062
|
-
if (this.isNativeMode()) {
|
|
1063
|
-
// The native path doesn't forward policies, so fail rather than drop it.
|
|
1064
|
-
if (decryption_policy != null) {
|
|
1065
|
-
throw new Error("decrypt with a decryption policy not supported in native mode");
|
|
1066
|
-
}
|
|
1067
|
-
return this._nativeDecrypt(data);
|
|
1068
|
-
}
|
|
1069
|
-
return this.getTideCloakClient().decrypt(data, decryption_policy);
|
|
1070
|
-
}
|
|
1071
|
-
/**
|
|
1072
|
-
* Draft an encryption request (two-step encryption flow).
|
|
1073
|
-
* Not available in hybrid or native mode.
|
|
1074
|
-
* @param {{ data: Uint8Array, tags: string[] }[]} data - Array of objects to draft encryption for
|
|
1075
|
-
* @returns {Promise<Uint8Array>} Draft encryption request
|
|
1076
|
-
*/
|
|
1077
|
-
async doDraftEncryption(data) {
|
|
1078
|
-
if (this.isHybridMode()) {
|
|
1079
|
-
throw new Error("draftEncryption not supported in hybrid mode (tokens are server-side)");
|
|
1080
|
-
}
|
|
1081
|
-
if (this.isNativeMode()) {
|
|
1082
|
-
throw new Error("draftEncryption not supported in native mode");
|
|
1083
|
-
}
|
|
1084
|
-
return this.getTideCloakClient().draftEncryption(data);
|
|
116
|
+
hasRealmRole(role) {
|
|
117
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).hasRealmRole(role);
|
|
1085
118
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
* Not available in hybrid or native mode.
|
|
1089
|
-
* @param {Uint8Array} request - Draft request from doDraftEncryption
|
|
1090
|
-
* @param {Uint8Array} decryption_policy - Policy to protect the encrypted data
|
|
1091
|
-
* @returns {Promise<Uint8Array[]>} Encrypted data
|
|
1092
|
-
*/
|
|
1093
|
-
async doCommitEncryption(request, decryption_policy) {
|
|
1094
|
-
if (this.isHybridMode()) {
|
|
1095
|
-
throw new Error("commitEncryption not supported in hybrid mode (tokens are server-side)");
|
|
1096
|
-
}
|
|
1097
|
-
if (this.isNativeMode()) {
|
|
1098
|
-
throw new Error("commitEncryption not supported in native mode");
|
|
1099
|
-
}
|
|
1100
|
-
return this.getTideCloakClient().commitEncryption(request, decryption_policy);
|
|
119
|
+
hasClientRole(role) {
|
|
120
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).hasResourceRole(role);
|
|
1101
121
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
* Not available in hybrid or native mode.
|
|
1105
|
-
* @param {{ encrypted: Uint8Array, tags: string[] }[]} data - Array of objects to draft decryption for
|
|
1106
|
-
* @returns {Promise<Uint8Array>} Draft decryption request
|
|
1107
|
-
*/
|
|
1108
|
-
async doDraftDecryption(data) {
|
|
1109
|
-
if (this.isHybridMode()) {
|
|
1110
|
-
throw new Error("draftDecryption not supported in hybrid mode (tokens are server-side)");
|
|
1111
|
-
}
|
|
1112
|
-
if (this.isNativeMode()) {
|
|
1113
|
-
throw new Error("draftDecryption not supported in native mode");
|
|
1114
|
-
}
|
|
1115
|
-
return this.getTideCloakClient().draftDecryption(data);
|
|
122
|
+
updateToken() {
|
|
123
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).updateToken();
|
|
1116
124
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
* Not available in hybrid or native mode.
|
|
1120
|
-
* @param {Uint8Array} request - Draft request from doDraftDecryption
|
|
1121
|
-
* @param {Uint8Array} decryption_policy - Policy to decrypt the data
|
|
1122
|
-
* @returns {Promise<Uint8Array[]>} Decrypted data
|
|
1123
|
-
*/
|
|
1124
|
-
async doCommitDecryption(request, decryption_policy) {
|
|
1125
|
-
if (this.isHybridMode()) {
|
|
1126
|
-
throw new Error("commitDecryption not supported in hybrid mode (tokens are server-side)");
|
|
1127
|
-
}
|
|
1128
|
-
if (this.isNativeMode()) {
|
|
1129
|
-
throw new Error("commitDecryption not supported in native mode");
|
|
1130
|
-
}
|
|
1131
|
-
return this.getTideCloakClient().commitDecryption(request, decryption_policy);
|
|
125
|
+
forceUpdateToken() {
|
|
126
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).updateToken(-1);
|
|
1132
127
|
}
|
|
1133
128
|
/**
|
|
1134
|
-
* Drop-in replacement for fetch that automatically handles DPoP authentication.
|
|
1135
|
-
* If the request includes a Bearer token matching the TideCloak-managed token,
|
|
1136
|
-
* it's replaced with DPoP authorization and proof. Also manages resource server
|
|
1137
|
-
* nonces automatically. Otherwise, behaves like regular fetch.
|
|
1138
129
|
*
|
|
1139
|
-
* @param {
|
|
1140
|
-
* @param {
|
|
1141
|
-
* @returns {Promise<Response>} A promise that resolves to the fetch Response
|
|
1142
|
-
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
1143
|
-
*
|
|
1144
|
-
* @example
|
|
1145
|
-
* ```js
|
|
1146
|
-
* const token = await IAMService.getToken();
|
|
1147
|
-
* const response = await IAMService.secureFetch('https://api.example.com/data', {
|
|
1148
|
-
* method: 'POST',
|
|
1149
|
-
* headers: {
|
|
1150
|
-
* 'Authorization': `Bearer ${token}`,
|
|
1151
|
-
* 'Content-Type': 'application/json',
|
|
1152
|
-
* },
|
|
1153
|
-
* body: JSON.stringify({ key: 'value' }),
|
|
1154
|
-
* });
|
|
1155
|
-
* ```
|
|
1156
|
-
*/
|
|
1157
|
-
async secureFetch(url, init) {
|
|
1158
|
-
if (this.isHybridMode()) {
|
|
1159
|
-
throw new Error("secureFetch() not available in hybrid mode - tokens are server-side");
|
|
1160
|
-
}
|
|
1161
|
-
if (this.isNativeMode()) {
|
|
1162
|
-
return this._nativeSecureFetch(url, init);
|
|
1163
|
-
}
|
|
1164
|
-
return this.getTideCloakClient().secureFetch(url, init);
|
|
1165
|
-
}
|
|
1166
|
-
/**
|
|
1167
|
-
* Native mode secure fetch with DPoP support.
|
|
1168
|
-
* @private
|
|
130
|
+
* @param {'ready'|'authSuccess'|'authError'|'authRefreshSuccess'|'authRefreshError'|'logout'|'tokenExpired'} event
|
|
131
|
+
* @param {*Function} callback
|
|
1169
132
|
*/
|
|
1170
|
-
|
|
1171
|
-
if (!this._dpopProvider) {
|
|
1172
|
-
// No DPoP configured, fall through to regular fetch
|
|
1173
|
-
return fetch(url, init);
|
|
1174
|
-
}
|
|
1175
|
-
const token = await this.getToken();
|
|
1176
|
-
if (!token) {
|
|
1177
|
-
return fetch(url, init);
|
|
1178
|
-
}
|
|
1179
|
-
const urlStr = typeof url === "string" ? url : url.toString();
|
|
1180
|
-
const method = (init.method || "GET").toUpperCase();
|
|
1181
|
-
const origin = new URL(urlStr).origin;
|
|
1182
|
-
// Check if the Authorization header contains the managed token
|
|
1183
|
-
const headers = new Headers(init.headers || {});
|
|
1184
|
-
const authHeader = headers.get("Authorization") || "";
|
|
1185
|
-
const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
1186
|
-
if (!bearerToken || bearerToken !== token) {
|
|
1187
|
-
// Not our managed token, use regular fetch
|
|
1188
|
-
return fetch(url, init);
|
|
1189
|
-
}
|
|
1190
|
-
// Generate DPoP proof and replace Bearer with DPoP scheme
|
|
1191
|
-
const nonce = this._dpopProvider.getResourceServerNonce(origin);
|
|
1192
|
-
const proof = await this._dpopProvider.generateDPoPProof(urlStr, method, token, nonce);
|
|
1193
|
-
headers.set("Authorization", `DPoP ${token}`);
|
|
1194
|
-
headers.set("DPoP", proof);
|
|
1195
|
-
let response = await fetch(url, { ...init, headers });
|
|
1196
|
-
// Handle DPoP-Nonce from response
|
|
1197
|
-
const dpopNonce = response.headers.get("DPoP-Nonce");
|
|
1198
|
-
if (dpopNonce) {
|
|
1199
|
-
this._dpopProvider.updateResourceServerNonce(origin, dpopNonce);
|
|
1200
|
-
}
|
|
1201
|
-
// Retry only when the server explicitly asks for a DPoP nonce
|
|
1202
|
-
// (WWW-Authenticate: ... error="use_dpop_nonce"). A blind retry on any 401
|
|
1203
|
-
// doubles unrelated auth failures into two requests.
|
|
1204
|
-
const wwwAuth = response.headers.get("WWW-Authenticate") || "";
|
|
1205
|
-
if (response.status === 401 && dpopNonce && wwwAuth.includes('error="use_dpop_nonce"')) {
|
|
1206
|
-
const retryProof = await this._dpopProvider.generateDPoPProof(urlStr, method, token, dpopNonce);
|
|
1207
|
-
headers.set("DPoP", retryProof);
|
|
1208
|
-
response = await fetch(url, { ...init, headers });
|
|
1209
|
-
const retryNonce = response.headers.get("DPoP-Nonce");
|
|
1210
|
-
if (retryNonce) {
|
|
1211
|
-
this._dpopProvider.updateResourceServerNonce(origin, retryNonce);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
return response;
|
|
1215
|
-
}
|
|
1216
|
-
/**
|
|
1217
|
-
* Logout, clear cookie/session, then redirect.
|
|
1218
|
-
* In hybrid mode, clears local state and emits logout event.
|
|
1219
|
-
* In native mode, deletes tokens via adapter and emits logout event.
|
|
1220
|
-
*/
|
|
1221
|
-
async doLogout() {
|
|
133
|
+
on(event, callback) {
|
|
1222
134
|
var _a;
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
this._emit("logout");
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
if (this.isNativeMode()) {
|
|
1230
|
-
await this._nativeAdapter.deleteTokens();
|
|
1231
|
-
this._nativeTokens = null;
|
|
1232
|
-
this._nativeAuthenticated = false;
|
|
1233
|
-
this._nativeDoken = null;
|
|
1234
|
-
this._nativeDokenParsed = null;
|
|
1235
|
-
this._nativeVoucher = null;
|
|
1236
|
-
// Reset callback flags so next login reinitializes properly
|
|
1237
|
-
this._nativeCallbackHandled = false;
|
|
1238
|
-
this._nativeCallbackProcessing = false;
|
|
1239
|
-
this._nativeCallbackPromise = null;
|
|
1240
|
-
// Unsubscribe from callbacks (will be resubscribed on next init)
|
|
1241
|
-
if (this._nativeCallbackUnsubscribe) {
|
|
1242
|
-
this._nativeCallbackUnsubscribe();
|
|
1243
|
-
this._nativeCallbackUnsubscribe = null;
|
|
1244
|
-
}
|
|
1245
|
-
if (this._nativeEncryptionCallbackUnsubscribe) {
|
|
1246
|
-
this._nativeEncryptionCallbackUnsubscribe();
|
|
1247
|
-
this._nativeEncryptionCallbackUnsubscribe = null;
|
|
1248
|
-
}
|
|
1249
|
-
// Close and cleanup the request enclave
|
|
1250
|
-
if (this._nativeRequestEnclave) {
|
|
1251
|
-
try {
|
|
1252
|
-
this._nativeRequestEnclave.close();
|
|
1253
|
-
}
|
|
1254
|
-
catch (e) {
|
|
1255
|
-
console.debug("[IAMService] Error closing request enclave:", e);
|
|
1256
|
-
}
|
|
1257
|
-
this._nativeRequestEnclave = null;
|
|
1258
|
-
}
|
|
1259
|
-
this._emit("logout");
|
|
1260
|
-
return;
|
|
1261
|
-
}
|
|
1262
|
-
this._clearTokenCookie();
|
|
1263
|
-
this.getTideCloakClient().logout({
|
|
1264
|
-
redirectUri: (_a = this._config["redirectUri"]) !== null && _a !== void 0 ? _a : `${window.location.origin}/auth/redirect`
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
/**
|
|
1268
|
-
* Base URL for Tidecloak realm (no trailing slash).
|
|
1269
|
-
* In hybrid mode returns empty string.
|
|
1270
|
-
*/
|
|
1271
|
-
getBaseUrl() {
|
|
1272
|
-
var _a, _b;
|
|
1273
|
-
if (this.isHybridMode())
|
|
1274
|
-
return "";
|
|
1275
|
-
return ((_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a["auth-server-url"]) === null || _b === void 0 ? void 0 : _b.replace(/\/$/, "")) || "";
|
|
1276
|
-
}
|
|
1277
|
-
// ---------------------------------------------------------------------------
|
|
1278
|
-
// HYBRID MODE SUPPORT (private helpers)
|
|
1279
|
-
// ---------------------------------------------------------------------------
|
|
1280
|
-
/**
|
|
1281
|
-
* Start hybrid login flow: generate PKCE, store verifier, redirect to IdP.
|
|
1282
|
-
* @private
|
|
1283
|
-
* @param {string} returnUrl - URL to redirect to after successful auth
|
|
1284
|
-
*/
|
|
1285
|
-
async _startHybridLogin(returnUrl = "") {
|
|
1286
|
-
var _a, _b;
|
|
1287
|
-
if (typeof window === "undefined") {
|
|
1288
|
-
throw new Error("Cannot login in SSR context");
|
|
1289
|
-
}
|
|
1290
|
-
const oidc = (_a = this._config) === null || _a === void 0 ? void 0 : _a.oidc;
|
|
1291
|
-
const tokenExchange = (_b = this._config) === null || _b === void 0 ? void 0 : _b.tokenExchange;
|
|
1292
|
-
console.debug("[IAMService._startHybridLogin] Config:", {
|
|
1293
|
-
authorizationEndpoint: oidc === null || oidc === void 0 ? void 0 : oidc.authorizationEndpoint,
|
|
1294
|
-
clientId: oidc === null || oidc === void 0 ? void 0 : oidc.clientId,
|
|
1295
|
-
redirectUri: oidc === null || oidc === void 0 ? void 0 : oidc.redirectUri,
|
|
1296
|
-
tokenExchangeEndpoint: tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.endpoint,
|
|
1297
|
-
});
|
|
1298
|
-
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)) {
|
|
1299
|
-
throw new Error("Hybrid mode requires config.oidc.authorizationEndpoint, clientId, and redirectUri");
|
|
1300
|
-
}
|
|
1301
|
-
if (!(tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.endpoint)) {
|
|
1302
|
-
throw new Error("Hybrid mode requires config.tokenExchange.endpoint");
|
|
1303
|
-
}
|
|
1304
|
-
console.debug("[IAMService] Generating PKCE...");
|
|
1305
|
-
const { verifier, challenge, method } = await (0, index_js_1.makePkce)();
|
|
1306
|
-
console.debug("[IAMService] PKCE generated, verifier length:", verifier.length);
|
|
1307
|
-
// Store PKCE verifier and return URL in sessionStorage
|
|
1308
|
-
sessionStorage.setItem("kc_pkce_verifier", verifier);
|
|
1309
|
-
sessionStorage.setItem("kc_return_url", returnUrl || "");
|
|
1310
|
-
// Verify storage worked
|
|
1311
|
-
const storedVerifier = sessionStorage.getItem("kc_pkce_verifier");
|
|
1312
|
-
console.debug("[IAMService] Stored verifier in sessionStorage, retrieved length:", storedVerifier === null || storedVerifier === void 0 ? void 0 : storedVerifier.length);
|
|
1313
|
-
// Encode return URL in state parameter
|
|
1314
|
-
const state = returnUrl ? `__url_${returnUrl}` : "";
|
|
1315
|
-
const scope = oidc.scope || "openid profile email";
|
|
1316
|
-
const authUrl = `${oidc.authorizationEndpoint}` +
|
|
1317
|
-
`?client_id=${encodeURIComponent(oidc.clientId)}` +
|
|
1318
|
-
`&redirect_uri=${encodeURIComponent(oidc.redirectUri)}` +
|
|
1319
|
-
`&response_type=code` +
|
|
1320
|
-
`&scope=${encodeURIComponent(scope)}` +
|
|
1321
|
-
`&state=${encodeURIComponent(state)}` +
|
|
1322
|
-
`&code_challenge=${encodeURIComponent(challenge)}` +
|
|
1323
|
-
`&code_challenge_method=${encodeURIComponent(method)}` +
|
|
1324
|
-
(oidc.prompt ? `&prompt=${encodeURIComponent(oidc.prompt)}` : "");
|
|
1325
|
-
console.debug("[IAMService] Redirecting to:", authUrl.substring(0, 100) + "...");
|
|
1326
|
-
window.location.assign(authUrl);
|
|
1327
|
-
}
|
|
1328
|
-
/**
|
|
1329
|
-
* Get hybrid callback data for custom token exchange.
|
|
1330
|
-
* Use this when you need to handle token exchange with your own auth system
|
|
1331
|
-
* (e.g., using a custom useLogin hook) instead of IAMService's built-in fetchJson.
|
|
1332
|
-
*
|
|
1333
|
-
* @param {Object} [opts] - Options
|
|
1334
|
-
* @param {boolean} [opts.clearStorage=true] - Whether to clear sessionStorage after reading
|
|
1335
|
-
* @param {string} [opts.redirectUri] - Override redirect URI (use when config isn't loaded)
|
|
1336
|
-
* @param {string} [opts.provider] - Override provider (defaults to "tidecloak-auth")
|
|
1337
|
-
* @returns {{
|
|
1338
|
-
* isCallback: boolean,
|
|
1339
|
-
* code: string,
|
|
1340
|
-
* verifier: string,
|
|
1341
|
-
* redirectUri: string,
|
|
1342
|
-
* returnUrl: string,
|
|
1343
|
-
* provider: string,
|
|
1344
|
-
* error: string|null,
|
|
1345
|
-
* errorDescription: string|null
|
|
1346
|
-
* }}
|
|
1347
|
-
*
|
|
1348
|
-
* @example
|
|
1349
|
-
* // With redirectUri override (recommended for callback pages after full navigation)
|
|
1350
|
-
* const data = IAMService.getHybridCallbackData({
|
|
1351
|
-
* redirectUri: process.env.KEYCLOAK_REDIRECTURI,
|
|
1352
|
-
* });
|
|
1353
|
-
* if (data.isCallback && data.code && data.verifier) {
|
|
1354
|
-
* login.execute({
|
|
1355
|
-
* accessToken: JSON.stringify({
|
|
1356
|
-
* code: data.code,
|
|
1357
|
-
* code_verifier: data.verifier,
|
|
1358
|
-
* redirect_uri: data.redirectUri,
|
|
1359
|
-
* }),
|
|
1360
|
-
* provider: data.provider,
|
|
1361
|
-
* });
|
|
1362
|
-
* }
|
|
1363
|
-
*/
|
|
1364
|
-
getHybridCallbackData(opts = {}) {
|
|
1365
|
-
var _a, _b, _c, _d;
|
|
1366
|
-
const { clearStorage = true, redirectUri: optsRedirectUri, provider: optsProvider } = opts;
|
|
1367
|
-
if (typeof window === "undefined") {
|
|
1368
|
-
return {
|
|
1369
|
-
isCallback: false,
|
|
1370
|
-
code: "",
|
|
1371
|
-
verifier: "",
|
|
1372
|
-
redirectUri: "",
|
|
1373
|
-
returnUrl: "",
|
|
1374
|
-
provider: "",
|
|
1375
|
-
error: null,
|
|
1376
|
-
errorDescription: null,
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
// Return cached data if available (prevents data loss from multiple calls or initIAM clearing storage)
|
|
1380
|
-
if (this._cachedCallbackData) {
|
|
1381
|
-
console.debug("[IAMService.getHybridCallbackData] Returning cached data");
|
|
1382
|
-
// Allow overriding redirectUri and provider even when returning cached data
|
|
1383
|
-
return {
|
|
1384
|
-
...this._cachedCallbackData,
|
|
1385
|
-
redirectUri: optsRedirectUri || this._cachedCallbackData.redirectUri,
|
|
1386
|
-
provider: optsProvider || this._cachedCallbackData.provider,
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
1389
|
-
const qs = new URLSearchParams(window.location.search);
|
|
1390
|
-
const error = qs.get("error");
|
|
1391
|
-
const errorDescription = qs.get("error_description");
|
|
1392
|
-
const code = qs.get("code") || "";
|
|
1393
|
-
const state = qs.get("state") || "";
|
|
1394
|
-
// Decode return URL from state, fallback to sessionStorage
|
|
1395
|
-
const stateReturnUrl = state.startsWith("__url_") ? state.substring(6) : "";
|
|
1396
|
-
const returnUrl = this._sanitizeReturnUrl(stateReturnUrl || sessionStorage.getItem("kc_return_url") || "");
|
|
1397
|
-
const verifier = sessionStorage.getItem("kc_pkce_verifier") || "";
|
|
1398
|
-
// Use opts override, then config, then empty string
|
|
1399
|
-
const redirectUri = optsRedirectUri || ((_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a.oidc) === null || _b === void 0 ? void 0 : _b.redirectUri) || "";
|
|
1400
|
-
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";
|
|
1401
|
-
const isCallback = !!(code || error);
|
|
1402
|
-
console.debug("[IAMService.getHybridCallbackData] code:", code ? code.substring(0, 20) + "..." : "(empty)");
|
|
1403
|
-
console.debug("[IAMService.getHybridCallbackData] verifier:", verifier ? `(length: ${verifier.length})` : "(empty)");
|
|
1404
|
-
console.debug("[IAMService.getHybridCallbackData] redirectUri:", redirectUri);
|
|
1405
|
-
console.debug("[IAMService.getHybridCallbackData] returnUrl:", returnUrl);
|
|
1406
|
-
console.debug("[IAMService.getHybridCallbackData] clearStorage:", clearStorage, "isCallback:", isCallback);
|
|
1407
|
-
const data = {
|
|
1408
|
-
isCallback,
|
|
1409
|
-
code,
|
|
1410
|
-
verifier,
|
|
1411
|
-
redirectUri,
|
|
1412
|
-
returnUrl,
|
|
1413
|
-
provider,
|
|
1414
|
-
error,
|
|
1415
|
-
errorDescription,
|
|
1416
|
-
};
|
|
1417
|
-
// Cache the data before clearing storage
|
|
1418
|
-
if (isCallback && verifier) {
|
|
1419
|
-
this._cachedCallbackData = data;
|
|
1420
|
-
console.debug("[IAMService.getHybridCallbackData] Cached callback data");
|
|
1421
|
-
}
|
|
1422
|
-
if (clearStorage && isCallback) {
|
|
1423
|
-
sessionStorage.removeItem("kc_pkce_verifier");
|
|
1424
|
-
sessionStorage.removeItem("kc_return_url");
|
|
1425
|
-
console.debug("[IAMService.getHybridCallbackData] Cleared sessionStorage");
|
|
1426
|
-
}
|
|
1427
|
-
return data;
|
|
135
|
+
var _b;
|
|
136
|
+
((_a = (_b = __classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f"))[event]) !== null && _a !== void 0 ? _a : (_b[event] = [])).push(callback);
|
|
137
|
+
return this;
|
|
1428
138
|
}
|
|
1429
139
|
/**
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (typeof window === "undefined") {
|
|
1439
|
-
return { handled: false, authenticated: false, returnUrl: "" };
|
|
1440
|
-
}
|
|
1441
|
-
const qs = new URLSearchParams(window.location.search);
|
|
1442
|
-
const error = qs.get("error");
|
|
1443
|
-
const errorDescription = qs.get("error_description") || "An error occurred";
|
|
1444
|
-
const code = qs.get("code") || "";
|
|
1445
|
-
const state = qs.get("state") || "";
|
|
1446
|
-
// Decode return URL from state, fallback to sessionStorage (Keycloak broker modifies state)
|
|
1447
|
-
const stateReturnUrl = state.startsWith("__url_") ? state.substring(6) : "";
|
|
1448
|
-
const returnUrl = this._sanitizeReturnUrl(stateReturnUrl || sessionStorage.getItem("kc_return_url") || "");
|
|
1449
|
-
// Handle error response from IdP
|
|
1450
|
-
if (error) {
|
|
1451
|
-
this._emit("authError", new Error(`${error}: ${errorDescription}`));
|
|
1452
|
-
this._emit("ready", false);
|
|
1453
|
-
return { handled: true, authenticated: false, returnUrl };
|
|
1454
|
-
}
|
|
1455
|
-
// No code = not a callback page
|
|
1456
|
-
if (!code) {
|
|
1457
|
-
console.debug("[IAMService] No code in URL, not a callback page");
|
|
1458
|
-
return { handled: false, authenticated: false, returnUrl: "" };
|
|
1459
|
-
}
|
|
1460
|
-
console.debug("[IAMService] Code found in URL, checking for PKCE verifier...");
|
|
1461
|
-
const verifier = sessionStorage.getItem("kc_pkce_verifier") || "";
|
|
1462
|
-
const redirectUri = ((_b = (_a = this._config) === null || _a === void 0 ? void 0 : _a.oidc) === null || _b === void 0 ? void 0 : _b.redirectUri) || "";
|
|
1463
|
-
console.debug("[IAMService] Retrieved verifier from sessionStorage, length:", verifier.length);
|
|
1464
|
-
console.debug("[IAMService] Current origin:", window.location.origin);
|
|
1465
|
-
// Cache the callback data so getHybridCallbackData() can access it even after we clear storage
|
|
1466
|
-
if (code && verifier) {
|
|
1467
|
-
const provider = ((_d = (_c = this._config) === null || _c === void 0 ? void 0 : _c.tokenExchange) === null || _d === void 0 ? void 0 : _d.provider) || "tidecloak-auth";
|
|
1468
|
-
this._cachedCallbackData = {
|
|
1469
|
-
isCallback: true,
|
|
1470
|
-
code,
|
|
1471
|
-
verifier,
|
|
1472
|
-
redirectUri,
|
|
1473
|
-
returnUrl,
|
|
1474
|
-
provider,
|
|
1475
|
-
error: null,
|
|
1476
|
-
errorDescription: null,
|
|
1477
|
-
};
|
|
1478
|
-
console.debug("[IAMService] Cached callback data from _handleHybridRedirectCallback");
|
|
1479
|
-
}
|
|
1480
|
-
// Code present but verifier missing (e.g., page refresh after verifier consumed)
|
|
1481
|
-
if (code.length > 0 && verifier.length === 0) {
|
|
1482
|
-
console.error("[IAMService] PKCE verifier missing! Code present but no verifier in sessionStorage.");
|
|
1483
|
-
console.debug("[IAMService] All sessionStorage keys:", Object.keys(sessionStorage));
|
|
1484
|
-
if (opts.onMissingVerifierRedirectTo) {
|
|
1485
|
-
window.location.assign(opts.onMissingVerifierRedirectTo);
|
|
1486
|
-
}
|
|
1487
|
-
this._emit("authError", new Error("Missing PKCE verifier (likely page refresh after it was consumed)"));
|
|
1488
|
-
this._emit("ready", false);
|
|
1489
|
-
return { handled: true, authenticated: false, returnUrl };
|
|
1490
|
-
}
|
|
1491
|
-
// Clear session storage
|
|
1492
|
-
sessionStorage.removeItem("kc_pkce_verifier");
|
|
1493
|
-
sessionStorage.removeItem("kc_return_url");
|
|
1494
|
-
const tokenExchange = (_e = this._config) === null || _e === void 0 ? void 0 : _e.tokenExchange;
|
|
1495
|
-
const exchangeEndpoint = tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.endpoint;
|
|
1496
|
-
const provider = (tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.provider) || "tidecloak-auth";
|
|
1497
|
-
// Support custom headers (static object or dynamic function)
|
|
1498
|
-
const customHeaders = typeof (tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.headers) === "function"
|
|
1499
|
-
? tokenExchange.headers()
|
|
1500
|
-
: ((tokenExchange === null || tokenExchange === void 0 ? void 0 : tokenExchange.headers) || {});
|
|
1501
|
-
console.debug("[IAMService] Token exchange endpoint:", exchangeEndpoint);
|
|
1502
|
-
console.debug("[IAMService] Custom headers:", customHeaders);
|
|
1503
|
-
try {
|
|
1504
|
-
// Exchange code for tokens via backend endpoint
|
|
1505
|
-
// Payload format matches existing backend expectation
|
|
1506
|
-
await (0, index_js_1.fetchJson)(exchangeEndpoint, {
|
|
1507
|
-
method: "POST",
|
|
1508
|
-
headers: customHeaders,
|
|
1509
|
-
body: JSON.stringify({
|
|
1510
|
-
accessToken: JSON.stringify({
|
|
1511
|
-
code: code,
|
|
1512
|
-
code_verifier: verifier,
|
|
1513
|
-
redirect_uri: redirectUri,
|
|
1514
|
-
}),
|
|
1515
|
-
provider,
|
|
1516
|
-
}),
|
|
1517
|
-
});
|
|
1518
|
-
this._hybridAuthenticated = true;
|
|
1519
|
-
this._hybridReturnUrl = returnUrl || null; // Set BEFORE emitting so getReturnUrl() works in handler
|
|
1520
|
-
this._emit("authSuccess");
|
|
1521
|
-
// Clean URL (remove code, state, etc.)
|
|
1522
|
-
const url = new URL(window.location.href);
|
|
1523
|
-
["code", "state", "session_state", "iss", "error", "error_description"].forEach(k => url.searchParams.delete(k));
|
|
1524
|
-
window.history.replaceState({}, document.title, url.toString());
|
|
1525
|
-
this._emit("ready", true);
|
|
1526
|
-
return { handled: true, authenticated: true, returnUrl };
|
|
1527
|
-
}
|
|
1528
|
-
catch (err) {
|
|
1529
|
-
this._hybridAuthenticated = false;
|
|
1530
|
-
this._emit("authError", err);
|
|
1531
|
-
this._emit("ready", false);
|
|
1532
|
-
return { handled: true, authenticated: false, returnUrl };
|
|
140
|
+
* Unregister an event listener.
|
|
141
|
+
* @param {string} event
|
|
142
|
+
* @param {Function} handler
|
|
143
|
+
* @returns {this}
|
|
144
|
+
*/
|
|
145
|
+
off(event, handler) {
|
|
146
|
+
if (__classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f")[event]) {
|
|
147
|
+
__classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f")[event] = __classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f")[event].filter(fn => fn !== handler);
|
|
1533
148
|
}
|
|
149
|
+
return this;
|
|
1534
150
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
// ---------------------------------------------------------------------------
|
|
1538
|
-
/**
|
|
1539
|
-
* Get OIDC configuration from the native config.
|
|
1540
|
-
* @private
|
|
1541
|
-
* @returns {{ authServerUrl: string, realm: string, clientId: string, scope: string }}
|
|
1542
|
-
*/
|
|
1543
|
-
_getNativeOIDCConfig() {
|
|
1544
|
-
const config = this._config;
|
|
1545
|
-
return {
|
|
1546
|
-
authServerUrl: config["auth-server-url"],
|
|
1547
|
-
realm: config.realm,
|
|
1548
|
-
clientId: config.resource,
|
|
1549
|
-
scope: config.scope || "openid profile email",
|
|
1550
|
-
};
|
|
151
|
+
fetch(url, init = {}) {
|
|
152
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).fetch(url, init);
|
|
1551
153
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
* Automatically selects clientOriginAuth based on window.location.origin.
|
|
1555
|
-
* @private
|
|
1556
|
-
* @returns {{ vendorId: string, homeOrkUrl: string, clientOriginAuth: string } | null}
|
|
1557
|
-
*/
|
|
1558
|
-
_getNativeEncryptionConfig() {
|
|
1559
|
-
const config = this._config;
|
|
1560
|
-
if (!config.vendorId) {
|
|
1561
|
-
return null;
|
|
1562
|
-
}
|
|
1563
|
-
// Auto-select clientOriginAuth based on current origin
|
|
1564
|
-
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
|
1565
|
-
const clientOriginAuthKey = `client-origin-auth-${origin}`;
|
|
1566
|
-
const clientOriginAuth = config[clientOriginAuthKey];
|
|
1567
|
-
return {
|
|
1568
|
-
vendorId: config.vendorId,
|
|
1569
|
-
homeOrkUrl: config.homeOrkUrl,
|
|
1570
|
-
clientOriginAuth,
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
/**
|
|
1574
|
-
* Start native login flow: generate PKCE, open external browser with auth URL.
|
|
1575
|
-
* @private
|
|
1576
|
-
* @param {string} returnUrl - URL to redirect to after successful auth
|
|
1577
|
-
*/
|
|
1578
|
-
async _startNativeLogin(returnUrl = "") {
|
|
1579
|
-
console.debug("[IAMService._startNativeLogin] Starting native login flow");
|
|
1580
|
-
// Reset auth state to allow new callback to be processed
|
|
1581
|
-
// This is needed for re-login after session expiry
|
|
1582
|
-
this._nativeAuthenticated = false;
|
|
1583
|
-
this._nativeCallbackProcessing = false;
|
|
1584
|
-
const adapter = this._nativeAdapter;
|
|
1585
|
-
if (!adapter) {
|
|
1586
|
-
throw new Error("Native adapter not configured");
|
|
1587
|
-
}
|
|
1588
|
-
// Get OIDC config from the main config
|
|
1589
|
-
const { authServerUrl, realm, clientId, scope } = this._getNativeOIDCConfig();
|
|
1590
|
-
// Generate PKCE
|
|
1591
|
-
const { verifier, challenge, method } = await (0, index_js_1.makePkce)();
|
|
1592
|
-
console.debug("[IAMService._startNativeLogin] PKCE generated, verifier length:", verifier.length);
|
|
1593
|
-
// Store PKCE verifier in sessionStorage (will be retrieved when callback is received)
|
|
1594
|
-
sessionStorage.setItem("kc_pkce_verifier", verifier);
|
|
1595
|
-
if (returnUrl) {
|
|
1596
|
-
sessionStorage.setItem("kc_return_url", returnUrl);
|
|
1597
|
-
}
|
|
1598
|
-
// Get redirect URI from adapter (can be async)
|
|
1599
|
-
const redirectUri = await Promise.resolve(adapter.getRedirectUri());
|
|
1600
|
-
sessionStorage.setItem("kc_redirect_uri", redirectUri);
|
|
1601
|
-
// Build auth URL
|
|
1602
|
-
const authUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/auth` +
|
|
1603
|
-
`?client_id=${encodeURIComponent(clientId)}` +
|
|
1604
|
-
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
1605
|
-
`&response_type=code` +
|
|
1606
|
-
`&scope=${encodeURIComponent(scope)}` +
|
|
1607
|
-
`&code_challenge=${encodeURIComponent(challenge)}` +
|
|
1608
|
-
`&code_challenge_method=${encodeURIComponent(method)}` +
|
|
1609
|
-
`&prompt=login`;
|
|
1610
|
-
console.debug("[IAMService._startNativeLogin] Opening auth URL in external browser");
|
|
1611
|
-
// Open in external browser via adapter
|
|
1612
|
-
await adapter.openExternalUrl(authUrl);
|
|
154
|
+
isLoggedIn() {
|
|
155
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this).authenticated;
|
|
1613
156
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
* @param {string} [voucher] - Optional voucher fetched during login (for encryption)
|
|
1619
|
-
*/
|
|
1620
|
-
async _handleNativeCallback(code, voucher) {
|
|
1621
|
-
// Guard against concurrent/duplicate callback processing
|
|
1622
|
-
if (this._nativeCallbackProcessing) {
|
|
1623
|
-
console.debug("[IAMService._handleNativeCallback] Already processing callback, ignoring duplicate");
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
this._nativeCallbackProcessing = true;
|
|
1627
|
-
this._nativeCallbackHandled = true;
|
|
1628
|
-
console.debug("[IAMService._handleNativeCallback] Handling native callback with code");
|
|
1629
|
-
const adapter = this._nativeAdapter;
|
|
1630
|
-
const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
|
|
1631
|
-
// Retrieve PKCE verifier
|
|
1632
|
-
const verifier = sessionStorage.getItem("kc_pkce_verifier");
|
|
1633
|
-
const redirectUri = sessionStorage.getItem("kc_redirect_uri") || await Promise.resolve(adapter.getRedirectUri());
|
|
1634
|
-
const returnUrl = sessionStorage.getItem("kc_return_url") || "";
|
|
1635
|
-
// Clear session storage immediately to prevent reuse
|
|
1636
|
-
sessionStorage.removeItem("kc_pkce_verifier");
|
|
1637
|
-
sessionStorage.removeItem("kc_redirect_uri");
|
|
1638
|
-
sessionStorage.removeItem("kc_return_url");
|
|
1639
|
-
if (!verifier) {
|
|
1640
|
-
console.debug("[IAMService._handleNativeCallback] PKCE verifier not found, callback already processed");
|
|
1641
|
-
this._nativeCallbackProcessing = false;
|
|
1642
|
-
// Don't emit error - this is likely a duplicate callback after successful auth
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
157
|
+
}
|
|
158
|
+
_IAMServiceImplementation_core = new WeakMap(), _IAMServiceImplementation_listeners = new WeakMap(), _IAMServiceImplementation_initPromise = new WeakMap(), _IAMServiceImplementation_tide = new WeakMap(), _IAMServiceImplementation_instances = new WeakSet(), _IAMServiceImplementation_emit = function _IAMServiceImplementation_emit(event, ...args) {
|
|
159
|
+
var _a;
|
|
160
|
+
for (const callback of (_a = __classPrivateFieldGet(this, _IAMServiceImplementation_listeners, "f")[event]) !== null && _a !== void 0 ? _a : []) {
|
|
1645
161
|
try {
|
|
1646
|
-
|
|
1647
|
-
const tokenUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/protocol/openid-connect/token`;
|
|
1648
|
-
const body = new URLSearchParams({
|
|
1649
|
-
grant_type: "authorization_code",
|
|
1650
|
-
client_id: clientId,
|
|
1651
|
-
code: code,
|
|
1652
|
-
redirect_uri: redirectUri,
|
|
1653
|
-
code_verifier: verifier,
|
|
1654
|
-
});
|
|
1655
|
-
// Build headers, including DPoP proof if enabled
|
|
1656
|
-
const headers = {
|
|
1657
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
1658
|
-
};
|
|
1659
|
-
if (this._dpopProvider) {
|
|
1660
|
-
const nonce = await this._dpopProvider.getAuthServerNonce();
|
|
1661
|
-
headers['DPoP'] = await this._dpopProvider.generateDPoPProof(tokenUrl, 'POST', undefined, nonce);
|
|
1662
|
-
}
|
|
1663
|
-
let response = await fetch(tokenUrl, {
|
|
1664
|
-
method: "POST",
|
|
1665
|
-
headers,
|
|
1666
|
-
body: body.toString(),
|
|
1667
|
-
});
|
|
1668
|
-
// Handle DPoP nonce retry: server may respond with use_dpop_nonce error
|
|
1669
|
-
if (!response.ok && this._dpopProvider) {
|
|
1670
|
-
const dpopNonce = response.headers.get('DPoP-Nonce');
|
|
1671
|
-
if (dpopNonce) {
|
|
1672
|
-
await this._dpopProvider.updateAuthServerNonce(dpopNonce);
|
|
1673
|
-
headers['DPoP'] = await this._dpopProvider.generateDPoPProof(tokenUrl, 'POST', undefined, dpopNonce);
|
|
1674
|
-
response = await fetch(tokenUrl, {
|
|
1675
|
-
method: "POST",
|
|
1676
|
-
headers,
|
|
1677
|
-
body: body.toString(),
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
if (!response.ok) {
|
|
1682
|
-
const errorText = await response.text();
|
|
1683
|
-
throw new Error(`Token exchange failed: ${errorText}`);
|
|
1684
|
-
}
|
|
1685
|
-
const data = await response.json();
|
|
1686
|
-
console.debug("[IAMService._handleNativeCallback] Token exchange successful");
|
|
1687
|
-
// Note: Voucher is NOT fetched here - the RequestEnclave will fetch it
|
|
1688
|
-
// via its ORK iframe when encryption/decryption is needed. This avoids
|
|
1689
|
-
// the complexity of session cookie handling during login.
|
|
1690
|
-
// Build token object for storage
|
|
1691
|
-
const tokens = {
|
|
1692
|
-
accessToken: data.access_token,
|
|
1693
|
-
refreshToken: data.refresh_token,
|
|
1694
|
-
idToken: data.id_token,
|
|
1695
|
-
doken: data.doken, // Tide doken for encryption/decryption
|
|
1696
|
-
expiresAt: Date.now() + data.expires_in * 1000,
|
|
1697
|
-
};
|
|
1698
|
-
// Save tokens via adapter
|
|
1699
|
-
await adapter.saveTokens(tokens);
|
|
1700
|
-
this._nativeTokens = tokens;
|
|
1701
|
-
this._nativeDoken = data.doken;
|
|
1702
|
-
this._nativeDokenParsed = data.doken ? this._parseToken(data.doken) : null;
|
|
1703
|
-
// Note: _nativeVoucher is NOT set here - RequestEnclave fetches it on-demand
|
|
1704
|
-
this._nativeAuthenticated = true;
|
|
1705
|
-
this._nativeCallbackProcessing = false;
|
|
1706
|
-
this._emit("authSuccess");
|
|
1707
|
-
console.debug("[IAMService._handleNativeCallback] Native auth complete, returnUrl:", returnUrl);
|
|
162
|
+
callback(...args);
|
|
1708
163
|
}
|
|
1709
164
|
catch (err) {
|
|
1710
|
-
console.error(
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
})
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
this._nativeDoken = data.doken;
|
|
1745
|
-
this._nativeDokenParsed = this._parseToken(data.doken);
|
|
1746
|
-
console.debug("[IAMService] Updated doken from token refresh");
|
|
1747
|
-
}
|
|
1748
|
-
return {
|
|
1749
|
-
accessToken: data.access_token,
|
|
1750
|
-
refreshToken: data.refresh_token,
|
|
1751
|
-
idToken: data.id_token,
|
|
1752
|
-
doken: data.doken,
|
|
1753
|
-
expiresAt: Date.now() + data.expires_in * 1000,
|
|
1754
|
-
};
|
|
1755
|
-
}
|
|
1756
|
-
// ---------------------------------------------------------------------------
|
|
1757
|
-
// NATIVE MODE ENCRYPTION SUPPORT (private helpers)
|
|
1758
|
-
// ---------------------------------------------------------------------------
|
|
1759
|
-
/**
|
|
1760
|
-
* Initialize the native RequestEnclave for encryption/decryption.
|
|
1761
|
-
* @private
|
|
1762
|
-
* @param {string} voucherDataUrl - Pre-fetched voucher as data URL
|
|
1763
|
-
*/
|
|
1764
|
-
_initNativeRequestEnclave(voucherDataUrl) {
|
|
1765
|
-
if (!this._nativeDoken) {
|
|
1766
|
-
throw new Error("[IAMService] No doken available for encryption - user must be authenticated with Tide");
|
|
1767
|
-
}
|
|
1768
|
-
if (!this._nativeDokenParsed) {
|
|
1769
|
-
throw new Error("[IAMService] Doken not parsed");
|
|
1770
|
-
}
|
|
1771
|
-
// Get encryption config from the main config (auto-selects clientOriginAuth based on origin)
|
|
1772
|
-
const encryptionConfig = this._getNativeEncryptionConfig();
|
|
1773
|
-
if (!encryptionConfig || !encryptionConfig.vendorId || !encryptionConfig.clientOriginAuth) {
|
|
1774
|
-
throw new Error("[IAMService] Native encryption requires vendorId and client-origin-auth-{origin} in config");
|
|
1775
|
-
}
|
|
1776
|
-
// Reuse existing enclave if already initialized
|
|
1777
|
-
if (this._nativeRequestEnclave) {
|
|
1778
|
-
console.debug("[IAMService] Reusing existing RequestEnclave");
|
|
1779
|
-
return;
|
|
1780
|
-
}
|
|
1781
|
-
const homeOrkOrigin = this._nativeDokenParsed['t.uho'] || encryptionConfig.homeOrkUrl;
|
|
1782
|
-
if (!homeOrkOrigin) {
|
|
1783
|
-
throw new Error("[IAMService] Home ORK URL not available - check doken or config.homeOrkUrl");
|
|
1784
|
-
}
|
|
1785
|
-
console.debug("[IAMService] Initializing native RequestEnclave", {
|
|
1786
|
-
homeOrkOrigin,
|
|
1787
|
-
vendorId: encryptionConfig.vendorId,
|
|
1788
|
-
voucherURL: voucherDataUrl,
|
|
1789
|
-
});
|
|
1790
|
-
this._nativeRequestEnclave = new tidecloak_js_1.RequestEnclave({
|
|
1791
|
-
homeOrkOrigin,
|
|
1792
|
-
signed_client_origin: encryptionConfig.clientOriginAuth,
|
|
1793
|
-
vendorId: encryptionConfig.vendorId,
|
|
1794
|
-
voucherURL: voucherDataUrl,
|
|
1795
|
-
isRunningLocal: false,
|
|
1796
|
-
}).init({
|
|
1797
|
-
doken: this._nativeDoken,
|
|
1798
|
-
dokenRefreshCallback: async () => {
|
|
1799
|
-
// Refresh tokens and return the new doken
|
|
1800
|
-
await this.forceUpdateToken();
|
|
1801
|
-
if (!this._nativeDoken) {
|
|
1802
|
-
throw new Error("[IAMService] No doken available after token refresh");
|
|
1803
|
-
}
|
|
1804
|
-
return this._nativeDoken;
|
|
1805
|
-
},
|
|
1806
|
-
requireReloginCallback: async () => {
|
|
1807
|
-
// User needs to re-authenticate
|
|
1808
|
-
console.warn("[IAMService] Re-authentication required for encryption");
|
|
1809
|
-
this._emit("authError", new Error("Re-authentication required"));
|
|
1810
|
-
},
|
|
1811
|
-
});
|
|
1812
|
-
}
|
|
1813
|
-
/**
|
|
1814
|
-
* Build the encryption page URL for external browser.
|
|
1815
|
-
* This page is hosted on the TideCloak server and has session cookies.
|
|
1816
|
-
* @private
|
|
1817
|
-
* @param {string} operation - 'encrypt' or 'decrypt'
|
|
1818
|
-
* @param {string} requestId - Unique request ID
|
|
1819
|
-
* @param {string} dataBase64 - Base64-encoded data to process
|
|
1820
|
-
* @param {string} tagsJson - JSON-encoded tags array
|
|
1821
|
-
* @param {string} callbackUrl - URL to redirect with result
|
|
1822
|
-
* @returns {string} URL to open in external browser
|
|
1823
|
-
*/
|
|
1824
|
-
_buildEncryptionPageUrl(operation, requestId, dataBase64, tagsJson, callbackUrl) {
|
|
1825
|
-
const { authServerUrl, realm, clientId } = this._getNativeOIDCConfig();
|
|
1826
|
-
const encryptionConfig = this._getNativeEncryptionConfig();
|
|
1827
|
-
// Build URL to a page on the TideCloak server that can perform encryption
|
|
1828
|
-
// This page will have access to session cookies
|
|
1829
|
-
const baseUrl = `${authServerUrl}/realms/${encodeURIComponent(realm)}/tide-encrypt`;
|
|
1830
|
-
const url = new URL(baseUrl);
|
|
1831
|
-
url.searchParams.set('operation', operation);
|
|
1832
|
-
url.searchParams.set('requestId', requestId);
|
|
1833
|
-
url.searchParams.set('data', dataBase64);
|
|
1834
|
-
url.searchParams.set('tags', tagsJson);
|
|
1835
|
-
url.searchParams.set('callback', callbackUrl);
|
|
1836
|
-
url.searchParams.set('vendorId', (encryptionConfig === null || encryptionConfig === void 0 ? void 0 : encryptionConfig.vendorId) || '');
|
|
1837
|
-
url.searchParams.set('clientId', clientId);
|
|
1838
|
-
return url.toString();
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* Perform an encryption operation via external browser.
|
|
1842
|
-
* @private
|
|
1843
|
-
* @param {'encrypt' | 'decrypt'} operation - The operation to perform
|
|
1844
|
-
* @param {string} dataBase64 - Base64-encoded data to process
|
|
1845
|
-
* @param {string[]} tags - Tags for the operation
|
|
1846
|
-
* @returns {Promise<string>} Base64-encoded result
|
|
1847
|
-
*/
|
|
1848
|
-
async _doExternalBrowserOperation(operation, dataBase64, tags) {
|
|
1849
|
-
const adapter = this._nativeAdapter;
|
|
1850
|
-
// Generate unique request ID
|
|
1851
|
-
const requestId = `${operation}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1852
|
-
// Get callback URL for encryption operations
|
|
1853
|
-
let callbackUrl;
|
|
1854
|
-
if (adapter.getEncryptionRedirectUri) {
|
|
1855
|
-
callbackUrl = await Promise.resolve(adapter.getEncryptionRedirectUri());
|
|
1856
|
-
}
|
|
1857
|
-
else {
|
|
1858
|
-
// Default: use auth redirect URI with /encrypt/callback suffix
|
|
1859
|
-
const authRedirectUri = await Promise.resolve(adapter.getRedirectUri());
|
|
1860
|
-
// Replace /callback with /encrypt/callback, or append if no /callback
|
|
1861
|
-
if (authRedirectUri.endsWith('/callback')) {
|
|
1862
|
-
callbackUrl = authRedirectUri.replace('/callback', '/encrypt/callback');
|
|
1863
|
-
}
|
|
1864
|
-
else {
|
|
1865
|
-
callbackUrl = authRedirectUri + '/encrypt/callback';
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
// Build the URL
|
|
1869
|
-
const tagsJson = JSON.stringify(tags);
|
|
1870
|
-
const encryptionUrl = this._buildEncryptionPageUrl(operation, requestId, dataBase64, tagsJson, callbackUrl);
|
|
1871
|
-
console.debug(`[IAMService] Opening external browser for ${operation}:`, {
|
|
1872
|
-
requestId,
|
|
1873
|
-
callbackUrl,
|
|
1874
|
-
urlLength: encryptionUrl.length,
|
|
1875
|
-
});
|
|
1876
|
-
// Create a promise that will resolve when we get the callback
|
|
1877
|
-
const resultPromise = new Promise((resolve, reject) => {
|
|
1878
|
-
// Store the pending request
|
|
1879
|
-
this._pendingEncryptionRequests.set(requestId, { resolve, reject });
|
|
1880
|
-
// Set a timeout (60 seconds)
|
|
1881
|
-
const timeout = setTimeout(() => {
|
|
1882
|
-
if (this._pendingEncryptionRequests.has(requestId)) {
|
|
1883
|
-
this._pendingEncryptionRequests.delete(requestId);
|
|
1884
|
-
reject(new Error(`Encryption ${operation} timed out after 60 seconds`));
|
|
1885
|
-
}
|
|
1886
|
-
}, 60000);
|
|
1887
|
-
// Update the stored request to include timeout cleanup
|
|
1888
|
-
const pending = this._pendingEncryptionRequests.get(requestId);
|
|
1889
|
-
const originalResolve = pending.resolve;
|
|
1890
|
-
const originalReject = pending.reject;
|
|
1891
|
-
pending.resolve = (result) => {
|
|
1892
|
-
clearTimeout(timeout);
|
|
1893
|
-
originalResolve(result);
|
|
1894
|
-
};
|
|
1895
|
-
pending.reject = (error) => {
|
|
1896
|
-
clearTimeout(timeout);
|
|
1897
|
-
originalReject(error);
|
|
1898
|
-
};
|
|
1899
|
-
});
|
|
1900
|
-
// Open the URL in external browser
|
|
1901
|
-
await adapter.openExternalUrl(encryptionUrl);
|
|
1902
|
-
// Wait for the callback
|
|
1903
|
-
return resultPromise;
|
|
1904
|
-
}
|
|
1905
|
-
/**
|
|
1906
|
-
* Get the voucher URL for native mode.
|
|
1907
|
-
* @private
|
|
1908
|
-
* @returns {string} Voucher URL
|
|
1909
|
-
*/
|
|
1910
|
-
_getNativeVoucherUrl() {
|
|
1911
|
-
var _a;
|
|
1912
|
-
if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken)) {
|
|
1913
|
-
throw new Error("[IAMService] No access token available for voucher URL");
|
|
1914
|
-
}
|
|
1915
|
-
const tokenPayload = this._parseToken(this._nativeTokens.accessToken);
|
|
1916
|
-
if (!tokenPayload) {
|
|
1917
|
-
throw new Error("[IAMService] Failed to parse access token for voucher URL");
|
|
1918
|
-
}
|
|
1919
|
-
const sid = tokenPayload.sid;
|
|
1920
|
-
if (!sid) {
|
|
1921
|
-
throw new Error("[IAMService] No session ID in access token for voucher URL");
|
|
1922
|
-
}
|
|
1923
|
-
const { authServerUrl, realm } = this._getNativeOIDCConfig();
|
|
1924
|
-
return `${authServerUrl}/realms/${encodeURIComponent(realm)}/tidevouchers/fromUserSession?sessionId=${encodeURIComponent(sid)}`;
|
|
1925
|
-
}
|
|
1926
|
-
/**
|
|
1927
|
-
* Check if user has a realm role (native mode helper).
|
|
1928
|
-
* @private
|
|
1929
|
-
* @param {string} role - Role to check
|
|
1930
|
-
* @returns {boolean}
|
|
1931
|
-
*/
|
|
1932
|
-
_nativeHasRealmRole(role) {
|
|
1933
|
-
var _a, _b;
|
|
1934
|
-
if (!((_a = this._nativeTokens) === null || _a === void 0 ? void 0 : _a.accessToken))
|
|
1935
|
-
return false;
|
|
1936
|
-
try {
|
|
1937
|
-
const payload = this._parseToken(this._nativeTokens.accessToken);
|
|
1938
|
-
const realmRoles = ((_b = payload === null || payload === void 0 ? void 0 : payload.realm_access) === null || _b === void 0 ? void 0 : _b.roles) || [];
|
|
1939
|
-
return realmRoles.includes(role);
|
|
1940
|
-
}
|
|
1941
|
-
catch (e) {
|
|
1942
|
-
return false;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
/**
|
|
1946
|
-
* Native mode encryption using RequestEnclave.
|
|
1947
|
-
* Uses either a pre-fetched voucher (as data URL) or the live voucherURL
|
|
1948
|
-
* that the ORK iframe will fetch with session cookies.
|
|
1949
|
-
* @private
|
|
1950
|
-
* @param {{ data: string | Uint8Array, tags: string[] }[]} toEncrypt
|
|
1951
|
-
* @returns {Promise<(string | Uint8Array)[]>}
|
|
1952
|
-
*/
|
|
1953
|
-
async _nativeEncrypt(toEncrypt) {
|
|
1954
|
-
// Ensure token is fresh
|
|
1955
|
-
await this.updateIAMToken();
|
|
1956
|
-
if (!Array.isArray(toEncrypt)) {
|
|
1957
|
-
throw new Error("Pass array as parameter");
|
|
1958
|
-
}
|
|
1959
|
-
if (!this._nativeAuthenticated) {
|
|
1960
|
-
throw new Error("Not authenticated");
|
|
1961
|
-
}
|
|
1962
|
-
// Convert and validate input
|
|
1963
|
-
const dataToSend = toEncrypt.map((e) => {
|
|
1964
|
-
if (typeof e !== 'object' || e === null) {
|
|
1965
|
-
throw new Error("All entries must be an object to encrypt");
|
|
1966
|
-
}
|
|
1967
|
-
for (const property of ['data', 'tags']) {
|
|
1968
|
-
if (!e[property]) {
|
|
1969
|
-
throw new Error(`The object is missing the required '${property}' property.`);
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
if (!Array.isArray(e.tags)) {
|
|
1973
|
-
throw new Error("tags must be provided as a string array");
|
|
1974
|
-
}
|
|
1975
|
-
if (typeof e.data !== 'string' && !(e.data instanceof Uint8Array)) {
|
|
1976
|
-
throw new Error("data must be provided as string or Uint8Array");
|
|
1977
|
-
}
|
|
1978
|
-
// Check roles
|
|
1979
|
-
for (const tag of e.tags) {
|
|
1980
|
-
if (typeof tag !== 'string') {
|
|
1981
|
-
throw new Error("tags must be provided as an array of strings");
|
|
1982
|
-
}
|
|
1983
|
-
const tagAccess = this._nativeHasRealmRole(`_tide_${tag}.selfencrypt`);
|
|
1984
|
-
if (!tagAccess) {
|
|
1985
|
-
throw new Error(`User has not been given any access to '${tag}'`);
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
return {
|
|
1989
|
-
data: typeof e.data === 'string' ? this._stringToUint8Array(e.data) : e.data,
|
|
1990
|
-
tags: e.tags,
|
|
1991
|
-
isRaw: typeof e.data === 'string' ? false : true,
|
|
1992
|
-
};
|
|
1993
|
-
});
|
|
1994
|
-
// Get voucher URL - RequestEnclave will fetch it via ORK iframe
|
|
1995
|
-
const voucherUrl = this._getNativeVoucherUrl();
|
|
1996
|
-
console.debug("[IAMService._nativeEncrypt] Using voucherURL:", voucherUrl);
|
|
1997
|
-
this._initNativeRequestEnclave(voucherUrl);
|
|
1998
|
-
const encrypted = await this._nativeRequestEnclave.encrypt(dataToSend);
|
|
1999
|
-
return encrypted.map((cipher, i) => (dataToSend[i].isRaw ? cipher : this._bytesToBase64(cipher)));
|
|
2000
|
-
}
|
|
2001
|
-
/**
|
|
2002
|
-
* Native mode decryption using RequestEnclave.
|
|
2003
|
-
* Uses either a pre-fetched voucher (as data URL) or the live voucherURL
|
|
2004
|
-
* that the ORK iframe will fetch with session cookies.
|
|
2005
|
-
* @private
|
|
2006
|
-
* @param {{ encrypted: string | Uint8Array, tags: string[] }[]} toDecrypt
|
|
2007
|
-
* @returns {Promise<(string | Uint8Array)[]>}
|
|
2008
|
-
*/
|
|
2009
|
-
async _nativeDecrypt(toDecrypt) {
|
|
2010
|
-
// Ensure token is fresh
|
|
2011
|
-
await this.updateIAMToken();
|
|
2012
|
-
if (!Array.isArray(toDecrypt)) {
|
|
2013
|
-
throw new Error("Pass array as parameter");
|
|
2014
|
-
}
|
|
2015
|
-
if (!this._nativeAuthenticated) {
|
|
2016
|
-
throw new Error("Not authenticated");
|
|
2017
|
-
}
|
|
2018
|
-
// Convert and validate input
|
|
2019
|
-
const dataToSend = toDecrypt.map((e) => {
|
|
2020
|
-
if (typeof e !== 'object' || e === null) {
|
|
2021
|
-
throw new Error("All entries must be an object to decrypt");
|
|
2022
|
-
}
|
|
2023
|
-
for (const property of ['encrypted', 'tags']) {
|
|
2024
|
-
if (!e[property]) {
|
|
2025
|
-
throw new Error(`The object is missing the required '${property}' property.`);
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
if (!Array.isArray(e.tags)) {
|
|
2029
|
-
throw new Error("tags must be provided as a string array");
|
|
2030
|
-
}
|
|
2031
|
-
if (typeof e.encrypted !== 'string' && !(e.encrypted instanceof Uint8Array)) {
|
|
2032
|
-
throw new Error("encrypted must be provided as string or Uint8Array");
|
|
2033
|
-
}
|
|
2034
|
-
// Check roles
|
|
2035
|
-
for (const tag of e.tags) {
|
|
2036
|
-
if (typeof tag !== 'string') {
|
|
2037
|
-
throw new Error("tags must be provided as an array of strings");
|
|
2038
|
-
}
|
|
2039
|
-
const tagAccess = this._nativeHasRealmRole(`_tide_${tag}.selfdecrypt`);
|
|
2040
|
-
if (!tagAccess) {
|
|
2041
|
-
throw new Error(`User has not been given any access to '${tag}'`);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
return {
|
|
2045
|
-
encrypted: typeof e.encrypted === 'string' ? this._base64ToBytes(e.encrypted) : e.encrypted,
|
|
2046
|
-
tags: e.tags,
|
|
2047
|
-
isRaw: typeof e.encrypted === 'string' ? false : true,
|
|
2048
|
-
};
|
|
2049
|
-
});
|
|
2050
|
-
// Get voucher URL - RequestEnclave will fetch it via ORK iframe
|
|
2051
|
-
const voucherUrl = this._getNativeVoucherUrl();
|
|
2052
|
-
console.debug("[IAMService._nativeDecrypt] Using voucherURL:", voucherUrl);
|
|
2053
|
-
this._initNativeRequestEnclave(voucherUrl);
|
|
2054
|
-
const decrypted = await this._nativeRequestEnclave.decrypt(dataToSend);
|
|
2055
|
-
return decrypted.map((d, i) => (dataToSend[i].isRaw ? d : this._stringFromUint8Array(d)));
|
|
2056
|
-
}
|
|
2057
|
-
/**
|
|
2058
|
-
* Convert string to Uint8Array.
|
|
2059
|
-
* @private
|
|
2060
|
-
*/
|
|
2061
|
-
_stringToUint8Array(str) {
|
|
2062
|
-
return new TextEncoder().encode(str);
|
|
2063
|
-
}
|
|
2064
|
-
/**
|
|
2065
|
-
* Convert Uint8Array to string.
|
|
2066
|
-
* @private
|
|
2067
|
-
*/
|
|
2068
|
-
_stringFromUint8Array(bytes) {
|
|
2069
|
-
return new TextDecoder('utf-8').decode(bytes);
|
|
2070
|
-
}
|
|
2071
|
-
/**
|
|
2072
|
-
* Convert bytes to base64.
|
|
2073
|
-
* @private
|
|
2074
|
-
*/
|
|
2075
|
-
_bytesToBase64(bytes) {
|
|
2076
|
-
const binString = String.fromCodePoint(...bytes);
|
|
2077
|
-
return btoa(binString);
|
|
2078
|
-
}
|
|
2079
|
-
/**
|
|
2080
|
-
* Convert base64 to bytes.
|
|
2081
|
-
* @private
|
|
2082
|
-
*/
|
|
2083
|
-
_base64ToBytes(base64) {
|
|
2084
|
-
const binString = atob(base64);
|
|
2085
|
-
const len = binString.length;
|
|
2086
|
-
const bytes = new Uint8Array(len);
|
|
2087
|
-
for (let i = 0; i < len; i++) {
|
|
2088
|
-
bytes[i] = binString.codePointAt(i);
|
|
2089
|
-
}
|
|
2090
|
-
return bytes;
|
|
2091
|
-
}
|
|
2092
|
-
/**
|
|
2093
|
-
* Parse a JWT token and return its payload.
|
|
2094
|
-
* @private
|
|
2095
|
-
* @param {string} token - JWT token string
|
|
2096
|
-
* @returns {Object|null} Parsed token payload or null on error
|
|
2097
|
-
*/
|
|
2098
|
-
_parseToken(token) {
|
|
2099
|
-
if (!token)
|
|
2100
|
-
return null;
|
|
2101
|
-
try {
|
|
2102
|
-
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
2103
|
-
return payload;
|
|
2104
|
-
}
|
|
2105
|
-
catch (e) {
|
|
2106
|
-
console.error("[IAMService] Failed to parse token:", e);
|
|
2107
|
-
return null;
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
/**
|
|
2111
|
-
* Cleanup native mode resources.
|
|
2112
|
-
*/
|
|
2113
|
-
destroy() {
|
|
2114
|
-
if (this._nativeCallbackUnsubscribe) {
|
|
2115
|
-
this._nativeCallbackUnsubscribe();
|
|
2116
|
-
this._nativeCallbackUnsubscribe = null;
|
|
2117
|
-
}
|
|
2118
|
-
if (this._nativeEncryptionCallbackUnsubscribe) {
|
|
2119
|
-
this._nativeEncryptionCallbackUnsubscribe();
|
|
2120
|
-
this._nativeEncryptionCallbackUnsubscribe = null;
|
|
2121
|
-
}
|
|
2122
|
-
// Cleanup pending encryption requests
|
|
2123
|
-
for (const [requestId, pending] of this._pendingEncryptionRequests) {
|
|
2124
|
-
pending.reject(new Error("IAMService destroyed"));
|
|
2125
|
-
}
|
|
2126
|
-
this._pendingEncryptionRequests.clear();
|
|
2127
|
-
// Cleanup request enclave
|
|
2128
|
-
if (this._nativeRequestEnclave) {
|
|
2129
|
-
try {
|
|
2130
|
-
this._nativeRequestEnclave.close();
|
|
2131
|
-
}
|
|
2132
|
-
catch (e) {
|
|
2133
|
-
console.debug("[IAMService] Error closing request enclave:", e);
|
|
2134
|
-
}
|
|
2135
|
-
this._nativeRequestEnclave = null;
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
const IAMServiceInstance = new IAMService();
|
|
2140
|
-
exports.IAMService = IAMServiceInstance;
|
|
2141
|
-
exports.default = IAMServiceInstance;
|
|
165
|
+
console.error(`[IAMService] '${event}' listener failed:`, err);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}, _IAMServiceImplementation_registerEvents = function _IAMServiceImplementation_registerEvents() {
|
|
169
|
+
const core = __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_getCore).call(this);
|
|
170
|
+
core.onReady = (auth) => { __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_setTokenCookie).call(this, core.token); __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'ready', auth); };
|
|
171
|
+
core.onAuthSuccess = () => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'authSuccess');
|
|
172
|
+
core.onAuthError = (err) => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'authError', err);
|
|
173
|
+
core.onAuthRefreshSuccess = () => { __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_setTokenCookie).call(this, core.token); __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'authRefreshSuccess'); };
|
|
174
|
+
core.onAuthRefreshError = () => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'authRefreshError');
|
|
175
|
+
core.onAuthLogout = () => { __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_setTokenCookie).call(this); __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'logout'); };
|
|
176
|
+
core.onTokenExpired = () => __classPrivateFieldGet(this, _IAMServiceImplementation_instances, "m", _IAMServiceImplementation_emit).call(this, 'tokenExpired');
|
|
177
|
+
}, _IAMServiceImplementation_setTokenCookie = function _IAMServiceImplementation_setTokenCookie(token) {
|
|
178
|
+
const attributes = `path=/; SameSite=Lax${window.location.protocol === 'https:' ? '; Secure' : ''}`;
|
|
179
|
+
document.cookie = token
|
|
180
|
+
? `kcToken=${token}; ${attributes}`
|
|
181
|
+
: `kcToken=; expires=Thu, 01 Jan 1970 00:00:00 GMT; ${attributes}`;
|
|
182
|
+
}, _IAMServiceImplementation_defaultInitOptions = function _IAMServiceImplementation_defaultInitOptions(json) {
|
|
183
|
+
var _a;
|
|
184
|
+
return {
|
|
185
|
+
onLoad: 'check-sso',
|
|
186
|
+
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
|
|
187
|
+
pkceMethod: 'S256',
|
|
188
|
+
setupRequestEnclave: (_a = json.setupRequestEnclave) !== null && _a !== void 0 ? _a : true,
|
|
189
|
+
// DPoP is opt-in: only enable it when tidecloak.json asks for it.
|
|
190
|
+
...(json.dpopConfig && { dpopConfig: json.dpopConfig }),
|
|
191
|
+
...(json.checkLoginIframe === false && { checkLoginIframe: false }),
|
|
192
|
+
};
|
|
193
|
+
}, _IAMServiceImplementation_getCore = function _IAMServiceImplementation_getCore() {
|
|
194
|
+
if (!__classPrivateFieldGet(this, _IAMServiceImplementation_core, "f"))
|
|
195
|
+
throw Error('[IAMService] TidecloakJS not initialized. Make sure to call IAMService.init before using functionalities.');
|
|
196
|
+
return __classPrivateFieldGet(this, _IAMServiceImplementation_core, "f");
|
|
197
|
+
};
|
|
198
|
+
exports.IAMService = new IAMServiceImplementation();
|
|
2142
199
|
//# sourceMappingURL=IAMService.js.map
|