diff --git a/src/azure.js b/src/azure.js index 4be64d6..8575540 100644 --- a/src/azure.js +++ b/src/azure.js @@ -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); } });