@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 +194 -0
- 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/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";
|
|
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
|