wcz-layout
Version:
101 lines (100 loc) • 4.16 kB
JavaScript
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