wcz-layout
Version:
1 lines • 7.03 kB
Source Map (JSON)
{"version":3,"file":"entra-DbC3aZkF.mjs","names":["ConfidentialClientApplication","CryptoProvider","TokenPayload","serverEnv","LOGIN_SCOPES","ENTRA_AUTHORITY","ENTRA_TENANT_ID","cryptoProvider","createConfidentialClient","auth","clientId","ENTRA_CLIENT_ID","clientSecret","ENTRA_CLIENT_SECRET","authority","extractRefreshToken","client","cache","JSON","parse","getTokenCache","serialize","RefreshToken","Record","secret","Object","values","buildAuthCodeUrl","opts","redirectUri","state","codeChallenge","Promise","getAuthCodeUrl","scopes","responseMode","codeChallengeMethod","exchangeCodeForSession","code","codeVerifier","refreshToken","claims","result","acquireTokenByCode","Error","idTokenClaims","acquireDelegatedToken","ReadonlyArray","accessToken","acquireTokenByRefreshToken","forceCache","buildLogoutUrl","postLogoutRedirectUri","base","encodeURIComponent"],"sources":["../src/lib/auth/entra.ts"],"sourcesContent":["import { ConfidentialClientApplication, CryptoProvider } from \"@azure/msal-node\";\nimport type { TokenPayload } from \"~/models/TokenPayload\";\nimport { serverEnv } from \"~/env\";\n\n/**\n * Scopes requested during the interactive login. `offline_access` yields the\n * refresh token that the session is built around; `User.Read` lets us read the\n * user's Graph profile (e.g. avatar). Resource API scopes (file, approval, …) are\n * acquired on demand via {@link acquireDelegatedToken} using the multi-resource\n * refresh token.\n */\nexport const LOGIN_SCOPES = [\"openid\", \"profile\", \"offline_access\", \"User.Read\"];\n\n/** Entra authority for this tenant. */\nexport const ENTRA_AUTHORITY = `https://login.microsoftonline.com/${serverEnv.ENTRA_TENANT_ID}`;\n\n/** Shared across login/callback/refresh calls — stateless and cheap to reuse. */\nexport const cryptoProvider = new CryptoProvider();\n\n/**\n * Creates a confidential client (no persistent cache plugin).\n *\n * The interactive/refresh flows below deliberately use a FRESH client per token\n * operation rather than a singleton: the client's in-memory cache then holds only\n * that operation's tokens, so {@link extractRefreshToken} reads the correct user's\n * refresh token — a shared client would accumulate (and leak) every user's tokens.\n * Persisting just the refresh token (not the whole multi-KB MSAL cache) is what\n * keeps the session in a small cookie with no server-side token store.\n */\nexport const createConfidentialClient = (): ConfidentialClientApplication =>\n new ConfidentialClientApplication({\n auth: {\n clientId: serverEnv.ENTRA_CLIENT_ID,\n clientSecret: serverEnv.ENTRA_CLIENT_SECRET,\n authority: ENTRA_AUTHORITY,\n },\n });\n\n/**\n * Pulls the refresh-token secret out of a client's in-memory MSAL cache. MSAL\n * intentionally hides refresh tokens, so reading the serialized cache is the only\n * way to persist just the RT (instead of the whole cache) in the session cookie.\n */\nfunction extractRefreshToken(client: ConfidentialClientApplication): string | undefined {\n const cache = JSON.parse(client.getTokenCache().serialize()) as {\n RefreshToken?: Record<string, { secret?: string }>;\n };\n return Object.values(cache.RefreshToken ?? {})[0]?.secret;\n}\n\n/** First leg of the auth-code flow: the URL to redirect the browser to. */\nexport function buildAuthCodeUrl(opts: {\n redirectUri: string;\n state: string;\n codeChallenge: string;\n}): Promise<string> {\n return createConfidentialClient().getAuthCodeUrl({\n scopes: LOGIN_SCOPES,\n redirectUri: opts.redirectUri,\n responseMode: \"query\",\n state: opts.state,\n codeChallenge: opts.codeChallenge,\n codeChallengeMethod: \"S256\",\n });\n}\n\n/**\n * Second leg of the auth-code flow: exchange the code for tokens, returning the\n * refresh token and the id-token claims to persist in the session.\n */\nexport async function exchangeCodeForSession(opts: {\n code: string;\n redirectUri: string;\n codeVerifier: string;\n}): Promise<{ refreshToken: string; claims: TokenPayload }> {\n const client = createConfidentialClient();\n const result = await client.acquireTokenByCode({\n code: opts.code,\n scopes: LOGIN_SCOPES,\n redirectUri: opts.redirectUri,\n codeVerifier: opts.codeVerifier,\n });\n\n const refreshToken = extractRefreshToken(client);\n if (!refreshToken)\n throw new Error(\"No refresh token returned (is the offline_access scope granted?)\");\n\n return { refreshToken, claims: (result.idTokenClaims ?? {}) as TokenPayload };\n}\n\n/**\n * Acquires a delegated access token for `scopes` from the stored refresh token.\n * Entra rotates the refresh token on each use, so the (possibly new) refresh\n * token is returned for the caller to persist. Throws when the refresh token is\n * expired/revoked and interaction is required.\n */\nexport async function acquireDelegatedToken(opts: {\n refreshToken: string;\n scopes: ReadonlyArray<string>;\n}): Promise<{ accessToken: string; refreshToken: string }> {\n const client = createConfidentialClient();\n const result = await client.acquireTokenByRefreshToken({\n refreshToken: opts.refreshToken,\n scopes: [...opts.scopes],\n forceCache: true,\n });\n if (!result) throw new Error(\"Failed to acquire token from refresh token\");\n\n return {\n accessToken: result.accessToken,\n refreshToken: extractRefreshToken(client) ?? opts.refreshToken,\n };\n}\n\n/** Entra's front-channel logout endpoint, which clears the user's IdP session. */\nexport function buildLogoutUrl(postLogoutRedirectUri: string): string {\n const base = `${ENTRA_AUTHORITY}/oauth2/v2.0/logout`;\n return `${base}?post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirectUri)}`;\n}\n"],"mappings":";;;;;;;;;;AAWA,MAAaI,eAAe;CAAC;CAAU;CAAW;CAAkB;AAAW;;AAG/E,MAAaC,kBAAkB,qCAAqCF,UAAUG;;AAG9E,MAAaC,iBAAiB,IAAIN,eAAe;;;;;;;;;;;AAYjD,MAAaO,iCACX,IAAIR,8BAA8B,EAChCS,MAAM;CACJC,UAAUP,UAAUQ;CACpBC,cAAcT,UAAUU;CACxBC,WAAWT;AACb,EACF,CAAC;;;;;;AAOH,SAASU,oBAAoBC,QAA2D;CACtF,MAAMC,QAAQC,KAAKC,MAAMH,OAAOI,cAAc,CAAC,CAACC,UAAU,CAAC;CAG3D,OAAOI,OAAOC,OAAOT,MAAMK,gBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,EAAEE;AACrD;;AAGA,SAAgBG,iBAAiBC,MAIb;CAClB,OAAOpB,yBAAyB,CAAC,CAACyB,eAAe;EAC/CC,QAAQ9B;EACRyB,aAAaD,KAAKC;EAClBM,cAAc;EACdL,OAAOF,KAAKE;EACZC,eAAeH,KAAKG;EACpBK,qBAAqB;CACvB,CAAC;AACH;;;;;AAMA,eAAsBC,uBAAuBT,MAIe;CAC1D,MAAMZ,SAASR,yBAAyB;CACxC,MAAMkC,SAAS,MAAM1B,OAAO2B,mBAAmB;EAC7CL,MAAMV,KAAKU;EACXJ,QAAQ9B;EACRyB,aAAaD,KAAKC;EAClBU,cAAcX,KAAKW;CACrB,CAAC;CAED,MAAMC,eAAezB,oBAAoBC,MAAM;CAC/C,IAAI,CAACwB,cACH,MAAM,IAAII,MAAM,kEAAkE;CAEpF,OAAO;EAAEJ;EAAcC,QAASC,OAAOG,iBAAiB,CAAC;CAAmB;AAC9E;;;;;;;AAQA,eAAsBC,sBAAsBlB,MAGe;CACzD,MAAMZ,SAASR,yBAAyB;CACxC,MAAMkC,SAAS,MAAM1B,OAAOiC,2BAA2B;EACrDT,cAAcZ,KAAKY;EACnBN,QAAQ,CAAC,GAAGN,KAAKM,MAAM;EACvBgB,YAAY;CACd,CAAC;CACD,IAAI,CAACR,QAAQ,MAAM,IAAIE,MAAM,4CAA4C;CAEzE,OAAO;EACLI,aAAaN,OAAOM;EACpBR,cAAczB,oBAAoBC,MAAM,KAAKY,KAAKY;CACpD;AACF;;AAGA,SAAgBW,eAAeC,uBAAuC;CAEpE,OAAO,GAAGC,GADMhD,gBAAe,qBACjB,4BAA6BiD,mBAAmBF,qBAAqB;AACrF"}