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