Reorganized the module structure.
This commit is contained in:
@@ -14,8 +14,8 @@
|
|||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.js",
|
".": "./src/index.js",
|
||||||
"./azure": "./src/azure.js",
|
"./azure": "./src/azure/index.js",
|
||||||
"./graph": "./src/graph.js",
|
"./graph": "./src/graph/index.js",
|
||||||
"./devops": "./src/devops.js"
|
"./devops": "./src/devops/index.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/azure/client-auth.js
Normal file
37
src/azure/client-auth.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { DefaultAzureCredential, ClientSecretCredential, DeviceCodeCredential } from "@azure/identity";
|
||||||
|
|
||||||
|
export async function getCredential(credentialType, options) {
|
||||||
|
switch (credentialType) {
|
||||||
|
case "d":
|
||||||
|
case "default":
|
||||||
|
return new DefaultAzureCredential();
|
||||||
|
case "cs":
|
||||||
|
case "clientSecret":
|
||||||
|
if (!options.tenantId || !options.clientId || !options.clientSecret) {
|
||||||
|
throw new Error(
|
||||||
|
"tenantId, clientId, and clientSecret are required for ClientSecretCredential",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new ClientSecretCredential(
|
||||||
|
options.tenantId,
|
||||||
|
options.clientId,
|
||||||
|
options.clientSecret,
|
||||||
|
);
|
||||||
|
case "dc":
|
||||||
|
case "deviceCode":
|
||||||
|
if (!options.tenantId || !options.clientId) {
|
||||||
|
throw new Error(
|
||||||
|
"tenantId and clientId are required for DeviceCodeCredential",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new DeviceCodeCredential({
|
||||||
|
tenantId: options.tenantId,
|
||||||
|
clientId: options.clientId,
|
||||||
|
userPromptCallback: (info) => {
|
||||||
|
console.log(info.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported credential type: ${credentialType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/azure.d.ts → src/azure/index.d.ts
vendored
0
src/azure.d.ts → src/azure/index.d.ts
vendored
9
src/azure/index.js
Normal file
9
src/azure/index.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* @module azure
|
||||||
|
*
|
||||||
|
* This module provides authentication functionalities for Azure services.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { getCredential } from "./client-auth.js";
|
||||||
|
export { loginInteractive } from "./pca-auth.js";
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// azure.js
|
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import { URL } from "node:url";
|
import { URL } from "node:url";
|
||||||
import open, { apps } from "open";
|
import open, { apps } from "open";
|
||||||
@@ -6,51 +6,8 @@ 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 {
|
|
||||||
DefaultAzureCredential,
|
|
||||||
ClientSecretCredential,
|
|
||||||
DeviceCodeCredential,
|
|
||||||
} from "@azure/identity";
|
|
||||||
|
|
||||||
export async function getCredential(credentialType, options) {
|
import { PublicClientApplication } from "@azure/msal-node";
|
||||||
switch (credentialType) {
|
|
||||||
case "d":
|
|
||||||
case "default":
|
|
||||||
return new DefaultAzureCredential();
|
|
||||||
case "cs":
|
|
||||||
case "clientSecret":
|
|
||||||
if (!options.tenantId || !options.clientId || !options.clientSecret) {
|
|
||||||
throw new Error(
|
|
||||||
"tenantId, clientId, and clientSecret are required for ClientSecretCredential",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new ClientSecretCredential(
|
|
||||||
options.tenantId,
|
|
||||||
options.clientId,
|
|
||||||
options.clientSecret,
|
|
||||||
);
|
|
||||||
case "dc":
|
|
||||||
case "deviceCode":
|
|
||||||
if (!options.tenantId || !options.clientId) {
|
|
||||||
throw new Error(
|
|
||||||
"tenantId and clientId are required for DeviceCodeCredential",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return new DeviceCodeCredential({
|
|
||||||
tenantId: options.tenantId,
|
|
||||||
clientId: options.clientId,
|
|
||||||
userPromptCallback: (info) => {
|
|
||||||
console.log(info.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported credential type: ${credentialType}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileCachePlugin(cachePath) {
|
function fileCachePlugin(cachePath) {
|
||||||
return {
|
return {
|
||||||
@@ -63,6 +20,7 @@ function fileCachePlugin(cachePath) {
|
|||||||
if (ctx.cacheHasChanged) {
|
if (ctx.cacheHasChanged) {
|
||||||
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
||||||
fs.writeFileSync(cachePath, ctx.tokenCache.serialize());
|
fs.writeFileSync(cachePath, ctx.tokenCache.serialize());
|
||||||
|
fs.chmodSync(cachePath, 0o600); // Owner read/write only
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -78,21 +36,15 @@ function generatePkce() {
|
|||||||
return { verifier, challenge, challengeMethod: "S256" };
|
return { verifier, challenge, challengeMethod: "S256" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loginInteractive({ appName, tenantId, clientId, scopes }) {
|
export async function loginInteractive({ 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/${sanitizedAppName}`,
|
`.config/sk-az-tools`,
|
||||||
`${clientId}-token-cache.json`,
|
`${clientId}-token-cache.json`,
|
||||||
);
|
);
|
||||||
|
|
||||||
0
src/devops.d.ts → src/devops/index.d.ts
vendored
0
src/devops.d.ts → src/devops/index.d.ts
vendored
@@ -2,7 +2,7 @@
|
|||||||
* A DevOps helpers module.
|
* A DevOps helpers module.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { loginInteractive } from "./azure.js";
|
import { loginInteractive } from "../azure/index.js";
|
||||||
import * as azdev from "azure-devops-node-api";
|
import * as azdev from "azure-devops-node-api";
|
||||||
|
|
||||||
const AZURE_DEVOPS_SCOPES = ["https://app.vssps.visualstudio.com/.default"];
|
const AZURE_DEVOPS_SCOPES = ["https://app.vssps.visualstudio.com/.default"];
|
||||||
87
src/graph.js
87
src/graph.js
@@ -1,87 +0,0 @@
|
|||||||
import { Client } from "@microsoft/microsoft-graph-client";
|
|
||||||
import { loginInteractive } from "./azure.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize and return a Microsoft Graph client
|
|
||||||
* along with the authentication token.
|
|
||||||
*
|
|
||||||
* @param { Object } options - Options for authentication
|
|
||||||
* @param { string } options.tenantId - The Azure AD tenant ID
|
|
||||||
* @param { string } options.clientId - The Azure AD client ID
|
|
||||||
* @returns { Object } An object containing the Graph API token and client
|
|
||||||
*/
|
|
||||||
export async function getGraphClient({ tenantId, clientId }) {
|
|
||||||
const graphApiToken = await loginInteractive({
|
|
||||||
tenantId,
|
|
||||||
clientId,
|
|
||||||
scopes: ["https://graph.microsoft.com/.default"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = Client.init({
|
|
||||||
authProvider: (done) => {
|
|
||||||
done(null, graphApiToken.accessToken);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return { graphApiToken, client };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an Azure application by its display name.
|
|
||||||
*
|
|
||||||
* @param { Object } client
|
|
||||||
* @param { string } appName
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export async function getApp(client, appName) {
|
|
||||||
const result = await client
|
|
||||||
.api("/applications")
|
|
||||||
.filter(`displayName eq '${appName}'`)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
// Return the first application found or null if none exists
|
|
||||||
return result.value.length > 0 ? result.value[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getServicePrincipal(client, appId) {
|
|
||||||
const result = await client
|
|
||||||
.api("/servicePrincipals")
|
|
||||||
.filter(`appId eq '${appId}'`)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
// Return the first service principal found or null if none exists
|
|
||||||
return result.value.length > 0 ? result.value[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createApp(client, appName) {
|
|
||||||
const app = await client.api("/applications").post({
|
|
||||||
displayName: appName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!app || !app.appId) {
|
|
||||||
throw new Error("Failed to create application");
|
|
||||||
}
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createSp(client, appId) {
|
|
||||||
const sp = await client.api("/servicePrincipals").post({
|
|
||||||
appId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sp || !sp.id) {
|
|
||||||
throw new Error("Failed to create service principal");
|
|
||||||
}
|
|
||||||
|
|
||||||
return sp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteSp(client, spId) {
|
|
||||||
await client.api(`/servicePrincipals/${spId}`).delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteApp(client, appObjectId) {
|
|
||||||
await client.api(`/applications/${appObjectId}`).delete();
|
|
||||||
}
|
|
||||||
32
src/graph/app.js
Normal file
32
src/graph/app.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Get an Azure application by its display name.
|
||||||
|
*
|
||||||
|
* @param { Object } client
|
||||||
|
* @param { string } appName
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getApp(client, appName) {
|
||||||
|
const result = await client
|
||||||
|
.api("/applications")
|
||||||
|
.filter(`displayName eq '${appName}'`)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Return the first application found or null if none exists
|
||||||
|
return result.value.length > 0 ? result.value[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createApp(client, appName) {
|
||||||
|
const app = await client.api("/applications").post({
|
||||||
|
displayName: appName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app || !app.appId) {
|
||||||
|
throw new Error("Failed to create application");
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteApp(client, appObjectId) {
|
||||||
|
await client.api(`/applications/${appObjectId}`).delete();
|
||||||
|
}
|
||||||
27
src/graph/auth.js
Normal file
27
src/graph/auth.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { loginInteractive } from "../azure/index.js";
|
||||||
|
import { Client } from "@microsoft/microsoft-graph-client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize and return a Microsoft Graph client
|
||||||
|
* along with the authentication token.
|
||||||
|
*
|
||||||
|
* @param { Object } options - Options for authentication
|
||||||
|
* @param { string } options.tenantId - The Azure AD tenant ID
|
||||||
|
* @param { string } options.clientId - The Azure AD client ID
|
||||||
|
* @returns { Object } An object containing the Graph API token and client
|
||||||
|
*/
|
||||||
|
export async function getGraphClient({ tenantId, clientId }) {
|
||||||
|
const graphApiToken = await loginInteractive({
|
||||||
|
tenantId,
|
||||||
|
clientId,
|
||||||
|
scopes: ["https://graph.microsoft.com/.default"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = Client.init({
|
||||||
|
authProvider: (done) => {
|
||||||
|
done(null, graphApiToken.accessToken);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { graphApiToken, client };
|
||||||
|
}
|
||||||
0
src/graph.d.ts → src/graph/index.d.ts
vendored
0
src/graph.d.ts → src/graph/index.d.ts
vendored
3
src/graph/index.js
Normal file
3
src/graph/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./auth.js";
|
||||||
|
export * from "./app.js";
|
||||||
|
export * from "./sp.js";
|
||||||
25
src/graph/sp.js
Normal file
25
src/graph/sp.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export async function getServicePrincipal(client, appId) {
|
||||||
|
const result = await client
|
||||||
|
.api("/servicePrincipals")
|
||||||
|
.filter(`appId eq '${appId}'`)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Return the first service principal found or null if none exists
|
||||||
|
return result.value.length > 0 ? result.value[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSp(client, appId) {
|
||||||
|
const sp = await client.api("/servicePrincipals").post({
|
||||||
|
appId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sp || !sp.id) {
|
||||||
|
throw new Error("Failed to create service principal");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteSp(client, spId) {
|
||||||
|
await client.api(`/servicePrincipals/${spId}`).delete();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user