@phila/sso-core 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # @phila/sso-core
2
+
3
+ Framework-agnostic SSO client for Azure AD B2C / Entra External ID, built on top of `@azure/msal-browser`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @phila/sso-core @azure/msal-browser
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import { SSOClient, B2CProvider } from "@phila/sso-core";
15
+
16
+ const client = new SSOClient({
17
+ provider: new B2CProvider({
18
+ clientId: "your-client-id",
19
+ b2cEnvironment: "YourTenant",
20
+ authorityDomain: "YourTenant.b2clogin.com",
21
+ redirectUri: "http://localhost:3000",
22
+ }),
23
+ debug: true,
24
+ });
25
+
26
+ await client.initialize();
27
+
28
+ if (!client.state.isAuthenticated) {
29
+ await client.signIn();
30
+ }
31
+ ```
32
+
33
+ ## Providers
34
+
35
+ ### B2CProvider
36
+
37
+ Azure AD B2C authentication provider.
38
+
39
+ ```ts
40
+ import { B2CProvider } from "@phila/sso-core";
41
+
42
+ const provider = new B2CProvider({
43
+ clientId: "your-client-id",
44
+ b2cEnvironment: "YourTenant",
45
+ authorityDomain: "YourTenant.b2clogin.com",
46
+ redirectUri: "http://localhost:3000",
47
+ postLogoutRedirectUri: "http://localhost:3000", // optional, defaults to redirectUri
48
+ apiScopes: ["https://YourTenant.onmicrosoft.com/api/read"], // optional
49
+ policies: {
50
+ signUpSignIn: "B2C_1A_SIGNUP_SIGNIN", // optional, shown values are defaults
51
+ signInOnly: "B2C_1A_AD_SIGNIN_ONLY",
52
+ resetPassword: "B2C_1A_PASSWORDRESET",
53
+ },
54
+ cacheLocation: "sessionStorage", // optional, "sessionStorage" | "localStorage"
55
+ });
56
+ ```
57
+
58
+ ### CIAMProvider
59
+
60
+ Entra External ID (CIAM) provider.
61
+
62
+ ```ts
63
+ import { CIAMProvider } from "@phila/sso-core";
64
+
65
+ const provider = new CIAMProvider({
66
+ clientId: "your-client-id",
67
+ tenantSubdomain: "yoursubdomain",
68
+ redirectUri: "http://localhost:3000",
69
+ scopes: ["api://your-api/scope"], // optional
70
+ });
71
+ ```
72
+
73
+ ### EntraProvider
74
+
75
+ Entra workforce (Azure AD) provider.
76
+
77
+ ```ts
78
+ import { EntraProvider } from "@phila/sso-core";
79
+
80
+ const provider = new EntraProvider({
81
+ clientId: "your-client-id",
82
+ tenantId: "your-tenant-guid",
83
+ redirectUri: "http://localhost:3000",
84
+ scopes: ["User.Read"], // optional
85
+ });
86
+ ```
87
+
88
+ ## SSOClient API
89
+
90
+ ### Constructor
91
+
92
+ ```ts
93
+ const client = new SSOClient({
94
+ provider: B2CProvider | CIAMProvider | EntraProvider,
95
+ debug?: boolean,
96
+ state?: Record<string, unknown>, // custom state preserved across redirects
97
+ });
98
+ ```
99
+
100
+ ### Lifecycle
101
+
102
+ | Method | Returns | Description |
103
+ | -------------- | ------------------------------- | ---------------------------------------------------------- |
104
+ | `initialize()` | `Promise<AuthResponse \| null>` | Initialize MSAL, process redirect, check existing sessions |
105
+ | `destroy()` | `void` | Clean up resources and reset state |
106
+
107
+ ### Authentication
108
+
109
+ | Method | Returns | Description |
110
+ | ------------------------------ | ------------------------- | ----------------------------------------------------- |
111
+ | `signIn(options?)` | `Promise<void>` | Start sign-in redirect flow |
112
+ | `signInCityEmployee(options?)` | `Promise<void>` | Sign in with the sign-in-only policy (B2C) |
113
+ | `signOut(options?)` | `Promise<void>` | Sign out and redirect |
114
+ | `forgotPassword()` | `Promise<void>` | Start password reset flow (B2C) |
115
+ | `acquireToken(options?)` | `Promise<string \| null>` | Get access token (silent first, interactive fallback) |
116
+
117
+ ### Options
118
+
119
+ ```ts
120
+ interface SignInOptions {
121
+ scopes?: string[];
122
+ loginHint?: string;
123
+ domainHint?: string;
124
+ state?: Record<string, unknown>;
125
+ }
126
+
127
+ interface SignOutOptions {
128
+ postLogoutRedirectUri?: string;
129
+ }
130
+
131
+ interface TokenOptions {
132
+ scopes?: string[];
133
+ forceRefresh?: boolean;
134
+ }
135
+ ```
136
+
137
+ ### State
138
+
139
+ Access the current auth state via `client.state`:
140
+
141
+ ```ts
142
+ interface SSOClientState {
143
+ isAuthenticated: boolean;
144
+ isLoading: boolean;
145
+ user: AccountInfo | null;
146
+ token: string | null;
147
+ error: Error | null;
148
+ activePolicy: string | null;
149
+ authReady: boolean;
150
+ }
151
+ ```
152
+
153
+ ### Events
154
+
155
+ Subscribe to auth lifecycle events:
156
+
157
+ ```ts
158
+ const unsubscribe = client.events.on("auth:signedIn", response => {
159
+ console.log("User signed in:", response.account);
160
+ });
161
+
162
+ // Clean up
163
+ unsubscribe();
164
+ ```
165
+
166
+ | Event | Payload | Description |
167
+ | --------------------- | ---------------- | ------------------------ |
168
+ | `auth:stateChanged` | `SSOClientState` | Any state change |
169
+ | `auth:signedIn` | `AuthResponse` | Sign-in completed |
170
+ | `auth:signedOut` | `void` | Sign-out initiated |
171
+ | `auth:tokenAcquired` | `string` | Token acquired silently |
172
+ | `auth:error` | `Error` | Authentication error |
173
+ | `auth:forgotPassword` | `void` | Password reset completed |
174
+ | `auth:loading` | `boolean` | Loading state changed |
175
+
176
+ ## Custom State
177
+
178
+ Preserve arbitrary data across auth redirects:
179
+
180
+ ```ts
181
+ const client = new SSOClient({
182
+ provider,
183
+ state: { returnUrl: "/dashboard" },
184
+ });
185
+
186
+ const response = await client.initialize();
187
+ if (response?.customPostbackObject) {
188
+ console.log(response.customPostbackObject.returnUrl); // "/dashboard"
189
+ }
190
+ ```
191
+
192
+ ## License
193
+
194
+ MIT
package/dist/index.js CHANGED
@@ -1,2 +1,469 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("@azure/msal-browser");class p{listeners=new Map;on(t,e){this.listeners.has(t)||this.listeners.set(t,new Set);const i=this.listeners.get(t);return i.add(e),()=>{i.delete(e)}}emit(t,e){const i=this.listeners.get(t);if(i)for(const r of i)r(e)}removeAllListeners(){this.listeners.clear()}}const a={SIGN_UP_SIGN_IN:"B2C_1A_SIGNUP_SIGNIN",SIGN_IN_ONLY:"B2C_1A_AD_SIGNIN_ONLY",RESET_PASSWORD:"B2C_1A_PASSWORDRESET"},l={OPENID:"openid",PROFILE:"profile",OFFLINE_ACCESS:"offline_access"},u={LOCATION:"sessionStorage",STORE_AUTH_STATE_IN_COOKIE:!1},S={USER_CANCELLED:"user_cancelled",NO_CACHED_AUTHORITY:"no_cached_authority_error",INTERACTION_REQUIRED:"interaction_required",FORGOT_PASSWORD:"AADB2C90118"},h="|";function f(s){return btoa(JSON.stringify(s))}function A(s){try{return JSON.parse(atob(s))}catch{return null}}function I(s){if(!s)return null;const t=s.split(h);if(t.length<2)return null;const e=t.slice(1).join(h);return A(e)}class c{type="b2c";clientId;b2cEnv;authorityDomain;redirectUri;postLogoutRedirectUri;policies;apiScopes;cacheLocation;constructor(t){this.clientId=t.clientId,this.b2cEnv=t.b2cEnvironment,this.authorityDomain=t.authorityDomain,this.redirectUri=t.redirectUri,this.postLogoutRedirectUri=t.postLogoutRedirectUri??t.redirectUri,this.policies={signUpSignIn:t.policies?.signUpSignIn??a.SIGN_UP_SIGN_IN,signInOnly:t.policies?.signInOnly??a.SIGN_IN_ONLY,resetPassword:t.policies?.resetPassword??a.RESET_PASSWORD},this.apiScopes=t.apiScopes??[],this.cacheLocation=t.cacheLocation??u.LOCATION}buildMsalConfig(){return{auth:{clientId:this.clientId,authority:this.getAuthority(this.policies.signUpSignIn),knownAuthorities:this.getKnownAuthorities(),redirectUri:this.redirectUri,postLogoutRedirectUri:this.postLogoutRedirectUri,navigateToLoginRequestUrl:!1},cache:{cacheLocation:this.cacheLocation,storeAuthStateInCookie:u.STORE_AUTH_STATE_IN_COOKIE},system:{loggerOptions:{logLevel:n.LogLevel.Warning,loggerCallback:(t,e)=>{console.warn("[sso-core/b2c]",e)}}}}}getAuthority(t){const e=t??this.policies.signUpSignIn;return`https://${this.authorityDomain}/${this.b2cEnv}.onmicrosoft.com/${e}`}getKnownAuthorities(){return[this.authorityDomain]}identifyPolicy(t){const e=t.idTokenClaims;return e?(e.acr??e.tfp)?.toUpperCase()??null:null}getDefaultScopes(){return[l.OPENID,l.PROFILE]}getApiScopes(){return this.apiScopes}getSignInOnlyAuthority(){return this.getAuthority(this.policies.signInOnly)}getResetPasswordAuthority(){return this.getAuthority(this.policies.resetPassword)}getPolicies(){return this.policies}}const g={isAuthenticated:!1,isLoading:!1,user:null,token:null,error:null,activePolicy:null,authReady:!1};class _{events=new p;provider;debug;encodedState;msalInstance=null;_state={...g};constructor(t){this.provider=t.provider,this.debug=t.debug??!1,this.encodedState=t.state?f(t.state):null}get state(){return this._state}async initialize(){this.log("Initializing SSOClient..."),this.updateState({isLoading:!0});const t=this.provider.buildMsalConfig();this.msalInstance=new n.PublicClientApplication(t),await this.msalInstance.initialize();const e=await this.handleRedirect();return this.updateState({isLoading:!1,authReady:!0}),e}destroy(){this.events.removeAllListeners(),this.msalInstance=null,this._state={...g}}async handleRedirect(){this.assertInitialized(),this.log("Handling redirect promise...");try{const t=await this.msalInstance.handleRedirectPromise();if(!t)return this.selectAccount(null),null;this.log("Redirect response received",t);const e=I(t.state),i=this.provider.identifyPolicy(t);if(this.updateState({activePolicy:i}),this.isForgotPasswordPolicy(i))return this.log("Forgot password flow completed"),this.events.emit("auth:forgotPassword",void 0),{...t,customPostbackObject:e??void 0};this.updateState({isLoading:!0}),this.selectAccount(i),await this.acquireTokenAfterRedirect(t);const r={...t,customPostbackObject:e??void 0};return this.events.emit("auth:signedIn",r),r}catch(t){return this.handleRedirectError(t)}}async signIn(t){this.assertInitialized(),this.log("Initiating sign-in..."),this.updateState({isLoading:!0}),this.events.emit("auth:loading",!0);const e=this.buildLoginRequest(this.provider.getAuthority(),t);await this.msalInstance.loginRedirect(e)}async signInCityEmployee(t){if(this.assertInitialized(),this.log("Initiating city employee sign-in..."),!(this.provider instanceof c))return this.signIn(t);const e=this.provider.getSignInOnlyAuthority(),i=this.buildLoginRequest(e,t);await this.msalInstance.loginRedirect(i)}async signOut(t){this.assertInitialized(),this.log("Initiating sign-out...");const e={postLogoutRedirectUri:t?.postLogoutRedirectUri,authority:this.provider.getAuthority()};this.updateState({isLoading:!0}),this.events.emit("auth:signedOut",void 0),await this.msalInstance.logoutRedirect(e)}async forgotPassword(){if(this.assertInitialized(),!(this.provider instanceof c)){this.log("Forgot password is only supported for B2C providers");return}this.log("Initiating forgot password flow...");const t=this.provider.getResetPasswordAuthority();await this.msalInstance.loginRedirect({scopes:[],authority:t})}async acquireToken(t){this.assertInitialized(),this.log("Acquiring token...");const e=this._state.user;if(!e)return this.log("No account found, cannot acquire token"),null;const r={scopes:t?.scopes??this.provider.getApiScopes(),forceRefresh:t?.forceRefresh??!1,account:e,authority:this._state.activePolicy?this.provider.getAuthority(this._state.activePolicy):this.provider.getAuthority()};try{const o=await this.msalInstance.acquireTokenSilent(r);if(!o.accessToken)throw new n.InteractionRequiredAuthError("empty_token");return this.log("Token acquired silently"),this.updateState({token:o.accessToken,error:null}),this.events.emit("auth:tokenAcquired",o.accessToken),o.accessToken}catch(o){if(o instanceof n.InteractionRequiredAuthError){this.log("Silent token acquisition failed, falling back to redirect");try{return await this.msalInstance.acquireTokenRedirect(r),null}catch(d){return this.handleError(d),null}}return this.handleError(o),null}}selectAccount(t){const e=this.msalInstance.getAllAccounts();if(e.length===0){this.updateState({isAuthenticated:!1,user:null});return}let i=null;if(e.length===1)i=e[0];else if(t){const r=e.filter(o=>{const y=o.idTokenClaims?.iss??"",m=this.provider.getKnownAuthorities().some(E=>y.toUpperCase().includes(E.toUpperCase())),R=o.homeAccountId.toUpperCase().includes(t.toUpperCase());return m&&R});r.length>=1&&(i=r[0])}!i&&e.length>0&&(i=e[0]),i&&(this.log("Account selected",i.username),this.updateState({isAuthenticated:!0,user:i}))}async acquireTokenAfterRedirect(t){const e=this.msalInstance.getAccountByHomeId(t.account?.homeAccountId??"")??t.account??null;e&&this.updateState({isAuthenticated:!0,user:e}),await this.acquireToken(),this.updateState({isLoading:!1})}buildLoginRequest(t,e){return{scopes:e?.scopes??this.provider.getDefaultScopes(),authority:t,state:this.encodedState?`${this.encodedState}`:void 0,loginHint:e?.loginHint,domainHint:e?.domainHint}}isForgotPasswordPolicy(t){return!t||!(this.provider instanceof c)?!1:t.toUpperCase()===this.provider.getPolicies().resetPassword.toUpperCase()}handleRedirectError(t){return t.errorMessage?.includes(S.FORGOT_PASSWORD)?(this.log("Forgot password error detected, redirecting..."),this.forgotPassword(),null):(this.handleError(t),this.updateState({isLoading:!1}),null)}handleError(t){const e=t instanceof Error?t:new Error(String(t));this.log("Error:",e.message),this.updateState({error:e}),this.events.emit("auth:error",e)}updateState(t){this._state={...this._state,...t},this.events.emit("auth:stateChanged",this._state)}assertInitialized(){if(!this.msalInstance)throw new Error("SSOClient not initialized. Call initialize() first.")}log(...t){this.debug&&console.log("[sso-core]",...t)}}class O{type="ciam";config;constructor(t){this.config=t}buildMsalConfig(){return{auth:{clientId:this.config.clientId,authority:this.getAuthority(),knownAuthorities:this.getKnownAuthorities(),redirectUri:this.config.redirectUri,postLogoutRedirectUri:this.config.postLogoutRedirectUri??this.config.redirectUri,navigateToLoginRequestUrl:!1},cache:{cacheLocation:this.config.cacheLocation??"sessionStorage"}}}getAuthority(){return`https://${this.config.tenantSubdomain}.ciamlogin.com/`}getKnownAuthorities(){return[`${this.config.tenantSubdomain}.ciamlogin.com`]}identifyPolicy(t){return null}getDefaultScopes(){return["openid","profile"]}getApiScopes(){return this.config.scopes??[]}}class C{type="entra";config;constructor(t){this.config=t}buildMsalConfig(){return{auth:{clientId:this.config.clientId,authority:this.getAuthority(),knownAuthorities:this.getKnownAuthorities(),redirectUri:this.config.redirectUri,postLogoutRedirectUri:this.config.postLogoutRedirectUri??this.config.redirectUri,navigateToLoginRequestUrl:!1},cache:{cacheLocation:this.config.cacheLocation??"sessionStorage"}}}getAuthority(){return`https://login.microsoftonline.com/${this.config.tenantId}`}getKnownAuthorities(){return["login.microsoftonline.com"]}identifyPolicy(t){return null}getDefaultScopes(){return["openid","profile"]}getApiScopes(){return this.config.scopes??[]}}exports.B2CProvider=c;exports.CACHE_CONFIG=u;exports.CIAMProvider=O;exports.DEFAULT_POLICIES=a;exports.DEFAULT_SCOPES=l;exports.EntraProvider=C;exports.MSAL_ERROR_CODES=S;exports.SSOClient=_;exports.SSOEventEmitter=p;exports.STATE_SEPARATOR=h;exports.decodeState=A;exports.encodeState=f;exports.extractCustomState=I;
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