@phila/sso-core
v0.0.9
Published
Framework-agnostic SSO client for Azure AD B2C / Entra External ID
Readme
@phila/sso-core
Framework-agnostic SSO client for Azure AD B2C / Entra External ID, built on top of @azure/msal-browser.
Installation
pnpm add @phila/sso-core @azure/msal-browserQuick Start
import { SSOClient, B2CProvider } from "@phila/sso-core";
const client = new SSOClient({
provider: new B2CProvider({
clientId: "your-client-id",
b2cEnvironment: "YourTenant",
authorityDomain: "YourTenant.b2clogin.com",
redirectUri: "http://localhost:3000",
}),
debug: true,
});
await client.initialize();
if (!client.state.isAuthenticated) {
await client.signIn();
}Providers
B2CProvider
Azure AD B2C authentication provider.
import { B2CProvider } from "@phila/sso-core";
const provider = new B2CProvider({
clientId: "your-client-id",
b2cEnvironment: "YourTenant",
authorityDomain: "YourTenant.b2clogin.com",
redirectUri: "http://localhost:3000",
postLogoutRedirectUri: "http://localhost:3000", // optional, defaults to redirectUri
apiScopes: ["https://YourTenant.onmicrosoft.com/api/read"], // optional
policies: {
signUpSignIn: "B2C_1A_SIGNUP_SIGNIN", // optional, shown values are defaults
signInOnly: "B2C_1A_AD_SIGNIN_ONLY",
resetPassword: "B2C_1A_PASSWORDRESET",
},
cacheLocation: "sessionStorage", // optional, "sessionStorage" | "localStorage"
});CIAMProvider
Entra External ID (CIAM) provider.
import { CIAMProvider } from "@phila/sso-core";
const provider = new CIAMProvider({
clientId: "your-client-id",
tenantSubdomain: "yoursubdomain",
redirectUri: "http://localhost:3000",
scopes: ["api://your-api/scope"], // optional
});EntraProvider
Entra workforce (Azure AD) provider.
import { EntraProvider } from "@phila/sso-core";
const provider = new EntraProvider({
clientId: "your-client-id",
tenantId: "your-tenant-guid",
redirectUri: "http://localhost:3000",
scopes: ["User.Read"], // optional
});SSOClient API
Constructor
const client = new SSOClient({
provider: B2CProvider | CIAMProvider | EntraProvider,
debug?: boolean,
state?: Record<string, unknown>, // custom state preserved across redirects
});Lifecycle
| Method | Returns | Description |
| -------------- | ------------------------------- | ---------------------------------------------------------- |
| initialize() | Promise<AuthResponse \| null> | Initialize MSAL, process redirect, check existing sessions |
| destroy() | void | Clean up resources and reset state |
Authentication
| Method | Returns | Description |
| ------------------------------ | ------------------------- | ----------------------------------------------------- |
| signIn(options?) | Promise<void> | Start sign-in redirect flow |
| signInCityEmployee(options?) | Promise<void> | Sign in with the sign-in-only policy (B2C) |
| signOut(options?) | Promise<void> | Sign out and redirect |
| forgotPassword() | Promise<void> | Start password reset flow (B2C) |
| acquireToken(options?) | Promise<string \| null> | Get access token (silent first, interactive fallback) |
Options
interface SignInOptions {
scopes?: string[];
loginHint?: string;
domainHint?: string;
state?: Record<string, unknown>;
prompt?: string;
}
interface SignOutOptions {
postLogoutRedirectUri?: string;
}
interface TokenOptions {
scopes?: string[];
forceRefresh?: boolean;
}State
Access the current auth state via client.state:
interface SSOClientState {
isAuthenticated: boolean;
isLoading: boolean;
user: AccountInfo | null;
token: string | null;
error: Error | null;
activePolicy: string | null;
authReady: boolean;
}Events
Subscribe to auth lifecycle events:
const unsubscribe = client.events.on("auth:signedIn", response => {
console.log("User signed in:", response.account);
});
// Clean up
unsubscribe();| Event | Payload | Description |
| --------------------- | ---------------- | ------------------------ |
| auth:stateChanged | SSOClientState | Any state change |
| auth:signedIn | AuthResponse | Sign-in completed |
| auth:signedOut | void | Sign-out initiated |
| auth:tokenAcquired | string | Token acquired silently |
| auth:error | Error | Authentication error |
| auth:forgotPassword | void | Password reset completed |
| auth:loading | boolean | Loading state changed |
Custom State
Preserve arbitrary data across auth redirects:
const client = new SSOClient({
provider,
state: { returnUrl: "/dashboard" },
});
const response = await client.initialize();
if (response?.customPostbackObject) {
console.log(response.customPostbackObject.returnUrl); // "/dashboard"
}License
MIT
