@telicent-oss/fe-auth-lib 0.0.1-TELFE-1371.32 โ†’ 0.0.1-TELFE-1371.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,869 +1,991 @@
1
1
  // Import schemas for validation (Node.js/bundler environments)
2
2
  let GetUserInfoSchema;
3
3
  let AuthServerOAuth2ClientConfigSchema;
4
- if (typeof require !== 'undefined') {
5
- try {
6
- const schemas = require('./schemas.js');
7
- GetUserInfoSchema = schemas.GetUserInfoSchema;
8
- AuthServerOAuth2ClientConfigSchema = schemas.AuthServerOAuth2ClientConfigSchema;
9
- } catch (e) {
10
- console.warn('Zod schema not available, validation will be skipped:', e.message);
11
- }
4
+ if (typeof require !== "undefined") {
5
+ try {
6
+ const schemas = require("./schemas.js");
7
+ GetUserInfoSchema = schemas.GetUserInfoSchema;
8
+ AuthServerOAuth2ClientConfigSchema =
9
+ schemas.AuthServerOAuth2ClientConfigSchema;
10
+ } catch (e) {
11
+ console.warn(
12
+ "Zod schema not available, validation will be skipped:",
13
+ e.message
14
+ );
15
+ }
12
16
  }
13
17
 
14
18
  // Unified OAuth2 Client Implementation using Auth Server Session Management
15
19
  // Works for both same-domain and cross-domain scenarios with automatic detection
16
20
  class AuthServerOAuth2Client {
17
- // Event constants for popup communication
18
- static OAUTH_SUCCESS = 'oauth-success';
19
- static OAUTH_ERROR = 'oauth-error';
20
- constructor(config) {
21
- if (AuthServerOAuth2ClientConfigSchema && config) {
22
- try {
23
- AuthServerOAuth2ClientConfigSchema.parse(config);
24
- } catch (error) {
25
- console.error('โŒ Invalid AuthServerOAuth2Client configuration:', error.errors || error.message);
26
- throw new Error(`Invalid AuthServerOAuth2Client configuration: ${error.message}`);
27
- }
28
- }
29
-
30
- this.config = {
31
- clientId: 'spa-client', // Default - should be overridden
32
- authServerUrl: 'http://auth.telicent.localhost',
33
- redirectUri: 'http://demo.telicent.localhost/callback.html', // Default - should be overridden
34
- popupRedirectUri: null, // Must be provided for popup flows
35
- scope: 'openid email profile offline_access',
36
- apiUrl: 'http://api.telicent.localhost',
37
- onLogout: () => {
38
- window.alert('You are now logged out. Redirecting to /');
39
- window.location.href = '/';
40
- },
41
- ...config
42
- };
43
-
44
- // Auto-detect if this is a cross-domain client
45
- this.isCrossDomain = this.detectCrossDomain();
46
- console.log(`๐Ÿ”ง Initialized ${this.isCrossDomain ? 'cross-domain' : 'same-domain'} OAuth2 client`);
47
- console.log(`๐Ÿ”ง Client config:`, {
48
- clientId: this.config.clientId,
49
- redirectUri: this.config.redirectUri,
50
- authServerUrl: this.config.authServerUrl,
51
- currentOrigin: window.location.origin
52
- });
21
+ // Event constants for popup communication
22
+ static OAUTH_SUCCESS = "oauth-success";
23
+ static OAUTH_ERROR = "oauth-error";
24
+ constructor(config) {
25
+ if (AuthServerOAuth2ClientConfigSchema && config) {
26
+ try {
27
+ AuthServerOAuth2ClientConfigSchema.parse(config);
28
+ } catch (error) {
29
+ console.error(
30
+ "โŒ Invalid AuthServerOAuth2Client configuration:",
31
+ error.errors || error.message
32
+ );
33
+ throw new Error(
34
+ `Invalid AuthServerOAuth2Client configuration: ${error.message}`
35
+ );
36
+ }
53
37
  }
54
38
 
55
- // Detect if this is a cross-domain client based on current origin vs auth server URL
56
- detectCrossDomain() {
57
- try {
58
- const currentOrigin = window.location.origin;
59
- const authServerUrl = new URL(this.config.authServerUrl);
60
- const authServerOrigin = authServerUrl.origin;
61
-
62
- const currentHost = new URL(currentOrigin).hostname;
63
- const authServerHost = authServerUrl.hostname;
64
-
65
- // Check if domains are different or if they don't share a common parent domain
66
- const isSameDomain = currentHost === authServerHost ||
67
- (currentHost.endsWith('.telicent.localhost') && authServerHost.endsWith('.telicent.localhost'));
68
-
69
- console.log(`๐ŸŒ Domain detection: current=${currentHost}, auth=${authServerHost}, sameDomain=${isSameDomain}`);
70
- return !isSameDomain;
71
- } catch (error) {
72
- console.warn('Error detecting domain context, defaulting to cross-domain:', error);
73
- return true; // Default to cross-domain for safety
74
- }
39
+ this.config = {
40
+ clientId: "spa-client", // Default - should be overridden
41
+ authServerUrl: "http://auth.telicent.localhost",
42
+ redirectUri: "http://demo.telicent.localhost/callback.html", // Default - should be overridden
43
+ popupRedirectUri: null, // Must be provided for popup flows
44
+ scope: "openid email profile offline_access",
45
+ apiUrl: "http://api.telicent.localhost",
46
+ onLogout: () => {
47
+ window.alert("You are now logged out. Redirecting to /");
48
+ window.location.href = "/";
49
+ },
50
+ ...config,
51
+ };
52
+
53
+ // Auto-detect if this is a cross-domain client
54
+ this.isCrossDomain = this.detectCrossDomain();
55
+ console.log(
56
+ `๐Ÿ”ง Initialized ${
57
+ this.isCrossDomain ? "cross-domain" : "same-domain"
58
+ } OAuth2 client`
59
+ );
60
+ console.log(`๐Ÿ”ง Client config:`, {
61
+ clientId: this.config.clientId,
62
+ redirectUri: this.config.redirectUri,
63
+ authServerUrl: this.config.authServerUrl,
64
+ currentOrigin: window.location.origin,
65
+ });
66
+ }
67
+
68
+ // Detect if this is a cross-domain client based on current origin vs auth server URL
69
+ detectCrossDomain() {
70
+ try {
71
+ const currentOrigin = window.location.origin;
72
+ const authServerUrl = new URL(this.config.authServerUrl);
73
+ const authServerOrigin = authServerUrl.origin;
74
+
75
+ const currentHost = new URL(currentOrigin).hostname;
76
+ const authServerHost = authServerUrl.hostname;
77
+
78
+ // Check if domains are different or if they don't share a common parent domain
79
+ const isSameDomain =
80
+ currentHost === authServerHost ||
81
+ (currentHost.endsWith(".telicent.localhost") &&
82
+ authServerHost.endsWith(".telicent.localhost"));
83
+
84
+ console.log(
85
+ `๐ŸŒ Domain detection: current=${currentHost}, auth=${authServerHost}, sameDomain=${isSameDomain}`
86
+ );
87
+ return !isSameDomain;
88
+ } catch (error) {
89
+ console.warn(
90
+ "Error detecting domain context, defaulting to cross-domain:",
91
+ error
92
+ );
93
+ return true; // Default to cross-domain for safety
75
94
  }
76
-
77
- // Generate code verifier for PKCE
78
- generateCodeVerifier() {
79
- const array = new Uint8Array(32);
80
- crypto.getRandomValues(array);
81
- return this.base64URLEncode(array);
95
+ }
96
+
97
+ // Generate code verifier for PKCE
98
+ generateCodeVerifier() {
99
+ const array = new Uint8Array(32);
100
+ crypto.getRandomValues(array);
101
+ return this.base64URLEncode(array);
102
+ }
103
+
104
+ // Generate code challenge from verifier
105
+ async generateCodeChallenge(codeVerifier) {
106
+ const encoder = new TextEncoder();
107
+ const data = encoder.encode(codeVerifier);
108
+ const digest = await crypto.subtle.digest("SHA-256", data);
109
+ return this.base64URLEncode(new Uint8Array(digest));
110
+ }
111
+
112
+ // Base64 URL encoding
113
+ base64URLEncode(array) {
114
+ return btoa(String.fromCharCode.apply(null, array))
115
+ .replace(/\+/g, "-")
116
+ .replace(/\//g, "_")
117
+ .replace(/=/g, "");
118
+ }
119
+
120
+ // Generate random state
121
+ generateState() {
122
+ const array = new Uint8Array(16);
123
+ crypto.getRandomValues(array);
124
+ return this.base64URLEncode(array);
125
+ }
126
+
127
+ // Generate random nonce for ID token binding
128
+ generateNonce() {
129
+ const array = new Uint8Array(16);
130
+ crypto.getRandomValues(array);
131
+ return this.base64URLEncode(array);
132
+ }
133
+
134
+ // Start OAuth2 login flow (redirects current window)
135
+ async login(redirectUri = null) {
136
+ const state = this.generateState();
137
+ const nonce = this.generateNonce();
138
+ const codeVerifier = this.generateCodeVerifier();
139
+ const codeChallenge = await this.generateCodeChallenge(codeVerifier);
140
+
141
+ // Use provided redirectUri or fall back to config default
142
+ const finalRedirectUri = redirectUri || this.config.redirectUri;
143
+
144
+ // Store PKCE values, nonce, and redirectUri in sessionStorage (temporary)
145
+ sessionStorage.setItem("oauth_state", state);
146
+ sessionStorage.setItem("oauth_nonce", nonce);
147
+ sessionStorage.setItem("oauth_code_verifier", codeVerifier);
148
+ sessionStorage.setItem("oauth_redirect_uri", finalRedirectUri);
149
+
150
+ // Build authorization URL
151
+ const params = new URLSearchParams({
152
+ response_type: "code",
153
+ client_id: this.config.clientId,
154
+ redirect_uri: finalRedirectUri,
155
+ return_to: window.location.href,
156
+ scope: this.config.scope,
157
+ state: state,
158
+ nonce: nonce,
159
+ code_challenge: codeChallenge,
160
+ code_challenge_method: "S256",
161
+ });
162
+
163
+ const authUrl = `${
164
+ this.config.authServerUrl
165
+ }/oauth2/authorize?${params.toString()}`;
166
+ console.log("Redirecting current window to:", authUrl);
167
+
168
+ // Always redirect current window
169
+ window.location.href = authUrl;
170
+ }
171
+
172
+ // Start OAuth2 login flow in popup window
173
+ async loginWithPopup(
174
+ redirectUri = null,
175
+ features = "width=600,height=700,scrollbars=yes,resizable=yes"
176
+ ) {
177
+ // Use provided redirectUri or fall back to config popupRedirectUri
178
+ const finalRedirectUri = redirectUri || this.config.popupRedirectUri;
179
+
180
+ if (!finalRedirectUri) {
181
+ throw new Error(
182
+ "redirectUri is required for popup login. Either provide it as a parameter or configure popupRedirectUri in the client config."
183
+ );
82
184
  }
83
185
 
84
- // Generate code challenge from verifier
85
- async generateCodeChallenge(codeVerifier) {
86
- const encoder = new TextEncoder();
87
- const data = encoder.encode(codeVerifier);
88
- const digest = await crypto.subtle.digest('SHA-256', data);
89
- return this.base64URLEncode(new Uint8Array(digest));
186
+ const state = this.generateState();
187
+ const nonce = this.generateNonce();
188
+ const codeVerifier = this.generateCodeVerifier();
189
+ const codeChallenge = await this.generateCodeChallenge(codeVerifier);
190
+
191
+ // Store PKCE values, nonce, and redirectUri in sessionStorage (temporary)
192
+ sessionStorage.setItem("oauth_state", state);
193
+ sessionStorage.setItem("oauth_nonce", nonce);
194
+ sessionStorage.setItem("oauth_code_verifier", codeVerifier);
195
+ sessionStorage.setItem("oauth_redirect_uri", finalRedirectUri);
196
+
197
+ // Build authorization URL
198
+ const params = new URLSearchParams({
199
+ response_type: "code",
200
+ client_id: this.config.clientId,
201
+ redirect_uri: finalRedirectUri,
202
+ scope: this.config.scope,
203
+ state: state,
204
+ nonce: nonce,
205
+ code_challenge: codeChallenge,
206
+ code_challenge_method: "S256",
207
+ });
208
+
209
+ const authUrl = `${
210
+ this.config.authServerUrl
211
+ }/oauth2/authorize?${params.toString()}`;
212
+ console.log("Opening popup with URL:", authUrl);
213
+ console.log("Popup features:", features);
214
+
215
+ // Use window.top to break out of iframe context (e.g., Storybook)
216
+ const targetWindow = window.top || window;
217
+ const popup = targetWindow.open(authUrl, "_blank", features);
218
+ console.log("Popup opened:", popup ? "Success" : "Blocked or failed");
219
+
220
+ // Start popup flow and listen for messages
221
+ if (popup) {
222
+ this.startPopupFlow(popup);
90
223
  }
224
+ }
225
+
226
+ // Handle callback after authorization - creates session via auth-server
227
+ async handleCallback(callbackParams = null) {
228
+ let urlParams;
229
+ if (callbackParams) {
230
+ // Use provided parameters directly
231
+ urlParams = new URLSearchParams(callbackParams);
232
+ } else {
233
+ // Fall back to reading from current URL
234
+ urlParams = new URLSearchParams(window.location.search);
235
+ }
236
+ const code = urlParams.get("code");
237
+ const state = urlParams.get("state");
238
+ const error = urlParams.get("error");
91
239
 
92
- // Base64 URL encoding
93
- base64URLEncode(array) {
94
- return btoa(String.fromCharCode.apply(null, array))
95
- .replace(/\+/g, '-')
96
- .replace(/\//g, '_')
97
- .replace(/=/g, '');
240
+ if (error) {
241
+ throw new Error(`OAuth error: ${error}`);
98
242
  }
99
243
 
100
- // Generate random state
101
- generateState() {
102
- const array = new Uint8Array(16);
103
- crypto.getRandomValues(array);
104
- return this.base64URLEncode(array);
244
+ if (!code || !state) {
245
+ throw new Error("Missing code or state parameter");
105
246
  }
106
247
 
107
- // Generate random nonce for ID token binding
108
- generateNonce() {
109
- const array = new Uint8Array(16);
110
- crypto.getRandomValues(array);
111
- return this.base64URLEncode(array);
248
+ const storedState = sessionStorage.getItem("oauth_state");
249
+ if (state !== storedState) {
250
+ throw new Error("Invalid state parameter");
112
251
  }
113
252
 
114
- // Start OAuth2 login flow (redirects current window)
115
- async login(redirectUri = null) {
116
- const state = this.generateState();
117
- const nonce = this.generateNonce();
118
- const codeVerifier = this.generateCodeVerifier();
119
- const codeChallenge = await this.generateCodeChallenge(codeVerifier);
120
-
121
- // Use provided redirectUri or fall back to config default
122
- const finalRedirectUri = redirectUri || this.config.redirectUri;
123
-
124
- // Store PKCE values, nonce, and redirectUri in sessionStorage (temporary)
125
- sessionStorage.setItem('oauth_state', state);
126
- sessionStorage.setItem('oauth_nonce', nonce);
127
- sessionStorage.setItem('oauth_code_verifier', codeVerifier);
128
- sessionStorage.setItem('oauth_redirect_uri', finalRedirectUri);
129
-
130
- // Build authorization URL
131
- const params = new URLSearchParams({
132
- response_type: 'code',
133
- client_id: this.config.clientId,
134
- redirect_uri: finalRedirectUri,
135
- scope: this.config.scope,
136
- state: state,
137
- nonce: nonce,
138
- code_challenge: codeChallenge,
139
- code_challenge_method: 'S256'
140
- });
253
+ const codeVerifier = sessionStorage.getItem("oauth_code_verifier");
254
+ if (!codeVerifier) {
255
+ throw new Error("Missing code verifier");
256
+ }
141
257
 
142
- const authUrl = `${this.config.authServerUrl}/oauth2/authorize?${params.toString()}`;
143
- console.log('Redirecting current window to:', authUrl);
258
+ const redirectUri = sessionStorage.getItem("oauth_redirect_uri");
259
+ if (!redirectUri) {
260
+ throw new Error("Missing redirect URI");
261
+ }
144
262
 
145
- // Always redirect current window
263
+ // Exchange authorization code for session (secure - access token never exposed to client)
264
+ const tokenResponse = await fetch(
265
+ `${this.config.authServerUrl}/oauth2/token`,
266
+ {
267
+ method: "POST",
268
+ headers: {
269
+ "Content-Type": "application/x-www-form-urlencoded",
270
+ Accept: "application/json",
271
+ },
272
+ credentials: "include", // Include cookies for session
273
+ body: new URLSearchParams({
274
+ grant_type: "authorization_code",
275
+ client_id: this.config.clientId,
276
+ code: code,
277
+ redirect_uri: redirectUri,
278
+ code_verifier: codeVerifier,
279
+ }),
280
+ }
281
+ );
282
+
283
+ if (!tokenResponse.ok) {
284
+ let errorDetails;
285
+ try {
286
+ // Try to parse JSON error response first
287
+ errorDetails = await tokenResponse.json();
288
+ } catch (e) {
289
+ // Fallback to text if not JSON
290
+ const errorText = await tokenResponse.text();
291
+ errorDetails = { error: "unknown", error_description: errorText };
292
+ }
293
+
294
+ // Handle specific consent_required error
295
+ if (errorDetails.error === "consent_required") {
296
+ console.log("Consent required - redirecting to proper OAuth2 flow");
297
+
298
+ // The SPA should not directly call /oauth2/session without consent
299
+ // Instead, redirect to start the proper OAuth2 authorization flow
300
+ // which will handle consent properly through OAuth2AuthenticationSuccessHandler
301
+ const authUrl = this.buildAuthorizationUrl();
302
+ console.log("Redirecting to OAuth2 authorization flow:", authUrl);
146
303
  window.location.href = authUrl;
304
+ return; // Don't throw error, we're redirecting
305
+ }
306
+
307
+ // For other errors, throw as before
308
+ const errorMessage =
309
+ errorDetails.error_description || errorDetails.error || "Unknown error";
310
+ throw new Error(
311
+ `Token exchange and session creation failed: ${errorMessage}`
312
+ );
147
313
  }
148
314
 
149
- // Start OAuth2 login flow in popup window
150
- async loginWithPopup(redirectUri = null, features = 'width=600,height=700,scrollbars=yes,resizable=yes') {
151
- // Use provided redirectUri or fall back to config popupRedirectUri
152
- const finalRedirectUri = redirectUri || this.config.popupRedirectUri;
153
-
154
- if (!finalRedirectUri) {
155
- throw new Error('redirectUri is required for popup login. Either provide it as a parameter or configure popupRedirectUri in the client config.');
156
- }
157
-
158
- const state = this.generateState();
159
- const nonce = this.generateNonce();
160
- const codeVerifier = this.generateCodeVerifier();
161
- const codeChallenge = await this.generateCodeChallenge(codeVerifier);
162
-
163
- // Store PKCE values, nonce, and redirectUri in sessionStorage (temporary)
164
- sessionStorage.setItem('oauth_state', state);
165
- sessionStorage.setItem('oauth_nonce', nonce);
166
- sessionStorage.setItem('oauth_code_verifier', codeVerifier);
167
- sessionStorage.setItem('oauth_redirect_uri', finalRedirectUri);
168
-
169
- // Build authorization URL
170
- const params = new URLSearchParams({
171
- response_type: 'code',
172
- client_id: this.config.clientId,
173
- redirect_uri: finalRedirectUri,
174
- scope: this.config.scope,
175
- state: state,
176
- nonce: nonce,
177
- code_challenge: codeChallenge,
178
- code_challenge_method: 'S256'
179
- });
180
-
181
- const authUrl = `${this.config.authServerUrl}/oauth2/authorize?${params.toString()}`;
182
- console.log('Opening popup with URL:', authUrl);
183
- console.log('Popup features:', features);
184
-
185
- // Use window.top to break out of iframe context (e.g., Storybook)
186
- const targetWindow = window.top || window;
187
- const popup = targetWindow.open(authUrl, '_blank', features);
188
- console.log('Popup opened:', popup ? 'Success' : 'Blocked or failed');
189
-
190
- // Start popup flow and listen for messages
191
- if (popup) {
192
- this.startPopupFlow(popup);
193
- }
315
+ const sessionData = await tokenResponse.json();
316
+ console.log(
317
+ "๐ŸŽ‰ Session created securely via domain cookies (access token never exposed)"
318
+ );
319
+ console.log("๐Ÿ“Š Session data:", sessionData);
320
+
321
+ // For cross-domain clients, store session ID for Authorization header use
322
+ // since cookies won't work across different domains
323
+ console.log("๐Ÿ” Cross-domain check:", {
324
+ isCrossDomain: this.isCrossDomain,
325
+ sessionIsCrossDomain: sessionData.isCrossDomain,
326
+ hasSessionToken: !!sessionData.sessionToken,
327
+ sessionToken: sessionData.sessionToken,
328
+ });
329
+ if (
330
+ this.isCrossDomain &&
331
+ sessionData.isCrossDomain &&
332
+ sessionData.sessionToken
333
+ ) {
334
+ sessionStorage.setItem("auth_session_id", sessionData.sessionToken);
335
+ console.log(
336
+ "๐Ÿ”‘ Cross-domain client: Session ID stored for Authorization header use"
337
+ );
338
+ } else {
339
+ console.log(
340
+ "โŒ Cross-domain session storage skipped - conditions not met"
341
+ );
194
342
  }
195
343
 
196
- // Handle callback after authorization - creates session via auth-server
197
- async handleCallback(callbackParams = null) {
198
- let urlParams;
199
- if (callbackParams) {
200
- // Use provided parameters directly
201
- urlParams = new URLSearchParams(callbackParams);
202
- } else {
203
- // Fall back to reading from current URL
204
- urlParams = new URLSearchParams(window.location.search);
205
- }
206
- const code = urlParams.get('code');
207
- const state = urlParams.get('state');
208
- const error = urlParams.get('error');
209
-
210
- if (error) {
211
- throw new Error(`OAuth error: ${error}`);
344
+ // Step 2: Retrieve ID token separately for enhanced security
345
+ // All clients now use session-based authentication for maximum security
346
+ try {
347
+ // Add a small delay to ensure session is fully propagated
348
+ await new Promise((resolve) => setTimeout(resolve, 100));
349
+
350
+ const idTokenResponse = await this.makeAuthenticatedRequest(
351
+ `${this.config.authServerUrl}/session/idtoken`,
352
+ { skipAutoLogout: true }
353
+ );
354
+
355
+ if (!idTokenResponse.ok) {
356
+ console.warn(
357
+ "Failed to retrieve ID token, but continuing with callback completion"
358
+ );
359
+ // Don't throw - allow callback to complete without ID token
360
+ } else {
361
+ const idTokenData = await idTokenResponse.json();
362
+ console.log(
363
+ "ID token retrieved in separate call for enhanced security"
364
+ );
365
+
366
+ // Store ID token for offline user information access
367
+ if (idTokenData.id_token) {
368
+ // Validate ID token with nonce before storing
369
+ if (!this.validateIdToken(idTokenData.id_token)) {
370
+ console.warn(
371
+ "ID token validation failed, but continuing with callback"
372
+ );
373
+ } else {
374
+ sessionStorage.setItem("auth_id_token", idTokenData.id_token);
375
+ console.log("ID token validated and stored in sessionStorage");
376
+ }
212
377
  }
378
+ }
379
+ } catch (error) {
380
+ console.warn(
381
+ "Error retrieving ID token during callback, but continuing:",
382
+ error
383
+ );
384
+ // Don't throw - allow callback to complete even if ID token retrieval fails
385
+ }
213
386
 
214
- if (!code || !state) {
215
- throw new Error('Missing code or state parameter');
216
- }
387
+ // Clean up PKCE values
388
+ sessionStorage.removeItem("oauth_state");
389
+ sessionStorage.removeItem("oauth_code_verifier");
390
+ // Note: oauth_nonce is removed in validateIdToken after successful validation
217
391
 
218
- const storedState = sessionStorage.getItem('oauth_state');
219
- if (state !== storedState) {
220
- throw new Error('Invalid state parameter');
221
- }
392
+ return sessionData;
393
+ }
222
394
 
223
- const codeVerifier = sessionStorage.getItem('oauth_code_verifier');
224
- if (!codeVerifier) {
225
- throw new Error('Missing code verifier');
395
+ // Check if user is authenticated by checking session via auth-server
396
+ async isAuthenticated() {
397
+ try {
398
+ const headers = {
399
+ Accept: "application/json",
400
+ };
401
+
402
+ // For cross-domain clients, include session ID in Authorization header
403
+ // For same-domain clients, cookies are included automatically
404
+ const sessionId = sessionStorage.getItem("auth_session_id");
405
+ if (this.isCrossDomain && sessionId) {
406
+ headers["Authorization"] = `Bearer ${sessionId}`;
407
+ console.log(
408
+ "๐Ÿ” Using session ID in Authorization header for authentication check"
409
+ );
410
+ }
411
+
412
+ const response = await fetch(
413
+ `${this.config.authServerUrl}/session/check`,
414
+ {
415
+ method: "GET",
416
+ headers: headers,
417
+ credentials: "include", // Include cookies for same-domain fallback
226
418
  }
419
+ );
420
+
421
+ if (response.ok) {
422
+ const result = await response.json();
423
+ console.log(result);
424
+ return true;
425
+ }
426
+
427
+ return false;
428
+ } catch (error) {
429
+ console.error("Authentication check error:", error);
430
+ return false;
431
+ }
432
+ }
227
433
 
228
- const redirectUri = sessionStorage.getItem('oauth_redirect_uri');
229
- if (!redirectUri) {
230
- throw new Error('Missing redirect URI');
231
- }
434
+ // Helper function to decode JWT token payload
435
+ decodeJWT(token) {
436
+ try {
437
+ const base64Url = token.split(".")[1];
438
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
439
+ const jsonPayload = decodeURIComponent(
440
+ atob(base64)
441
+ .split("")
442
+ .map(function (c) {
443
+ return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
444
+ })
445
+ .join("")
446
+ );
447
+ return JSON.parse(jsonPayload);
448
+ } catch (error) {
449
+ console.error("Error decoding JWT:", error);
450
+ return null;
451
+ }
452
+ }
232
453
 
233
- // Exchange authorization code for session (secure - access token never exposed to client)
234
- const tokenResponse = await fetch(`${this.config.authServerUrl}/oauth2/token`, {
235
- method: 'POST',
236
- headers: {
237
- 'Content-Type': 'application/x-www-form-urlencoded',
238
- 'Accept': 'application/json'
239
- },
240
- credentials: 'include', // Include cookies for session
241
- body: new URLSearchParams({
242
- grant_type: 'authorization_code',
243
- client_id: this.config.clientId,
244
- code: code,
245
- redirect_uri: redirectUri,
246
- code_verifier: codeVerifier
247
- })
454
+ // Validate ID token for session recovery (without nonce)
455
+ validateIdTokenForRecovery(idToken) {
456
+ try {
457
+ const payload = this.decodeJWT(idToken);
458
+ if (!payload) {
459
+ console.error("Failed to decode ID token payload");
460
+ return false;
461
+ }
462
+
463
+ // Validate audience (client ID)
464
+ if (payload.aud !== this.config.clientId) {
465
+ console.error("Audience validation failed", {
466
+ expected: this.config.clientId,
467
+ actual: payload.aud,
248
468
  });
249
-
250
- if (!tokenResponse.ok) {
251
- let errorDetails;
252
- try {
253
- // Try to parse JSON error response first
254
- errorDetails = await tokenResponse.json();
255
- } catch (e) {
256
- // Fallback to text if not JSON
257
- const errorText = await tokenResponse.text();
258
- errorDetails = { error: 'unknown', error_description: errorText };
259
- }
260
-
261
- // Handle specific consent_required error
262
- if (errorDetails.error === 'consent_required') {
263
- console.log('Consent required - redirecting to proper OAuth2 flow');
264
-
265
- // The SPA should not directly call /oauth2/session without consent
266
- // Instead, redirect to start the proper OAuth2 authorization flow
267
- // which will handle consent properly through OAuth2AuthenticationSuccessHandler
268
- const authUrl = this.buildAuthorizationUrl();
269
- console.log('Redirecting to OAuth2 authorization flow:', authUrl);
270
- window.location.href = authUrl;
271
- return; // Don't throw error, we're redirecting
272
- }
273
-
274
- // For other errors, throw as before
275
- const errorMessage = errorDetails.error_description || errorDetails.error || 'Unknown error';
276
- throw new Error(`Token exchange and session creation failed: ${errorMessage}`);
277
- }
278
-
279
- const sessionData = await tokenResponse.json();
280
- console.log('๐ŸŽ‰ Session created securely via domain cookies (access token never exposed)');
281
- console.log('๐Ÿ“Š Session data:', sessionData);
282
-
283
- // For cross-domain clients, store session ID for Authorization header use
284
- // since cookies won't work across different domains
285
- console.log('๐Ÿ” Cross-domain check:', {
286
- isCrossDomain: this.isCrossDomain,
287
- sessionIsCrossDomain: sessionData.isCrossDomain,
288
- hasSessionToken: !!sessionData.sessionToken,
289
- sessionToken: sessionData.sessionToken
469
+ return false;
470
+ }
471
+
472
+ // Validate expiration
473
+ if (!payload.exp || payload.exp * 1000 < Date.now()) {
474
+ console.error("Token expired", {
475
+ exp: payload.exp,
476
+ now: Math.floor(Date.now() / 1000),
290
477
  });
291
- if (this.isCrossDomain && sessionData.isCrossDomain && sessionData.sessionToken) {
292
- sessionStorage.setItem('auth_session_id', sessionData.sessionToken);
293
- console.log('๐Ÿ”‘ Cross-domain client: Session ID stored for Authorization header use');
294
- } else {
295
- console.log('โŒ Cross-domain session storage skipped - conditions not met');
296
- }
297
-
298
- // Step 2: Retrieve ID token separately for enhanced security
299
- // All clients now use session-based authentication for maximum security
300
- try {
301
- // Add a small delay to ensure session is fully propagated
302
- await new Promise(resolve => setTimeout(resolve, 100));
303
-
304
- const idTokenResponse = await this.makeAuthenticatedRequest(
305
- `${this.config.authServerUrl}/session/idtoken`,
306
- { skipAutoLogout: true }
307
- );
308
-
309
- if (!idTokenResponse.ok) {
310
- console.warn('Failed to retrieve ID token, but continuing with callback completion');
311
- // Don't throw - allow callback to complete without ID token
312
- } else {
313
- const idTokenData = await idTokenResponse.json();
314
- console.log('ID token retrieved in separate call for enhanced security');
315
-
316
- // Store ID token for offline user information access
317
- if (idTokenData.id_token) {
318
- // Validate ID token with nonce before storing
319
- if (!this.validateIdToken(idTokenData.id_token)) {
320
- console.warn('ID token validation failed, but continuing with callback');
321
- } else {
322
- sessionStorage.setItem('auth_id_token', idTokenData.id_token);
323
- console.log('ID token validated and stored in sessionStorage');
324
- }
325
- }
326
- }
327
- } catch (error) {
328
- console.warn('Error retrieving ID token during callback, but continuing:', error);
329
- // Don't throw - allow callback to complete even if ID token retrieval fails
330
- }
331
-
332
- // Clean up PKCE values
333
- sessionStorage.removeItem('oauth_state');
334
- sessionStorage.removeItem('oauth_code_verifier');
335
- // Note: oauth_nonce is removed in validateIdToken after successful validation
336
-
337
- return sessionData;
478
+ return false;
479
+ }
480
+
481
+ // Validate issued at time (not too old)
482
+ if (payload.iat && payload.iat * 1000 < Date.now() - 60 * 60 * 1000) {
483
+ // 1 hour max age
484
+ console.error("Token issued too long ago", {
485
+ iat: payload.iat,
486
+ maxAge: "1 hour",
487
+ });
488
+ return false;
489
+ }
490
+
491
+ console.log(
492
+ "ID token validation for recovery successful (no nonce check)"
493
+ );
494
+ return true;
495
+ } catch (error) {
496
+ console.error("ID token validation error:", error);
497
+ return false;
338
498
  }
499
+ }
339
500
 
340
- // Check if user is authenticated by checking session via auth-server
341
- async isAuthenticated() {
342
- try {
343
- const headers = {
344
- 'Accept': 'application/json'
345
- };
346
-
347
- // For cross-domain clients, include session ID in Authorization header
348
- // For same-domain clients, cookies are included automatically
349
- const sessionId = sessionStorage.getItem('auth_session_id');
350
- if (this.isCrossDomain && sessionId) {
351
- headers['Authorization'] = `Bearer ${sessionId}`;
352
- console.log('๐Ÿ” Using session ID in Authorization header for authentication check');
353
- }
354
-
355
- const response = await fetch(`${this.config.authServerUrl}/session/check`, {
356
- method: 'GET',
357
- headers: headers,
358
- credentials: 'include' // Include cookies for same-domain fallback
359
- });
360
-
361
- if (response.ok) {
362
- const result = await response.json();
363
- console.log(result)
364
- return true;
365
- }
366
-
367
- return false;
368
- } catch (error) {
369
- console.error('Authentication check error:', error);
370
- return false;
371
- }
501
+ // Validate ID token with nonce and other security checks
502
+ validateIdToken(idToken) {
503
+ try {
504
+ const payload = this.decodeJWT(idToken);
505
+ if (!payload) {
506
+ console.error("Failed to decode ID token payload");
507
+ return false;
508
+ }
509
+
510
+ // Validate nonce
511
+ const storedNonce = sessionStorage.getItem("oauth_nonce");
512
+ if (!storedNonce) {
513
+ console.error("No stored nonce found for validation");
514
+ return false;
515
+ }
516
+
517
+ if (!payload.nonce || payload.nonce !== storedNonce) {
518
+ console.error("Nonce validation failed", {
519
+ stored: storedNonce,
520
+ token: payload.nonce,
521
+ });
522
+ return false;
523
+ }
524
+
525
+ // Validate audience (client ID)
526
+ if (payload.aud !== this.config.clientId) {
527
+ console.error("Audience validation failed", {
528
+ expected: this.config.clientId,
529
+ actual: payload.aud,
530
+ });
531
+ return false;
532
+ }
533
+
534
+ // Validate expiration
535
+ if (!payload.exp || payload.exp * 1000 < Date.now()) {
536
+ console.error("Token expired", {
537
+ exp: payload.exp,
538
+ now: Math.floor(Date.now() / 1000),
539
+ });
540
+ return false;
541
+ }
542
+
543
+ // Validate issued at time (not too old)
544
+ if (payload.iat && payload.iat * 1000 < Date.now() - 5 * 60 * 1000) {
545
+ console.error("Token issued too long ago", {
546
+ iat: payload.iat,
547
+ maxAge: "5 minutes",
548
+ });
549
+ return false;
550
+ }
551
+
552
+ // Clean up nonce after successful validation
553
+ sessionStorage.removeItem("oauth_nonce");
554
+ console.log("ID token validation successful");
555
+ return true;
556
+ } catch (error) {
557
+ console.error("ID token validation error:", error);
558
+ return false;
372
559
  }
560
+ }
373
561
 
374
- // Helper function to decode JWT token payload
375
- decodeJWT(token) {
376
- try {
377
- const base64Url = token.split('.')[1];
378
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
379
- const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
380
- return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
381
- }).join(''));
382
- return JSON.parse(jsonPayload);
383
- } catch (error) {
384
- console.error('Error decoding JWT:', error);
385
- return null;
386
- }
562
+ // Check if ID token is expired
563
+ isIdTokenExpired() {
564
+ try {
565
+ const idToken = sessionStorage.getItem("auth_id_token");
566
+ if (!idToken) return true;
567
+
568
+ const payload = this.decodeJWT(idToken);
569
+ if (!payload || !payload.exp) return true;
570
+
571
+ // Check if token is expired (with 30 second buffer)
572
+ const now = Math.floor(Date.now() / 1000);
573
+ return payload.exp < now + 30;
574
+ } catch (error) {
575
+ console.error("Error checking ID token expiration:", error);
576
+ return true;
387
577
  }
578
+ }
388
579
 
389
- // Validate ID token for session recovery (without nonce)
390
- validateIdTokenForRecovery(idToken) {
391
- try {
392
- const payload = this.decodeJWT(idToken);
393
- if (!payload) {
394
- console.error('Failed to decode ID token payload');
395
- return false;
396
- }
397
-
398
- // Validate audience (client ID)
399
- if (payload.aud !== this.config.clientId) {
400
- console.error('Audience validation failed', {
401
- expected: this.config.clientId,
402
- actual: payload.aud
403
- });
404
- return false;
405
- }
406
-
407
- // Validate expiration
408
- if (!payload.exp || payload.exp * 1000 < Date.now()) {
409
- console.error('Token expired', {
410
- exp: payload.exp,
411
- now: Math.floor(Date.now() / 1000)
412
- });
413
- return false;
414
- }
415
-
416
- // Validate issued at time (not too old)
417
- if (payload.iat && payload.iat * 1000 < Date.now() - (60 * 60 * 1000)) { // 1 hour max age
418
- console.error('Token issued too long ago', {
419
- iat: payload.iat,
420
- maxAge: '1 hour'
421
- });
422
- return false;
423
- }
424
-
425
- console.log('ID token validation for recovery successful (no nonce check)');
426
- return true;
427
-
428
- } catch (error) {
429
- console.error('ID token validation error:', error);
430
- return false;
431
- }
432
- }
580
+ // Get user info from ID token (offline method)
581
+ getUserInfo() {
582
+ try {
583
+ const idToken = sessionStorage.getItem("auth_id_token");
584
+ if (!idToken) {
585
+ console.log("No ID token found in storage");
586
+ return null;
587
+ }
433
588
 
434
- // Validate ID token with nonce and other security checks
435
- validateIdToken(idToken) {
436
- try {
437
- const payload = this.decodeJWT(idToken);
438
- if (!payload) {
439
- console.error('Failed to decode ID token payload');
440
- return false;
441
- }
442
-
443
- // Validate nonce
444
- const storedNonce = sessionStorage.getItem('oauth_nonce');
445
- if (!storedNonce) {
446
- console.error('No stored nonce found for validation');
447
- return false;
448
- }
449
-
450
- if (!payload.nonce || payload.nonce !== storedNonce) {
451
- console.error('Nonce validation failed', {
452
- stored: storedNonce,
453
- token: payload.nonce
454
- });
455
- return false;
456
- }
457
-
458
- // Validate audience (client ID)
459
- if (payload.aud !== this.config.clientId) {
460
- console.error('Audience validation failed', {
461
- expected: this.config.clientId,
462
- actual: payload.aud
463
- });
464
- return false;
465
- }
466
-
467
- // Validate expiration
468
- if (!payload.exp || payload.exp * 1000 < Date.now()) {
469
- console.error('Token expired', {
470
- exp: payload.exp,
471
- now: Math.floor(Date.now() / 1000)
472
- });
473
- return false;
474
- }
475
-
476
- // Validate issued at time (not too old)
477
- if (payload.iat && payload.iat * 1000 < Date.now() - (5 * 60 * 1000)) {
478
- console.error('Token issued too long ago', {
479
- iat: payload.iat,
480
- maxAge: '5 minutes'
481
- });
482
- return false;
483
- }
484
-
485
- // Clean up nonce after successful validation
486
- sessionStorage.removeItem('oauth_nonce');
487
- console.log('ID token validation successful');
488
- return true;
489
-
490
- } catch (error) {
491
- console.error('ID token validation error:', error);
492
- return false;
493
- }
494
- }
589
+ const payload = this.decodeJWT(idToken);
590
+ if (!payload) {
591
+ console.log("Failed to decode ID token");
592
+ return null;
593
+ }
495
594
 
496
- // Check if ID token is expired
497
- isIdTokenExpired() {
595
+ // Check if token is expired
596
+ if (this.isIdTokenExpired()) {
597
+ console.log("ID token is expired");
598
+ return null;
599
+ }
600
+
601
+ // Build user info object from token payload
602
+ const userInfo = {
603
+ sub: payload.sub,
604
+ email: payload.email,
605
+ preferred_name: payload.preferred_name,
606
+ iss: payload.iss,
607
+ aud: payload.aud,
608
+ exp: payload.exp,
609
+ iat: payload.iat,
610
+ jti: payload.jti,
611
+ // Optional OIDC claims
612
+ nonce: payload.nonce,
613
+ auth_time: payload.auth_time,
614
+ sid: payload.sid,
615
+ azp: payload.azp,
616
+ // FE client additions
617
+ name: payload.preferred_name || payload.name || payload.sub,
618
+ token_expired: false,
619
+ token_expires_at: new Date(payload.exp * 1000).toISOString(),
620
+ source: "id_token",
621
+ externalProvider: payload,
622
+ };
623
+
624
+ // Validate against schema if available
625
+ if (GetUserInfoSchema) {
498
626
  try {
499
- const idToken = sessionStorage.getItem('auth_id_token');
500
- if (!idToken) return true;
501
-
502
- const payload = this.decodeJWT(idToken);
503
- if (!payload || !payload.exp) return true;
504
-
505
- // Check if token is expired (with 30 second buffer)
506
- const now = Math.floor(Date.now() / 1000);
507
- return payload.exp < (now + 30);
508
- } catch (error) {
509
- console.error('Error checking ID token expiration:', error);
510
- return true;
627
+ const validated = GetUserInfoSchema.parse(userInfo);
628
+ console.log("User info validated successfully against schema");
629
+ return validated;
630
+ } catch (validationError) {
631
+ console.error("User info validation failed:", validationError);
632
+ // Return unvalidated data but log the error
633
+ console.warn(
634
+ "Returning unvalidated user info. Validation errors:",
635
+ validationError.errors
636
+ );
637
+ return userInfo;
511
638
  }
512
- }
639
+ }
513
640
 
514
- // Get user info from ID token (offline method)
515
- getUserInfo() {
516
- try {
517
- const idToken = sessionStorage.getItem('auth_id_token');
518
- if (!idToken) {
519
- console.log('No ID token found in storage');
520
- return null;
521
- }
522
-
523
- const payload = this.decodeJWT(idToken);
524
- if (!payload) {
525
- console.log('Failed to decode ID token');
526
- return null;
527
- }
528
-
529
- // Check if token is expired
530
- if (this.isIdTokenExpired()) {
531
- console.log('ID token is expired');
532
- return null;
533
- }
534
-
535
- // Build user info object from token payload
536
- const userInfo = {
537
- sub: payload.sub,
538
- email: payload.email,
539
- preferred_name: payload.preferred_name,
540
- iss: payload.iss,
541
- aud: payload.aud,
542
- exp: payload.exp,
543
- iat: payload.iat,
544
- jti: payload.jti,
545
- // Optional OIDC claims
546
- nonce: payload.nonce,
547
- auth_time: payload.auth_time,
548
- sid: payload.sid,
549
- azp: payload.azp,
550
- // FE client additions
551
- name: payload.preferred_name || payload.name || payload.sub,
552
- token_expired: false,
553
- token_expires_at: new Date(payload.exp * 1000).toISOString(),
554
- source: 'id_token',
555
- externalProvider: payload
556
- };
557
-
558
- // Validate against schema if available
559
- if (GetUserInfoSchema) {
560
- try {
561
- const validated = GetUserInfoSchema.parse(userInfo);
562
- console.log('User info validated successfully against schema');
563
- return validated;
564
- } catch (validationError) {
565
- console.error('User info validation failed:', validationError);
566
- // Return unvalidated data but log the error
567
- console.warn('Returning unvalidated user info. Validation errors:', validationError.errors);
568
- return userInfo;
569
- }
570
- }
571
-
572
- // Return without validation if schema not available
573
- return userInfo;
574
- } catch (error) {
575
- console.error('Error getting user info from ID token:', error);
576
- return null;
577
- }
641
+ // Return without validation if schema not available
642
+ return userInfo;
643
+ } catch (error) {
644
+ console.error("Error getting user info from ID token:", error);
645
+ return null;
578
646
  }
647
+ }
579
648
 
580
- // Get fresh user info from OAuth2 userinfo endpoint (UNIFIED ENDPOINT)
581
- async getUserInfoFromAPI() {
582
- try {
583
- const response = await this.makeAuthenticatedRequest(
584
- `${this.config.authServerUrl}/userinfo`
585
- );
586
-
587
- if (response.ok) {
588
- const data = await response.json();
589
- return {
590
- ...data,
591
- source: this.isCrossDomain ? 'oauth2_userinfo_api_cross_domain' : 'oauth2_userinfo_api_same_domain'
592
- };
593
- }
594
- return null;
595
- } catch (error) {
596
- console.error('Error getting user info from OAuth2 API:', error);
597
- return null;
598
- }
599
- }
649
+ // Get fresh user info from OAuth2 userinfo endpoint (UNIFIED ENDPOINT)
650
+ async getUserInfoFromAPI() {
651
+ try {
652
+ const response = await this.makeAuthenticatedRequest(
653
+ `${this.config.authServerUrl}/userinfo`
654
+ );
600
655
 
601
- // Get raw ID token from storage
602
- getRawIdToken() {
603
- return sessionStorage.getItem('auth_id_token');
656
+ if (response.ok) {
657
+ const data = await response.json();
658
+ return {
659
+ ...data,
660
+ source: this.isCrossDomain
661
+ ? "oauth2_userinfo_api_cross_domain"
662
+ : "oauth2_userinfo_api_same_domain",
663
+ };
664
+ }
665
+ return null;
666
+ } catch (error) {
667
+ console.error("Error getting user info from OAuth2 API:", error);
668
+ return null;
604
669
  }
670
+ }
605
671
 
606
- // Get CSRF token from cookie (same-domain only)
607
- getCsrfToken() {
608
- if (this.isCrossDomain) {
609
- return null; // Cross-domain clients don't use CSRF tokens
610
- }
672
+ // Get raw ID token from storage
673
+ getRawIdToken() {
674
+ return sessionStorage.getItem("auth_id_token");
675
+ }
611
676
 
612
- const cookies = document.cookie.split(';');
613
- for (let cookie of cookies) {
614
- const [name, value] = cookie.trim().split('=');
615
- if (name === 'XSRF-TOKEN') {
616
- return decodeURIComponent(value);
617
- }
618
- }
619
- return null;
677
+ // Get CSRF token from cookie (same-domain only)
678
+ getCsrfToken() {
679
+ if (this.isCrossDomain) {
680
+ return null; // Cross-domain clients don't use CSRF tokens
620
681
  }
621
682
 
622
- // Make authenticated API request using appropriate authentication method
623
- async makeAuthenticatedRequest(url, options = {}) {
624
- const headers = {
625
- 'Accept': 'application/json',
626
- ...options.headers
627
- };
628
-
629
- // Choose authentication method based on domain context
630
- if (this.isCrossDomain) {
631
- // Cross-domain: Use session ID in Authorization header
632
- const sessionId = sessionStorage.getItem('auth_session_id');
633
- if (sessionId) {
634
- headers['Authorization'] = `Bearer ${sessionId}`;
635
- console.log('Using session ID in Authorization header for cross-domain request to:', url);
636
- }
637
- } else {
638
- // Same-domain: Add CSRF token for state-changing requests
639
- if (options.method && ['POST', 'PUT', 'DELETE'].includes(options.method.toUpperCase())) {
640
- const csrfToken = this.getCsrfToken();
641
- if (csrfToken) {
642
- headers['X-XSRF-TOKEN'] = csrfToken;
643
- }
644
- }
645
- }
646
-
647
- const requestOptions = {
648
- ...options,
649
- headers: headers,
650
- credentials: 'include' // Always include cookies for same-domain fallback
651
- };
652
-
653
- const response = await fetch(url, requestOptions);
654
-
655
- if (response.status === 401) {
656
- // Don't auto-logout during callback flow or logout operations to prevent infinite loops
657
- const isCallbackFlow = options.skipAutoLogout || url.includes('/session/idtoken');
658
- const isLogoutOperation = url.includes('/session/logout');
659
- const isSessionCheck = url.includes('/session/check');
660
- const isUserInfoOperation = url.includes('/userinfo');
661
-
662
- if (!isCallbackFlow && !isLogoutOperation && !isSessionCheck && !isUserInfoOperation) {
663
- console.log('Session expired, redirecting to login');
664
- // Clear expired session data
665
- this.clearLocalStorage();
666
- // Redirect to login instead of logout
667
- this.login();
668
- throw new Error('Session expired');
669
- } else {
670
- console.warn('401 response during protected operation, not auto-logging out to prevent loops');
671
- }
672
- }
673
-
674
- return response;
683
+ const cookies = document.cookie.split(";");
684
+ for (let cookie of cookies) {
685
+ const [name, value] = cookie.trim().split("=");
686
+ if (name === "XSRF-TOKEN") {
687
+ return decodeURIComponent(value);
688
+ }
675
689
  }
676
-
677
- // Prepare request options with authentication
678
- beforeRequest(options = {}) {
679
- const headers = {
680
- 'Accept': 'application/json', // Exact match with original
681
- ...options.headers
682
- };
683
-
684
- // Choose authentication method based on domain context
685
- if (this.isCrossDomain) {
686
- // Cross-domain: Use session ID in Authorization header
687
- const sessionId = sessionStorage.getItem('auth_session_id');
688
- if (sessionId) {
689
- headers['Authorization'] = `Bearer ${sessionId}`;
690
- }
691
- } else {
692
- // Same-domain: Add CSRF token for state-changing requests
693
- if (options.method && ['POST', 'PUT', 'DELETE'].includes(options.method.toUpperCase())) {
694
- const csrfToken = this.getCsrfToken();
695
- if (csrfToken) {
696
- headers['X-XSRF-TOKEN'] = csrfToken;
697
- }
698
- }
690
+ return null;
691
+ }
692
+
693
+ // Make authenticated API request using appropriate authentication method
694
+ async makeAuthenticatedRequest(url, options = {}) {
695
+ const headers = {
696
+ Accept: "application/json",
697
+ ...options.headers,
698
+ };
699
+
700
+ // Choose authentication method based on domain context
701
+ if (this.isCrossDomain) {
702
+ // Cross-domain: Use session ID in Authorization header
703
+ const sessionId = sessionStorage.getItem("auth_session_id");
704
+ if (sessionId) {
705
+ headers["Authorization"] = `Bearer ${sessionId}`;
706
+ console.log(
707
+ "Using session ID in Authorization header for cross-domain request to:",
708
+ url
709
+ );
710
+ }
711
+ } else {
712
+ // Same-domain: Add CSRF token for state-changing requests
713
+ if (
714
+ options.method &&
715
+ ["POST", "PUT", "DELETE"].includes(options.method.toUpperCase())
716
+ ) {
717
+ const csrfToken = this.getCsrfToken();
718
+ if (csrfToken) {
719
+ headers["X-XSRF-TOKEN"] = csrfToken;
699
720
  }
721
+ }
722
+ }
700
723
 
701
- return {
702
- ...options,
703
- headers,
704
- credentials: 'include' // Always include cookies for same-domain fallback
705
- };
724
+ const requestOptions = {
725
+ ...options,
726
+ headers: headers,
727
+ credentials: "include", // Always include cookies for same-domain fallback
728
+ };
729
+
730
+ const response = await fetch(url, requestOptions);
731
+
732
+ if (response.status === 401) {
733
+ // QUESTION: would I ever use this method to call session check?
734
+ // I can only envisage scenarios where app endpoints get hit using this?
735
+ //
736
+ // Don't auto-logout during callback flow or logout operations to prevent infinite loops
737
+ const isCallbackFlow =
738
+ options.skipAutoLogout || url.includes("/session/idtoken");
739
+ const isLogoutOperation = url.includes("/session/logout");
740
+ const isSessionCheck = url.includes("/session/check");
741
+ const isUserInfoOperation = url.includes("/userinfo");
742
+
743
+ if (
744
+ !isCallbackFlow &&
745
+ !isLogoutOperation &&
746
+ !isSessionCheck &&
747
+ !isUserInfoOperation
748
+ ) {
749
+ console.log("Session expired, redirecting to login");
750
+ // Clear expired session data
751
+ this.clearLocalStorage();
752
+ // Redirect to login instead of logout
753
+ this.login();
754
+ throw new Error("Session expired");
755
+ } else {
756
+ console.warn(
757
+ "401 response during protected operation, not auto-logging out to prevent loops"
758
+ );
759
+ }
706
760
  }
707
761
 
708
- // Handle response and authentication errors
709
- afterRequest(response, url, options = {}) {
710
- if (response.status === 401) {
711
- // Don't auto-logout during callback flow or logout operations to prevent infinite loops
712
- const isCallbackFlow = options.skipAutoLogout || url.includes('/session/idtoken');
713
- const isLogoutOperation = url.includes('/session/logout');
714
- const isSessionCheck = url.includes('/session/check');
715
- const isUserInfoOperation = url.includes('/userinfo');
716
-
717
- if (!isCallbackFlow && !isLogoutOperation && !isSessionCheck && !isUserInfoOperation) {
718
- console.log('Session expired, redirecting to login');
719
- // Clear expired session data
720
- this.clearLocalStorage();
721
- // Redirect to login instead of logout
722
- this.login();
723
- throw new Error('Session expired');
724
- } else {
725
- console.warn('401 response during protected operation, not auto-logging out to prevent loops');
726
- }
762
+ return response;
763
+ }
764
+
765
+ // Prepare request options with authentication
766
+ beforeRequest(options = {}) {
767
+ const headers = {
768
+ Accept: "application/json", // Exact match with original
769
+ ...options.headers,
770
+ };
771
+
772
+ // Choose authentication method based on domain context
773
+ if (this.isCrossDomain) {
774
+ // Cross-domain: Use session ID in Authorization header
775
+ const sessionId = sessionStorage.getItem("auth_session_id");
776
+ if (sessionId) {
777
+ headers["Authorization"] = `Bearer ${sessionId}`;
778
+ }
779
+ } else {
780
+ // Same-domain: Add CSRF token for state-changing requests
781
+ if (
782
+ options.method &&
783
+ ["POST", "PUT", "DELETE"].includes(options.method.toUpperCase())
784
+ ) {
785
+ const csrfToken = this.getCsrfToken();
786
+ if (csrfToken) {
787
+ headers["X-XSRF-TOKEN"] = csrfToken;
727
788
  }
789
+ }
790
+ }
728
791
 
729
- return response;
792
+ return {
793
+ ...options,
794
+ headers,
795
+ credentials: "include", // Always include cookies for same-domain fallback
796
+ };
797
+ }
798
+
799
+ // Handle response and authentication errors
800
+ afterRequest(response, url, options = {}) {
801
+ if (response.status === 401) {
802
+ // Don't auto-logout during callback flow or logout operations to prevent infinite loops
803
+ const isCallbackFlow =
804
+ options.skipAutoLogout || url.includes("/session/idtoken");
805
+ const isLogoutOperation = url.includes("/session/logout");
806
+ const isSessionCheck = url.includes("/session/check");
807
+ const isUserInfoOperation = url.includes("/userinfo");
808
+
809
+ if (
810
+ !isCallbackFlow &&
811
+ !isLogoutOperation &&
812
+ !isSessionCheck &&
813
+ !isUserInfoOperation
814
+ ) {
815
+ console.log("Session expired, redirecting to login");
816
+ // Clear expired session data
817
+ this.clearLocalStorage();
818
+ // Redirect to login instead of logout
819
+ this.login();
820
+ throw new Error("Session expired");
821
+ } else {
822
+ console.warn(
823
+ "401 response during protected operation, not auto-logging out to prevent loops"
824
+ );
825
+ }
730
826
  }
731
827
 
732
- // Finish popup flow - sends callback URL to parent for processing
733
- finishPopupFlow() {
734
- // Send callback URL to parent with clientId for disambiguation
735
- if (window.opener) {
736
- window.opener.postMessage({
737
- type: 'oauth-callback',
738
- clientId: this.config.clientId,
739
- callbackUrl: window.location.href
740
- }, '*');
741
- }
828
+ return response;
829
+ }
830
+
831
+ // Finish popup flow - sends callback URL to parent for processing
832
+ finishPopupFlow() {
833
+ // Send callback URL to parent with clientId for disambiguation
834
+ if (window.opener) {
835
+ window.opener.postMessage(
836
+ {
837
+ type: "oauth-callback",
838
+ clientId: this.config.clientId,
839
+ callbackUrl: window.location.href,
840
+ },
841
+ "*"
842
+ );
742
843
  }
844
+ }
743
845
 
744
- // Logout - destroys session via auth-server with external IDP Single Logout support
745
- async logout() {
746
- try {
747
- const headers = {
748
- 'Accept': 'application/json'
749
- };
750
-
751
- // For cross-domain clients, include session ID in Authorization header
752
- const sessionId = sessionStorage.getItem('auth_session_id');
753
- if (this.isCrossDomain && sessionId) {
754
- headers['Authorization'] = `Bearer ${sessionId}`;
755
- console.log('๐Ÿšช Using session ID in Authorization header for logout');
756
- }
757
-
758
- const response = await fetch(`${this.config.authServerUrl}/session/logout`, {
759
- method: 'POST',
760
- headers: headers,
761
- credentials: 'include' // Include cookies for same-domain fallback
762
- });
763
-
764
- // Check if auth server returned external logout redirect
765
- if (response.ok) {
766
- try {
767
- const logoutData = await response.json();
768
-
769
- if (logoutData.external_logout && logoutData.logout_url) {
770
- console.log('๐Ÿ”— External IDP logout required, redirecting to:', logoutData.logout_url);
771
-
772
- // Clear local storage before external redirect
773
- this.clearLocalStorage();
774
-
775
- // Redirect to external IDP logout (e.g., Keycloak)
776
- window.location.href = logoutData.logout_url;
777
- return; // Don't proceed with local redirect
778
- }
779
- } catch (jsonError) {
780
- console.warn('Logout response is not JSON, continuing with standard logout');
781
- }
782
- }
783
- } catch (error) {
784
- console.error('Logout error (ignoring):', error);
785
- // Ignore logout errors - we're logging out anyway
846
+ // Logout - destroys session via auth-server with external IDP Single Logout support
847
+ async logout() {
848
+ try {
849
+ const headers = {
850
+ Accept: "application/json",
851
+ };
852
+
853
+ // For cross-domain clients, include session ID in Authorization header
854
+ const sessionId = sessionStorage.getItem("auth_session_id");
855
+ if (this.isCrossDomain && sessionId) {
856
+ headers["Authorization"] = `Bearer ${sessionId}`;
857
+ console.log("๐Ÿšช Using session ID in Authorization header for logout");
858
+ }
859
+
860
+ const response = await fetch(
861
+ `${this.config.authServerUrl}/session/logout`,
862
+ {
863
+ method: "POST",
864
+ headers: headers,
865
+ credentials: "include", // Include cookies for same-domain fallback
786
866
  }
867
+ );
787
868
 
788
- // Standard logout flow - clear storage and redirect locally
789
- this.clearLocalStorage();
790
-
791
-
792
- this.config.onLogout();
793
- }
869
+ // Check if auth server returned external logout redirect
870
+ if (response.ok) {
871
+ try {
872
+ const logoutData = await response.json();
794
873
 
795
- // Start popup flow - track popup window and listen for OAuth callback messages
796
- startPopupFlow(popup) {
797
- console.log('Starting popup tracking...');
798
-
799
- const handleMessage = (event) => {
800
- console.log('Popup message received:', event.data);
801
-
802
- // Only respond to messages from our client
803
- if (event.data.clientId !== this.config.clientId) {
804
- console.log('Ignoring message from different client:', event.data.clientId);
805
- return;
806
- }
807
-
808
- if (event.data.type === 'oauth-success') {
809
- console.log('OAuth success received from popup');
810
- popup.close();
811
- window.removeEventListener('message', handleMessage);
812
- // Notify parent that auth completed
813
- window.dispatchEvent(new CustomEvent(AuthServerOAuth2Client.OAUTH_SUCCESS, { detail: event.data }));
814
- } else if (event.data.type === 'oauth-error') {
815
- console.log('OAuth error received from popup');
816
- popup.close();
817
- window.removeEventListener('message', handleMessage);
818
- // Notify parent that auth failed
819
- window.dispatchEvent(new CustomEvent(AuthServerOAuth2Client.OAUTH_ERROR, { detail: event.data }));
820
- } else if (event.data.type === 'oauth-callback') {
821
- console.log('OAuth callback received from popup');
822
- popup.close();
823
- window.removeEventListener('message', handleMessage);
824
- // Notify parent that callback is ready
825
- window.dispatchEvent(new CustomEvent('oauth-callback', { detail: event.data }));
826
- }
827
- };
874
+ if (logoutData.external_logout && logoutData.logout_url) {
875
+ console.log(
876
+ "๐Ÿ”— External IDP logout required, redirecting to:",
877
+ logoutData.logout_url
878
+ );
828
879
 
829
- window.addEventListener('message', handleMessage);
830
- console.log('Added message listener for popup');
831
-
832
- // Also check if popup is closed manually
833
- const checkClosed = setInterval(() => {
834
- if (popup.closed) {
835
- clearInterval(checkClosed);
836
- window.removeEventListener('message', handleMessage);
837
- console.log('Popup closed manually - cleaning up listeners');
838
- }
839
- }, 1000);
880
+ // Clear local storage before external redirect
881
+ this.clearLocalStorage();
882
+
883
+ // Redirect to external IDP logout (e.g., Keycloak)
884
+ window.location.href = logoutData.logout_url;
885
+ return; // Don't proceed with local redirect
886
+ }
887
+ } catch (jsonError) {
888
+ console.warn(
889
+ "Logout response is not JSON, continuing with standard logout"
890
+ );
891
+ }
892
+ }
893
+ } catch (error) {
894
+ console.error("Logout error (ignoring):", error);
895
+ // Ignore logout errors - we're logging out anyway
840
896
  }
841
897
 
842
- // Helper method to clear all auth-related storage
843
- clearLocalStorage() {
844
- sessionStorage.removeItem('auth_id_token');
845
- sessionStorage.removeItem('auth_session_id'); // Clear cross-domain session ID
846
- sessionStorage.removeItem('oauth_state'); // cleanup in case of incomplete flows
847
- sessionStorage.removeItem('oauth_code_verifier'); // cleanup in case of incomplete flows
848
- sessionStorage.removeItem('oauth_nonce'); // cleanup in case of incomplete flows
849
- console.log('๐Ÿงน Cleared all auth-related storage');
850
- }
898
+ // Standard logout flow - clear storage and redirect locally
899
+ this.clearLocalStorage();
900
+
901
+ this.config.onLogout();
902
+ }
903
+
904
+ // Start popup flow - track popup window and listen for OAuth callback messages
905
+ startPopupFlow(popup) {
906
+ console.log("Starting popup tracking...");
907
+
908
+ const handleMessage = (event) => {
909
+ console.log("Popup message received:", event.data);
910
+
911
+ // Only respond to messages from our client
912
+ if (event.data.clientId !== this.config.clientId) {
913
+ console.log(
914
+ "Ignoring message from different client:",
915
+ event.data.clientId
916
+ );
917
+ return;
918
+ }
919
+
920
+ if (event.data.type === "oauth-success") {
921
+ console.log("OAuth success received from popup");
922
+ popup.close();
923
+ window.removeEventListener("message", handleMessage);
924
+ // Notify parent that auth completed
925
+ window.dispatchEvent(
926
+ new CustomEvent(AuthServerOAuth2Client.OAUTH_SUCCESS, {
927
+ detail: event.data,
928
+ })
929
+ );
930
+ } else if (event.data.type === "oauth-error") {
931
+ console.log("OAuth error received from popup");
932
+ popup.close();
933
+ window.removeEventListener("message", handleMessage);
934
+ // Notify parent that auth failed
935
+ window.dispatchEvent(
936
+ new CustomEvent(AuthServerOAuth2Client.OAUTH_ERROR, {
937
+ detail: event.data,
938
+ })
939
+ );
940
+ } else if (event.data.type === "oauth-callback") {
941
+ console.log("OAuth callback received from popup");
942
+ popup.close();
943
+ window.removeEventListener("message", handleMessage);
944
+ // Notify parent that callback is ready
945
+ window.dispatchEvent(
946
+ new CustomEvent("oauth-callback", { detail: event.data })
947
+ );
948
+ }
949
+ };
950
+
951
+ window.addEventListener("message", handleMessage);
952
+ console.log("Added message listener for popup");
953
+
954
+ // Also check if popup is closed manually
955
+ const checkClosed = setInterval(() => {
956
+ if (popup.closed) {
957
+ clearInterval(checkClosed);
958
+ window.removeEventListener("message", handleMessage);
959
+ console.log("Popup closed manually - cleaning up listeners");
960
+ }
961
+ }, 1000);
962
+ }
963
+
964
+ // Helper method to clear all auth-related storage
965
+ clearLocalStorage() {
966
+ sessionStorage.removeItem("auth_id_token");
967
+ sessionStorage.removeItem("auth_session_id"); // Clear cross-domain session ID
968
+ sessionStorage.removeItem("oauth_state"); // cleanup in case of incomplete flows
969
+ sessionStorage.removeItem("oauth_code_verifier"); // cleanup in case of incomplete flows
970
+ sessionStorage.removeItem("oauth_nonce"); // cleanup in case of incomplete flows
971
+ console.log("๐Ÿงน Cleared all auth-related storage");
972
+ }
851
973
  }
852
974
 
853
975
  // ES module exports (for modern bundlers) - only when not in browser global context
854
- if (typeof module !== 'undefined' && module.exports) {
855
- // CommonJS (Node.js)
856
- module.exports = AuthServerOAuth2Client;
857
- module.exports.default = AuthServerOAuth2Client;
858
- module.exports.AuthServerOAuth2Client = AuthServerOAuth2Client;
859
- } else if (typeof exports !== 'undefined') {
860
- // ES modules
861
- exports.default = AuthServerOAuth2Client;
862
- exports.AuthServerOAuth2Client = AuthServerOAuth2Client;
976
+ if (typeof module !== "undefined" && module.exports) {
977
+ // CommonJS (Node.js)
978
+ module.exports = AuthServerOAuth2Client;
979
+ module.exports.default = AuthServerOAuth2Client;
980
+ module.exports.AuthServerOAuth2Client = AuthServerOAuth2Client;
981
+ } else if (typeof exports !== "undefined") {
982
+ // ES modules
983
+ exports.default = AuthServerOAuth2Client;
984
+ exports.AuthServerOAuth2Client = AuthServerOAuth2Client;
863
985
  }
864
986
 
865
987
  // Create global OAuth client instance for browser use
866
- if (typeof window !== 'undefined') {
867
- window.AuthServerOAuth2Client = AuthServerOAuth2Client;
868
- window.authServerOAuth2Client = new AuthServerOAuth2Client();
869
- }
988
+ if (typeof window !== "undefined") {
989
+ window.AuthServerOAuth2Client = AuthServerOAuth2Client;
990
+ window.authServerOAuth2Client = new AuthServerOAuth2Client();
991
+ }