@civic/auth 0.6.0-beta.3 → 0.6.1-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/dist/shared/components/CivicAuthIframeContainer.js +1 -1
- package/dist/shared/components/CivicAuthIframeContainer.js.map +1 -1
- package/dist/shared/lib/BrowserAuthenticationRefresher.d.ts +7 -1
- package/dist/shared/lib/BrowserAuthenticationRefresher.d.ts.map +1 -1
- package/dist/shared/lib/BrowserAuthenticationRefresher.js +15 -2
- package/dist/shared/lib/BrowserAuthenticationRefresher.js.map +1 -1
- package/dist/shared/lib/util.d.ts +1 -1
- package/dist/shared/lib/util.d.ts.map +1 -1
- package/dist/shared/lib/util.js +6 -1
- package/dist/shared/lib/util.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 +11 -0
- package/dist/vanillajs/auth/AuthenticationEvents.d.ts.map +1 -0
- package/dist/vanillajs/auth/AuthenticationEvents.js +36 -0
- package/dist/vanillajs/auth/AuthenticationEvents.js.map +1 -0
- package/dist/vanillajs/auth/CivicAuth.d.ts +205 -0
- package/dist/vanillajs/auth/CivicAuth.d.ts.map +1 -0
- package/dist/vanillajs/auth/CivicAuth.js +689 -0
- package/dist/vanillajs/auth/CivicAuth.js.map +1 -0
- package/dist/vanillajs/auth/OAuthCallbackHandler.d.ts +90 -0
- package/dist/vanillajs/auth/OAuthCallbackHandler.d.ts.map +1 -0
- package/dist/vanillajs/auth/OAuthCallbackHandler.js +143 -0
- package/dist/vanillajs/auth/OAuthCallbackHandler.js.map +1 -0
- package/dist/vanillajs/auth/SessionManager.d.ts +48 -0
- package/dist/vanillajs/auth/SessionManager.d.ts.map +1 -0
- package/dist/vanillajs/auth/SessionManager.js +121 -0
- package/dist/vanillajs/auth/SessionManager.js.map +1 -0
- package/dist/vanillajs/auth/TokenRefresher.d.ts +54 -0
- package/dist/vanillajs/auth/TokenRefresher.d.ts.map +1 -0
- package/dist/vanillajs/auth/TokenRefresher.js +166 -0
- package/dist/vanillajs/auth/TokenRefresher.js.map +1 -0
- package/dist/vanillajs/iframe/IframeManager.d.ts +82 -0
- package/dist/vanillajs/iframe/IframeManager.d.ts.map +1 -0
- package/dist/vanillajs/iframe/IframeManager.js +487 -0
- package/dist/vanillajs/iframe/IframeManager.js.map +1 -0
- package/dist/vanillajs/iframe/IframeResizer.d.ts +15 -0
- package/dist/vanillajs/iframe/IframeResizer.d.ts.map +1 -0
- package/dist/vanillajs/iframe/IframeResizer.js +127 -0
- package/dist/vanillajs/iframe/IframeResizer.js.map +1 -0
- package/dist/vanillajs/iframe/SignalObserver.d.ts +33 -0
- package/dist/vanillajs/iframe/SignalObserver.d.ts.map +1 -0
- package/dist/vanillajs/iframe/SignalObserver.js +162 -0
- package/dist/vanillajs/iframe/SignalObserver.js.map +1 -0
- package/dist/vanillajs/index.d.ts +17 -0
- package/dist/vanillajs/index.d.ts.map +1 -0
- package/dist/vanillajs/index.js +18 -0
- package/dist/vanillajs/index.js.map +1 -0
- package/dist/vanillajs/services/ApiService.d.ts +22 -0
- package/dist/vanillajs/services/ApiService.d.ts.map +1 -0
- package/dist/vanillajs/services/ApiService.js +82 -0
- package/dist/vanillajs/services/ApiService.js.map +1 -0
- package/dist/vanillajs/types/index.d.ts +28 -0
- package/dist/vanillajs/types/index.d.ts.map +1 -0
- package/dist/vanillajs/types/index.js +14 -0
- package/dist/vanillajs/types/index.js.map +1 -0
- package/dist/vanillajs/ui/LoadingComponents.d.ts +51 -0
- package/dist/vanillajs/ui/LoadingComponents.d.ts.map +1 -0
- package/dist/vanillajs/ui/LoadingComponents.js +363 -0
- package/dist/vanillajs/ui/LoadingComponents.js.map +1 -0
- package/dist/vanillajs/utils/auth-utils.d.ts +13 -0
- package/dist/vanillajs/utils/auth-utils.d.ts.map +1 -0
- package/dist/vanillajs/utils/auth-utils.js +15 -0
- package/dist/vanillajs/utils/auth-utils.js.map +1 -0
- package/dist/vanillajs/utils/logger.d.ts +29 -0
- package/dist/vanillajs/utils/logger.d.ts.map +1 -0
- package/dist/vanillajs/utils/logger.js +62 -0
- package/dist/vanillajs/utils/logger.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
import { AuthEvent } from "../types/index.js";
|
|
2
|
+
import { LocalStorageAdapter } from "../../browser/storage.js";
|
|
3
|
+
import { handleOAuthRedirectPage } from "./OAuthCallbackHandler.js";
|
|
4
|
+
import { buildAuthUrl } from "../utils/auth-utils.js";
|
|
5
|
+
import { createLogger, configureLogging, setCurrentLogger, } from "../utils/logger.js";
|
|
6
|
+
import { SignalObserver } from "../iframe/SignalObserver.js";
|
|
7
|
+
import { GenericPublicClientPKCEProducer } from "../../services/PKCE.js";
|
|
8
|
+
import { generateState } from "../../lib/oauth.js";
|
|
9
|
+
import { SessionManager } from "./SessionManager.js";
|
|
10
|
+
import { IframeManager } from "../iframe/IframeManager.js";
|
|
11
|
+
/**
|
|
12
|
+
* Error codes for CivicAuth errors
|
|
13
|
+
*/
|
|
14
|
+
export var CivicAuthErrorCode;
|
|
15
|
+
(function (CivicAuthErrorCode) {
|
|
16
|
+
CivicAuthErrorCode["CONFIG_REQUIRED"] = "CONFIG_REQUIRED";
|
|
17
|
+
CivicAuthErrorCode["INIT_FAILED"] = "INIT_FAILED";
|
|
18
|
+
CivicAuthErrorCode["ENDPOINTS_NOT_INITIALIZED"] = "ENDPOINTS_NOT_INITIALIZED";
|
|
19
|
+
CivicAuthErrorCode["CONTAINER_NOT_FOUND"] = "CONTAINER_NOT_FOUND";
|
|
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
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* CivicAuth client for handling OAuth authentication
|
|
62
|
+
*/
|
|
63
|
+
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
|
+
config;
|
|
72
|
+
iframeElement;
|
|
73
|
+
observer;
|
|
74
|
+
authPromise;
|
|
75
|
+
authPromiseResolve;
|
|
76
|
+
authPromiseReject;
|
|
77
|
+
authProcessTimeoutHandle;
|
|
78
|
+
messageEventHandler;
|
|
79
|
+
storage;
|
|
80
|
+
endpoints;
|
|
81
|
+
logger;
|
|
82
|
+
sessionManager;
|
|
83
|
+
iframeManager;
|
|
84
|
+
/**
|
|
85
|
+
* Private constructor for CivicAuth.
|
|
86
|
+
* 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
|
+
*/
|
|
91
|
+
constructor(config) {
|
|
92
|
+
// Process config with defaults
|
|
93
|
+
this.config = processConfigWithDefaults(config);
|
|
94
|
+
// Configure logging based on config
|
|
95
|
+
configureLogging(this.config.logging);
|
|
96
|
+
// Initialize logger based on config
|
|
97
|
+
if (this.config.logging?.enabled) {
|
|
98
|
+
const namespace = this.config.logging.namespace || "iframe";
|
|
99
|
+
this.logger = createLogger(namespace);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Create a no-op logger when logging is disabled
|
|
103
|
+
this.logger = {
|
|
104
|
+
debug: () => { },
|
|
105
|
+
info: () => { },
|
|
106
|
+
warn: () => { },
|
|
107
|
+
error: () => { },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Set this logger as the current logger
|
|
111
|
+
setCurrentLogger(this.logger);
|
|
112
|
+
// Use the storage adapter from processed config (guaranteed to be present)
|
|
113
|
+
this.storage = this.config.storageAdapter;
|
|
114
|
+
// Initialize SessionManager if events are provided
|
|
115
|
+
if (config.events) {
|
|
116
|
+
this.sessionManager = new SessionManager(this.storage, config.events);
|
|
117
|
+
}
|
|
118
|
+
// Validate required configuration
|
|
119
|
+
const requiredConfigs = [
|
|
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);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Creates and initializes a new instance of CivicAuth.
|
|
133
|
+
* This is the recommended way to create a CivicAuth instance.
|
|
134
|
+
*
|
|
135
|
+
* @param config - Configuration options for the auth client
|
|
136
|
+
* @returns A promise that resolves with the initialized CivicAuth instance
|
|
137
|
+
* @throws {CivicAuthError} If initialization fails or required configuration is missing
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const auth = await CivicAuth.create({
|
|
142
|
+
* clientId: "your-client-id",
|
|
143
|
+
* redirectUrl: "https://your-app.com/callback",
|
|
144
|
+
* oauthServerBaseUrl: "https://auth-server.com/",
|
|
145
|
+
* scopes: ["openid", "profile"],
|
|
146
|
+
* targetContainerElement: "auth-container",
|
|
147
|
+
* textSignals: {
|
|
148
|
+
* success: "Authentication successful!"
|
|
149
|
+
* }
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
static async create(config) {
|
|
154
|
+
const instance = new CivicAuth(config);
|
|
155
|
+
await instance.init();
|
|
156
|
+
return instance;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Initializes the auth client and checks for callback handling
|
|
160
|
+
* @throws {CivicAuthError} If initialization fails
|
|
161
|
+
*/
|
|
162
|
+
async init() {
|
|
163
|
+
try {
|
|
164
|
+
// Get OAuth endpoints from well-known configuration
|
|
165
|
+
this.endpoints = {
|
|
166
|
+
auth: `${this.config.oauthServerBaseUrl}auth`,
|
|
167
|
+
token: `${this.config.oauthServerBaseUrl}token`,
|
|
168
|
+
jwks: `${this.config.oauthServerBaseUrl}jwks`,
|
|
169
|
+
userinfo: `${this.config.oauthServerBaseUrl}userinfo`,
|
|
170
|
+
endsession: `${this.config.oauthServerBaseUrl}endsession`,
|
|
171
|
+
};
|
|
172
|
+
// Initialize SessionManager with auth config for token refresh
|
|
173
|
+
if (this.sessionManager) {
|
|
174
|
+
const authConfig = {
|
|
175
|
+
clientId: this.config.clientId,
|
|
176
|
+
redirectUrl: this.config.redirectUrl,
|
|
177
|
+
oauthServer: this.config.oauthServerBaseUrl,
|
|
178
|
+
scopes: this.config.scopes,
|
|
179
|
+
endpoints: this.endpoints,
|
|
180
|
+
};
|
|
181
|
+
await this.sessionManager.initializeWithAuthConfig(authConfig);
|
|
182
|
+
}
|
|
183
|
+
// Check if we're on the callback page
|
|
184
|
+
if (window.location.href.startsWith(this.config.redirectUrl)) {
|
|
185
|
+
await this.handleCallback();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
const errorMessage = error instanceof Error
|
|
190
|
+
? error.message
|
|
191
|
+
: "Failed to initialize authentication";
|
|
192
|
+
this.logger.error("Failed to initialize CivicAuth:", { error });
|
|
193
|
+
this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
194
|
+
detail: errorMessage,
|
|
195
|
+
});
|
|
196
|
+
throw new CivicAuthError(errorMessage, CivicAuthErrorCode.INIT_FAILED);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Gets the container element for the auth iframe
|
|
201
|
+
* @returns The container element or null if not found
|
|
202
|
+
*/
|
|
203
|
+
getContainerElement() {
|
|
204
|
+
if (typeof this.config.targetContainerElement === "string") {
|
|
205
|
+
const element = document.getElementById(this.config.targetContainerElement);
|
|
206
|
+
if (!element) {
|
|
207
|
+
this.logger.warn(`Container element with ID "${this.config.targetContainerElement}" not found`);
|
|
208
|
+
}
|
|
209
|
+
return element;
|
|
210
|
+
}
|
|
211
|
+
return this.config.targetContainerElement;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Determines the appropriate iframe display mode based on container characteristics
|
|
215
|
+
* @param container The HTML element that will contain the iframe
|
|
216
|
+
* @returns "modal" for full-screen overlay or "embedded" for in-container display
|
|
217
|
+
*/
|
|
218
|
+
determineIframeDisplayMode(container) {
|
|
219
|
+
// Use explicit config if provided
|
|
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";
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Builds the authentication URL with PKCE challenge
|
|
268
|
+
* @returns The complete authentication URL
|
|
269
|
+
* @throws {CivicAuthError} If endpoints are not initialized
|
|
270
|
+
*/
|
|
271
|
+
async buildAuthUrl() {
|
|
272
|
+
if (!this.endpoints) {
|
|
273
|
+
throw new CivicAuthError("OAuth endpoints not initialized. Please wait for initialization to complete.", CivicAuthErrorCode.ENDPOINTS_NOT_INITIALIZED);
|
|
274
|
+
}
|
|
275
|
+
// Use storage directly since it's now AuthStorage
|
|
276
|
+
const pkceProducer = new GenericPublicClientPKCEProducer(this.storage);
|
|
277
|
+
const codeChallenge = await pkceProducer.getCodeChallenge();
|
|
278
|
+
const state = this.config.initialState ||
|
|
279
|
+
generateState({
|
|
280
|
+
displayMode: this.config.displayMode || "iframe",
|
|
281
|
+
});
|
|
282
|
+
return buildAuthUrl({
|
|
283
|
+
endpoints: this.endpoints,
|
|
284
|
+
clientId: this.config.clientId,
|
|
285
|
+
redirectUrl: this.config.redirectUrl,
|
|
286
|
+
scopes: this.config.scopes,
|
|
287
|
+
codeChallenge,
|
|
288
|
+
state,
|
|
289
|
+
prompt: this.config.prompt,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
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
|
+
/**
|
|
482
|
+
* Starts the authentication process
|
|
483
|
+
* @returns A promise that resolves with the authentication result
|
|
484
|
+
* @throws {CivicAuthError} If authentication fails or times out
|
|
485
|
+
*/
|
|
486
|
+
async startAuthentication() {
|
|
487
|
+
if (!this.endpoints) {
|
|
488
|
+
const error = new CivicAuthError("OAuth endpoints not initialized. Please wait for initialization to complete.", CivicAuthErrorCode.ENDPOINTS_NOT_INITIALIZED);
|
|
489
|
+
this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
490
|
+
detail: error.message,
|
|
491
|
+
});
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
if (this.authPromise) {
|
|
495
|
+
this.logger.info("Authentication already in progress, returning existing promise");
|
|
496
|
+
return this.authPromise;
|
|
497
|
+
}
|
|
498
|
+
const fullAuthUrl = await this.buildAuthUrl();
|
|
499
|
+
this.logger.info("Starting authentication process", {
|
|
500
|
+
constructedIframeUrl: fullAuthUrl,
|
|
501
|
+
displayMode: this.config.displayMode,
|
|
502
|
+
authProcessTimeout: this.config.authProcessTimeout,
|
|
503
|
+
});
|
|
504
|
+
this.authPromise = new Promise((resolve, reject) => {
|
|
505
|
+
this.authPromiseResolve = resolve;
|
|
506
|
+
this.authPromiseReject = reject;
|
|
507
|
+
// Handle different display modes
|
|
508
|
+
switch (this.config.displayMode) {
|
|
509
|
+
case "redirect":
|
|
510
|
+
// For redirect mode, just navigate to the auth URL
|
|
511
|
+
window.location.href = fullAuthUrl;
|
|
512
|
+
break;
|
|
513
|
+
case "new_tab":
|
|
514
|
+
this.handleNewTabAuth(fullAuthUrl, reject);
|
|
515
|
+
break;
|
|
516
|
+
case "iframe":
|
|
517
|
+
default:
|
|
518
|
+
this.handleIframeAuth(fullAuthUrl, reject);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
// Set up timeout for all display modes
|
|
522
|
+
if (this.config.authProcessTimeout &&
|
|
523
|
+
this.config.authProcessTimeout > 0) {
|
|
524
|
+
this.logger.debug("Setting up authentication timeout", {
|
|
525
|
+
authProcessTimeout: this.config.authProcessTimeout,
|
|
526
|
+
displayMode: this.config.displayMode,
|
|
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);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
return this.authPromise;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Cleans up resources and event listeners
|
|
545
|
+
*/
|
|
546
|
+
cleanup() {
|
|
547
|
+
this.logger.info("Cleaning up iframe authentication client");
|
|
548
|
+
if (this.observer) {
|
|
549
|
+
this.logger.debug("Disconnecting mutation observer");
|
|
550
|
+
this.observer.disconnect();
|
|
551
|
+
this.observer = undefined;
|
|
552
|
+
}
|
|
553
|
+
if (this.messageEventHandler) {
|
|
554
|
+
window.removeEventListener("message", this.messageEventHandler);
|
|
555
|
+
this.logger.debug("Removed 'message' event listener from window.");
|
|
556
|
+
}
|
|
557
|
+
if (this.iframeManager) {
|
|
558
|
+
this.logger.debug("Cleaning up iframe manager");
|
|
559
|
+
this.iframeManager.cleanup();
|
|
560
|
+
this.iframeManager = undefined;
|
|
561
|
+
}
|
|
562
|
+
// Reset iframe element reference (actual removal handled by IframeManager)
|
|
563
|
+
if (this.iframeElement) {
|
|
564
|
+
this.iframeElement = undefined;
|
|
565
|
+
}
|
|
566
|
+
if (this.authProcessTimeoutHandle) {
|
|
567
|
+
this.logger.debug("Clearing authentication process timeout");
|
|
568
|
+
window.clearTimeout(this.authProcessTimeoutHandle);
|
|
569
|
+
this.authProcessTimeoutHandle = undefined;
|
|
570
|
+
}
|
|
571
|
+
this.logger.debug("Resetting promise state");
|
|
572
|
+
this.authPromise = undefined;
|
|
573
|
+
this.authPromiseResolve = undefined;
|
|
574
|
+
this.authPromiseReject = undefined;
|
|
575
|
+
}
|
|
576
|
+
async handleCallback() {
|
|
577
|
+
try {
|
|
578
|
+
const callbackHandled = await handleOAuthRedirectPage({
|
|
579
|
+
clientId: this.config.clientId,
|
|
580
|
+
oauthServer: this.config.oauthServerBaseUrl,
|
|
581
|
+
redirectUrl: this.config.redirectUrl,
|
|
582
|
+
textSignals: {
|
|
583
|
+
success: this.config.textSignals.success,
|
|
584
|
+
error: this.config.textSignals.error || "Authentication failed",
|
|
585
|
+
},
|
|
586
|
+
storageAdapter: this.storage,
|
|
587
|
+
});
|
|
588
|
+
if (callbackHandled) {
|
|
589
|
+
const successSignal = document.getElementById(CONSTANTS.SUCCESS_SIGNAL_ID);
|
|
590
|
+
const errorSignal = document.getElementById(CONSTANTS.ERROR_SIGNAL_ID);
|
|
591
|
+
if (successSignal) {
|
|
592
|
+
let userInfo = null;
|
|
593
|
+
const userInfoAttr = successSignal.getAttribute("data-user-info");
|
|
594
|
+
if (userInfoAttr) {
|
|
595
|
+
try {
|
|
596
|
+
userInfo = JSON.parse(userInfoAttr);
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
this.logger.error("Failed to parse user info:", { error });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
this.config.events?.emit(AuthEvent.SIGN_IN_COMPLETE, {
|
|
603
|
+
detail: "Callback processed successfully",
|
|
604
|
+
user: userInfo,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
else if (errorSignal) {
|
|
608
|
+
this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
609
|
+
detail: errorSignal.textContent || "Unknown error during callback",
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
this.config.events?.emit(AuthEvent.SIGN_IN_ERROR, {
|
|
616
|
+
detail: error instanceof Error
|
|
617
|
+
? error.message
|
|
618
|
+
: "Unknown error during callback",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Get the current session
|
|
624
|
+
*/
|
|
625
|
+
async getCurrentSession() {
|
|
626
|
+
return this.sessionManager?.getCurrentSession() || null;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Check if user is authenticated
|
|
630
|
+
*/
|
|
631
|
+
async isAuthenticated() {
|
|
632
|
+
return this.sessionManager?.isAuthenticated() || false;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Get the current user
|
|
636
|
+
*/
|
|
637
|
+
async getCurrentUser() {
|
|
638
|
+
return this.sessionManager?.getCurrentUser() || null;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Clear the current session
|
|
642
|
+
*/
|
|
643
|
+
async clearSession() {
|
|
644
|
+
if (!this.sessionManager) {
|
|
645
|
+
throw new Error("SessionManager not initialized. Provide events in config.");
|
|
646
|
+
}
|
|
647
|
+
return this.sessionManager.clearSession();
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Manually refresh tokens
|
|
651
|
+
*/
|
|
652
|
+
async refreshTokens() {
|
|
653
|
+
if (!this.sessionManager) {
|
|
654
|
+
throw new Error("SessionManager not initialized. Provide events in config.");
|
|
655
|
+
}
|
|
656
|
+
return this.sessionManager.refreshTokens();
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Get token refresher state for debugging
|
|
660
|
+
*/
|
|
661
|
+
getTokenRefresherState() {
|
|
662
|
+
return this.sessionManager?.getTokenRefresherState() || null;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Update the iframe display mode
|
|
666
|
+
* @param mode - The display mode to use for the iframe
|
|
667
|
+
*/
|
|
668
|
+
setIframeDisplayMode(mode) {
|
|
669
|
+
this.config.iframeDisplayMode = mode;
|
|
670
|
+
this.logger.debug(`Iframe display mode updated to: ${mode}`);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Get the current iframe display mode
|
|
674
|
+
* @returns The current iframe display mode
|
|
675
|
+
*/
|
|
676
|
+
getIframeDisplayMode() {
|
|
677
|
+
return this.config.iframeDisplayMode;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Destroy the auth client and clean up all resources
|
|
681
|
+
*/
|
|
682
|
+
async destroy() {
|
|
683
|
+
this.cleanup();
|
|
684
|
+
await this.sessionManager?.destroy();
|
|
685
|
+
this.sessionManager = undefined;
|
|
686
|
+
this.logger.info("CivicAuth destroyed");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
//# sourceMappingURL=CivicAuth.js.map
|