Refactor of authentication code. Added configuration file selectable authentication method. Selectable from built-in Azure Identity, and custom PCA using MSAL.
Some checks failed
build / build (push) Failing after 14s

This commit is contained in:
2026-03-08 19:07:10 +01:00
parent 0829b35113
commit a98c77cd2e
17 changed files with 297 additions and 131 deletions

View File

@@ -14,15 +14,10 @@ import type {
TokenCacheContext,
} from "@azure/msal-node";
const RESOURCE_SCOPE_BY_NAME = {
graph: "https://graph.microsoft.com/.default",
devops: "499b84ac-1321-427f-aa17-267ca6975798/.default",
arm: "https://management.azure.com/.default",
} as const;
import type { ResourceName } from "../azure/index.ts";
import { RESOURCE_SCOPE_BY_NAME, DEFAULT_RESOURCES } from "../azure/index.ts";
import { translateResourceNamesToScopes } from "./index.ts";
type ResourceName = keyof typeof RESOURCE_SCOPE_BY_NAME;
const DEFAULT_RESOURCES: ResourceName[] = ["graph", "devops", "arm"];
const LOGIN_REQUIRED_MESSAGE = "Login required. Run: sk-az-tools login";
const BROWSER_KEYWORDS = Object.keys(apps).sort();
const OPEN_APPS = apps as Record<string, string | readonly string[]>;
@@ -296,8 +291,8 @@ export async function loginInteractive(
}
export async function loginDeviceCode(
tenantId: string | undefined,
clientId: string | undefined,
tenantId: string,
clientId: string,
scopes: string[],
): Promise<AuthenticationResult | null> {
if (!tenantId) throw new Error("tenantId is required");
@@ -320,8 +315,8 @@ export async function loginDeviceCode(
}
export async function login(
tenantId: string | undefined,
clientId: string | undefined,
tenantId: string,
clientId: string,
resourcesCsv?: string,
useDeviceCode = false,
noBrowser = false,
@@ -338,7 +333,7 @@ export async function login(
validateBrowserOptions(browser, browserProfile);
const resources = parseResources(resourcesCsv);
const scopes = resources.map((resourceName) => RESOURCE_SCOPE_BY_NAME[resourceName]);
const scopes = translateResourceNamesToScopes(resources);
const pca = await createPca(tenantId, clientId);
const session = await readSessionState();
const preferredAccount = session.activeAccountUpn
@@ -347,34 +342,30 @@ export async function login(
const results: Array<{ resource: string; expiresOn: string | null }> = [];
let selectedAccount: AccountInfo | null = preferredAccount;
for (let index = 0; index < resources.length; index += 1) {
const resource = resources[index];
const scope = [scopes[index]];
let token = await acquireTokenWithCache(pca, scope, selectedAccount);
let token = await acquireTokenWithCache(pca, scopes, selectedAccount);
if (!token) {
if (useDeviceCode) {
token = await pca.acquireTokenByDeviceCode({
scopes: scope,
deviceCodeCallback: (response) => {
writeStderr(response.message);
},
});
} else {
token = await pca.acquireTokenInteractive({
scopes: scope,
openBrowser: async (url: string) => {
if (noBrowser) {
writeStderr(`Visit:\n${url}`);
return;
}
const options = getBrowserOpenOptions(browser, browserProfile);
await open(url, options).catch(() => {
writeStderr(`Visit:\n${url}`);
});
},
});
}
if (!token) {
if (useDeviceCode) {
token = await pca.acquireTokenByDeviceCode({
scopes: scopes,
deviceCodeCallback: (response) => {
writeStderr(response.message);
},
});
} else {
token = await pca.acquireTokenInteractive({
scopes: scopes,
openBrowser: async (url: string) => {
if (noBrowser) {
writeStderr(`Visit:\n${url}`);
return;
}
const options = getBrowserOpenOptions(browser, browserProfile);
await open(url, options).catch(() => {
writeStderr(`Visit:\n${url}`);
});
},
});
}
if (token?.account) {
@@ -382,7 +373,7 @@ export async function login(
}
results.push({
resource,
resource: resources.join(","),
expiresOn: token?.expiresOn?.toISOString?.() ?? null,
});
}
@@ -400,20 +391,14 @@ export async function login(
};
}
export async function acquireResourceToken(
export async function getTokenUsingMsal(
tenantId: string,
clientId: string,
resource: string,
resources: string[],
): Promise<AuthenticationResult | null> {
if (!tenantId) throw new Error("tenantId is required");
if (!clientId) throw new Error("clientId is required");
if (!resource) throw new Error("resource is required");
if (!Object.prototype.hasOwnProperty.call(RESOURCE_SCOPE_BY_NAME, resource)) {
throw new Error(`Invalid resource '${resource}'. Allowed: ${DEFAULT_RESOURCES.join(", ")}`);
}
const scope = RESOURCE_SCOPE_BY_NAME[resource as ResourceName];
if (!resources || resources.length === 0) throw new Error("resources are required");
const session = await readSessionState();
if (!session.activeAccountUpn) {
@@ -426,10 +411,13 @@ export async function acquireResourceToken(
throw new Error(LOGIN_REQUIRED_MESSAGE);
}
// Convert short names of scopes to full resource scopes
const scopes = resources.map((res) => RESOURCE_SCOPE_BY_NAME[res as ResourceName] || res);
try {
return await pca.acquireTokenSilent({
account,
scopes: [scope],
scopes,
});
} catch {
throw new Error(LOGIN_REQUIRED_MESSAGE);