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

@@ -1,6 +1,6 @@
{ {
"name": "@slawek/sk-az-tools", "name": "@slawek/sk-az-tools",
"version": "0.5.2", "version": "0.6.0",
"type": "module", "type": "module",
"files": [ "files": [
"dist", "dist",

View File

@@ -1,17 +1,32 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { DefaultAzureCredential, ClientSecretCredential, DeviceCodeCredential } from "@azure/identity"; import {
import type { AuthenticationResult } from "@azure/msal-node"; DefaultAzureCredential,
import { acquireResourceToken as acquireResourceTokenPca } from "./pca-auth.ts"; ClientSecretCredential,
DeviceCodeCredential,
getBearerTokenProvider,
} from "@azure/identity";
import type { TokenCredential } from "@azure/core-auth";
import { SkAzureCredential } from "./sk-credential.ts";
type CredentialType = "d" | "default" | "cs" | "clientSecret" | "dc" | "deviceCode"; import { translateResourceNamesToScopes } from "./index.ts";
export async function getCredential( type CredentialType =
| "d"
| "default"
| "cs"
| "clientSecret"
| "dc"
| "deviceCode"
| "sk"
| "skCredential";
export function getCredential(
credentialType: CredentialType, credentialType: CredentialType,
tenantId?: string, tenantId?: string,
clientId?: string, clientId?: string,
clientSecret?: string, clientSecret?: string,
): Promise<DefaultAzureCredential | ClientSecretCredential | DeviceCodeCredential> { ): TokenCredential {
switch (credentialType) { switch (credentialType) {
case "d": case "d":
case "default": case "default":
@@ -23,11 +38,7 @@ export async function getCredential(
"tenantId, clientId, and clientSecret are required for ClientSecretCredential", "tenantId, clientId, and clientSecret are required for ClientSecretCredential",
); );
} }
return new ClientSecretCredential( return new ClientSecretCredential(tenantId, clientId, clientSecret);
tenantId,
clientId,
clientSecret,
);
case "dc": case "dc":
case "deviceCode": case "deviceCode":
if (!tenantId || !clientId) { if (!tenantId || !clientId) {
@@ -42,15 +53,33 @@ export async function getCredential(
console.log(info.message); console.log(info.message);
}, },
}); });
case "sk":
case "skCredential":
if (!tenantId || !clientId) {
throw new Error(
"tenantId and clientId are required for SkAzureCredential",
);
}
return new SkAzureCredential(tenantId, clientId);
default: default:
throw new Error(`Unsupported credential type: ${credentialType}`); throw new Error(`Unsupported credential type: ${credentialType}`);
} }
} }
export async function acquireResourceToken( export async function getTokenUsingAzureIdentity(
tenantId: string, tenantId: string,
clientId: string, clientId: string,
resource: string, resources: string[],
): Promise<AuthenticationResult | null> { ): Promise<string> {
return acquireResourceTokenPca(tenantId, clientId, resource); const scopes = translateResourceNamesToScopes(resources);
const credential = getCredential("default", tenantId, clientId);
const getBearerToken = getBearerTokenProvider(credential, scopes);
const accessToken = await getBearerToken();
if (!accessToken) {
throw new Error("Failed to acquire access token with Azure Identity.");
}
return accessToken;
} }

View File

