From c06b3fcef61fada65e1f285556dbfce3f9cc1a91 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Wed, 4 Feb 2026 19:54:34 +0100 Subject: [PATCH] Moved azure and graph modules to a standalone package. --- src/azure.js | 194 --------------------------------------------------- src/graph.js | 70 ------------------- 2 files changed, 264 deletions(-) delete mode 100644 src/azure.js delete mode 100644 src/graph.js diff --git a/src/azure.js b/src/azure.js deleted file mode 100644 index 8575540..0000000 --- a/src/azure.js +++ /dev/null @@ -1,194 +0,0 @@ -// 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); - } - }); - }); -} diff --git a/src/graph.js b/src/graph.js deleted file mode 100644 index 7195abe..0000000 --- a/src/graph.js +++ /dev/null @@ -1,70 +0,0 @@ -import { Client } from "@microsoft/microsoft-graph-client"; -import { loginInteractive } from "./azure.js"; - -export async function getGraphClient({ tenantId, clientId }) { - const graphApiToken = await loginInteractive({ - tenantId, - clientId, - scopes: ["https://graph.microsoft.com/.default"], - }); - - const client = Client.init({ - authProvider: (done) => { - done(null, graphApiToken.accessToken); - }, - }); - - return { graphApiToken, client }; -} - -export async function getApp(client, appName) { - const result = await client - .api("/applications") - .filter(`displayName eq '${appName}'`) - .get(); - - // Return the first application found or null if none exists - return result.value.length > 0 ? result.value[0] : null; -} - -export async function getServicePrincipal(client, appId) { - const result = await client - .api("/servicePrincipals") - .filter(`appId eq '${appId}'`) - .get(); - - // Return the first service principal found or null if none exists - return result.value.length > 0 ? result.value[0] : null; -} - -export async function createApp(client, appName) { - const app = await client.api("/applications").post({ - displayName: appName, - }); - - if (!app || !app.appId) { - throw new Error("Failed to create application"); - } - - return app; -} - -export async function createSp(client, appId) { - const sp = await client.api("/servicePrincipals").post({ - appId, - }); - - if (!sp || !sp.id) { - throw new Error("Failed to create service principal"); - } - - return sp; -} - -export async function deleteSp(client, spId) { - await client.api(`/servicePrincipals/${spId}`).delete(); -} - -export async function deleteApp(client, appObjectId) { - await client.api(`/applications/${appObjectId}`).delete(); -}