// azure.js import http from "node:http"; import { URL } from "node:url"; import open, { apps } 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 "d": case "default": return new DefaultAzureCredential(); case "cs": 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 "dc": 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({ appName, 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"); // Make app name lowercase with all non-alphanumeric characters removed // spaces replaced with dashes and all letters converted to lowercase const sanitizedAppName = (appName || "Azure Node Login") .toLowerCase() .replace(/[^a-z0-9]+/g, "-"); const cachePath = path.join( os.homedir(), `.config/${sanitizedAppName}`, `${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, app: { name: apps.edge, // Enforce using Microsoft Edge browser }, }); } catch {} console.log("Visit:\n" + authUrl); } catch (e) { try { server.close(); } catch {} reject(e); } }); }); }