UNPKG

wcz-layout

Version:

101 lines (100 loc) 4.16 kB
import { n as serverEnv } from "./env-Bm6rrgwT.mjs"; import { ConfidentialClientApplication, CryptoProvider } from "@azure/msal-node"; //#region src/lib/auth/entra.ts /** * Scopes requested during the interactive login. `offline_access` yields the * refresh token that the session is built around; `User.Read` lets us read the * user's Graph profile (e.g. avatar). Resource API scopes (file, approval, …) are * acquired on demand via {@link acquireDelegatedToken} using the multi-resource * refresh token. */ const LOGIN_SCOPES = [ "openid", "profile", "offline_access", "User.Read" ]; /** Entra authority for this tenant. */ const ENTRA_AUTHORITY = `https://login.microsoftonline.com/${serverEnv.ENTRA_TENANT_ID}`; /** Shared across login/callback/refresh calls — stateless and cheap to reuse. */ const cryptoProvider = new CryptoProvider(); /** * Creates a confidential client (no persistent cache plugin). * * The interactive/refresh flows below deliberately use a FRESH client per token * operation rather than a singleton: the client's in-memory cache then holds only * that operation's tokens, so {@link extractRefreshToken} reads the correct user's * refresh token — a shared client would accumulate (and leak) every user's tokens. * Persisting just the refresh token (not the whole multi-KB MSAL cache) is what * keeps the session in a small cookie with no server-side token store. */ const createConfidentialClient = () => new ConfidentialClientApplication({ auth: { clientId: serverEnv.ENTRA_CLIENT_ID, clientSecret: serverEnv.ENTRA_CLIENT_SECRET, authority: ENTRA_AUTHORITY } }); /** * Pulls the refresh-token secret out of a client's in-memory MSAL cache. MSAL * intentionally hides refresh tokens, so reading the serialized cache is the only * way to persist just the RT (instead of the whole cache) in the session cookie. */ function extractRefreshToken(client) { const cache = JSON.parse(client.getTokenCache().serialize()); return Object.values(cache.RefreshToken ?? {})[0]?.secret; } /** First leg of the auth-code flow: the URL to redirect the browser to. */ function buildAuthCodeUrl(opts) { return createConfidentialClient().getAuthCodeUrl({ scopes: LOGIN_SCOPES, redirectUri: opts.redirectUri, responseMode: "query", state: opts.state, codeChallenge: opts.codeChallenge, codeChallengeMethod: "S256" }); } /** * Second leg of the auth-code flow: exchange the code for tokens, returning the * refresh token and the id-token claims to persist in the session. */ async function exchangeCodeForSession(opts) { const client = createConfidentialClient(); const result = await client.acquireTokenByCode({ code: opts.code, scopes: LOGIN_SCOPES, redirectUri: opts.redirectUri, codeVerifier: opts.codeVerifier }); const refreshToken = extractRefreshToken(client); if (!refreshToken) throw new Error("No refresh token returned (is the offline_access scope granted?)"); return { refreshToken, claims: result.idTokenClaims ?? {} }; } /** * Acquires a delegated access token for `scopes` from the stored refresh token. * Entra rotates the refresh token on each use, so the (possibly new) refresh * token is returned for the caller to persist. Throws when the refresh token is * expired/revoked and interaction is required. */ async function acquireDelegatedToken(opts) { const client = createConfidentialClient(); const result = await client.acquireTokenByRefreshToken({ refreshToken: opts.refreshToken, scopes: [...opts.scopes], forceCache: true }); if (!result) throw new Error("Failed to acquire token from refresh token"); return { accessToken: result.accessToken, refreshToken: extractRefreshToken(client) ?? opts.refreshToken }; } /** Entra's front-channel logout endpoint, which clears the user's IdP session. */ function buildLogoutUrl(postLogoutRedirectUri) { return `${`${ENTRA_AUTHORITY}/oauth2/v2.0/logout`}?post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirectUri)}`; } //#endregion export { acquireDelegatedToken, buildAuthCodeUrl, buildLogoutUrl, createConfidentialClient, cryptoProvider, exchangeCodeForSession }; //# sourceMappingURL=entra-DbC3aZkF.mjs.map