@civic/auth 0.6.1-beta.4 → 0.7.0-beta.1
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/CHANGELOG.md +7 -0
- package/dist/nextjs/config.d.ts.map +1 -1
- package/dist/nextjs/config.js +3 -1
- package/dist/nextjs/config.js.map +1 -1
- package/dist/nextjs/hooks/useUserCookie.d.ts.map +1 -1
- package/dist/nextjs/hooks/useUserCookie.js.map +1 -1
- package/dist/nextjs/providers/NextAuthProvider.d.ts.map +1 -1
- package/dist/nextjs/providers/NextAuthProvider.js +1 -0
- package/dist/nextjs/providers/NextAuthProvider.js.map +1 -1
- package/dist/shared/hooks/useSignIn.d.ts +9 -4
- package/dist/shared/hooks/useSignIn.d.ts.map +1 -1
- package/dist/shared/hooks/useSignIn.js +75 -42
- package/dist/shared/hooks/useSignIn.js.map +1 -1
- package/dist/shared/providers/AuthContext.d.ts +7 -2
- package/dist/shared/providers/AuthContext.d.ts.map +1 -1
- package/dist/shared/providers/AuthContext.js.map +1 -1
- package/dist/shared/providers/UserProvider.d.ts +5 -1
- package/dist/shared/providers/UserProvider.d.ts.map +1 -1
- package/dist/shared/providers/UserProvider.js.map +1 -1
- package/dist/shared/version.d.ts +1 -1
- package/dist/shared/version.js +1 -1
- package/dist/shared/version.js.map +1 -1
- package/dist/vanillajs/auth/AuthenticationEvents.d.ts.map +1 -1
- package/dist/vanillajs/auth/AuthenticationEvents.js +2 -2
- package/dist/vanillajs/auth/AuthenticationEvents.js.map +1 -1
- package/dist/vanillajs/auth/CivicAuth.d.ts +69 -107
- package/dist/vanillajs/auth/CivicAuth.d.ts.map +1 -1
- package/dist/vanillajs/auth/CivicAuth.js +415 -440
- package/dist/vanillajs/auth/CivicAuth.js.map +1 -1
- package/dist/vanillajs/auth/SessionManager.d.ts.map +1 -1
- package/dist/vanillajs/auth/SessionManager.js +2 -2
- package/dist/vanillajs/auth/SessionManager.js.map +1 -1
- package/dist/vanillajs/auth/TokenRefresher.d.ts.map +1 -1
- package/dist/vanillajs/auth/TokenRefresher.js +2 -2
- package/dist/vanillajs/auth/TokenRefresher.js.map +1 -1
- package/dist/vanillajs/auth/config/ConfigProcessor.d.ts +6 -0
- package/dist/vanillajs/auth/config/ConfigProcessor.d.ts.map +1 -0
- package/dist/vanillajs/auth/config/ConfigProcessor.js +68 -0
- package/dist/vanillajs/auth/config/ConfigProcessor.js.map +1 -0
- package/dist/vanillajs/auth/handlers/IframeAuthHandler.d.ts +40 -0
- package/dist/vanillajs/auth/handlers/IframeAuthHandler.d.ts.map +1 -0
- package/dist/vanillajs/auth/handlers/IframeAuthHandler.js +388 -0
- package/dist/vanillajs/auth/handlers/IframeAuthHandler.js.map +1 -0
- package/dist/vanillajs/auth/handlers/MessageHandler.d.ts +170 -0
- package/dist/vanillajs/auth/handlers/MessageHandler.d.ts.map +1 -0
- package/dist/vanillajs/auth/handlers/MessageHandler.js +367 -0
- package/dist/vanillajs/auth/handlers/MessageHandler.js.map +1 -0
- package/dist/vanillajs/auth/{OAuthCallbackHandler.d.ts → handlers/OAuthCallbackHandler.d.ts} +18 -27
- package/dist/vanillajs/auth/handlers/OAuthCallbackHandler.d.ts.map +1 -0
- package/dist/vanillajs/auth/handlers/OAuthCallbackHandler.js +292 -0
- package/dist/vanillajs/auth/handlers/OAuthCallbackHandler.js.map +1 -0
- package/dist/vanillajs/auth/handlers/PopupHandler.d.ts +108 -0
- package/dist/vanillajs/auth/handlers/PopupHandler.d.ts.map +1 -0
- package/dist/vanillajs/auth/handlers/PopupHandler.js +333 -0
- package/dist/vanillajs/auth/handlers/PopupHandler.js.map +1 -0
- package/dist/vanillajs/auth/types/AuthTypes.d.ts +135 -0
- package/dist/vanillajs/auth/types/AuthTypes.d.ts.map +1 -0
- package/dist/vanillajs/auth/types/AuthTypes.js +40 -0
- package/dist/vanillajs/auth/types/AuthTypes.js.map +1 -0
- package/dist/vanillajs/iframe/IframeManager.d.ts +33 -0
- package/dist/vanillajs/iframe/IframeManager.d.ts.map +1 -1
- package/dist/vanillajs/iframe/IframeManager.js +163 -36
- package/dist/vanillajs/iframe/IframeManager.js.map +1 -1
- package/dist/vanillajs/index.d.ts +2 -2
- package/dist/vanillajs/index.d.ts.map +1 -1
- package/dist/vanillajs/index.js +2 -2
- package/dist/vanillajs/index.js.map +1 -1
- package/dist/vanillajs/services/ApiService.d.ts.map +1 -1
- package/dist/vanillajs/services/ApiService.js +2 -2
- package/dist/vanillajs/services/ApiService.js.map +1 -1
- package/dist/vanillajs/types/index.d.ts +15 -10
- package/dist/vanillajs/types/index.d.ts.map +1 -1
- package/dist/vanillajs/types/index.js +15 -10
- package/dist/vanillajs/types/index.js.map +1 -1
- package/dist/vanillajs/utils/auth-utils.d.ts +2 -1
- package/dist/vanillajs/utils/auth-utils.d.ts.map +1 -1
- package/dist/vanillajs/utils/auth-utils.js +6 -3
- package/dist/vanillajs/utils/auth-utils.js.map +1 -1
- package/dist/vanillajs/utils/logger.d.ts +16 -15
- package/dist/vanillajs/utils/logger.d.ts.map +1 -1
- package/dist/vanillajs/utils/logger.js +35 -19
- package/dist/vanillajs/utils/logger.js.map +1 -1
- package/package.json +6 -1
- package/dist/vanillajs/auth/OAuthCallbackHandler.d.ts.map +0 -1
- package/dist/vanillajs/auth/OAuthCallbackHandler.js +0 -143
- package/dist/vanillajs/auth/OAuthCallbackHandler.js.map +0 -1
|
@@ -1,105 +1,58 @@
|
|
|
1
1
|
import { AuthEvent } from "../types/index.js";
|
|
2
|
-
import { LocalStorageAdapter } from "../../browser/storage.js";
|
|
3
|
-
import { handleOAuthRedirectPage } from "./OAuthCallbackHandler.js";
|
|
4
2
|
import { buildAuthUrl } from "../utils/auth-utils.js";
|
|
5
|
-
import {
|
|
6
|
-
import { SignalObserver } from "../iframe/SignalObserver.js";
|
|
3
|
+
import { createMainLogger, configureLogging, setCurrentLogger, } from "../utils/logger.js";
|
|
7
4
|
import { GenericPublicClientPKCEProducer } from "../../services/PKCE.js";
|
|
8
5
|
import { generateState } from "../../lib/oauth.js";
|
|
9
6
|
import { SessionManager } from "./SessionManager.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
CivicAuthErrorCode["AUTH_PROCESS_TIMEOUT"] = "AUTH_PROCESS_TIMEOUT";
|
|
21
|
-
CivicAuthErrorCode["IFRAME_LOAD_ERROR"] = "IFRAME_LOAD_ERROR";
|
|
22
|
-
CivicAuthErrorCode["INVALID_MESSAGE"] = "INVALID_MESSAGE";
|
|
23
|
-
})(CivicAuthErrorCode || (CivicAuthErrorCode = {}));
|
|
24
|
-
/**
|
|
25
|
-
* Constants for the auth client
|
|
26
|
-
*/
|
|
27
|
-
const CONSTANTS = {
|
|
28
|
-
DEFAULT_IFRAME_ID: "civic-auth-iframe",
|
|
29
|
-
DEFAULT_AUTH_PROCESS_TIMEOUT: 60000, // 60 seconds
|
|
30
|
-
SUCCESS_SIGNAL_ID: "civic-auth-success-signal",
|
|
31
|
-
ERROR_SIGNAL_ID: "civic-auth-error-signal",
|
|
32
|
-
};
|
|
33
|
-
class CivicAuthError extends Error {
|
|
34
|
-
code;
|
|
35
|
-
constructor(message, code) {
|
|
36
|
-
super(message);
|
|
37
|
-
this.code = code;
|
|
38
|
-
this.name = "CivicAuthError";
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Process the configuration with defaults
|
|
43
|
-
*/
|
|
44
|
-
function processConfigWithDefaults(config) {
|
|
45
|
-
const loggingConfig = {
|
|
46
|
-
enabled: true,
|
|
47
|
-
namespace: "iframe",
|
|
48
|
-
level: "debug",
|
|
49
|
-
...config.logging,
|
|
50
|
-
};
|
|
51
|
-
return {
|
|
52
|
-
...config,
|
|
53
|
-
displayMode: config.displayMode || "iframe",
|
|
54
|
-
authProcessTimeout: config.authProcessTimeout || CONSTANTS.DEFAULT_AUTH_PROCESS_TIMEOUT,
|
|
55
|
-
iframeId: config.iframeId || CONSTANTS.DEFAULT_IFRAME_ID,
|
|
56
|
-
logging: loggingConfig,
|
|
57
|
-
storageAdapter: config.storageAdapter || new LocalStorageAdapter(),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
7
|
+
import { AuthenticationEvents } from "./AuthenticationEvents.js";
|
|
8
|
+
import { PopupError } from "../../services/types.js";
|
|
9
|
+
import { handleOAuthRedirectPage } from "./handlers/OAuthCallbackHandler.js";
|
|
10
|
+
import { generateOauthLogoutUrl, clearTokens, retrieveTokens, } from "../../shared/lib/util.js";
|
|
11
|
+
import { getOauthEndpoints } from "../../lib/oauth.js";
|
|
12
|
+
import { CivicAuthError, CivicAuthErrorCode, CIVIC_AUTH_CONSTANTS, } from "./types/AuthTypes.js";
|
|
13
|
+
import { processConfigWithDefaults } from "./config/ConfigProcessor.js";
|
|
14
|
+
import { MessageHandler } from "./handlers/MessageHandler.js";
|
|
15
|
+
import { PopupHandler } from "./handlers/PopupHandler.js";
|
|
16
|
+
import { IframeAuthHandler } from "./handlers/IframeAuthHandler.js";
|
|
60
17
|
/**
|
|
61
18
|
* CivicAuth client for handling OAuth authentication
|
|
19
|
+
*
|
|
20
|
+
* This is a refactored version that uses a modular architecture for better maintainability.
|
|
62
21
|
*/
|
|
63
22
|
export class CivicAuth {
|
|
64
|
-
/**
|
|
65
|
-
* Internal configuration with all optional properties resolved to required ones.
|
|
66
|
-
*
|
|
67
|
-
* We extend CivicAuthClientConfig rather than making these properties required
|
|
68
|
-
* in the public interface to maintain better DX (optional properties) while
|
|
69
|
-
* ensuring type safety internally after defaults are applied.
|
|
70
|
-
*/
|
|
71
23
|
config;
|
|
72
|
-
iframeElement;
|
|
73
|
-
observer;
|
|
74
|
-
authPromise;
|
|
75
|
-
authPromiseResolve;
|
|
76
|
-
authPromiseReject;
|
|
77
|
-
authProcessTimeoutHandle;
|
|
78
|
-
messageEventHandler;
|
|
79
24
|
storage;
|
|
80
25
|
endpoints;
|
|
81
26
|
logger;
|
|
82
27
|
sessionManager;
|
|
83
|
-
|
|
28
|
+
events;
|
|
29
|
+
initialDisplayMode;
|
|
30
|
+
// Authentication state
|
|
31
|
+
authPromise;
|
|
32
|
+
authPromiseResolve;
|
|
33
|
+
authPromiseReject;
|
|
34
|
+
authProcessTimeoutHandle;
|
|
35
|
+
popupFailureTimeoutHandle;
|
|
36
|
+
hasPopupFailed = false;
|
|
37
|
+
// Handlers
|
|
38
|
+
messageHandler;
|
|
39
|
+
popupHandler;
|
|
40
|
+
iframeAuthHandler;
|
|
84
41
|
/**
|
|
85
|
-
* Private constructor
|
|
42
|
+
* Private constructor - initializes configuration and handlers.
|
|
86
43
|
* Use {@link CivicAuth.create} to create a new instance.
|
|
87
|
-
* @param config - Configuration options for the auth client
|
|
88
|
-
* @throws {CivicAuthError} If required configuration is missing
|
|
89
|
-
* @private
|
|
90
44
|
*/
|
|
91
45
|
constructor(config) {
|
|
92
|
-
// Process config with defaults
|
|
46
|
+
// Process config with defaults and validation
|
|
93
47
|
this.config = processConfigWithDefaults(config);
|
|
94
|
-
|
|
48
|
+
this.initialDisplayMode = this.config.displayMode;
|
|
49
|
+
// Configure logging
|
|
95
50
|
configureLogging(this.config.logging);
|
|
96
|
-
// Initialize logger
|
|
51
|
+
// Initialize logger - always use "vanillajs" as base namespace
|
|
97
52
|
if (this.config.logging?.enabled) {
|
|
98
|
-
|
|
99
|
-
this.logger = createLogger(namespace);
|
|
53
|
+
this.logger = createMainLogger("vanillajs"); // Always use "vanillajs"
|
|
100
54
|
}
|
|
101
55
|
else {
|
|
102
|
-
// Create a no-op logger when logging is disabled
|
|
103
56
|
this.logger = {
|
|
104
57
|
debug: () => { },
|
|
105
58
|
info: () => { },
|
|
@@ -107,26 +60,14 @@ export class CivicAuth {
|
|
|
107
60
|
error: () => { },
|
|
108
61
|
};
|
|
109
62
|
}
|
|
110
|
-
// Set this logger as the current logger
|
|
111
63
|
setCurrentLogger(this.logger);
|
|
112
|
-
// Use the storage adapter from processed config (guaranteed to be present)
|
|
113
64
|
this.storage = this.config.storageAdapter;
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
{ key: "clientId", value: config.clientId },
|
|
121
|
-
{ key: "targetContainerElement", value: config.targetContainerElement },
|
|
122
|
-
{ key: "textSignals.success", value: config.textSignals?.success },
|
|
123
|
-
];
|
|
124
|
-
for (const { key, value } of requiredConfigs) {
|
|
125
|
-
if (!value) {
|
|
126
|
-
throw new CivicAuthError(`CivicAuth: ${key} is required.`, CivicAuthErrorCode.CONFIG_REQUIRED);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
this.messageEventHandler = this.handleIframeMessage.bind(this);
|
|
65
|
+
// Always initialize events - use provided events or create default instance
|
|
66
|
+
this.events = config.events || new AuthenticationEvents();
|
|
67
|
+
// Always initialize SessionManager
|
|
68
|
+
this.sessionManager = new SessionManager(this.storage, this.events);
|
|
69
|
+
// Initialize handlers
|
|
70
|
+
this.initializeHandlers();
|
|
130
71
|
}
|
|
131
72
|
/**
|
|
132
73
|
* Creates and initializes a new instance of CivicAuth.
|
|
@@ -140,9 +81,12 @@ export class CivicAuth {
|
|
|
140
81
|
* ```typescript
|
|
141
82
|
* const auth = await CivicAuth.create({
|
|
142
83
|
* clientId: "your-client-id",
|
|
143
|
-
* redirectUrl
|
|
144
|
-
*
|
|
145
|
-
*
|
|
84
|
+
* // redirectUrl is optional - defaults to current page (window.location.origin + window.location.pathname)
|
|
85
|
+
* redirectUrl: "https://your-app.com/callback", // optional
|
|
86
|
+
* // oauthServerBaseUrl is optional - defaults to "https://auth.civic.com/oauth"
|
|
87
|
+
* oauthServerBaseUrl: "https://auth-server.com/", // optional
|
|
88
|
+
* // scopes is optional - defaults to ['openid', 'profile', 'email', 'offline_access']
|
|
89
|
+
* scopes: ["openid", "profile"], // optional
|
|
146
90
|
* targetContainerElement: "auth-container",
|
|
147
91
|
* textSignals: {
|
|
148
92
|
* success: "Authentication successful!"
|
|
@@ -157,19 +101,21 @@ export class CivicAuth {
|
|
|
157
101
|
}
|
|
158
102
|
/**
|
|
159
103
|
* Initializes the auth client and checks for callback handling
|
|
160
|
-
* @throws {CivicAuthError} If initialization fails
|
|
161
104
|
*/
|
|
162
105
|
async init() {
|
|
106
|
+
this.logger.info("🚀 Initializing CivicAuth", {
|
|
107
|
+
currentUrl: window.location.href,
|
|
108
|
+
redirectUrl: this.config.redirectUrl,
|
|
109
|
+
oauthServerBaseUrl: this.config.oauthServerBaseUrl,
|
|
110
|
+
isCallbackUrl: window.location.href.startsWith(this.config.redirectUrl),
|
|
111
|
+
});
|
|
163
112
|
try {
|
|
164
|
-
// Get OAuth endpoints
|
|
165
|
-
this.endpoints =
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
endsession: `${this.config.oauthServerBaseUrl}endsession`,
|
|
171
|
-
};
|
|
172
|
-
// Initialize SessionManager with auth config for token refresh
|
|
113
|
+
// Get OAuth endpoints using shared function (handles trailing slash automatically)
|
|
114
|
+
this.endpoints = await getOauthEndpoints(this.config.oauthServerBaseUrl);
|
|
115
|
+
this.logger.info("🔗 OAuth endpoints configured", {
|
|
116
|
+
endpoints: this.endpoints,
|
|
117
|
+
});
|
|
118
|
+
// Initialize SessionManager with auth config
|
|
173
119
|
if (this.sessionManager) {
|
|
174
120
|
const authConfig = {
|
|
175
121
|
clientId: this.config.clientId,
|
|
@@ -178,101 +124,66 @@ export class CivicAuth {
|
|
|
178
124
|
scopes: this.config.scopes,
|
|
179
125
|
endpoints: this.endpoints,
|
|
180
126
|
};
|
|
127
|
+
this.logger.info("🔧 Initializing SessionManager", { authConfig });
|
|
181
128
|
await this.sessionManager.initializeWithAuthConfig(authConfig);
|
|
182
129
|
}
|
|
183
130
|
// Check if we're on the callback page
|
|
184
|
-
|
|
131
|
+
const isCallbackPage = window.location.href.startsWith(this.config.redirectUrl);
|
|
132
|
+
this.logger.info("🔍 Callback page check", {
|
|
133
|
+
isCallbackPage,
|
|
134
|
+
currentUrl: window.location.href,
|
|
135
|
+
redirectUrl: this.config.redirectUrl,
|
|
136
|
+
});
|
|
137
|
+
if (isCallbackPage) {
|
|
138
|
+
this.logger.info("📞 Processing callback page");
|
|
185
139
|
await this.handleCallback();
|
|
186
140
|
}
|
|
141
|
+
else {
|
|
142
|
+
this.logger.info("🏠 Not a callback page, initialization complete");
|
|
143
|
+
}
|
|
187
144
|
}
|
|
188
145
|
catch (error) {
|
|
189
146
|
const errorMessage = error instanceof Error
|
|
190
147
|
? error.message
|
|
191
148
|
: "Failed to initialize authentication";
|
|
192
|
-
this.logger.error("
|
|
193
|
-
|
|
149
|
+
this.logger.error("❌ CivicAuth initialization failed", {
|
|
150
|
+
error: errorMessage,
|
|
151
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
152
|
+
});
|
|
153
|
+
this.events.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
194
154
|
detail: errorMessage,
|
|
195
155
|
});
|
|
196
156
|
throw new CivicAuthError(errorMessage, CivicAuthErrorCode.INIT_FAILED);
|
|
197
157
|
}
|
|
198
158
|
}
|
|
199
159
|
/**
|
|
200
|
-
*
|
|
201
|
-
* @returns The container element or null if not found
|
|
160
|
+
* Initialize all handlers with proper configuration
|
|
202
161
|
*/
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (this.config.iframeDisplayMode) {
|
|
221
|
-
this.logger.debug(`Using configured iframe display mode: ${this.config.iframeDisplayMode}`);
|
|
222
|
-
return this.config.iframeDisplayMode;
|
|
223
|
-
}
|
|
224
|
-
// Analyze container characteristics to determine best display mode
|
|
225
|
-
const containerRect = container.getBoundingClientRect();
|
|
226
|
-
const containerStyles = window.getComputedStyle(container);
|
|
227
|
-
// Get container dimensions
|
|
228
|
-
const containerWidth = containerRect.width;
|
|
229
|
-
const containerHeight = containerRect.height;
|
|
230
|
-
// Check if container is positioned in a way that suggests it's meant for modal display
|
|
231
|
-
const isFullScreenContainer = containerStyles.position === "fixed" &&
|
|
232
|
-
(containerWidth >= window.innerWidth * 0.9 ||
|
|
233
|
-
containerHeight >= window.innerHeight * 0.9);
|
|
234
|
-
// Modal mode thresholds - if container is too small, use modal for better UX
|
|
235
|
-
const MIN_EMBEDDED_WIDTH = 320; // Same as modal content wrapper width
|
|
236
|
-
const MIN_EMBEDDED_HEIGHT = 400; // Reasonable minimum for embedded auth flow
|
|
237
|
-
// Determine mode based on container characteristics
|
|
238
|
-
if (isFullScreenContainer) {
|
|
239
|
-
this.logger.debug("Container appears to be full-screen positioned, using modal mode", { containerWidth, containerHeight, position: containerStyles.position });
|
|
240
|
-
return "modal";
|
|
241
|
-
}
|
|
242
|
-
if (containerWidth < MIN_EMBEDDED_WIDTH ||
|
|
243
|
-
containerHeight < MIN_EMBEDDED_HEIGHT) {
|
|
244
|
-
this.logger.debug(`Container too small for embedded mode (${containerWidth}x${containerHeight}), using modal mode`, {
|
|
245
|
-
containerWidth,
|
|
246
|
-
containerHeight,
|
|
247
|
-
MIN_EMBEDDED_WIDTH,
|
|
248
|
-
MIN_EMBEDDED_HEIGHT,
|
|
249
|
-
});
|
|
250
|
-
return "modal";
|
|
251
|
-
}
|
|
252
|
-
// Check if container has sufficient space and is properly positioned for embedding
|
|
253
|
-
const hasAdequateSpace = containerWidth >= MIN_EMBEDDED_WIDTH &&
|
|
254
|
-
containerHeight >= MIN_EMBEDDED_HEIGHT;
|
|
255
|
-
const isEmbeddablePosition = containerStyles.position === "relative" ||
|
|
256
|
-
containerStyles.position === "static" ||
|
|
257
|
-
containerStyles.position === "";
|
|
258
|
-
if (hasAdequateSpace && isEmbeddablePosition) {
|
|
259
|
-
this.logger.debug("Container has adequate space and positioning for embedded mode", { containerWidth, containerHeight, position: containerStyles.position });
|
|
260
|
-
return "embedded";
|
|
261
|
-
}
|
|
262
|
-
// Default to modal mode if container characteristics are unclear
|
|
263
|
-
this.logger.debug("Container characteristics unclear, defaulting to modal mode for better UX", { containerWidth, containerHeight, position: containerStyles.position });
|
|
264
|
-
return "modal";
|
|
162
|
+
initializeHandlers() {
|
|
163
|
+
const handlerConfig = {
|
|
164
|
+
config: this.config,
|
|
165
|
+
logger: this.logger,
|
|
166
|
+
onAuthSuccess: this.handleAuthSuccess.bind(this),
|
|
167
|
+
onAuthError: this.handleAuthError.bind(this),
|
|
168
|
+
cleanup: this.cleanup.bind(this),
|
|
169
|
+
};
|
|
170
|
+
this.messageHandler = new MessageHandler({
|
|
171
|
+
...handlerConfig,
|
|
172
|
+
onPopupFailure: this.handlePopupFailure.bind(this),
|
|
173
|
+
});
|
|
174
|
+
this.popupHandler = new PopupHandler(handlerConfig);
|
|
175
|
+
this.iframeAuthHandler = new IframeAuthHandler({
|
|
176
|
+
...handlerConfig,
|
|
177
|
+
messageHandler: this.messageHandler.handleMessage,
|
|
178
|
+
});
|
|
265
179
|
}
|
|
266
180
|
/**
|
|
267
181
|
* Builds the authentication URL with PKCE challenge
|
|
268
|
-
* @returns The complete authentication URL
|
|
269
|
-
* @throws {CivicAuthError} If endpoints are not initialized
|
|
270
182
|
*/
|
|
271
183
|
async buildAuthUrl() {
|
|
272
184
|
if (!this.endpoints) {
|
|
273
185
|
throw new CivicAuthError("OAuth endpoints not initialized. Please wait for initialization to complete.", CivicAuthErrorCode.ENDPOINTS_NOT_INITIALIZED);
|
|
274
186
|
}
|
|
275
|
-
// Use storage directly since it's now AuthStorage
|
|
276
187
|
const pkceProducer = new GenericPublicClientPKCEProducer(this.storage);
|
|
277
188
|
const codeChallenge = await pkceProducer.getCodeChallenge();
|
|
278
189
|
const state = this.config.initialState ||
|
|
@@ -287,293 +198,276 @@ export class CivicAuth {
|
|
|
287
198
|
codeChallenge,
|
|
288
199
|
state,
|
|
289
200
|
prompt: this.config.prompt,
|
|
201
|
+
nonce: this.config.nonce,
|
|
290
202
|
});
|
|
291
203
|
}
|
|
292
|
-
handleIframeMessage(event) {
|
|
293
|
-
const expectedOrigin = new URL(this.config.oauthServerBaseUrl).origin;
|
|
294
|
-
this.logIncomingMessage(event, expectedOrigin);
|
|
295
|
-
if (!this.isValidMessageSource(event, expectedOrigin)) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
this.handleValidMessage(event);
|
|
299
|
-
}
|
|
300
|
-
logIncomingMessage(event, expectedOrigin) {
|
|
301
|
-
this.logger.debug("Global window received message:", {
|
|
302
|
-
data: event.data,
|
|
303
|
-
origin: event.origin,
|
|
304
|
-
sourceProvided: !!event.source,
|
|
305
|
-
iframeContentWindow: this.iframeElement?.contentWindow,
|
|
306
|
-
expectedIframeOrigin: expectedOrigin,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
isValidMessageSource(event, expectedOrigin) {
|
|
310
|
-
const isValidOrigin = event.origin === expectedOrigin;
|
|
311
|
-
const isValidSource = event.source === this.iframeElement?.contentWindow;
|
|
312
|
-
if (!isValidOrigin) {
|
|
313
|
-
this.logger.warn("Ignored message from unexpected origin.", {
|
|
314
|
-
receivedOrigin: event.origin,
|
|
315
|
-
expectedOrigin,
|
|
316
|
-
iframeSrc: this.iframeElement?.src,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
if (!isValidSource) {
|
|
320
|
-
this.logger.warn("Ignored message from unexpected source.", {
|
|
321
|
-
isSourceProvided: !!event.source,
|
|
322
|
-
isIframeContentWindowAvailable: !!this.iframeElement?.contentWindow,
|
|
323
|
-
iframeSrc: this.iframeElement?.src,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
return isValidOrigin && isValidSource;
|
|
327
|
-
}
|
|
328
|
-
handleValidMessage(event) {
|
|
329
|
-
this.logger.info("Message from configured iframe source and origin received", {
|
|
330
|
-
data: event.data,
|
|
331
|
-
iframeSrc: this.iframeElement?.src,
|
|
332
|
-
});
|
|
333
|
-
const message = event.data;
|
|
334
|
-
const messageType = message?.type;
|
|
335
|
-
switch (messageType) {
|
|
336
|
-
case "auth_success":
|
|
337
|
-
this.handleAuthSuccess(message);
|
|
338
|
-
break;
|
|
339
|
-
case "auth_error":
|
|
340
|
-
this.handleAuthError(message);
|
|
341
|
-
break;
|
|
342
|
-
default:
|
|
343
|
-
this.logger.debug("Message from iframe did not match expected types (auth_success, auth_error)", { data: event.data });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
handleAuthSuccess(data) {
|
|
347
|
-
this.config.events?.emit(AuthEvent.SIGN_IN_COMPLETE, {
|
|
348
|
-
detail: "Success signal received via postMessage",
|
|
349
|
-
data,
|
|
350
|
-
});
|
|
351
|
-
this.authPromiseResolve?.(data?.data || {});
|
|
352
|
-
this.cleanup();
|
|
353
|
-
}
|
|
354
|
-
handleAuthError(data) {
|
|
355
|
-
this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
356
|
-
detail: "Error signal received via postMessage",
|
|
357
|
-
error: data,
|
|
358
|
-
});
|
|
359
|
-
this.authPromiseReject?.(new CivicAuthError(data?.detail || "Error signal received via postMessage", CivicAuthErrorCode.INVALID_MESSAGE));
|
|
360
|
-
this.cleanup();
|
|
361
|
-
}
|
|
362
|
-
setupSignalObserver(iframeDoc) {
|
|
363
|
-
const signalObserver = new SignalObserver({
|
|
364
|
-
textSignals: this.config.textSignals,
|
|
365
|
-
events: this.config.events,
|
|
366
|
-
logger: this.logger,
|
|
367
|
-
}, this.authPromiseResolve, this.authPromiseReject, () => this.cleanup());
|
|
368
|
-
signalObserver.setup(iframeDoc);
|
|
369
|
-
}
|
|
370
|
-
async handleNewTabAuth(fullAuthUrl, reject) {
|
|
371
|
-
const popupWindow = window.open(fullAuthUrl, "_blank");
|
|
372
|
-
if (!popupWindow) {
|
|
373
|
-
const error = new CivicAuthError("Failed to open popup window. Please check your browser's popup settings.", CivicAuthErrorCode.INIT_FAILED);
|
|
374
|
-
this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
375
|
-
detail: error.message,
|
|
376
|
-
});
|
|
377
|
-
reject(error);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
async handleIframeAuth(fullAuthUrl, reject) {
|
|
381
|
-
const container = this.getContainerElement();
|
|
382
|
-
if (!container) {
|
|
383
|
-
const error = new CivicAuthError("Target container element not found.", CivicAuthErrorCode.CONTAINER_NOT_FOUND);
|
|
384
|
-
this.logger.error(error.message);
|
|
385
|
-
reject(error);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
this.logger.debug("Creating iframe with modal backdrop", {
|
|
389
|
-
url: fullAuthUrl,
|
|
390
|
-
containerId: container?.id,
|
|
391
|
-
iframeId: this.config.iframeId,
|
|
392
|
-
origin: window.location.origin,
|
|
393
|
-
});
|
|
394
|
-
// Determine the actual display mode for IframeManager
|
|
395
|
-
// For iframe displayMode, we need to decide between modal and embedded based on container
|
|
396
|
-
const iframeDisplayMode = this.determineIframeDisplayMode(container);
|
|
397
|
-
this.logger.debug(`🎯 CivicAuth: Creating IframeManager with display mode: ${iframeDisplayMode}`);
|
|
398
|
-
// Create IframeManager with appropriate display mode
|
|
399
|
-
this.iframeManager = new IframeManager({
|
|
400
|
-
container: container,
|
|
401
|
-
displayMode: iframeDisplayMode,
|
|
402
|
-
iframeId: this.config.iframeId,
|
|
403
|
-
onClose: () => {
|
|
404
|
-
this.logger.debug("Authentication close requested by user (backdrop click, close button, or Escape key)");
|
|
405
|
-
this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
406
|
-
detail: "Authentication cancelled by user",
|
|
407
|
-
});
|
|
408
|
-
reject(new CivicAuthError("Authentication cancelled by user", CivicAuthErrorCode.AUTH_PROCESS_TIMEOUT));
|
|
409
|
-
this.cleanup();
|
|
410
|
-
},
|
|
411
|
-
});
|
|
412
|
-
// Create the iframe using IframeManager
|
|
413
|
-
this.iframeElement = this.iframeManager.createIframe(fullAuthUrl);
|
|
414
|
-
this.config.events?.emit(AuthEvent.SIGN_IN_STARTED, {
|
|
415
|
-
detail: "Iframe created with modal backdrop",
|
|
416
|
-
});
|
|
417
|
-
this.iframeElement.onload = () => {
|
|
418
|
-
this.logger.info("Iframe loaded", {
|
|
419
|
-
iframeSrc: this.iframeElement?.src,
|
|
420
|
-
currentOrigin: window.location.origin,
|
|
421
|
-
expectedAuthServerOrigin: new URL(this.config.oauthServerBaseUrl)
|
|
422
|
-
.origin,
|
|
423
|
-
});
|
|
424
|
-
if (!this.iframeElement?.contentWindow) {
|
|
425
|
-
const errorMsg = "Iframe content window not available after load.";
|
|
426
|
-
this.logger.error(errorMsg, {
|
|
427
|
-
iframeSrc: this.iframeElement?.src,
|
|
428
|
-
});
|
|
429
|
-
reject(new Error(errorMsg));
|
|
430
|
-
this.cleanup();
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
// Set up postMessage listener for cross-origin communication
|
|
434
|
-
if (this.messageEventHandler) {
|
|
435
|
-
window.addEventListener("message", this.messageEventHandler);
|
|
436
|
-
this.logger.info("Added cross-origin message event listener for auth server communication", {
|
|
437
|
-
parentOrigin: window.location.origin,
|
|
438
|
-
authServerOrigin: new URL(this.config.oauthServerBaseUrl).origin,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
else {
|
|
442
|
-
this.logger.error("messageEventHandler was not defined when trying to add listener.");
|
|
443
|
-
}
|
|
444
|
-
// Try to detect redirect to our domain
|
|
445
|
-
try {
|
|
446
|
-
const currentIframeHref = this.iframeElement.contentWindow.location.href;
|
|
447
|
-
if (currentIframeHref.startsWith(this.config.redirectUrl)) {
|
|
448
|
-
this.logger.info("Iframe has navigated to redirectUrl (same-origin). Setting up DOM observer.");
|
|
449
|
-
if (this.iframeElement.contentDocument &&
|
|
450
|
-
this.iframeElement.contentDocument.body) {
|
|
451
|
-
this.setupSignalObserver(this.iframeElement.contentDocument);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
catch (error) {
|
|
456
|
-
this.logger.error("Error checking iframe href", {
|
|
457
|
-
error,
|
|
458
|
-
iframeSrc: this.iframeElement?.src,
|
|
459
|
-
});
|
|
460
|
-
// This is expected when the iframe is on the auth server domain
|
|
461
|
-
this.logger.info("Iframe is on auth server domain - using postMessage for communication", {
|
|
462
|
-
parentOrigin: window.location.origin,
|
|
463
|
-
authServerOrigin: new URL(this.config.oauthServerBaseUrl).origin,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
this.iframeElement.onerror = (event) => {
|
|
468
|
-
this.logger.error("Iframe load error", {
|
|
469
|
-
event,
|
|
470
|
-
iframeSrc: this.iframeElement?.src,
|
|
471
|
-
currentOrigin: window.location.origin,
|
|
472
|
-
});
|
|
473
|
-
this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
474
|
-
detail: "Iframe load error",
|
|
475
|
-
error: event,
|
|
476
|
-
});
|
|
477
|
-
reject(new Error("Iframe failed to load."));
|
|
478
|
-
this.cleanup();
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
204
|
/**
|
|
482
205
|
* Starts the authentication process
|
|
483
206
|
* @returns A promise that resolves with the authentication result
|
|
484
207
|
* @throws {CivicAuthError} If authentication fails or times out
|
|
485
208
|
*/
|
|
486
209
|
async startAuthentication() {
|
|
210
|
+
this.logger.info("🎬 Starting authentication process", {
|
|
211
|
+
displayMode: this.config.displayMode,
|
|
212
|
+
userAgent: navigator.userAgent,
|
|
213
|
+
currentUrl: window.location.href,
|
|
214
|
+
});
|
|
487
215
|
if (!this.endpoints) {
|
|
488
216
|
const error = new CivicAuthError("OAuth endpoints not initialized. Please wait for initialization to complete.", CivicAuthErrorCode.ENDPOINTS_NOT_INITIALIZED);
|
|
489
|
-
this.
|
|
217
|
+
this.logger.error("❌ Endpoints not initialized", {
|
|
218
|
+
error: error.message,
|
|
219
|
+
});
|
|
220
|
+
this.events.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
490
221
|
detail: error.message,
|
|
491
222
|
});
|
|
492
223
|
throw error;
|
|
493
224
|
}
|
|
494
225
|
if (this.authPromise) {
|
|
495
|
-
this.logger.info("Authentication already in progress, returning existing promise");
|
|
226
|
+
this.logger.info("⏳ Authentication already in progress, returning existing promise");
|
|
496
227
|
return this.authPromise;
|
|
497
228
|
}
|
|
498
229
|
const fullAuthUrl = await this.buildAuthUrl();
|
|
499
|
-
this.logger.info("
|
|
500
|
-
|
|
230
|
+
this.logger.info("🔗 Built authentication URL", {
|
|
231
|
+
url: fullAuthUrl,
|
|
501
232
|
displayMode: this.config.displayMode,
|
|
502
233
|
authProcessTimeout: this.config.authProcessTimeout,
|
|
503
234
|
});
|
|
504
235
|
this.authPromise = new Promise((resolve, reject) => {
|
|
505
236
|
this.authPromiseResolve = resolve;
|
|
506
237
|
this.authPromiseReject = reject;
|
|
507
|
-
|
|
238
|
+
this.handleAuthenticationByDisplayMode(fullAuthUrl);
|
|
239
|
+
this.setupAuthenticationTimeout();
|
|
240
|
+
});
|
|
241
|
+
return this.authPromise;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Handle authentication based on display mode
|
|
245
|
+
*/
|
|
246
|
+
async handleAuthenticationByDisplayMode(fullAuthUrl) {
|
|
247
|
+
this.logger.info("🎯 Handling authentication with display mode", {
|
|
248
|
+
displayMode: this.config.displayMode,
|
|
249
|
+
});
|
|
250
|
+
try {
|
|
508
251
|
switch (this.config.displayMode) {
|
|
509
252
|
case "redirect":
|
|
510
|
-
|
|
253
|
+
this.logger.info("🌐 Using redirect mode");
|
|
511
254
|
window.location.href = fullAuthUrl;
|
|
512
255
|
break;
|
|
513
256
|
case "new_tab":
|
|
514
|
-
this.
|
|
257
|
+
this.logger.info("📱 Using new_tab mode");
|
|
258
|
+
if (!this.popupHandler) {
|
|
259
|
+
throw new Error("Popup handler not initialized");
|
|
260
|
+
}
|
|
261
|
+
await this.popupHandler.handleNewTabAuth(fullAuthUrl);
|
|
515
262
|
break;
|
|
516
263
|
case "iframe":
|
|
517
|
-
default:
|
|
518
|
-
this.
|
|
264
|
+
default: {
|
|
265
|
+
this.logger.info("🖼️ Using iframe mode");
|
|
266
|
+
if (!this.iframeAuthHandler || !this.messageHandler) {
|
|
267
|
+
throw new Error("Iframe handler not initialized");
|
|
268
|
+
}
|
|
269
|
+
const iframeElement = await this.iframeAuthHandler.handleIframeAuth(fullAuthUrl);
|
|
270
|
+
this.messageHandler.updateIframeElement(iframeElement);
|
|
519
271
|
break;
|
|
272
|
+
}
|
|
520
273
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
this.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
this.authProcessTimeoutHandle = window.setTimeout(() => {
|
|
529
|
-
this.logger.warn("Authentication timed out", {
|
|
530
|
-
displayMode: this.config.displayMode,
|
|
531
|
-
currentOrigin: window.location.origin,
|
|
532
|
-
});
|
|
533
|
-
this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
534
|
-
detail: "Authentication timed out",
|
|
535
|
-
});
|
|
536
|
-
reject(new Error("Authentication timed out."));
|
|
537
|
-
this.cleanup();
|
|
538
|
-
}, this.config.authProcessTimeout);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (error instanceof PopupError) {
|
|
277
|
+
await this.handlePopupErrorWithFallback(fullAuthUrl, error);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this.handleAuthError(error instanceof Error ? error : new Error(String(error)));
|
|
539
281
|
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Handle popup error with redirect fallback
|
|
286
|
+
*/
|
|
287
|
+
async handlePopupErrorWithFallback(fullAuthUrl, error) {
|
|
288
|
+
this.logger.warn("🚫 Popup failed, falling back to redirect mode", {
|
|
289
|
+
originalDisplayMode: this.config.displayMode,
|
|
290
|
+
error: error.message,
|
|
540
291
|
});
|
|
541
|
-
|
|
292
|
+
this.events.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
293
|
+
detail: "Popup blocked, falling back to redirect mode",
|
|
294
|
+
});
|
|
295
|
+
try {
|
|
296
|
+
this.logger.info("🔄 Attempting redirect fallback");
|
|
297
|
+
// Clean up current authentication attempt
|
|
298
|
+
this.cleanup();
|
|
299
|
+
// Always switch to redirect mode for Safari compatibility
|
|
300
|
+
this.config.displayMode = "redirect";
|
|
301
|
+
// Regenerate the auth URL with updated display mode in state
|
|
302
|
+
const fallbackAuthUrl = await this.buildAuthUrl();
|
|
303
|
+
this.logger.info("🌐 Redirecting to auth URL", { url: fallbackAuthUrl });
|
|
304
|
+
window.location.href = fallbackAuthUrl;
|
|
305
|
+
this.logger.info("✅ Redirect initiated successfully");
|
|
306
|
+
}
|
|
307
|
+
catch (redirectError) {
|
|
308
|
+
this.logger.error("❌ Redirect fallback failed", {
|
|
309
|
+
error: redirectError,
|
|
310
|
+
redirectUrl: fullAuthUrl,
|
|
311
|
+
});
|
|
312
|
+
const fallbackError = new CivicAuthError("Failed to open popup window and redirect fallback failed. Please check your browser's popup settings.", CivicAuthErrorCode.INIT_FAILED);
|
|
313
|
+
this.events.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
314
|
+
detail: fallbackError.message,
|
|
315
|
+
});
|
|
316
|
+
this.handleAuthError(fallbackError);
|
|
317
|
+
}
|
|
542
318
|
}
|
|
543
319
|
/**
|
|
544
|
-
*
|
|
320
|
+
* Setup authentication timeout
|
|
545
321
|
*/
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (this.
|
|
549
|
-
this.
|
|
550
|
-
this.
|
|
551
|
-
|
|
322
|
+
setupAuthenticationTimeout() {
|
|
323
|
+
// Skip timeout for embedded iframe mode - embedded iframes should stay persistent
|
|
324
|
+
if (this.config.displayMode === "iframe" &&
|
|
325
|
+
this.config.iframeDisplayMode === "embedded") {
|
|
326
|
+
this.logger.debug("⏰ Skipping authentication timeout for embedded iframe mode", {
|
|
327
|
+
displayMode: this.config.displayMode,
|
|
328
|
+
iframeDisplayMode: this.config.iframeDisplayMode,
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
552
331
|
}
|
|
553
|
-
if (this.
|
|
554
|
-
|
|
555
|
-
|
|
332
|
+
if (this.config.authProcessTimeout && this.config.authProcessTimeout > 0) {
|
|
333
|
+
this.logger.debug("⏰ Setting up authentication timeout", {
|
|
334
|
+
authProcessTimeout: this.config.authProcessTimeout,
|
|
335
|
+
displayMode: this.config.displayMode,
|
|
336
|
+
iframeDisplayMode: this.config.iframeDisplayMode,
|
|
337
|
+
});
|
|
338
|
+
this.authProcessTimeoutHandle = window.setTimeout(() => {
|
|
339
|
+
this.logger.error("⏰ Authentication timed out", {
|
|
340
|
+
displayMode: this.config.displayMode,
|
|
341
|
+
iframeDisplayMode: this.config.iframeDisplayMode,
|
|
342
|
+
currentOrigin: window.location.origin,
|
|
343
|
+
authProcessTimeout: this.config.authProcessTimeout,
|
|
344
|
+
});
|
|
345
|
+
this.events.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
346
|
+
detail: "Authentication timed out",
|
|
347
|
+
});
|
|
348
|
+
const error = new CivicAuthError("Authentication timed out", CivicAuthErrorCode.AUTH_PROCESS_TIMEOUT);
|
|
349
|
+
this.handleAuthError(error);
|
|
350
|
+
}, this.config.authProcessTimeout);
|
|
556
351
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Handle successful authentication
|
|
355
|
+
*/
|
|
356
|
+
handleAuthSuccess(result) {
|
|
357
|
+
this.logger.info("✅ Authentication successful");
|
|
358
|
+
this.authPromiseResolve?.(result);
|
|
359
|
+
this.cleanup();
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Handle authentication error
|
|
363
|
+
*/
|
|
364
|
+
handleAuthError(error) {
|
|
365
|
+
this.logger.error("❌ Authentication failed", { error: error.message });
|
|
366
|
+
this.authPromiseReject?.(error);
|
|
367
|
+
this.cleanup();
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Handle popup failure - simplified like React implementation
|
|
371
|
+
*/
|
|
372
|
+
handlePopupFailure(failedUrl) {
|
|
373
|
+
this.hasPopupFailed = true;
|
|
374
|
+
this.logger.warn("Popup failed, using redirect mode instead...", {
|
|
375
|
+
failedUrl,
|
|
376
|
+
});
|
|
377
|
+
// Clean up iframe if it exists
|
|
378
|
+
if (this.iframeAuthHandler) {
|
|
379
|
+
this.iframeAuthHandler.cleanupIframe();
|
|
561
380
|
}
|
|
562
|
-
//
|
|
563
|
-
if (
|
|
564
|
-
|
|
381
|
+
// Always redirect to the failed URL or build a new one
|
|
382
|
+
if (failedUrl) {
|
|
383
|
+
window.location.href = failedUrl;
|
|
565
384
|
}
|
|
566
|
-
|
|
567
|
-
this.
|
|
568
|
-
|
|
569
|
-
|
|
385
|
+
else {
|
|
386
|
+
this.buildAuthUrl()
|
|
387
|
+
.then((authUrl) => {
|
|
388
|
+
window.location.href = authUrl;
|
|
389
|
+
})
|
|
390
|
+
.catch((error) => {
|
|
391
|
+
this.logger.error("Failed to build auth URL for redirect fallback", {
|
|
392
|
+
error,
|
|
393
|
+
});
|
|
394
|
+
const fallbackError = new CivicAuthError("Failed to redirect for authentication", CivicAuthErrorCode.INIT_FAILED);
|
|
395
|
+
this.handleAuthError(fallbackError);
|
|
396
|
+
});
|
|
570
397
|
}
|
|
571
|
-
this.logger.debug("Resetting promise state");
|
|
572
|
-
this.authPromise = undefined;
|
|
573
|
-
this.authPromiseResolve = undefined;
|
|
574
|
-
this.authPromiseReject = undefined;
|
|
575
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Show popup failure message to user
|
|
401
|
+
*/
|
|
402
|
+
showPopupFailureMessage(customMessage = "Authentication will continue in this window. Please wait...") {
|
|
403
|
+
const container = this.getContainerElement();
|
|
404
|
+
if (!container) {
|
|
405
|
+
this.logger.warn("Cannot show popup failure message - container not found");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const existingMessage = document.getElementById("civic-auth-popup-failure-message");
|
|
409
|
+
if (existingMessage && existingMessage.parentNode) {
|
|
410
|
+
existingMessage.parentNode.removeChild(existingMessage);
|
|
411
|
+
}
|
|
412
|
+
const messageOverlay = document.createElement("div");
|
|
413
|
+
messageOverlay.id = "civic-auth-popup-failure-message";
|
|
414
|
+
messageOverlay.style.cssText =
|
|
415
|
+
"position:absolute;top:0;left:0;right:0;background:rgba(255,249,196,.95);border:1px solid #f59e0b;border-radius:6px;padding:12px;margin:8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:14px;color:#92400e;z-index:1000;box-shadow:0 2px 4px rgba(0,0,0,.1);";
|
|
416
|
+
messageOverlay.innerHTML =
|
|
417
|
+
'<div style="display:flex;align-items:center;gap:8px;">' +
|
|
418
|
+
'<span style="font-size:16px;">⚠️</span>' +
|
|
419
|
+
"<div>" +
|
|
420
|
+
"<strong>Popup blocked</strong><br>" +
|
|
421
|
+
'<span style="font-size:12px;">' +
|
|
422
|
+
customMessage +
|
|
423
|
+
"</span>" +
|
|
424
|
+
"</div>" +
|
|
425
|
+
"</div>";
|
|
426
|
+
if (getComputedStyle(container).position === "static") {
|
|
427
|
+
container.style.position = "relative";
|
|
428
|
+
}
|
|
429
|
+
container.appendChild(messageOverlay);
|
|
430
|
+
setTimeout(() => {
|
|
431
|
+
if (messageOverlay.parentNode) {
|
|
432
|
+
messageOverlay.parentNode.removeChild(messageOverlay);
|
|
433
|
+
}
|
|
434
|
+
}, 10000);
|
|
435
|
+
this.logger.info("Popup failure message displayed to user");
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Setup popup failure timeout
|
|
439
|
+
*/
|
|
440
|
+
setupPopupFailureTimeout() {
|
|
441
|
+
this.popupFailureTimeoutHandle = window.setTimeout(() => {
|
|
442
|
+
this.logger.info("⏰ Popup failure timeout reached");
|
|
443
|
+
this.events.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
444
|
+
detail: "Authentication timeout - popup failure scenario",
|
|
445
|
+
});
|
|
446
|
+
const error = new CivicAuthError("Authentication timeout - popup failure scenario", CivicAuthErrorCode.AUTH_PROCESS_TIMEOUT);
|
|
447
|
+
this.handleAuthError(error);
|
|
448
|
+
}, 20000); // 20 seconds
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Gets the container element for the auth iframe
|
|
452
|
+
*/
|
|
453
|
+
getContainerElement() {
|
|
454
|
+
if (typeof this.config.targetContainerElement === "string") {
|
|
455
|
+
const element = document.getElementById(this.config.targetContainerElement);
|
|
456
|
+
if (!element) {
|
|
457
|
+
this.logger.warn(`Container element with ID "${this.config.targetContainerElement}" not found`);
|
|
458
|
+
}
|
|
459
|
+
return element;
|
|
460
|
+
}
|
|
461
|
+
return this.config.targetContainerElement ?? null;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Handle OAuth callback
|
|
465
|
+
*/
|
|
576
466
|
async handleCallback() {
|
|
467
|
+
this.logger.info("🔄 Handling OAuth callback", {
|
|
468
|
+
currentUrl: window.location.href,
|
|
469
|
+
redirectUrl: this.config.redirectUrl,
|
|
470
|
+
});
|
|
577
471
|
try {
|
|
578
472
|
const callbackHandled = await handleOAuthRedirectPage({
|
|
579
473
|
clientId: this.config.clientId,
|
|
@@ -585,10 +479,12 @@ export class CivicAuth {
|
|
|
585
479
|
},
|
|
586
480
|
storageAdapter: this.storage,
|
|
587
481
|
});
|
|
482
|
+
this.logger.info("📋 Callback processing result", { callbackHandled });
|
|
588
483
|
if (callbackHandled) {
|
|
589
|
-
const successSignal = document.getElementById(
|
|
590
|
-
const errorSignal = document.getElementById(
|
|
484
|
+
const successSignal = document.getElementById(CIVIC_AUTH_CONSTANTS.SUCCESS_SIGNAL_ID);
|
|
485
|
+
const errorSignal = document.getElementById(CIVIC_AUTH_CONSTANTS.ERROR_SIGNAL_ID);
|
|
591
486
|
if (successSignal) {
|
|
487
|
+
this.logger.info("✅ Success signal found");
|
|
592
488
|
let userInfo = null;
|
|
593
489
|
const userInfoAttr = successSignal.getAttribute("data-user-info");
|
|
594
490
|
if (userInfoAttr) {
|
|
@@ -596,70 +492,92 @@ export class CivicAuth {
|
|
|
596
492
|
userInfo = JSON.parse(userInfoAttr);
|
|
597
493
|
}
|
|
598
494
|
catch (error) {
|
|
599
|
-
this.logger.error("Failed to parse user info
|
|
495
|
+
this.logger.error("❌ Failed to parse user info", { error });
|
|
600
496
|
}
|
|
601
497
|
}
|
|
602
|
-
this.
|
|
498
|
+
this.events.emit(AuthEvent.SIGN_IN_COMPLETE, {
|
|
603
499
|
detail: "Callback processed successfully",
|
|
604
500
|
user: userInfo,
|
|
605
501
|
});
|
|
606
502
|
}
|
|
607
503
|
else if (errorSignal) {
|
|
608
|
-
this.
|
|
504
|
+
this.logger.error("❌ Error signal found");
|
|
505
|
+
this.events.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
609
506
|
detail: errorSignal.textContent || "Unknown error during callback",
|
|
610
507
|
});
|
|
611
508
|
}
|
|
612
509
|
}
|
|
613
510
|
}
|
|
614
511
|
catch (error) {
|
|
615
|
-
this.
|
|
512
|
+
this.logger.error("❌ Callback handling failed", { error });
|
|
513
|
+
this.events.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
616
514
|
detail: error instanceof Error
|
|
617
515
|
? error.message
|
|
618
516
|
: "Unknown error during callback",
|
|
619
517
|
});
|
|
620
518
|
}
|
|
621
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Cleans up resources and event listeners
|
|
522
|
+
*/
|
|
523
|
+
cleanup() {
|
|
524
|
+
this.logger.info("Cleaning up authentication client");
|
|
525
|
+
// Clean up handlers
|
|
526
|
+
this.iframeAuthHandler?.cleanupIframe();
|
|
527
|
+
// Clean up timeouts
|
|
528
|
+
if (this.authProcessTimeoutHandle) {
|
|
529
|
+
window.clearTimeout(this.authProcessTimeoutHandle);
|
|
530
|
+
this.authProcessTimeoutHandle = undefined;
|
|
531
|
+
}
|
|
532
|
+
if (this.popupFailureTimeoutHandle) {
|
|
533
|
+
window.clearTimeout(this.popupFailureTimeoutHandle);
|
|
534
|
+
this.popupFailureTimeoutHandle = undefined;
|
|
535
|
+
}
|
|
536
|
+
// Reset state
|
|
537
|
+
this.hasPopupFailed = false;
|
|
538
|
+
this.authPromise = undefined;
|
|
539
|
+
this.authPromiseResolve = undefined;
|
|
540
|
+
this.authPromiseReject = undefined;
|
|
541
|
+
// Remove message event listener
|
|
542
|
+
if (this.messageHandler) {
|
|
543
|
+
window.removeEventListener("message", this.messageHandler.handleMessage);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
622
546
|
/**
|
|
623
547
|
* Get the current session
|
|
624
548
|
*/
|
|
625
549
|
async getCurrentSession() {
|
|
626
|
-
return this.sessionManager
|
|
550
|
+
return this.sessionManager.getCurrentSession() || null;
|
|
627
551
|
}
|
|
628
552
|
/**
|
|
629
553
|
* Check if user is authenticated
|
|
630
554
|
*/
|
|
631
555
|
async isAuthenticated() {
|
|
632
|
-
return this.sessionManager
|
|
556
|
+
return this.sessionManager.isAuthenticated() || false;
|
|
633
557
|
}
|
|
634
558
|
/**
|
|
635
559
|
* Get the current user
|
|
636
560
|
*/
|
|
637
561
|
async getCurrentUser() {
|
|
638
|
-
return this.sessionManager
|
|
562
|
+
return this.sessionManager.getCurrentUser() || null;
|
|
639
563
|
}
|
|
640
564
|
/**
|
|
641
565
|
* Clear the current session
|
|
642
566
|
*/
|
|
643
567
|
async clearSession() {
|
|
644
|
-
if (!this.sessionManager) {
|
|
645
|
-
throw new Error("SessionManager not initialized. Provide events in config.");
|
|
646
|
-
}
|
|
647
568
|
return this.sessionManager.clearSession();
|
|
648
569
|
}
|
|
649
570
|
/**
|
|
650
571
|
* Manually refresh tokens
|
|
651
572
|
*/
|
|
652
573
|
async refreshTokens() {
|
|
653
|
-
if (!this.sessionManager) {
|
|
654
|
-
throw new Error("SessionManager not initialized. Provide events in config.");
|
|
655
|
-
}
|
|
656
574
|
return this.sessionManager.refreshTokens();
|
|
657
575
|
}
|
|
658
576
|
/**
|
|
659
577
|
* Get token refresher state for debugging
|
|
660
578
|
*/
|
|
661
579
|
getTokenRefresherState() {
|
|
662
|
-
return this.sessionManager
|
|
580
|
+
return this.sessionManager.getTokenRefresherState() || null;
|
|
663
581
|
}
|
|
664
582
|
/**
|
|
665
583
|
* Update the iframe display mode
|
|
@@ -681,9 +599,66 @@ export class CivicAuth {
|
|
|
681
599
|
*/
|
|
682
600
|
async destroy() {
|
|
683
601
|
this.cleanup();
|
|
684
|
-
await this.sessionManager
|
|
685
|
-
this.sessionManager = undefined;
|
|
602
|
+
await this.sessionManager.destroy();
|
|
686
603
|
this.logger.info("CivicAuth destroyed");
|
|
687
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Handle logout
|
|
607
|
+
*/
|
|
608
|
+
async logout() {
|
|
609
|
+
try {
|
|
610
|
+
this.logger.info("🔄 Starting logout process");
|
|
611
|
+
this.events.emit(AuthEvent.SIGN_OUT_STARTED, {
|
|
612
|
+
detail: "Logout process started",
|
|
613
|
+
});
|
|
614
|
+
// Get current tokens before clearing them
|
|
615
|
+
const tokens = await retrieveTokens(this.storage);
|
|
616
|
+
if (!tokens?.id_token) {
|
|
617
|
+
this.logger.warn("⚠️ No ID token found, clearing local session only");
|
|
618
|
+
await clearTokens(this.storage);
|
|
619
|
+
await this.sessionManager.clearSession();
|
|
620
|
+
this.events.emit(AuthEvent.SIGN_OUT_COMPLETE, {
|
|
621
|
+
detail: "Local session cleared",
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (!this.endpoints) {
|
|
626
|
+
throw new Error("OAuth endpoints not initialized");
|
|
627
|
+
}
|
|
628
|
+
// Generate a state for logout
|
|
629
|
+
const state = generateState({
|
|
630
|
+
displayMode: this.config.displayMode || "iframe",
|
|
631
|
+
});
|
|
632
|
+
// Generate logout URL
|
|
633
|
+
const logoutUrl = await generateOauthLogoutUrl({
|
|
634
|
+
clientId: this.config.clientId,
|
|
635
|
+
redirectUrl: this.config.redirectUrl,
|
|
636
|
+
idToken: tokens.id_token,
|
|
637
|
+
state: state,
|
|
638
|
+
oauthServer: this.config.oauthServerBaseUrl,
|
|
639
|
+
});
|
|
640
|
+
this.logger.info("🔗 Generated logout URL", {
|
|
641
|
+
logoutUrl: logoutUrl.toString(),
|
|
642
|
+
});
|
|
643
|
+
// Clear local tokens and session
|
|
644
|
+
await clearTokens(this.storage);
|
|
645
|
+
await this.sessionManager.clearSession();
|
|
646
|
+
// Emit logout complete event before redirect
|
|
647
|
+
this.events.emit(AuthEvent.SIGN_OUT_COMPLETE, {
|
|
648
|
+
detail: "Logout successful",
|
|
649
|
+
});
|
|
650
|
+
// Redirect to logout URL
|
|
651
|
+
this.logger.info("🌐 Redirecting to logout URL");
|
|
652
|
+
window.location.href = logoutUrl.toString();
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
this.logger.error("❌ Logout failed", { error });
|
|
656
|
+
this.events.emit(AuthEvent.SIGN_OUT_ERROR, {
|
|
657
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
658
|
+
});
|
|
659
|
+
throw new CivicAuthError("Logout failed", CivicAuthErrorCode.LOGOUT_FAILED);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
688
662
|
}
|
|
663
|
+
export { CivicAuthErrorCode } from "./types/AuthTypes.js";
|
|
689
664
|
//# sourceMappingURL=CivicAuth.js.map
|