// azure.js import http from "node:http"; import { URL } from "node:url"; import open from "open"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import { PublicClientApplication, ConfidentialClientApplication } from "@azure/msal-node"; import { DefaultAzureCredential, ClientSecretCredential, DeviceCodeCredential } from "@azure/identity"; export async function getCredential(credentialType, options) { switch (credentialType) { case "default": return new DefaultAzureCredential(); case "clientSecret": if (!options.tenantId || !options.clientId || !options.clientSecret) { throw new Error("tenantId, clientId, and clientSecret are required for ClientSecretCredential"); } return new ClientSecretCredential( options.tenantId, options.clientId, options.clientSecret ); case "deviceCode": if (!options.tenantId || !options.clientId) { throw new Error("tenantId and clientId are required for DeviceCodeCredential"); } return new DeviceCodeCredential({ tenantId: options.tenantId, clientId: options.clientId, userPromptCallback: (info) => { console.log(info.message); }, }); default: throw new Error(`Unsupported credential type: ${credentialType}`); } } function fileCachePlugin(cachePath) { return { beforeCacheAccess: async (ctx) => { if (fs.existsSync(cachePath)) { ctx.tokenCache.deserialize( fs.readFileSync(cachePath, "utf8") ); } }, afterCacheAccess: async (ctx) => { if (ctx.cacheHasChanged) { fs.mkdirSync(path.dirname(cachePath), { recursive: true }); fs.writeFileSync( cachePath, ctx.tokenCache.serialize() ); } }, }; } function generatePkce() { const verifier = crypto.randomBytes(32).toString("base64url"); // 43 chars, valid const challenge = crypto .createHash("sha256") .update(verifier) .digest("base64url"); return { verifier, challenge, challengeMethod: "S256" }; } export async function loginInteractive({ tenantId, clientId, scopes }) { if (!tenantId) throw new Error("tenantId is required"); if (!clientId) throw new Error("clientId is required"); if (!Array.isArray(scopes) || scopes.length === 0) throw new Error("scopes[] is required"); const cachePath = path.join( os.homedir(), ".config/azure-node-playground", `${clientId}-token-cache.json` ); const pca = new PublicClientApplication({ auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}`, }, cache: { cachePlugin: fileCachePlugin(cachePath), }, }); const accounts = await pca.getTokenCache().getAllAccounts(); if (accounts.length > 0) { try { const silentResult = await pca.acquireTokenSilent({ account: accounts[0], scopes, }); return silentResult; } catch (e) { // proceed to interactive login } } const pkce = generatePkce(); return new Promise((resolve, reject) => { let redirectUri; const server = http.createServer(async (req, res) => { try { const url = new URL(req.url ?? "/", `http://${req.headers.host}`); if (url.pathname !== "/callback") { res.writeHead(404).end(); return; } const code = url.searchParams.get("code"); if (!code) { res.writeHead(400).end("Missing authorization code"); server.close(); reject(new Error("Missing authorization code")); return; } res.end("Authentication complete. You may close this tab."); server.close(); const token = await pca.acquireTokenByCode({ code, scopes, redirectUri, codeVerifier: pkce.verifier, }); resolve(token); } catch (e) { try { server.close(); } catch {} reject(e); } }); server.listen(0, "127.0.0.1", async () => { try { const { port } = server.address(); redirectUri = `http://localhost:${port}/callback`; console.log("Using redirectUri:", redirectUri); const authUrl = await pca.getAuthCodeUrl({ scopes, redirectUri, codeChallenge: pkce.challenge, codeChallengeMethod: pkce.challengeMethod, }); try { await open(authUrl, { wait: false }); } catch {} console.log("If the browser didn't open, visit:\n" + authUrl); } catch (e) { try { server.close(); } catch {} reject(e); } }); }); }