Moved azure and graph modules to a standalone package.

This commit is contained in:
2026-02-04 19:54:34 +01:00
parent 9137688a55
commit c06b3fcef6
2 changed files with 0 additions and 264 deletions

View File

@@ -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);
}
});
});
}

View File

@@ -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();
}