@phila/sso-core 0.0.1 → 0.0.3
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/index.js +468 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +264 -173
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,2 +1,469 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const msalBrowser = require("@azure/msal-browser");
|
|
4
|
+
class SSOEventEmitter {
|
|
5
|
+
listeners = /* @__PURE__ */ new Map();
|
|
6
|
+
on(event, listener) {
|
|
7
|
+
if (!this.listeners.has(event)) {
|
|
8
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
9
|
+
}
|
|
10
|
+
const set = this.listeners.get(event);
|
|
11
|
+
set.add(listener);
|
|
12
|
+
return () => {
|
|
13
|
+
set.delete(listener);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
emit(event, data) {
|
|
17
|
+
const set = this.listeners.get(event);
|
|
18
|
+
if (set) {
|
|
19
|
+
for (const listener of set) {
|
|
20
|
+
listener(data);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
removeAllListeners() {
|
|
25
|
+
this.listeners.clear();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const DEFAULT_POLICIES = {
|
|
29
|
+
SIGN_UP_SIGN_IN: "B2C_1A_SIGNUP_SIGNIN",
|
|
30
|
+
SIGN_IN_ONLY: "B2C_1A_AD_SIGNIN_ONLY",
|
|
31
|
+
RESET_PASSWORD: "B2C_1A_PASSWORDRESET"
|
|
32
|
+
};
|
|
33
|
+
const DEFAULT_SCOPES = {
|
|
34
|
+
OPENID: "openid",
|
|
35
|
+
PROFILE: "profile",
|
|
36
|
+
OFFLINE_ACCESS: "offline_access"
|
|
37
|
+
};
|
|
38
|
+
const CACHE_CONFIG = {
|
|
39
|
+
LOCATION: "sessionStorage",
|
|
40
|
+
STORE_AUTH_STATE_IN_COOKIE: false
|
|
41
|
+
};
|
|
42
|
+
const MSAL_ERROR_CODES = {
|
|
43
|
+
USER_CANCELLED: "user_cancelled",
|
|
44
|
+
NO_CACHED_AUTHORITY: "no_cached_authority_error",
|
|
45
|
+
INTERACTION_REQUIRED: "interaction_required",
|
|
46
|
+
FORGOT_PASSWORD: "AADB2C90118"
|
|
47
|
+
};
|
|
48
|
+
const STATE_SEPARATOR = "|";
|
|
49
|
+
function encodeState(stateObj) {
|
|
50
|
+
return btoa(JSON.stringify(stateObj));
|
|
51
|
+
}
|
|
52
|
+
function decodeState(encoded) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(atob(encoded));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function extractCustomState(msalState) {
|
|
60
|
+
if (!msalState) return null;
|
|
61
|
+
const parts = msalState.split(STATE_SEPARATOR);
|
|
62
|
+
if (parts.length < 2) return null;
|
|
63
|
+
const customPart = parts.slice(1).join(STATE_SEPARATOR);
|
|
64
|
+
return decodeState(customPart);
|
|
65
|
+
}
|
|
66
|
+
class B2CProvider {
|
|
67
|
+
type = "b2c";
|
|
68
|
+
clientId;
|
|
69
|
+
b2cEnv;
|
|
70
|
+
authorityDomain;
|
|
71
|
+
redirectUri;
|
|
72
|
+
postLogoutRedirectUri;
|
|
73
|
+
policies;
|
|
74
|
+
apiScopes;
|
|
75
|
+
cacheLocation;
|
|
76
|
+
constructor(config) {
|
|
77
|
+
this.clientId = config.clientId;
|
|
78
|
+
this.b2cEnv = config.b2cEnvironment;
|
|
79
|
+
this.authorityDomain = config.authorityDomain;
|
|
80
|
+
this.redirectUri = config.redirectUri;
|
|
81
|
+
this.postLogoutRedirectUri = config.postLogoutRedirectUri ?? config.redirectUri;
|
|
82
|
+
this.policies = {
|
|
83
|
+
signUpSignIn: config.policies?.signUpSignIn ?? DEFAULT_POLICIES.SIGN_UP_SIGN_IN,
|
|
84
|
+
signInOnly: config.policies?.signInOnly ?? DEFAULT_POLICIES.SIGN_IN_ONLY,
|
|
85
|
+
resetPassword: config.policies?.resetPassword ?? DEFAULT_POLICIES.RESET_PASSWORD
|
|
86
|
+
};
|
|
87
|
+
this.apiScopes = config.apiScopes ?? [];
|
|
88
|
+
this.cacheLocation = config.cacheLocation ?? CACHE_CONFIG.LOCATION;
|
|
89
|
+
}
|
|
90
|
+
buildMsalConfig() {
|
|
91
|
+
return {
|
|
92
|
+
auth: {
|
|
93
|
+
clientId: this.clientId,
|
|
94
|
+
authority: this.getAuthority(this.policies.signUpSignIn),
|
|
95
|
+
knownAuthorities: this.getKnownAuthorities(),
|
|
96
|
+
redirectUri: this.redirectUri,
|
|
97
|
+
postLogoutRedirectUri: this.postLogoutRedirectUri,
|
|
98
|
+
navigateToLoginRequestUrl: false
|
|
99
|
+
},
|
|
100
|
+
cache: {
|
|
101
|
+
cacheLocation: this.cacheLocation,
|
|
102
|
+
storeAuthStateInCookie: CACHE_CONFIG.STORE_AUTH_STATE_IN_COOKIE
|
|
103
|
+
},
|
|
104
|
+
system: {
|
|
105
|
+
loggerOptions: {
|
|
106
|
+
logLevel: msalBrowser.LogLevel.Warning,
|
|
107
|
+
loggerCallback: (_level, message) => {
|
|
108
|
+
console.warn("[sso-core/b2c]", message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
getAuthority(policy) {
|
|
115
|
+
const p = policy ?? this.policies.signUpSignIn;
|
|
116
|
+
return `https://${this.authorityDomain}/${this.b2cEnv}.onmicrosoft.com/${p}`;
|
|
117
|
+
}
|
|
118
|
+
getKnownAuthorities() {
|
|
119
|
+
return [this.authorityDomain];
|
|
120
|
+
}
|
|
121
|
+
identifyPolicy(response) {
|
|
122
|
+
const claims = response.idTokenClaims;
|
|
123
|
+
if (!claims) return null;
|
|
124
|
+
const acr = claims.acr ?? claims.tfp;
|
|
125
|
+
return acr?.toUpperCase() ?? null;
|
|
126
|
+
}
|
|
127
|
+
getDefaultScopes() {
|
|
128
|
+
return [DEFAULT_SCOPES.OPENID, DEFAULT_SCOPES.PROFILE];
|
|
129
|
+
}
|
|
130
|
+
getApiScopes() {
|
|
131
|
+
return this.apiScopes;
|
|
132
|
+
}
|
|
133
|
+
/** Get the authority URL for the sign-in-only (city employee) policy */
|
|
134
|
+
getSignInOnlyAuthority() {
|
|
135
|
+
return this.getAuthority(this.policies.signInOnly);
|
|
136
|
+
}
|
|
137
|
+
/** Get the authority URL for the password reset policy */
|
|
138
|
+
getResetPasswordAuthority() {
|
|
139
|
+
return this.getAuthority(this.policies.resetPassword);
|
|
140
|
+
}
|
|
141
|
+
/** Get all configured policy names */
|
|
142
|
+
getPolicies() {
|
|
143
|
+
return this.policies;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const INITIAL_STATE = {
|
|
147
|
+
isAuthenticated: false,
|
|
148
|
+
isLoading: false,
|
|
149
|
+
user: null,
|
|
150
|
+
token: null,
|
|
151
|
+
error: null,
|
|
152
|
+
activePolicy: null,
|
|
153
|
+
authReady: false
|
|
154
|
+
};
|
|
155
|
+
class SSOClient {
|
|
156
|
+
events = new SSOEventEmitter();
|
|
157
|
+
provider;
|
|
158
|
+
debug;
|
|
159
|
+
encodedState;
|
|
160
|
+
msalInstance = null;
|
|
161
|
+
_state = { ...INITIAL_STATE };
|
|
162
|
+
constructor(config) {
|
|
163
|
+
this.provider = config.provider;
|
|
164
|
+
this.debug = config.debug ?? false;
|
|
165
|
+
this.encodedState = config.state ? encodeState(config.state) : null;
|
|
166
|
+
}
|
|
167
|
+
get state() {
|
|
168
|
+
return this._state;
|
|
169
|
+
}
|
|
170
|
+
// ── Lifecycle ──
|
|
171
|
+
async initialize() {
|
|
172
|
+
this.log("Initializing SSOClient...");
|
|
173
|
+
this.updateState({ isLoading: true });
|
|
174
|
+
const msalConfig = this.provider.buildMsalConfig();
|
|
175
|
+
this.msalInstance = new msalBrowser.PublicClientApplication(msalConfig);
|
|
176
|
+
await this.msalInstance.initialize();
|
|
177
|
+
const result = await this.handleRedirect();
|
|
178
|
+
this.updateState({ isLoading: false, authReady: true });
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
destroy() {
|
|
182
|
+
this.events.removeAllListeners();
|
|
183
|
+
this.msalInstance = null;
|
|
184
|
+
this._state = { ...INITIAL_STATE };
|
|
185
|
+
}
|
|
186
|
+
// ── Auth Actions ──
|
|
187
|
+
async handleRedirect() {
|
|
188
|
+
this.assertInitialized();
|
|
189
|
+
this.log("Handling redirect promise...");
|
|
190
|
+
try {
|
|
191
|
+
const response = await this.msalInstance.handleRedirectPromise();
|
|
192
|
+
if (!response) {
|
|
193
|
+
this.selectAccount(null);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
this.log("Redirect response received", response);
|
|
197
|
+
const customState = extractCustomState(response.state);
|
|
198
|
+
const policy = this.provider.identifyPolicy(response);
|
|
199
|
+
this.updateState({ activePolicy: policy });
|
|
200
|
+
if (this.isForgotPasswordPolicy(policy)) {
|
|
201
|
+
this.log("Forgot password flow completed");
|
|
202
|
+
this.events.emit("auth:forgotPassword", void 0);
|
|
203
|
+
return { ...response, customPostbackObject: customState ?? void 0 };
|
|
204
|
+
}
|
|
205
|
+
this.updateState({ isLoading: true });
|
|
206
|
+
this.selectAccount(policy);
|
|
207
|
+
await this.acquireTokenAfterRedirect(response);
|
|
208
|
+
const authResponse = {
|
|
209
|
+
...response,
|
|
210
|
+
customPostbackObject: customState ?? void 0
|
|
211
|
+
};
|
|
212
|
+
this.events.emit("auth:signedIn", authResponse);
|
|
213
|
+
return authResponse;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return this.handleRedirectError(error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async signIn(options) {
|
|
219
|
+
this.assertInitialized();
|
|
220
|
+
this.log("Initiating sign-in...");
|
|
221
|
+
this.updateState({ isLoading: true });
|
|
222
|
+
this.events.emit("auth:loading", true);
|
|
223
|
+
const request = this.buildLoginRequest(this.provider.getAuthority(), options);
|
|
224
|
+
await this.msalInstance.loginRedirect(request);
|
|
225
|
+
}
|
|
226
|
+
async signInCityEmployee(options) {
|
|
227
|
+
this.assertInitialized();
|
|
228
|
+
this.log("Initiating city employee sign-in...");
|
|
229
|
+
if (!(this.provider instanceof B2CProvider)) {
|
|
230
|
+
return this.signIn(options);
|
|
231
|
+
}
|
|
232
|
+
const authority = this.provider.getSignInOnlyAuthority();
|
|
233
|
+
const request = this.buildLoginRequest(authority, options);
|
|
234
|
+
await this.msalInstance.loginRedirect(request);
|
|
235
|
+
}
|
|
236
|
+
async signOut(options) {
|
|
237
|
+
this.assertInitialized();
|
|
238
|
+
this.log("Initiating sign-out...");
|
|
239
|
+
const logoutRequest = {
|
|
240
|
+
postLogoutRedirectUri: options?.postLogoutRedirectUri,
|
|
241
|
+
authority: this.provider.getAuthority()
|
|
242
|
+
};
|
|
243
|
+
this.updateState({ isLoading: true });
|
|
244
|
+
this.events.emit("auth:signedOut", void 0);
|
|
245
|
+
await this.msalInstance.logoutRedirect(logoutRequest);
|
|
246
|
+
}
|
|
247
|
+
async forgotPassword() {
|
|
248
|
+
this.assertInitialized();
|
|
249
|
+
if (!(this.provider instanceof B2CProvider)) {
|
|
250
|
+
this.log("Forgot password is only supported for B2C providers");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.log("Initiating forgot password flow...");
|
|
254
|
+
const authority = this.provider.getResetPasswordAuthority();
|
|
255
|
+
await this.msalInstance.loginRedirect({ scopes: [], authority });
|
|
256
|
+
}
|
|
257
|
+
async acquireToken(options) {
|
|
258
|
+
this.assertInitialized();
|
|
259
|
+
this.log("Acquiring token...");
|
|
260
|
+
const account = this._state.user;
|
|
261
|
+
if (!account) {
|
|
262
|
+
this.log("No account found, cannot acquire token");
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const scopes = options?.scopes ?? this.provider.getApiScopes();
|
|
266
|
+
const tokenRequest = {
|
|
267
|
+
scopes,
|
|
268
|
+
forceRefresh: options?.forceRefresh ?? false,
|
|
269
|
+
account,
|
|
270
|
+
authority: this._state.activePolicy ? this.provider.getAuthority(this._state.activePolicy) : this.provider.getAuthority()
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
const response = await this.msalInstance.acquireTokenSilent(tokenRequest);
|
|
274
|
+
if (!response.accessToken) {
|
|
275
|
+
throw new msalBrowser.InteractionRequiredAuthError("empty_token");
|
|
276
|
+
}
|
|
277
|
+
this.log("Token acquired silently");
|
|
278
|
+
this.updateState({ token: response.accessToken, error: null });
|
|
279
|
+
this.events.emit("auth:tokenAcquired", response.accessToken);
|
|
280
|
+
return response.accessToken;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error instanceof msalBrowser.InteractionRequiredAuthError) {
|
|
283
|
+
this.log("Silent token acquisition failed, falling back to redirect");
|
|
284
|
+
try {
|
|
285
|
+
await this.msalInstance.acquireTokenRedirect(tokenRequest);
|
|
286
|
+
return null;
|
|
287
|
+
} catch (redirectError) {
|
|
288
|
+
this.handleError(redirectError);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
this.handleError(error);
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// ── Internal Helpers ──
|
|
297
|
+
selectAccount(policy) {
|
|
298
|
+
const accounts = this.msalInstance.getAllAccounts();
|
|
299
|
+
if (accounts.length === 0) {
|
|
300
|
+
this.updateState({ isAuthenticated: false, user: null });
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
let selected = null;
|
|
304
|
+
if (accounts.length === 1) {
|
|
305
|
+
selected = accounts[0];
|
|
306
|
+
} else if (policy) {
|
|
307
|
+
const filtered = accounts.filter((account) => {
|
|
308
|
+
const claims = account.idTokenClaims;
|
|
309
|
+
const iss = claims?.iss ?? "";
|
|
310
|
+
const knownAuthorities = this.provider.getKnownAuthorities();
|
|
311
|
+
const matchesAuthority = knownAuthorities.some((auth) => iss.toUpperCase().includes(auth.toUpperCase()));
|
|
312
|
+
const matchesPolicy = account.homeAccountId.toUpperCase().includes(policy.toUpperCase());
|
|
313
|
+
return matchesAuthority && matchesPolicy;
|
|
314
|
+
});
|
|
315
|
+
if (filtered.length >= 1) {
|
|
316
|
+
selected = filtered[0];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!selected && accounts.length > 0) {
|
|
320
|
+
selected = accounts[0];
|
|
321
|
+
}
|
|
322
|
+
if (selected) {
|
|
323
|
+
this.log("Account selected", selected.username);
|
|
324
|
+
this.updateState({ isAuthenticated: true, user: selected });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async acquireTokenAfterRedirect(response) {
|
|
328
|
+
const account = this.msalInstance.getAccountByHomeId(response.account?.homeAccountId ?? "") ?? response.account ?? null;
|
|
329
|
+
if (account) {
|
|
330
|
+
this.updateState({ isAuthenticated: true, user: account });
|
|
331
|
+
}
|
|
332
|
+
await this.acquireToken();
|
|
333
|
+
this.updateState({ isLoading: false });
|
|
334
|
+
}
|
|
335
|
+
buildLoginRequest(authority, options) {
|
|
336
|
+
const scopes = options?.scopes ?? this.provider.getDefaultScopes();
|
|
337
|
+
return {
|
|
338
|
+
scopes,
|
|
339
|
+
authority,
|
|
340
|
+
state: this.encodedState ? `${this.encodedState}` : void 0,
|
|
341
|
+
loginHint: options?.loginHint,
|
|
342
|
+
domainHint: options?.domainHint
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
isForgotPasswordPolicy(policy) {
|
|
346
|
+
if (!policy) return false;
|
|
347
|
+
if (!(this.provider instanceof B2CProvider)) return false;
|
|
348
|
+
return policy.toUpperCase() === this.provider.getPolicies().resetPassword.toUpperCase();
|
|
349
|
+
}
|
|
350
|
+
handleRedirectError(error) {
|
|
351
|
+
const err = error;
|
|
352
|
+
if (err.errorMessage?.includes(MSAL_ERROR_CODES.FORGOT_PASSWORD)) {
|
|
353
|
+
this.log("Forgot password error detected, redirecting...");
|
|
354
|
+
this.forgotPassword();
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
this.handleError(error);
|
|
358
|
+
this.updateState({ isLoading: false });
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
handleError(error) {
|
|
362
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
363
|
+
this.log("Error:", err.message);
|
|
364
|
+
this.updateState({ error: err });
|
|
365
|
+
this.events.emit("auth:error", err);
|
|
366
|
+
}
|
|
367
|
+
updateState(partial) {
|
|
368
|
+
this._state = { ...this._state, ...partial };
|
|
369
|
+
this.events.emit("auth:stateChanged", this._state);
|
|
370
|
+
}
|
|
371
|
+
assertInitialized() {
|
|
372
|
+
if (!this.msalInstance) {
|
|
373
|
+
throw new Error("SSOClient not initialized. Call initialize() first.");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
log(...args) {
|
|
377
|
+
if (this.debug) {
|
|
378
|
+
console.log("[sso-core]", ...args);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
class CIAMProvider {
|
|
383
|
+
type = "ciam";
|
|
384
|
+
config;
|
|
385
|
+
constructor(config) {
|
|
386
|
+
this.config = config;
|
|
387
|
+
}
|
|
388
|
+
buildMsalConfig() {
|
|
389
|
+
return {
|
|
390
|
+
auth: {
|
|
391
|
+
clientId: this.config.clientId,
|
|
392
|
+
authority: this.getAuthority(),
|
|
393
|
+
knownAuthorities: this.getKnownAuthorities(),
|
|
394
|
+
redirectUri: this.config.redirectUri,
|
|
395
|
+
postLogoutRedirectUri: this.config.postLogoutRedirectUri ?? this.config.redirectUri,
|
|
396
|
+
navigateToLoginRequestUrl: false
|
|
397
|
+
},
|
|
398
|
+
cache: {
|
|
399
|
+
cacheLocation: this.config.cacheLocation ?? "sessionStorage"
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
getAuthority() {
|
|
404
|
+
return `https://${this.config.tenantSubdomain}.ciamlogin.com/`;
|
|
405
|
+
}
|
|
406
|
+
getKnownAuthorities() {
|
|
407
|
+
return [`${this.config.tenantSubdomain}.ciamlogin.com`];
|
|
408
|
+
}
|
|
409
|
+
identifyPolicy(_response) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
getDefaultScopes() {
|
|
413
|
+
return ["openid", "profile"];
|
|
414
|
+
}
|
|
415
|
+
getApiScopes() {
|
|
416
|
+
return this.config.scopes ?? [];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
class EntraProvider {
|
|
420
|
+
type = "entra";
|
|
421
|
+
config;
|
|
422
|
+
constructor(config) {
|
|
423
|
+
this.config = config;
|
|
424
|
+
}
|
|
425
|
+
buildMsalConfig() {
|
|
426
|
+
return {
|
|
427
|
+
auth: {
|
|
428
|
+
clientId: this.config.clientId,
|
|
429
|
+
authority: this.getAuthority(),
|
|
430
|
+
knownAuthorities: this.getKnownAuthorities(),
|
|
431
|
+
redirectUri: this.config.redirectUri,
|
|
432
|
+
postLogoutRedirectUri: this.config.postLogoutRedirectUri ?? this.config.redirectUri,
|
|
433
|
+
navigateToLoginRequestUrl: false
|
|
434
|
+
},
|
|
435
|
+
cache: {
|
|
436
|
+
cacheLocation: this.config.cacheLocation ?? "sessionStorage"
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
getAuthority() {
|
|
441
|
+
return `https://login.microsoftonline.com/${this.config.tenantId}`;
|
|
442
|
+
}
|
|
443
|
+
getKnownAuthorities() {
|
|
444
|
+
return ["login.microsoftonline.com"];
|
|
445
|
+
}
|
|
446
|
+
identifyPolicy(_response) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
getDefaultScopes() {
|
|
450
|
+
return ["openid", "profile"];
|
|
451
|
+
}
|
|
452
|
+
getApiScopes() {
|
|
453
|
+
return this.config.scopes ?? [];
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
exports.B2CProvider = B2CProvider;
|
|
457
|
+
exports.CACHE_CONFIG = CACHE_CONFIG;
|
|
458
|
+
exports.CIAMProvider = CIAMProvider;
|
|
459
|
+
exports.DEFAULT_POLICIES = DEFAULT_POLICIES;
|
|
460
|
+
exports.DEFAULT_SCOPES = DEFAULT_SCOPES;
|
|
461
|
+
exports.EntraProvider = EntraProvider;
|
|
462
|
+
exports.MSAL_ERROR_CODES = MSAL_ERROR_CODES;
|
|
463
|
+
exports.SSOClient = SSOClient;
|
|
464
|
+
exports.SSOEventEmitter = SSOEventEmitter;
|
|
465
|
+
exports.STATE_SEPARATOR = STATE_SEPARATOR;
|
|
466
|
+
exports.decodeState = decodeState;
|
|
467
|
+
exports.encodeState = encodeState;
|
|
468
|
+
exports.extractCustomState = extractCustomState;
|
|
2
469
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/events.ts","../src/constants.ts","../src/state-encoding.ts","../src/providers/b2c.ts","../src/client.ts","../src/providers/ciam.ts","../src/providers/entra.ts"],"sourcesContent":["import type { SSOEventMap, SSOEventName } from \"./types\";\n\ntype Listener<T> = (data: T) => void;\n\nexport class SSOEventEmitter {\n private listeners = new Map<SSOEventName, Set<Listener<unknown>>>();\n\n on<K extends SSOEventName>(event: K, listener: Listener<SSOEventMap[K]>): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n const set = this.listeners.get(event)!;\n set.add(listener as Listener<unknown>);\n\n // Return unsubscribe function\n return () => {\n set.delete(listener as Listener<unknown>);\n };\n }\n\n emit<K extends SSOEventName>(event: K, data: SSOEventMap[K]): void {\n const set = this.listeners.get(event);\n if (set) {\n for (const listener of set) {\n listener(data);\n }\n }\n }\n\n removeAllListeners(): void {\n this.listeners.clear();\n }\n}\n","export const DEFAULT_POLICIES = {\n SIGN_UP_SIGN_IN: \"B2C_1A_SIGNUP_SIGNIN\",\n SIGN_IN_ONLY: \"B2C_1A_AD_SIGNIN_ONLY\",\n RESET_PASSWORD: \"B2C_1A_PASSWORDRESET\",\n} as const;\n\nexport const DEFAULT_SCOPES = {\n OPENID: \"openid\",\n PROFILE: \"profile\",\n OFFLINE_ACCESS: \"offline_access\",\n} as const;\n\nexport const CACHE_CONFIG = {\n LOCATION: \"sessionStorage\" as const,\n STORE_AUTH_STATE_IN_COOKIE: false,\n} as const;\n\nexport const MSAL_ERROR_CODES = {\n USER_CANCELLED: \"user_cancelled\",\n NO_CACHED_AUTHORITY: \"no_cached_authority_error\",\n INTERACTION_REQUIRED: \"interaction_required\",\n FORGOT_PASSWORD: \"AADB2C90118\",\n} as const;\n\nexport const STATE_SEPARATOR = \"|\";\n","import { STATE_SEPARATOR } from \"./constants\";\n\n/**\n * Encode a custom state object into a base64 string that can be\n * appended to the MSAL state parameter using a pipe separator.\n */\nexport function encodeState(stateObj: Record<string, unknown>): string {\n return btoa(JSON.stringify(stateObj));\n}\n\n/**\n * Decode a base64-encoded state string back into an object.\n */\nexport function decodeState(encoded: string): Record<string, unknown> | null {\n try {\n return JSON.parse(atob(encoded));\n } catch {\n return null;\n }\n}\n\n/**\n * Extract custom postback data from the MSAL state string.\n * MSAL prepends its own GUID before the pipe separator.\n */\nexport function extractCustomState(msalState: string | undefined): Record<string, unknown> | null {\n if (!msalState) return null;\n\n const parts = msalState.split(STATE_SEPARATOR);\n if (parts.length < 2) return null;\n\n // Everything after the first pipe is our encoded state\n const customPart = parts.slice(1).join(STATE_SEPARATOR);\n return decodeState(customPart);\n}\n","import { LogLevel } from \"@azure/msal-browser\";\nimport type { Configuration, AuthenticationResult } from \"@azure/msal-browser\";\nimport type { IAuthProvider, B2CProviderConfig, B2CPolicies } from \"../types\";\nimport { DEFAULT_POLICIES, DEFAULT_SCOPES, CACHE_CONFIG } from \"../constants\";\n\nexport class B2CProvider implements IAuthProvider {\n readonly type = \"b2c\" as const;\n\n private readonly clientId: string;\n private readonly b2cEnv: string;\n private readonly authorityDomain: string;\n private readonly redirectUri: string;\n private readonly postLogoutRedirectUri: string;\n private readonly policies: B2CPolicies;\n private readonly apiScopes: string[];\n private readonly cacheLocation: \"sessionStorage\" | \"localStorage\";\n\n constructor(config: B2CProviderConfig) {\n this.clientId = config.clientId;\n this.b2cEnv = config.b2cEnvironment;\n this.authorityDomain = config.authorityDomain;\n this.redirectUri = config.redirectUri;\n this.postLogoutRedirectUri = config.postLogoutRedirectUri ?? config.redirectUri;\n this.policies = {\n signUpSignIn: config.policies?.signUpSignIn ?? DEFAULT_POLICIES.SIGN_UP_SIGN_IN,\n signInOnly: config.policies?.signInOnly ?? DEFAULT_POLICIES.SIGN_IN_ONLY,\n resetPassword: config.policies?.resetPassword ?? DEFAULT_POLICIES.RESET_PASSWORD,\n };\n this.apiScopes = config.apiScopes ?? [];\n this.cacheLocation = config.cacheLocation ?? CACHE_CONFIG.LOCATION;\n }\n\n buildMsalConfig(): Configuration {\n return {\n auth: {\n clientId: this.clientId,\n authority: this.getAuthority(this.policies.signUpSignIn),\n knownAuthorities: this.getKnownAuthorities(),\n redirectUri: this.redirectUri,\n postLogoutRedirectUri: this.postLogoutRedirectUri,\n navigateToLoginRequestUrl: false,\n },\n cache: {\n cacheLocation: this.cacheLocation,\n storeAuthStateInCookie: CACHE_CONFIG.STORE_AUTH_STATE_IN_COOKIE,\n },\n system: {\n loggerOptions: {\n logLevel: LogLevel.Warning,\n loggerCallback: (_level: LogLevel, message: string) => {\n console.warn(\"[sso-core/b2c]\", message);\n },\n },\n },\n };\n }\n\n getAuthority(policy?: string): string {\n const p = policy ?? this.policies.signUpSignIn;\n return `https://${this.authorityDomain}/${this.b2cEnv}.onmicrosoft.com/${p}`;\n }\n\n getKnownAuthorities(): string[] {\n return [this.authorityDomain];\n }\n\n identifyPolicy(response: AuthenticationResult): string | null {\n const claims = response.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return null;\n\n const acr = (claims.acr ?? claims.tfp) as string | undefined;\n return acr?.toUpperCase() ?? null;\n }\n\n getDefaultScopes(): string[] {\n return [DEFAULT_SCOPES.OPENID, DEFAULT_SCOPES.PROFILE];\n }\n\n getApiScopes(): string[] {\n return this.apiScopes;\n }\n\n /** Get the authority URL for the sign-in-only (city employee) policy */\n getSignInOnlyAuthority(): string {\n return this.getAuthority(this.policies.signInOnly);\n }\n\n /** Get the authority URL for the password reset policy */\n getResetPasswordAuthority(): string {\n return this.getAuthority(this.policies.resetPassword);\n }\n\n /** Get all configured policy names */\n getPolicies(): Readonly<B2CPolicies> {\n return this.policies;\n }\n}\n","import { PublicClientApplication, InteractionRequiredAuthError } from \"@azure/msal-browser\";\nimport type { AccountInfo, AuthenticationResult } from \"@azure/msal-browser\";\nimport { SSOEventEmitter } from \"./events\";\nimport { encodeState, extractCustomState } from \"./state-encoding\";\nimport { MSAL_ERROR_CODES } from \"./constants\";\nimport { B2CProvider } from \"./providers/b2c\";\nimport type {\n IAuthProvider,\n SSOClientConfig,\n SSOClientState,\n SignInOptions,\n SignOutOptions,\n TokenOptions,\n AuthResponse,\n} from \"./types\";\n\nconst INITIAL_STATE: SSOClientState = {\n isAuthenticated: false,\n isLoading: false,\n user: null,\n token: null,\n error: null,\n activePolicy: null,\n authReady: false,\n};\n\nexport class SSOClient {\n readonly events = new SSOEventEmitter();\n private readonly provider: IAuthProvider;\n private readonly debug: boolean;\n private readonly encodedState: string | null;\n\n private msalInstance: PublicClientApplication | null = null;\n private _state: SSOClientState = { ...INITIAL_STATE };\n\n constructor(config: SSOClientConfig) {\n this.provider = config.provider;\n this.debug = config.debug ?? false;\n this.encodedState = config.state ? encodeState(config.state) : null;\n }\n\n get state(): Readonly<SSOClientState> {\n return this._state;\n }\n\n // ── Lifecycle ──\n\n async initialize(): Promise<AuthResponse | null> {\n this.log(\"Initializing SSOClient...\");\n this.updateState({ isLoading: true });\n\n const msalConfig = this.provider.buildMsalConfig();\n this.msalInstance = new PublicClientApplication(msalConfig);\n\n await this.msalInstance.initialize();\n\n const result = await this.handleRedirect();\n\n this.updateState({ isLoading: false, authReady: true });\n return result;\n }\n\n destroy(): void {\n this.events.removeAllListeners();\n this.msalInstance = null;\n this._state = { ...INITIAL_STATE };\n }\n\n // ── Auth Actions ──\n\n async handleRedirect(): Promise<AuthResponse | null> {\n this.assertInitialized();\n this.log(\"Handling redirect promise...\");\n\n try {\n const response = await this.msalInstance!.handleRedirectPromise();\n\n if (!response) {\n // No redirect response — check for existing sessions\n this.selectAccount(null);\n return null;\n }\n\n this.log(\"Redirect response received\", response);\n\n // Extract custom postback state\n const customState = extractCustomState(response.state);\n\n // Identify which policy was used via the acr/tfp claim\n const policy = this.provider.identifyPolicy(response);\n this.updateState({ activePolicy: policy });\n\n // Check if this was a forgot-password completion\n if (this.isForgotPasswordPolicy(policy)) {\n this.log(\"Forgot password flow completed\");\n this.events.emit(\"auth:forgotPassword\", undefined as never);\n return { ...response, customPostbackObject: customState ?? undefined } as AuthResponse;\n }\n\n // Normal sign-in flow\n this.updateState({ isLoading: true });\n this.selectAccount(policy);\n await this.acquireTokenAfterRedirect(response);\n\n const authResponse: AuthResponse = {\n ...response,\n customPostbackObject: customState ?? undefined,\n };\n\n this.events.emit(\"auth:signedIn\", authResponse);\n return authResponse;\n } catch (error) {\n return this.handleRedirectError(error);\n }\n }\n\n async signIn(options?: SignInOptions): Promise<void> {\n this.assertInitialized();\n this.log(\"Initiating sign-in...\");\n this.updateState({ isLoading: true });\n this.events.emit(\"auth:loading\", true);\n\n const request = this.buildLoginRequest(this.provider.getAuthority(), options);\n await this.msalInstance!.loginRedirect(request);\n }\n\n async signInCityEmployee(options?: SignInOptions): Promise<void> {\n this.assertInitialized();\n this.log(\"Initiating city employee sign-in...\");\n\n if (!(this.provider instanceof B2CProvider)) {\n // For non-B2C providers, fall back to regular sign-in\n return this.signIn(options);\n }\n\n const authority = this.provider.getSignInOnlyAuthority();\n const request = this.buildLoginRequest(authority, options);\n await this.msalInstance!.loginRedirect(request);\n }\n\n async signOut(options?: SignOutOptions): Promise<void> {\n this.assertInitialized();\n this.log(\"Initiating sign-out...\");\n\n const logoutRequest = {\n postLogoutRedirectUri: options?.postLogoutRedirectUri,\n authority: this.provider.getAuthority(),\n };\n\n this.updateState({ isLoading: true });\n this.events.emit(\"auth:signedOut\", undefined as never);\n await this.msalInstance!.logoutRedirect(logoutRequest);\n }\n\n async forgotPassword(): Promise<void> {\n this.assertInitialized();\n\n if (!(this.provider instanceof B2CProvider)) {\n this.log(\"Forgot password is only supported for B2C providers\");\n return;\n }\n\n this.log(\"Initiating forgot password flow...\");\n const authority = this.provider.getResetPasswordAuthority();\n await this.msalInstance!.loginRedirect({ scopes: [], authority });\n }\n\n async acquireToken(options?: TokenOptions): Promise<string | null> {\n this.assertInitialized();\n this.log(\"Acquiring token...\");\n\n const account = this._state.user;\n if (!account) {\n this.log(\"No account found, cannot acquire token\");\n return null;\n }\n\n const scopes = options?.scopes ?? this.provider.getApiScopes();\n const tokenRequest = {\n scopes,\n forceRefresh: options?.forceRefresh ?? false,\n account,\n authority: this._state.activePolicy\n ? this.provider.getAuthority(this._state.activePolicy)\n : this.provider.getAuthority(),\n };\n\n try {\n const response = await this.msalInstance!.acquireTokenSilent(tokenRequest);\n\n if (!response.accessToken) {\n throw new InteractionRequiredAuthError(\"empty_token\");\n }\n\n this.log(\"Token acquired silently\");\n this.updateState({ token: response.accessToken, error: null });\n this.events.emit(\"auth:tokenAcquired\", response.accessToken);\n return response.accessToken;\n } catch (error) {\n if (error instanceof InteractionRequiredAuthError) {\n this.log(\"Silent token acquisition failed, falling back to redirect\");\n try {\n await this.msalInstance!.acquireTokenRedirect(tokenRequest);\n return null; // Page will redirect\n } catch (redirectError) {\n this.handleError(redirectError);\n return null;\n }\n }\n this.handleError(error);\n return null;\n }\n }\n\n // ── Internal Helpers ──\n\n private selectAccount(policy: string | null): void {\n const accounts = this.msalInstance!.getAllAccounts();\n\n if (accounts.length === 0) {\n this.updateState({ isAuthenticated: false, user: null });\n return;\n }\n\n let selected: AccountInfo | null = null;\n\n if (accounts.length === 1) {\n selected = accounts[0];\n } else if (policy) {\n // Filter accounts by policy and authority domain\n const filtered = accounts.filter(account => {\n const claims = account.idTokenClaims as Record<string, unknown> | undefined;\n const iss = (claims?.iss as string) ?? \"\";\n const knownAuthorities = this.provider.getKnownAuthorities();\n const matchesAuthority = knownAuthorities.some(auth => iss.toUpperCase().includes(auth.toUpperCase()));\n const matchesPolicy = account.homeAccountId.toUpperCase().includes(policy.toUpperCase());\n return matchesAuthority && matchesPolicy;\n });\n\n if (filtered.length >= 1) {\n selected = filtered[0];\n }\n }\n\n if (!selected && accounts.length > 0) {\n selected = accounts[0];\n }\n\n if (selected) {\n this.log(\"Account selected\", selected.username);\n this.updateState({ isAuthenticated: true, user: selected });\n }\n }\n\n private async acquireTokenAfterRedirect(response: AuthenticationResult): Promise<void> {\n // Set the account first so acquireToken can use it\n const account =\n this.msalInstance!.getAccountByHomeId(response.account?.homeAccountId ?? \"\") ?? response.account ?? null;\n\n if (account) {\n this.updateState({ isAuthenticated: true, user: account });\n }\n\n await this.acquireToken();\n this.updateState({ isLoading: false });\n }\n\n private buildLoginRequest(authority: string, options?: SignInOptions) {\n const scopes = options?.scopes ?? this.provider.getDefaultScopes();\n return {\n scopes,\n authority,\n state: this.encodedState ? `${this.encodedState}` : undefined,\n loginHint: options?.loginHint,\n domainHint: options?.domainHint,\n };\n }\n\n private isForgotPasswordPolicy(policy: string | null): boolean {\n if (!policy) return false;\n if (!(this.provider instanceof B2CProvider)) return false;\n return policy.toUpperCase() === this.provider.getPolicies().resetPassword.toUpperCase();\n }\n\n private handleRedirectError(error: unknown): null {\n const err = error as { errorMessage?: string; errorCode?: string };\n\n // Detect B2C forgot-password error code\n if (err.errorMessage?.includes(MSAL_ERROR_CODES.FORGOT_PASSWORD)) {\n this.log(\"Forgot password error detected, redirecting...\");\n this.forgotPassword();\n return null;\n }\n\n this.handleError(error);\n this.updateState({ isLoading: false });\n return null;\n }\n\n private handleError(error: unknown): void {\n const err = error instanceof Error ? error : new Error(String(error));\n this.log(\"Error:\", err.message);\n this.updateState({ error: err });\n this.events.emit(\"auth:error\", err);\n }\n\n private updateState(partial: Partial<SSOClientState>): void {\n this._state = { ...this._state, ...partial };\n this.events.emit(\"auth:stateChanged\", this._state);\n }\n\n private assertInitialized(): void {\n if (!this.msalInstance) {\n throw new Error(\"SSOClient not initialized. Call initialize() first.\");\n }\n }\n\n private log(...args: unknown[]): void {\n if (this.debug) {\n console.log(\"[sso-core]\", ...args);\n }\n }\n}\n","import type { Configuration, AuthenticationResult } from \"@azure/msal-browser\";\nimport type { IAuthProvider, CIAMProviderConfig } from \"../types\";\n\n/**\n * Entra External ID (CIAM) provider — future implementation.\n * Placeholder scaffolding to validate the provider interface.\n */\nexport class CIAMProvider implements IAuthProvider {\n readonly type = \"ciam\" as const;\n\n private readonly config: CIAMProviderConfig;\n\n constructor(config: CIAMProviderConfig) {\n this.config = config;\n }\n\n buildMsalConfig(): Configuration {\n return {\n auth: {\n clientId: this.config.clientId,\n authority: this.getAuthority(),\n knownAuthorities: this.getKnownAuthorities(),\n redirectUri: this.config.redirectUri,\n postLogoutRedirectUri: this.config.postLogoutRedirectUri ?? this.config.redirectUri,\n navigateToLoginRequestUrl: false,\n },\n cache: {\n cacheLocation: this.config.cacheLocation ?? \"sessionStorage\",\n },\n };\n }\n\n getAuthority(): string {\n return `https://${this.config.tenantSubdomain}.ciamlogin.com/`;\n }\n\n getKnownAuthorities(): string[] {\n return [`${this.config.tenantSubdomain}.ciamlogin.com`];\n }\n\n identifyPolicy(_response: AuthenticationResult): string | null {\n // CIAM doesn't use client-side policy selection\n return null;\n }\n\n getDefaultScopes(): string[] {\n return [\"openid\", \"profile\"];\n }\n\n getApiScopes(): string[] {\n return this.config.scopes ?? [];\n }\n}\n","import type { Configuration, AuthenticationResult } from \"@azure/msal-browser\";\nimport type { IAuthProvider, EntraProviderConfig } from \"../types\";\n\n/**\n * Entra workforce (city employees) provider — future implementation.\n * Placeholder scaffolding to validate the provider interface.\n */\nexport class EntraProvider implements IAuthProvider {\n readonly type = \"entra\" as const;\n\n private readonly config: EntraProviderConfig;\n\n constructor(config: EntraProviderConfig) {\n this.config = config;\n }\n\n buildMsalConfig(): Configuration {\n return {\n auth: {\n clientId: this.config.clientId,\n authority: this.getAuthority(),\n knownAuthorities: this.getKnownAuthorities(),\n redirectUri: this.config.redirectUri,\n postLogoutRedirectUri: this.config.postLogoutRedirectUri ?? this.config.redirectUri,\n navigateToLoginRequestUrl: false,\n },\n cache: {\n cacheLocation: this.config.cacheLocation ?? \"sessionStorage\",\n },\n };\n }\n\n getAuthority(): string {\n return `https://login.microsoftonline.com/${this.config.tenantId}`;\n }\n\n getKnownAuthorities(): string[] {\n return [\"login.microsoftonline.com\"];\n }\n\n identifyPolicy(_response: AuthenticationResult): string | null {\n // Entra workforce doesn't use B2C policies\n return null;\n }\n\n getDefaultScopes(): string[] {\n return [\"openid\", \"profile\"];\n }\n\n getApiScopes(): string[] {\n return this.config.scopes ?? [];\n }\n}\n"],"names":["SSOEventEmitter","event","listener","set","data","DEFAULT_POLICIES","DEFAULT_SCOPES","CACHE_CONFIG","MSAL_ERROR_CODES","STATE_SEPARATOR","encodeState","stateObj","decodeState","encoded","extractCustomState","msalState","parts","customPart","B2CProvider","config","LogLevel","_level","message","policy","p","response","claims","INITIAL_STATE","SSOClient","msalConfig","PublicClientApplication","result","customState","authResponse","error","options","request","authority","logoutRequest","account","tokenRequest","InteractionRequiredAuthError","redirectError","accounts","selected","filtered","iss","matchesAuthority","auth","matchesPolicy","err","partial","args","CIAMProvider","_response","EntraProvider"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/events.ts","../src/constants.ts","../src/state-encoding.ts","../src/providers/b2c.ts","../src/client.ts","../src/providers/ciam.ts","../src/providers/entra.ts"],"sourcesContent":["import type { SSOEventMap, SSOEventName } from \"./types\";\n\ntype Listener<T> = (data: T) => void;\n\nexport class SSOEventEmitter {\n private listeners = new Map<SSOEventName, Set<Listener<unknown>>>();\n\n on<K extends SSOEventName>(event: K, listener: Listener<SSOEventMap[K]>): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n const set = this.listeners.get(event)!;\n set.add(listener as Listener<unknown>);\n\n // Return unsubscribe function\n return () => {\n set.delete(listener as Listener<unknown>);\n };\n }\n\n emit<K extends SSOEventName>(event: K, data: SSOEventMap[K]): void {\n const set = this.listeners.get(event);\n if (set) {\n for (const listener of set) {\n listener(data);\n }\n }\n }\n\n removeAllListeners(): void {\n this.listeners.clear();\n }\n}\n","export const DEFAULT_POLICIES = {\n SIGN_UP_SIGN_IN: \"B2C_1A_SIGNUP_SIGNIN\",\n SIGN_IN_ONLY: \"B2C_1A_AD_SIGNIN_ONLY\",\n RESET_PASSWORD: \"B2C_1A_PASSWORDRESET\",\n} as const;\n\nexport const DEFAULT_SCOPES = {\n OPENID: \"openid\",\n PROFILE: \"profile\",\n OFFLINE_ACCESS: \"offline_access\",\n} as const;\n\nexport const CACHE_CONFIG = {\n LOCATION: \"sessionStorage\" as const,\n STORE_AUTH_STATE_IN_COOKIE: false,\n} as const;\n\nexport const MSAL_ERROR_CODES = {\n USER_CANCELLED: \"user_cancelled\",\n NO_CACHED_AUTHORITY: \"no_cached_authority_error\",\n INTERACTION_REQUIRED: \"interaction_required\",\n FORGOT_PASSWORD: \"AADB2C90118\",\n} as const;\n\nexport const STATE_SEPARATOR = \"|\";\n","import { STATE_SEPARATOR } from \"./constants\";\n\n/**\n * Encode a custom state object into a base64 string that can be\n * appended to the MSAL state parameter using a pipe separator.\n */\nexport function encodeState(stateObj: Record<string, unknown>): string {\n return btoa(JSON.stringify(stateObj));\n}\n\n/**\n * Decode a base64-encoded state string back into an object.\n */\nexport function decodeState(encoded: string): Record<string, unknown> | null {\n try {\n return JSON.parse(atob(encoded));\n } catch {\n return null;\n }\n}\n\n/**\n * Extract custom postback data from the MSAL state string.\n * MSAL prepends its own GUID before the pipe separator.\n */\nexport function extractCustomState(msalState: string | undefined): Record<string, unknown> | null {\n if (!msalState) return null;\n\n const parts = msalState.split(STATE_SEPARATOR);\n if (parts.length < 2) return null;\n\n // Everything after the first pipe is our encoded state\n const customPart = parts.slice(1).join(STATE_SEPARATOR);\n return decodeState(customPart);\n}\n","import { LogLevel } from \"@azure/msal-browser\";\nimport type { Configuration, AuthenticationResult } from \"@azure/msal-browser\";\nimport type { IAuthProvider, B2CProviderConfig, B2CPolicies } from \"../types\";\nimport { DEFAULT_POLICIES, DEFAULT_SCOPES, CACHE_CONFIG } from \"../constants\";\n\nexport class B2CProvider implements IAuthProvider {\n readonly type = \"b2c\" as const;\n\n private readonly clientId: string;\n private readonly b2cEnv: string;\n private readonly authorityDomain: string;\n private readonly redirectUri: string;\n private readonly postLogoutRedirectUri: string;\n private readonly policies: B2CPolicies;\n private readonly apiScopes: string[];\n private readonly cacheLocation: \"sessionStorage\" | \"localStorage\";\n\n constructor(config: B2CProviderConfig) {\n this.clientId = config.clientId;\n this.b2cEnv = config.b2cEnvironment;\n this.authorityDomain = config.authorityDomain;\n this.redirectUri = config.redirectUri;\n this.postLogoutRedirectUri = config.postLogoutRedirectUri ?? config.redirectUri;\n this.policies = {\n signUpSignIn: config.policies?.signUpSignIn ?? DEFAULT_POLICIES.SIGN_UP_SIGN_IN,\n signInOnly: config.policies?.signInOnly ?? DEFAULT_POLICIES.SIGN_IN_ONLY,\n resetPassword: config.policies?.resetPassword ?? DEFAULT_POLICIES.RESET_PASSWORD,\n };\n this.apiScopes = config.apiScopes ?? [];\n this.cacheLocation = config.cacheLocation ?? CACHE_CONFIG.LOCATION;\n }\n\n buildMsalConfig(): Configuration {\n return {\n auth: {\n clientId: this.clientId,\n authority: this.getAuthority(this.policies.signUpSignIn),\n knownAuthorities: this.getKnownAuthorities(),\n redirectUri: this.redirectUri,\n postLogoutRedirectUri: this.postLogoutRedirectUri,\n navigateToLoginRequestUrl: false,\n },\n cache: {\n cacheLocation: this.cacheLocation,\n storeAuthStateInCookie: CACHE_CONFIG.STORE_AUTH_STATE_IN_COOKIE,\n },\n system: {\n loggerOptions: {\n logLevel: LogLevel.Warning,\n loggerCallback: (_level: LogLevel, message: string) => {\n console.warn(\"[sso-core/b2c]\", message);\n },\n },\n },\n };\n }\n\n getAuthority(policy?: string): string {\n const p = policy ?? this.policies.signUpSignIn;\n return `https://${this.authorityDomain}/${this.b2cEnv}.onmicrosoft.com/${p}`;\n }\n\n getKnownAuthorities(): string[] {\n return [this.authorityDomain];\n }\n\n identifyPolicy(response: AuthenticationResult): string | null {\n const claims = response.idTokenClaims as Record<string, unknown> | undefined;\n if (!claims) return null;\n\n const acr = (claims.acr ?? claims.tfp) as string | undefined;\n return acr?.toUpperCase() ?? null;\n }\n\n getDefaultScopes(): string[] {\n return [DEFAULT_SCOPES.OPENID, DEFAULT_SCOPES.PROFILE];\n }\n\n getApiScopes(): string[] {\n return this.apiScopes;\n }\n\n /** Get the authority URL for the sign-in-only (city employee) policy */\n getSignInOnlyAuthority(): string {\n return this.getAuthority(this.policies.signInOnly);\n }\n\n /** Get the authority URL for the password reset policy */\n getResetPasswordAuthority(): string {\n return this.getAuthority(this.policies.resetPassword);\n }\n\n /** Get all configured policy names */\n getPolicies(): Readonly<B2CPolicies> {\n return this.policies;\n }\n}\n","import { PublicClientApplication, InteractionRequiredAuthError } from \"@azure/msal-browser\";\nimport type { AccountInfo, AuthenticationResult } from \"@azure/msal-browser\";\nimport { SSOEventEmitter } from \"./events\";\nimport { encodeState, extractCustomState } from \"./state-encoding\";\nimport { MSAL_ERROR_CODES } from \"./constants\";\nimport { B2CProvider } from \"./providers/b2c\";\nimport type {\n IAuthProvider,\n SSOClientConfig,\n SSOClientState,\n SignInOptions,\n SignOutOptions,\n TokenOptions,\n AuthResponse,\n} from \"./types\";\n\nconst INITIAL_STATE: SSOClientState = {\n isAuthenticated: false,\n isLoading: false,\n user: null,\n token: null,\n error: null,\n activePolicy: null,\n authReady: false,\n};\n\nexport class SSOClient {\n readonly events = new SSOEventEmitter();\n private readonly provider: IAuthProvider;\n private readonly debug: boolean;\n private readonly encodedState: string | null;\n\n private msalInstance: PublicClientApplication | null = null;\n private _state: SSOClientState = { ...INITIAL_STATE };\n\n constructor(config: SSOClientConfig) {\n this.provider = config.provider;\n this.debug = config.debug ?? false;\n this.encodedState = config.state ? encodeState(config.state) : null;\n }\n\n get state(): Readonly<SSOClientState> {\n return this._state;\n }\n\n // ── Lifecycle ──\n\n async initialize(): Promise<AuthResponse | null> {\n this.log(\"Initializing SSOClient...\");\n this.updateState({ isLoading: true });\n\n const msalConfig = this.provider.buildMsalConfig();\n this.msalInstance = new PublicClientApplication(msalConfig);\n\n await this.msalInstance.initialize();\n\n const result = await this.handleRedirect();\n\n this.updateState({ isLoading: false, authReady: true });\n return result;\n }\n\n destroy(): void {\n this.events.removeAllListeners();\n this.msalInstance = null;\n this._state = { ...INITIAL_STATE };\n }\n\n // ── Auth Actions ──\n\n async handleRedirect(): Promise<AuthResponse | null> {\n this.assertInitialized();\n this.log(\"Handling redirect promise...\");\n\n try {\n const response = await this.msalInstance!.handleRedirectPromise();\n\n if (!response) {\n // No redirect response — check for existing sessions\n this.selectAccount(null);\n return null;\n }\n\n this.log(\"Redirect response received\", response);\n\n // Extract custom postback state\n const customState = extractCustomState(response.state);\n\n // Identify which policy was used via the acr/tfp claim\n const policy = this.provider.identifyPolicy(response);\n this.updateState({ activePolicy: policy });\n\n // Check if this was a forgot-password completion\n if (this.isForgotPasswordPolicy(policy)) {\n this.log(\"Forgot password flow completed\");\n this.events.emit(\"auth:forgotPassword\", undefined as never);\n return { ...response, customPostbackObject: customState ?? undefined } as AuthResponse;\n }\n\n // Normal sign-in flow\n this.updateState({ isLoading: true });\n this.selectAccount(policy);\n await this.acquireTokenAfterRedirect(response);\n\n const authResponse: AuthResponse = {\n ...response,\n customPostbackObject: customState ?? undefined,\n };\n\n this.events.emit(\"auth:signedIn\", authResponse);\n return authResponse;\n } catch (error) {\n return this.handleRedirectError(error);\n }\n }\n\n async signIn(options?: SignInOptions): Promise<void> {\n this.assertInitialized();\n this.log(\"Initiating sign-in...\");\n this.updateState({ isLoading: true });\n this.events.emit(\"auth:loading\", true);\n\n const request = this.buildLoginRequest(this.provider.getAuthority(), options);\n await this.msalInstance!.loginRedirect(request);\n }\n\n async signInCityEmployee(options?: SignInOptions): Promise<void> {\n this.assertInitialized();\n this.log(\"Initiating city employee sign-in...\");\n\n if (!(this.provider instanceof B2CProvider)) {\n // For non-B2C providers, fall back to regular sign-in\n return this.signIn(options);\n }\n\n const authority = this.provider.getSignInOnlyAuthority();\n const request = this.buildLoginRequest(authority, options);\n await this.msalInstance!.loginRedirect(request);\n }\n\n async signOut(options?: SignOutOptions): Promise<void> {\n this.assertInitialized();\n this.log(\"Initiating sign-out...\");\n\n const logoutRequest = {\n postLogoutRedirectUri: options?.postLogoutRedirectUri,\n authority: this.provider.getAuthority(),\n };\n\n this.updateState({ isLoading: true });\n this.events.emit(\"auth:signedOut\", undefined as never);\n await this.msalInstance!.logoutRedirect(logoutRequest);\n }\n\n async forgotPassword(): Promise<void> {\n this.assertInitialized();\n\n if (!(this.provider instanceof B2CProvider)) {\n this.log(\"Forgot password is only supported for B2C providers\");\n return;\n }\n\n this.log(\"Initiating forgot password flow...\");\n const authority = this.provider.getResetPasswordAuthority();\n await this.msalInstance!.loginRedirect({ scopes: [], authority });\n }\n\n async acquireToken(options?: TokenOptions): Promise<string | null> {\n this.assertInitialized();\n this.log(\"Acquiring token...\");\n\n const account = this._state.user;\n if (!account) {\n this.log(\"No account found, cannot acquire token\");\n return null;\n }\n\n const scopes = options?.scopes ?? this.provider.getApiScopes();\n const tokenRequest = {\n scopes,\n forceRefresh: options?.forceRefresh ?? false,\n account,\n authority: this._state.activePolicy\n ? this.provider.getAuthority(this._state.activePolicy)\n : this.provider.getAuthority(),\n };\n\n try {\n const response = await this.msalInstance!.acquireTokenSilent(tokenRequest);\n\n if (!response.accessToken) {\n throw new InteractionRequiredAuthError(\"empty_token\");\n }\n\n this.log(\"Token acquired silently\");\n this.updateState({ token: response.accessToken, error: null });\n this.events.emit(\"auth:tokenAcquired\", response.accessToken);\n return response.accessToken;\n } catch (error) {\n if (error instanceof InteractionRequiredAuthError) {\n this.log(\"Silent token acquisition failed, falling back to redirect\");\n try {\n await this.msalInstance!.acquireTokenRedirect(tokenRequest);\n return null; // Page will redirect\n } catch (redirectError) {\n this.handleError(redirectError);\n return null;\n }\n }\n this.handleError(error);\n return null;\n }\n }\n\n // ── Internal Helpers ──\n\n private selectAccount(policy: string | null): void {\n const accounts = this.msalInstance!.getAllAccounts();\n\n if (accounts.length === 0) {\n this.updateState({ isAuthenticated: false, user: null });\n return;\n }\n\n let selected: AccountInfo | null = null;\n\n if (accounts.length === 1) {\n selected = accounts[0];\n } else if (policy) {\n // Filter accounts by policy and authority domain\n const filtered = accounts.filter(account => {\n const claims = account.idTokenClaims as Record<string, unknown> | undefined;\n const iss = (claims?.iss as string) ?? \"\";\n const knownAuthorities = this.provider.getKnownAuthorities();\n const matchesAuthority = knownAuthorities.some(auth => iss.toUpperCase().includes(auth.toUpperCase()));\n const matchesPolicy = account.homeAccountId.toUpperCase().includes(policy.toUpperCase());\n return matchesAuthority && matchesPolicy;\n });\n\n if (filtered.length >= 1) {\n selected = filtered[0];\n }\n }\n\n if (!selected && accounts.length > 0) {\n selected = accounts[0];\n }\n\n if (selected) {\n this.log(\"Account selected\", selected.username);\n this.updateState({ isAuthenticated: true, user: selected });\n }\n }\n\n private async acquireTokenAfterRedirect(response: AuthenticationResult): Promise<void> {\n // Set the account first so acquireToken can use it\n const account =\n this.msalInstance!.getAccountByHomeId(response.account?.homeAccountId ?? \"\") ?? response.account ?? null;\n\n if (account) {\n this.updateState({ isAuthenticated: true, user: account });\n }\n\n await this.acquireToken();\n this.updateState({ isLoading: false });\n }\n\n private buildLoginRequest(authority: string, options?: SignInOptions) {\n const scopes = options?.scopes ?? this.provider.getDefaultScopes();\n return {\n scopes,\n authority,\n state: this.encodedState ? `${this.encodedState}` : undefined,\n loginHint: options?.loginHint,\n domainHint: options?.domainHint,\n };\n }\n\n private isForgotPasswordPolicy(policy: string | null): boolean {\n if (!policy) return false;\n if (!(this.provider instanceof B2CProvider)) return false;\n return policy.toUpperCase() === this.provider.getPolicies().resetPassword.toUpperCase();\n }\n\n private handleRedirectError(error: unknown): null {\n const err = error as { errorMessage?: string; errorCode?: string };\n\n // Detect B2C forgot-password error code\n if (err.errorMessage?.includes(MSAL_ERROR_CODES.FORGOT_PASSWORD)) {\n this.log(\"Forgot password error detected, redirecting...\");\n this.forgotPassword();\n return null;\n }\n\n this.handleError(error);\n this.updateState({ isLoading: false });\n return null;\n }\n\n private handleError(error: unknown): void {\n const err = error instanceof Error ? error : new Error(String(error));\n this.log(\"Error:\", err.message);\n this.updateState({ error: err });\n this.events.emit(\"auth:error\", err);\n }\n\n private updateState(partial: Partial<SSOClientState>): void {\n this._state = { ...this._state, ...partial };\n this.events.emit(\"auth:stateChanged\", this._state);\n }\n\n private assertInitialized(): void {\n if (!this.msalInstance) {\n throw new Error(\"SSOClient not initialized. Call initialize() first.\");\n }\n }\n\n private log(...args: unknown[]): void {\n if (this.debug) {\n console.log(\"[sso-core]\", ...args);\n }\n }\n}\n","import type { Configuration, AuthenticationResult } from \"@azure/msal-browser\";\nimport type { IAuthProvider, CIAMProviderConfig } from \"../types\";\n\n/**\n * Entra External ID (CIAM) provider — future implementation.\n * Placeholder scaffolding to validate the provider interface.\n */\nexport class CIAMProvider implements IAuthProvider {\n readonly type = \"ciam\" as const;\n\n private readonly config: CIAMProviderConfig;\n\n constructor(config: CIAMProviderConfig) {\n this.config = config;\n }\n\n buildMsalConfig(): Configuration {\n return {\n auth: {\n clientId: this.config.clientId,\n authority: this.getAuthority(),\n knownAuthorities: this.getKnownAuthorities(),\n redirectUri: this.config.redirectUri,\n postLogoutRedirectUri: this.config.postLogoutRedirectUri ?? this.config.redirectUri,\n navigateToLoginRequestUrl: false,\n },\n cache: {\n cacheLocation: this.config.cacheLocation ?? \"sessionStorage\",\n },\n };\n }\n\n getAuthority(): string {\n return `https://${this.config.tenantSubdomain}.ciamlogin.com/`;\n }\n\n getKnownAuthorities(): string[] {\n return [`${this.config.tenantSubdomain}.ciamlogin.com`];\n }\n\n identifyPolicy(_response: AuthenticationResult): string | null {\n // CIAM doesn't use client-side policy selection\n return null;\n }\n\n getDefaultScopes(): string[] {\n return [\"openid\", \"profile\"];\n }\n\n getApiScopes(): string[] {\n return this.config.scopes ?? [];\n }\n}\n","import type { Configuration, AuthenticationResult } from \"@azure/msal-browser\";\nimport type { IAuthProvider, EntraProviderConfig } from \"../types\";\n\n/**\n * Entra workforce (city employees) provider — future implementation.\n * Placeholder scaffolding to validate the provider interface.\n */\nexport class EntraProvider implements IAuthProvider {\n readonly type = \"entra\" as const;\n\n private readonly config: EntraProviderConfig;\n\n constructor(config: EntraProviderConfig) {\n this.config = config;\n }\n\n buildMsalConfig(): Configuration {\n return {\n auth: {\n clientId: this.config.clientId,\n authority: this.getAuthority(),\n knownAuthorities: this.getKnownAuthorities(),\n redirectUri: this.config.redirectUri,\n postLogoutRedirectUri: this.config.postLogoutRedirectUri ?? this.config.redirectUri,\n navigateToLoginRequestUrl: false,\n },\n cache: {\n cacheLocation: this.config.cacheLocation ?? \"sessionStorage\",\n },\n };\n }\n\n getAuthority(): string {\n return `https://login.microsoftonline.com/${this.config.tenantId}`;\n }\n\n getKnownAuthorities(): string[] {\n return [\"login.microsoftonline.com\"];\n }\n\n identifyPolicy(_response: AuthenticationResult): string | null {\n // Entra workforce doesn't use B2C policies\n return null;\n }\n\n getDefaultScopes(): string[] {\n return [\"openid\", \"profile\"];\n }\n\n getApiScopes(): string[] {\n return this.config.scopes ?? [];\n }\n}\n"],"names":["LogLevel","PublicClientApplication","InteractionRequiredAuthError"],"mappings":"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"}
|