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
Some checks failed
build / build (push) Failing after 14s
This commit is contained in:
@@ -1,17 +1,32 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { DefaultAzureCredential, ClientSecretCredential, DeviceCodeCredential } from "@azure/identity";
|
||||
import type { AuthenticationResult } from "@azure/msal-node";
|
||||
import { acquireResourceToken as acquireResourceTokenPca } from "./pca-auth.ts";
|
||||
import {
|
||||
DefaultAzureCredential,
|
||||
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,
|
||||
tenantId?: string,
|
||||
clientId?: string,
|
||||
clientSecret?: string,
|
||||
): Promise<DefaultAzureCredential | ClientSecretCredential | DeviceCodeCredential> {
|
||||
): TokenCredential {
|
||||
switch (credentialType) {
|
||||
case "d":
|
||||
case "default":
|
||||
@@ -23,11 +38,7 @@ export async function getCredential(
|
||||
"tenantId, clientId, and clientSecret are required for ClientSecretCredential",
|
||||
);
|
||||
}
|
||||
return new ClientSecretCredential(
|
||||
tenantId,
|
||||
clientId,
|
||||
clientSecret,
|
||||
);
|
||||
return new ClientSecretCredential(tenantId, clientId, clientSecret);
|
||||
case "dc":
|
||||
case "deviceCode":
|
||||
if (!tenantId || !clientId) {
|
||||
@@ -42,15 +53,33 @@ export async function getCredential(
|
||||
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:
|
||||
throw new Error(`Unsupported credential type: ${credentialType}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function acquireResourceToken(
|
||||
export async function getTokenUsingAzureIdentity(
|
||||
tenantId: string,
|
||||
clientId: string,
|
||||
resource: string,
|
||||
): Promise<AuthenticationResult | null> {
|
||||
return acquireResourceTokenPca(tenantId, clientId, resource);
|
||||
resources: string[],
|
||||
): Promise<string> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
* This module provides authentication functionalities for Azure services.
|
||||
*/
|
||||
|
||||
export { getCredential } from "./client-auth.ts";
|
||||
import { acquireResourceToken as acquireResourceTokenPca } from "./pca-auth.ts";
|
||||
import { getTokenUsingMsal } from "./pca-auth.ts";
|
||||
import { getTokenUsingAzureIdentity } from "./client-auth.ts";
|
||||
import { loadConfig } from "../index.ts";
|
||||
|
||||
// Reexporting functions and types from submodules
|
||||
export {
|
||||
loginInteractive,
|
||||
loginDeviceCode,
|
||||
@@ -16,10 +19,60 @@ export {
|
||||
parseResources,
|
||||
} 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,
|
||||
clientId: string,
|
||||
resource: string,
|
||||
) {
|
||||
return acquireResourceTokenPca(tenantId, clientId, resource);
|
||||
resources: string[]
|
||||
): Promise<string> {
|
||||
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));
|
||||
// };
|
||||
// }
|
||||
@@ -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);
|
||||
|
||||
29
src/azure/sk-credential.ts
Normal file
29
src/azure/sk-credential.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user