UNPKG

wcz-layout

Version:

1 lines 8.3 kB
{"version":3,"file":"entra-47nBhMK-.mjs","names":["ConfidentialClientApplication","CryptoProvider","InteractionRequiredAuthError","ServerError","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","SessionExpiredError","acquireDelegatedToken","ReadonlyArray","accessToken","acquireTokenByRefreshToken","forceCache","error","errorCode","message","buildLogoutUrl","postLogoutRedirectUri","base","encodeURIComponent"],"sources":["../src/lib/auth/entra.ts"],"sourcesContent":["import {\r\n ConfidentialClientApplication,\r\n CryptoProvider,\r\n InteractionRequiredAuthError,\r\n ServerError,\r\n} from \"@azure/msal-node\";\r\nimport type { TokenPayload } from \"~/models/TokenPayload\";\r\nimport { serverEnv } from \"~/env\";\r\n\r\n/**\r\n * Scopes requested during the interactive login. `offline_access` yields the\r\n * refresh token that the session is built around; `User.Read` lets us read the\r\n * user's Graph profile (e.g. avatar). Resource API scopes (file, approval, …) are\r\n * acquired on demand via {@link acquireDelegatedToken} using the multi-resource\r\n * refresh token.\r\n */\r\nexport const LOGIN_SCOPES = [\"openid\", \"profile\", \"offline_access\", \"User.Read\"];\r\n\r\n/** Entra authority for this tenant. */\r\nexport const ENTRA_AUTHORITY = `https://login.microsoftonline.com/${serverEnv.ENTRA_TENANT_ID}`;\r\n\r\n/** Shared across login/callback/refresh calls — stateless and cheap to reuse. */\r\nexport const cryptoProvider = new CryptoProvider();\r\n\r\n/**\r\n * Creates a confidential client (no persistent cache plugin).\r\n *\r\n * The interactive/refresh flows below deliberately use a FRESH client per token\r\n * operation rather than a singleton: the client's in-memory cache then holds only\r\n * that operation's tokens, so {@link extractRefreshToken} reads the correct user's\r\n * refresh token — a shared client would accumulate (and leak) every user's tokens.\r\n * Persisting just the refresh token (not the whole multi-KB MSAL cache) is what\r\n * keeps the session in a small cookie with no server-side token store.\r\n */\r\nexport const createConfidentialClient = (): ConfidentialClientApplication =>\r\n new ConfidentialClientApplication({\r\n auth: {\r\n clientId: serverEnv.ENTRA_CLIENT_ID,\r\n clientSecret: serverEnv.ENTRA_CLIENT_SECRET,\r\n authority: ENTRA_AUTHORITY,\r\n },\r\n });\r\n\r\n/**\r\n * Pulls the refresh-token secret out of a client's in-memory MSAL cache. MSAL\r\n * intentionally hides refresh tokens, so reading the serialized cache is the only\r\n * way to persist just the RT (instead of the whole cache) in the session cookie.\r\n */\r\nfunction extractRefreshToken(client: ConfidentialClientApplication): string | undefined {\r\n const cache = JSON.parse(client.getTokenCache().serialize()) as {\r\n RefreshToken?: Record<string, { secret?: string }>;\r\n };\r\n return Object.values(cache.RefreshToken ?? {})[0]?.secret;\r\n}\r\n\r\n/** First leg of the auth-code flow: the URL to redirect the browser to. */\r\nexport function buildAuthCodeUrl(opts: {\r\n redirectUri: string;\r\n state: string;\r\n codeChallenge: string;\r\n}): Promise<string> {\r\n return createConfidentialClient().getAuthCodeUrl({\r\n scopes: LOGIN_SCOPES,\r\n redirectUri: opts.redirectUri,\r\n responseMode: \"query\",\r\n state: opts.state,\r\n codeChallenge: opts.codeChallenge,\r\n codeChallengeMethod: \"S256\",\r\n });\r\n}\r\n\r\n/**\r\n * Second leg of the auth-code flow: exchange the code for tokens, returning the\r\n * refresh token and the id-token claims to persist in the session.\r\n */\r\nexport async function exchangeCodeForSession(opts: {\r\n code: string;\r\n redirectUri: string;\r\n codeVerifier: string;\r\n}): Promise<{ refreshToken: string; claims: TokenPayload }> {\r\n const client = createConfidentialClient();\r\n const result = await client.acquireTokenByCode({\r\n code: opts.code,\r\n scopes: LOGIN_SCOPES,\r\n redirectUri: opts.redirectUri,\r\n codeVerifier: opts.codeVerifier,\r\n });\r\n\r\n const refreshToken = extractRefreshToken(client);\r\n if (!refreshToken)\r\n throw new Error(\"No refresh token returned (is the offline_access scope granted?)\");\r\n\r\n return { refreshToken, claims: (result.idTokenClaims ?? {}) as TokenPayload };\r\n}\r\n\r\n/**\r\n * Acquires a delegated access token for `scopes` from the stored refresh token.\r\n * Entra rotates the refresh token on each use, so the (possibly new) refresh\r\n * token is returned for the caller to persist. Throws when the refresh token is\r\n * expired/revoked and interaction is required.\r\n */\r\n/** The refresh token is expired/revoked — the user must sign in interactively. */\r\nexport class SessionExpiredError extends Error {}\r\n\r\nexport async function acquireDelegatedToken(opts: {\r\n refreshToken: string;\r\n scopes: ReadonlyArray<string>;\r\n}): Promise<{ accessToken: string; refreshToken: string }> {\r\n const client = createConfidentialClient();\r\n let result;\r\n try {\r\n result = await client.acquireTokenByRefreshToken({\r\n refreshToken: opts.refreshToken,\r\n scopes: [...opts.scopes],\r\n forceCache: true,\r\n });\r\n } catch (error) {\r\n // Revocation, inactivity expiry (AADSTS70008/700082/50173) and Conditional\r\n // Access sign-in frequency (AADSTS70043) all surface as interaction-required\r\n // or invalid_grant — the session is dead, only an interactive login helps.\r\n if (\r\n error instanceof InteractionRequiredAuthError ||\r\n (error instanceof ServerError && error.errorCode === \"invalid_grant\")\r\n )\r\n throw new SessionExpiredError(error.message);\r\n throw error;\r\n }\r\n if (!result) throw new Error(\"Failed to acquire token from refresh token\");\r\n\r\n return {\r\n accessToken: result.accessToken,\r\n refreshToken: extractRefreshToken(client) ?? opts.refreshToken,\r\n };\r\n}\r\n\r\n/** Entra's front-channel logout endpoint, which clears the user's IdP session. */\r\nexport function buildLogoutUrl(postLogoutRedirectUri: string): string {\r\n const base = `${ENTRA_AUTHORITY}/oauth2/v2.0/logout`;\r\n return `${base}?post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirectUri)}`;\r\n}\r\n"],"mappings":";;;;;;;;;;AAgBA,MAAaM,eAAe;CAAC;CAAU;CAAW;CAAkB;AAAW;;AAG/E,MAAaC,kBAAkB,qCAAqCF,UAAUG;;AAG9E,MAAaC,iBAAiB,IAAIR,eAAe;;;;;;;;;;;AAYjD,MAAaS,iCACX,IAAIV,8BAA8B,EAChCW,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;;;;;;;;AASA,IAAaC,sBAAb,cAAyCF,MAAM,CAAA;AAE/C,eAAsBG,sBAAsBnB,MAGe;CACzD,MAAMZ,SAASR,yBAAyB;CACxC,IAAIkC;CACJ,IAAI;EACFA,SAAS,MAAM1B,OAAOkC,2BAA2B;GAC/CV,cAAcZ,KAAKY;GACnBN,QAAQ,CAAC,GAAGN,KAAKM,MAAM;GACvBiB,YAAY;EACd,CAAC;CACH,SAASC,OAAO;EAId,IACEA,iBAAiBpD,gCAChBoD,iBAAiBnD,eAAemD,MAAMC,cAAc,iBAErD,MAAM,IAAIP,oBAAoBM,MAAME,OAAO;EAC7C,MAAMF;CACR;CACA,IAAI,CAACV,QAAQ,MAAM,IAAIE,MAAM,4CAA4C;CAEzE,OAAO;EACLK,aAAaP,OAAOO;EACpBT,cAAczB,oBAAoBC,MAAM,KAAKY,KAAKY;CACpD;AACF;;AAGA,SAAgBe,eAAeC,uBAAuC;CAEpE,OAAO,GAAGC,GADMpD,gBAAe,qBACjB,4BAA6BqD,mBAAmBF,qBAAqB;AACrF"}