Refactor getCredential and loginInteractive functions for improved error handling and browser specification

This commit is contained in:
2026-02-03 23:56:24 +01:00
parent 1a70f7efcf
commit a805ed415f

View File

@@ -1,13 +1,20 @@
// azure.js
import http from "node:http";
import { URL } from "node:url";
import open from "open";
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";
import {
PublicClientApplication,
ConfidentialClientApplication,
} from "@azure/msal-node";
import {
DefaultAzureCredential,
ClientSecretCredential,
DeviceCodeCredential,
} from "@azure/identity";
export async function getCredential(credentialType, options) {
switch (credentialType) {
@@ -17,17 +24,21 @@ export async function getCredential(credentialType, options) {
case "cs":
case "clientSecret":
if (!options.tenantId || !options.clientId || !options.clientSecret) {
throw new Error("tenantId, clientId, and clientSecret are required for ClientSecretCredential");
throw new Error(
"tenantId, clientId, and clientSecret are required for ClientSecretCredential",
);
}
return new ClientSecretCredential(
options.tenantId,
options.clientId,
options.clientSecret
options.clientSecret,
);
case "dc":
case "deviceCode":
if (!options.tenantId || !options.clientId) {
throw new Error("tenantId and clientId are required for DeviceCodeCredential");
throw new Error(
"tenantId and clientId are required for DeviceCodeCredential",
);
}
return new DeviceCodeCredential({
tenantId: options.tenantId,
@@ -45,18 +56,13 @@ function fileCachePlugin(cachePath) {
return {
beforeCacheAccess: async (ctx) => {
if (fs.existsSync(cachePath)) {
ctx.tokenCache.deserialize(
fs.readFileSync(cachePath, "utf8")
);
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()
);
fs.writeFileSync(cachePath, ctx.tokenCache.serialize());
}
},
};
@@ -72,16 +78,22 @@ function generatePkce() {
return { verifier, challenge, challengeMethod: "S256" };
}
export async function loginInteractive({ tenantId, clientId, scopes }) {
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/azure-node-playground",
`${clientId}-token-cache.json`
`.config/${sanitizedAppName}`,
`${clientId}-token-cache.json`,
);
const pca = new PublicClientApplication({
@@ -142,7 +154,9 @@ export async function loginInteractive({ tenantId, clientId, scopes }) {
resolve(token);
} catch (e) {
try { server.close(); } catch {}
try {
server.close();
} catch {}
reject(e);
}
});
@@ -160,10 +174,19 @@ export async function loginInteractive({ tenantId, clientId, scopes }) {
codeChallengeMethod: pkce.challengeMethod,
});
try { await open(authUrl, { wait: false }); } catch {}
console.log("If the browser didn't open, visit:\n" + authUrl);
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 {}
try {
server.close();
} catch {}
reject(e);
}
});