@@ -6,8 +6,11 @@
* This module provides authentication functionalities for Azure services. * This module provides authentication functionalities for Azure services.
*/ */
export { getCredential } from "./client-auth.ts"; import { getTokenUsingMsal } from "./pca-auth.ts";
import { acquireResourceToken as acquireResourceTokenPca } from "./pca-auth.ts"; import { getTokenUsingAzureIdentity } from "./client-auth.ts";
import { loadConfig } from "../index.ts";
// Reexporting functions and types from submodules
export { export {
loginInteractive, loginInteractive,
loginDeviceCode, loginDeviceCode,
@@ -16,10 +19,60 @@ export {
parseResources, parseResources,
} from "./pca-auth.ts"; } from "./pca-auth.ts";
export async function acquireResourceToken( export { getCredential } from "./client-auth.ts";
export const RESOURCE_SCOPE_BY_NAME = {
graph: "https://graph.microsoft.com/.default",
devops: "499b84ac-1321-427f-aa17-267ca6975798/.default",
arm: "https://management.azure.com/.default",
openai: "https://cognitiveservices.azure.com/.default",
} as const;
export type ResourceName = keyof typeof RESOURCE_SCOPE_BY_NAME;
export const DEFAULT_RESOURCES: ResourceName[] = ["graph", "devops", "arm"];
// A helper function to translate short resource names to their corresponding scopes
export function translateResourceNamesToScopes(resourceNames: string[]): string[] {
return resourceNames.map((name) => RESOURCE_SCOPE_BY_NAME[name as ResourceName]);
}
// Generic utility functions
export type AuthMode = "azure-identity" | "msal";
export async function getAccessToken(
tenantId: string, tenantId: string,
clientId: string, clientId: string,
resource: string, resources: string[]
) { ): Promise<string> {
return acquireResourceTokenPca(tenantId, clientId, resource); const config = await loadConfig();
if (config.authMode === "msal") {
const result = await getTokenUsingMsal(tenantId, clientId, resources);
if (!result?.accessToken) {
throw new Error("Failed to acquire access token with MSAL.");
}
return result.accessToken;
} else {
return getTokenUsingAzureIdentity(tenantId, clientId, resources);
}
} }
// export function getAzureIdentityGraphAuthProvider(
// tenantId: string,
// clientId: string,
// ) {
// const credential = new DefaultAzureCredential({
// tenantId,
// managedIdentityClientId: clientId,
// });
// const getBearerToken = getBearerTokenProvider(
// credential,
// "https://graph.microsoft.com/.default",
// );
// return (done: (error: Error | null, accessToken: string | null) => void) => {
// void getBearerToken()
// .then((token) => done(null, token))
// .catch((err) => done(err as Error, null));
// };
// }

View File

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

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
import type { AccessToken, GetTokenOptions, TokenCredential } from "@azure/core-auth";
import { getTokenUsingMsal } from "./pca-auth.ts";
export class SkAzureCredential implements TokenCredential {
constructor(
private tenantId: string,
private clientId: string,
) {}
async getToken(
scopes: string | string[],
_options?: GetTokenOptions,
): Promise<AccessToken | null> {
const resources = Array.isArray(scopes) ? scopes : [scopes];
const result = await getTokenUsingMsal(this.tenantId, this.clientId, resources);
if (!result?.accessToken) {
return null;
}
return {
token: result.accessToken,
expiresOnTimestamp: result.expiresOn
? result.expiresOn.getTime()
: Date.now() + 55 * 60 * 1000,
};
}
}

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { acquireResourceToken } from "../../azure/index.ts"; import { getAccessToken } from "../../azure/index.ts";
import { getDevOpsApiToken } from "../../devops/index.ts"; import { getDevOpsApiToken } from "../../devops/index.ts";
import { loadConfig } from "../../index.ts"; import { loadAuthConfig } from "../../index.ts";
import type { CommandValues } from "./types.ts"; import type { CommandValues } from "./types.ts";
@@ -13,22 +13,21 @@ Options:
--type, -t <value> Token type: azurerm|devops`; --type, -t <value> Token type: azurerm|devops`;
} }
export async function runGetTokenCommand(values: CommandValues): Promise<unknown> { export async function runGetTokenCommand(
values: CommandValues,
): Promise<unknown> {
const tokenType = (values.type ?? "").toString().trim().toLowerCase(); const tokenType = (values.type ?? "").toString().trim().toLowerCase();
if (!tokenType) { if (!tokenType) {
throw new Error("--type is required for get-token (allowed: azurerm, devops)"); throw new Error(
"--type is required for get-token (allowed: azurerm, devops)",
);
} }
const config = await loadConfig("public-config"); const config = await loadAuthConfig("public-config");
if (tokenType === "azurerm") { if (tokenType === "azurerm") {
const result = await acquireResourceToken( const accessToken = await getAccessToken(config.tenantId, config.clientId, ["arm"]);
config.tenantId,
config.clientId,
"arm",
);
const accessToken = result?.accessToken;
if (!accessToken) { if (!accessToken) {
throw new Error("Failed to obtain AzureRM token"); throw new Error("Failed to obtain AzureRM token");
} }
@@ -40,7 +39,10 @@ export async function runGetTokenCommand(values: CommandValues): Promise<unknown
} }
if (tokenType === "devops") { if (tokenType === "devops") {
const accessToken = await getDevOpsApiToken(config.tenantId, config.clientId); const accessToken = await getDevOpsApiToken(
config.tenantId,
config.clientId,
);
return { return {
tokenType, tokenType,
accessToken, accessToken,

View File

@@ -1,8 +1,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { listAppGrants } from "../../graph/app.ts"; import { listAppGrants } from "../../graph/app.ts";
import { getGraphClient } from "../../graph/index.ts";
import { getGraphClientFromPublicConfig } from "./shared.ts";
import type { CommandValues } from "./types.ts"; import type { CommandValues } from "./types.ts";
export function usageListAppGrants(): string { export function usageListAppGrants(): string {
@@ -17,6 +16,6 @@ export async function runListAppGrantsCommand(values: CommandValues): Promise<un
throw new Error("--app-id is required for list-app-grants"); throw new Error("--app-id is required for list-app-grants");
} }
const { client } = await getGraphClientFromPublicConfig(); const client = await getGraphClient();
return listAppGrants(client, values["app-id"]); return listAppGrants(client, values["app-id"]);
} }

View File

@@ -2,7 +2,8 @@
import { listAppPermissions, listAppPermissionsResolved } from "../../graph/app.ts"; import { listAppPermissions, listAppPermissionsResolved } from "../../graph/app.ts";
import { filterByPermissionName, getGraphClientFromPublicConfig } from "./shared.ts"; import { filterByPermissionName } from "./shared.ts";
import { getGraphClient } from "../../graph/index.ts";
import type { CommandValues } from "./types.ts"; import type { CommandValues } from "./types.ts";
function isRecord(value: unknown): value is Record<string, unknown> { function isRecord(value: unknown): value is Record<string, unknown> {
@@ -37,7 +38,7 @@ export async function runListAppPermissionsCommand(values: CommandValues): Promi
throw new Error("--app-id is required for list-app-permissions"); throw new Error("--app-id is required for list-app-permissions");
} }
const { client } = await getGraphClientFromPublicConfig(); const client = await getGraphClient();
let result: unknown = values.resolve || values.filter let result: unknown = values.resolve || values.filter
? await listAppPermissionsResolved(client, values["app-id"]) ? await listAppPermissionsResolved(client, values["app-id"])
: await listAppPermissions(client, values["app-id"]); : await listAppPermissions(client, values["app-id"]);

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { listApps } from "../../graph/app.ts"; import { listApps } from "../../graph/app.ts";
import { filterByDisplayName } from "./shared.ts";
import { filterByDisplayName, getGraphClientFromPublicConfig } from "./shared.ts"; import { getGraphClient } from "../../graph/index.ts";
import type { CommandValues } from "./types.ts"; import type { CommandValues } from "./types.ts";
export function usageListApps(): string { export function usageListApps(): string {
@@ -15,7 +15,8 @@ Options:
} }
export async function runListAppsCommand(values: CommandValues): Promise<unknown> { export async function runListAppsCommand(values: CommandValues): Promise<unknown> {
const { client } = await getGraphClientFromPublicConfig(); const client = await getGraphClient();
let result = await listApps(client, values["display-name"], values["app-id"]); let result = await listApps(client, values["display-name"], values["app-id"]);
if (values["app-id"] && result.length > 1) { if (values["app-id"] && result.length > 1) {
throw new Error(`Expected a single app for --app-id ${values["app-id"]}, but got ${result.length}`); throw new Error(`Expected a single app for --app-id ${values["app-id"]}, but got ${result.length}`);

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { listResourcePermissions } from "../../graph/app.ts"; import { listResourcePermissions } from "../../graph/app.ts";
import { getGraphClient } from "../../graph/index.ts";
import { filterByPermissionName, getGraphClientFromPublicConfig } from "./shared.ts"; import { filterByPermissionName } from "./shared.ts";
import type { CommandValues } from "./types.ts"; import type { CommandValues } from "./types.ts";
export function usageListResourcePermissions(): string { export function usageListResourcePermissions(): string {
@@ -22,7 +22,7 @@ export async function runListResourcePermissionsCommand(values: CommandValues):
throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both"); throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both");
} }
const { client } = await getGraphClientFromPublicConfig(); const client = await getGraphClient();
let result = await listResourcePermissions( let result = await listResourcePermissions(
client, client,
values["app-id"], values["app-id"],

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { login } from "../../azure/index.ts"; import { login } from "../../azure/index.ts";
import { loadConfig } from "../../index.ts"; import { loadAuthConfig } from "../../index.ts";
import type { CommandValues } from "./types.ts"; import type { CommandValues } from "./types.ts";
@@ -17,7 +17,7 @@ Options:
} }
export async function runLoginCommand(values: CommandValues): Promise<unknown> { export async function runLoginCommand(values: CommandValues): Promise<unknown> {
const config = await loadConfig("public-config"); const config = await loadAuthConfig("public-config");
return login( return login(
config.tenantId, config.tenantId,
config.clientId, config.clientId,

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { logout } from "../../azure/index.ts"; import { logout } from "../../azure/index.ts";
import { loadConfig } from "../../index.ts"; import { loadAuthConfig } from "../../index.ts";
import type { CommandValues } from "./types.ts"; import type { CommandValues } from "./types.ts";
@@ -13,6 +13,6 @@ Options:
} }
export async function runLogoutCommand(values: CommandValues): Promise<unknown> { export async function runLogoutCommand(values: CommandValues): Promise<unknown> {
const config = await loadConfig("public-config"); const config = await loadAuthConfig("public-config");
return logout(config.tenantId, config.clientId, Boolean(values.all)); return logout(config.tenantId, config.clientId, Boolean(values.all));
} }

View File

@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { acquireResourceToken } from "../../azure/index.ts"; import { getAccessToken } from "../../azure/index.ts";
import { getDevOpsApiToken } from "../../devops/index.ts"; import { getDevOpsApiToken } from "../../devops/index.ts";
import { loadConfig } from "../../index.ts"; import { loadAuthConfig } from "../../index.ts";
import type { CommandValues } from "./types.ts"; import type { CommandValues } from "./types.ts";
@@ -19,7 +19,9 @@ Authorization is added automatically for:
dev.azure.com Uses devops token`; dev.azure.com Uses devops token`;
} }
function parseHeaderLine(header?: string): { name: string; value: string } | null { function parseHeaderLine(
header?: string,
): { name: string; value: string } | null {
if (!header || header.trim() === "") { if (!header || header.trim() === "") {
return null; return null;
} }
@@ -54,15 +56,10 @@ async function getAutoAuthorizationHeader(url: URL): Promise<string | null> {
return null; return null;
} }
const config = await loadConfig("public-config"); const config = await loadAuthConfig("public-config");
if (host === "management.azure.com") { if (host === "management.azure.com") {
const result = await acquireResourceToken( const accessToken = await getAccessToken(config.tenantId, config.clientId, ["arm"]);
config.tenantId,
config.clientId,
"arm",
);
const accessToken = result?.accessToken;
if (!accessToken) { if (!accessToken) {
throw new Error("Failed to obtain AzureRM token"); throw new Error("Failed to obtain AzureRM token");
} }
@@ -74,7 +71,8 @@ async function getAutoAuthorizationHeader(url: URL): Promise<string | null> {
} }
export async function runRestCommand(values: CommandValues): Promise<unknown> { export async function runRestCommand(values: CommandValues): Promise<unknown> {
const method = (values.method ?? "GET").toString().trim().toUpperCase() || "GET"; const method =
(values.method ?? "GET").toString().trim().toUpperCase() || "GET";
const urlValue = (values.url ?? "").toString().trim(); const urlValue = (values.url ?? "").toString().trim();
if (!urlValue) { if (!urlValue) {

View File

@@ -1,24 +1,54 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { Client } from "@microsoft/microsoft-graph-client"; import { Client } from "@microsoft/microsoft-graph-client";
import { acquireResourceToken } from "../azure/index.ts"; import { getAccessToken } from "../azure/index.ts";
import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity";
type GraphApiToken = { // export async function getGraphClientUsingMsal(
accessToken: string; // tenantId: string,
[key: string]: unknown; // clientId: string,
}; // ): Promise<Client> {
// const graphApiToken = await getAccessToken(tenantId, clientId, ["graph"]);
export async function getGraphClient( // return Client.init({
// authProvider: (done) => {
// done(null, graphApiToken);
// },
// });
// }
type GraphAuthProvider = (
done: (error: Error | null, accessToken: string | null) => void
) => void;
export function getMsalAuthProvider(
tenantId: string, tenantId: string,
clientId: string, clientId: string,
): Promise<{ graphApiToken: GraphApiToken; client: any }> { ): GraphAuthProvider {
const graphApiToken = await acquireResourceToken(tenantId, clientId, "graph") as GraphApiToken; return (done) => {
void getAccessToken(tenantId, clientId, ["graph"])
.then((accessToken) => done(null, accessToken))
.catch((err) => done(err as Error, null));
};
}
const client = Client.init({ export function getAzureIdentityAuthProvider(
authProvider: (done) => { tenantId: string,
done(null, graphApiToken.accessToken); clientId: string,
}, ) {
const credential = new DefaultAzureCredential({
tenantId,
managedIdentityClientId: clientId,
}); });
return { graphApiToken, client }; const getBearerToken = getBearerTokenProvider(
credential,
"https://graph.microsoft.com/.default",
);
return (done: (error: Error | null, accessToken: string | null) => void) => {
void getBearerToken()
.then((token) => done(null, token))
.catch((err) => done(err as Error, null));
};
} }

View File

@@ -3,3 +3,22 @@
export * from "./auth.ts"; export * from "./auth.ts";
export * from "./app.ts"; export * from "./app.ts";
export * from "./sp.ts"; export * from "./sp.ts";
import { loadAuthConfig, loadConfig } from "../index.ts";
import { Client } from "@microsoft/microsoft-graph-client";
import { getMsalAuthProvider, getAzureIdentityAuthProvider } from "./auth.ts";
export async function getGraphClient(): Promise<Client> {
const config = await loadConfig();
const authConfig = await loadAuthConfig("public-config");
const authProvider =
config.authMode === "azure-identity"
? getAzureIdentityAuthProvider(authConfig.tenantId, authConfig.clientId)
: getMsalAuthProvider(authConfig.tenantId, authConfig.clientId);
return Client.init({
authProvider: authProvider,
});
}

View File

@@ -2,14 +2,10 @@
import { validate as validateUuid } from "uuid"; import { validate as validateUuid } from "uuid";
import { getConfig } from "@slawek/sk-tools"; import { getConfig } from "@slawek/sk-tools";
import type { AuthConfig, Config } from "./types.ts";
type Config = { export async function loadAuthConfig(configName: string): Promise<AuthConfig> {
tenantId: string; if (configName.trim() === "") {
clientId: string;
};
export async function loadConfig(configName: string): Promise<Config> {
if (typeof configName !== "string" || configName.trim() === "") {
throw new Error( throw new Error(
'Invalid config name. Expected a non-empty string like "public-config" or "confidential-config".', 'Invalid config name. Expected a non-empty string like "public-config" or "confidential-config".',
); );
@@ -34,3 +30,13 @@ export async function loadConfig(configName: string): Promise<Config> {
clientId, clientId,
}; };
} }
export async function loadConfig(): Promise<Config> {
const json = (await getConfig("sk-az-tools", "config")) as Record<string, unknown>;
return {
activeAccountUpn: typeof json.activeAccountUpn === "string" ? json.activeAccountUpn : undefined,
authMode: typeof json.authMode === "string" ? json.authMode : "msal"
};
}

11
src/types.ts Normal file
View File

@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT
export type AuthConfig = {
tenantId: string;
clientId: string;
};
export type Config = {
activeAccountUpn: string | undefined;
authMode: string;
};