Compare commits
8 Commits
8acd5640d3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9137688a55 | |||
| a805ed415f | |||
| 1a70f7efcf | |||
| 2645d6c1f4 | |||
| a8725a7c22 | |||
| 0806a2b588 | |||
| 0cde901ec2 | |||
| ed5253b1a1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
# Ignore node modules and config files
|
# Ignore node modules and config files
|
||||||
node_modules
|
node_modules
|
||||||
*config.js
|
*config.js
|
||||||
|
*config.json
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# MacOS system files
|
# MacOS system files
|
||||||
|
|||||||
20
README.md
Normal file
20
README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Azure Node Playground
|
||||||
|
|
||||||
|
This repository contains sample code for authenticating and interacting with Azure services using Node.js. It demonstrates how to use the Microsoft Authentication Library (MSAL) to acquire tokens and access various Azure resources.
|
||||||
|
|
||||||
|
## How to impersonate Azure CLI
|
||||||
|
|
||||||
|
Azure CLI is a well-known public application that can be used to authenticate and access Azure resources. To impersonate Azure CLI in your Node.js application, you can use the following configuration:
|
||||||
|
|
||||||
|
Use interactive login or device code flow to authenticate as Azure CLI. Use the following configuration parameters:
|
||||||
|
|
||||||
|
- **Application (client) ID**: `04b07795-8ddb-461a-bbee-02f9e1bf7b46`
|
||||||
|
- **Tenant ID**: your tenant ID
|
||||||
|
|
||||||
|
You can verify the application ID of Azure CLI by running the following command in your terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
az account get-access-token --scope "https://management.azure.com/.default" --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
and then look for the `client_id` field in the debug output. Unfortunately, you can't use `az ad app show` to get details about this public application.
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { ClientSecretCredential } from "@azure/identity";
|
|
||||||
import { config } from "../config.js";
|
|
||||||
import { createHash } from "crypto";
|
|
||||||
|
|
||||||
// We need to wrap the async code in an IIFE
|
|
||||||
// Check, authentication using @azure/identity requires a client secret.
|
|
||||||
if (config.clientSecret) {
|
|
||||||
console.log("Client secret is set.");
|
|
||||||
// Create the client
|
|
||||||
const credential = new ClientSecretCredential(
|
|
||||||
config.tenantId,
|
|
||||||
config.clientId,
|
|
||||||
config.clientSecret,
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = await credential.getToken(
|
|
||||||
"https://management.azure.com/.default",
|
|
||||||
);
|
|
||||||
if (token) {
|
|
||||||
console.log("Authentication with client secret successful.");
|
|
||||||
const hash = createHash("sha256").update(token.token).digest("hex");
|
|
||||||
console.log("SHA-256 hash of access token:", hash);
|
|
||||||
console.log("Token expires on:", new Date(token.expiresOnTimestamp).toISOString());
|
|
||||||
} else {
|
|
||||||
console.error("Authentication with client secret failed.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
"Warning: No client secret generated. Authentication may fail if the application requires a client secret.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
60
bin/create-app-and-sp.mjs
Normal file
60
bin/create-app-and-sp.mjs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { config } from "../public-config.js";
|
||||||
|
import {
|
||||||
|
createApp,
|
||||||
|
createSp,
|
||||||
|
getApp,
|
||||||
|
getGraphClient,
|
||||||
|
getServicePrincipal,
|
||||||
|
} from "../src/graph.js";
|
||||||
|
import { parseArgs } from "node:util";
|
||||||
|
|
||||||
|
async function usage() {
|
||||||
|
console.log("Usage: create-app-and-sp.mjs --app-name <name>");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { client } = await getGraphClient({
|
||||||
|
tenantId: config.tenantId,
|
||||||
|
clientId: config.clientId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = parseArgs({
|
||||||
|
options: {
|
||||||
|
"app-name": {
|
||||||
|
type: "string",
|
||||||
|
short: "n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!args.values["app-name"]) {
|
||||||
|
await usage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Will create app with name:", args.values["app-name"]);
|
||||||
|
|
||||||
|
let app = await getApp(client, args.values["app-name"]);
|
||||||
|
if (!app) {
|
||||||
|
app = await createApp(client, args.values["app-name"]);
|
||||||
|
console.log("Created app:", app.appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sp = await getServicePrincipal(client, app.appId);
|
||||||
|
if (!sp) {
|
||||||
|
sp = await createSp(client, app.appId);
|
||||||
|
console.log("Created service principal:", sp.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`The application and associated service principal are ready.
|
||||||
|
App ID: ${app.appId}
|
||||||
|
Service Principal ID: ${sp.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main().catch((e) => {
|
||||||
|
console.error("Error in main:", e);
|
||||||
|
console.error(e.stack);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
65
bin/delete-app-and-sp.mjs
Normal file
65
bin/delete-app-and-sp.mjs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { config } from "../public-config.js";
|
||||||
|
import {
|
||||||
|
deleteApp,
|
||||||
|
deleteSp,
|
||||||
|
getApp,
|
||||||
|
getGraphClient,
|
||||||
|
getServicePrincipal,
|
||||||
|
} from "../src/graph.js";
|
||||||
|
import { parseArgs } from "node:util";
|
||||||
|
|
||||||
|
async function usage() {
|
||||||
|
console.log("Usage: delete-app-and-sp.mjs --app-name <name>");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { client } = await getGraphClient({
|
||||||
|
tenantId: config.tenantId,
|
||||||
|
clientId: config.clientId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = parseArgs({
|
||||||
|
options: {
|
||||||
|
"app-name": {
|
||||||
|
type: "string",
|
||||||
|
short: "n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!args.values["app-name"]) {
|
||||||
|
await usage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Will delete app with name:", args.values["app-name"]);
|
||||||
|
|
||||||
|
const app = await getApp(client, args.values["app-name"]);
|
||||||
|
if (!app) {
|
||||||
|
console.log("No app found with name:", args.values["app-name"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sp = await getServicePrincipal(client, app.appId);
|
||||||
|
if (sp && sp.id) {
|
||||||
|
await deleteSp(client, sp.id);
|
||||||
|
console.log("Deleted service principal:", sp.id);
|
||||||
|
} else {
|
||||||
|
console.log("No service principal found for appId:", app.appId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app && app.id) {
|
||||||
|
await deleteApp(client, app.id);
|
||||||
|
console.log("Deleted app:", app.appId);
|
||||||
|
} else {
|
||||||
|
console.log("App object id missing; cannot delete application");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main().catch((e) => {
|
||||||
|
console.error("Error in main:", e);
|
||||||
|
console.error(e.stack);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -3,8 +3,10 @@
|
|||||||
import { loginInteractive } from "../src/azure.js";
|
import { loginInteractive } from "../src/azure.js";
|
||||||
import { config } from "../public-config.js";
|
import { config } from "../public-config.js";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
|
import { Client } from "@microsoft/microsoft-graph-client";
|
||||||
|
|
||||||
const scopes = ["https://management.azure.com/.default"];
|
// const scopes = ["https://management.azure.com/.default"];
|
||||||
|
const scopes = ["https://graph.microsoft.com/.default"];
|
||||||
|
|
||||||
let token;
|
let token;
|
||||||
|
|
||||||
@@ -15,9 +17,9 @@ try {
|
|||||||
scopes,
|
scopes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Login failed:", e);
|
console.error("Login failed:", e);
|
||||||
console.error(e.stack);
|
console.error(e.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Access token acquired.");
|
console.log("Access token acquired.");
|
||||||
@@ -26,3 +28,30 @@ console.log("Access token acquired.");
|
|||||||
const hash = createHash("sha256").update(token.accessToken).digest("hex");
|
const hash = createHash("sha256").update(token.accessToken).digest("hex");
|
||||||
console.log("SHA-256 hash of access token:", hash);
|
console.log("SHA-256 hash of access token:", hash);
|
||||||
console.log("Token expires on:", token.expiresOn);
|
console.log("Token expires on:", token.expiresOn);
|
||||||
|
|
||||||
|
const client = Client.init({
|
||||||
|
authProvider: (done) => {
|
||||||
|
done(null, token.accessToken);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
result = await client
|
||||||
|
.api("/applications")
|
||||||
|
.filter("displayName eq 'Azure Node Playground Public'")
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const apps = result.value ?? [];
|
||||||
|
console.log(
|
||||||
|
`Registered applications with the name 'Azure Node Playground' (${apps.length}):`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (apps.length !== 1) {
|
||||||
|
console.error(
|
||||||
|
"Expected exactly one application with the name 'Azure Node Playground'. Please ensure it is registered in your Azure AD tenant.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Application details:", apps[0]);
|
||||||
|
|||||||
67
bin/login.mjs
Normal file
67
bin/login.mjs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { parseArgs } from "util";
|
||||||
|
import { getCredential } from "../src/azure.js";
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Parse command line arguments to determine which credential type to use
|
||||||
|
const args = parseArgs({
|
||||||
|
options: {
|
||||||
|
help: { type: "boolean", short: "h" },
|
||||||
|
"credential-type": { type: "string", short: "t", default: "default" },
|
||||||
|
"config": { type: "string", short: "c", default: "app-config.json" },
|
||||||
|
"tenant-id": { type: "string", default: process.env.AZURE_TENANT_ID || "" },
|
||||||
|
"client-id": { type: "string", default: process.env.AZURE_CLIENT_ID || "" },
|
||||||
|
"client-secret": { type: "string", default: process.env.AZURE_CLIENT_SECRET || "" },
|
||||||
|
"client-secret-file": { type: "string", default: process.env.AZURE_CLIENT_SECRET_FILE || "" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (args.values.help) {
|
||||||
|
console.log("Usage: login.mjs [options]");
|
||||||
|
console.log("Options:");
|
||||||
|
console.log(" -h, --help Show this help message");
|
||||||
|
console.log(" -c, --config Path to the configuration file (default: config.js)");
|
||||||
|
console.log(" --tenant-id Azure Tenant ID");
|
||||||
|
console.log(" --client-id Azure Client ID");
|
||||||
|
console.log(" --client-secret Azure Client Secret");
|
||||||
|
console.log(" --client-secret-file Path to file containing Azure Client Secret");
|
||||||
|
console.log(" -t, --credential-type Specify the credential type to use (default: default)");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, check if configuration file is spefiedd
|
||||||
|
const configPath = args.values.config;
|
||||||
|
if (configPath) {
|
||||||
|
// Load the JSON configuration file using readFileSync
|
||||||
|
const configFile = readFileSync(configPath, "utf-8");
|
||||||
|
config = JSON.parse(configFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process command line overrides
|
||||||
|
if (args.values["tenant-id"]) {
|
||||||
|
config.tenantId = args.values["tenant-id"];
|
||||||
|
}
|
||||||
|
if (args.values["client-id"]) {
|
||||||
|
config.clientId = args.values["client-id"];
|
||||||
|
}
|
||||||
|
if (args.values["client-secret"]) {
|
||||||
|
config.clientSecret = args.values["client-secret"];
|
||||||
|
} else if (args.values["client-secret-file"]) {
|
||||||
|
// Read client secret from file
|
||||||
|
config.clientSecret = readFileSync(args.values["client-secret-file"], "utf-8").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the appropriate credential based on the specified type
|
||||||
|
const credential = await getCredential(args.values["credential-type"], config);
|
||||||
|
|
||||||
|
console.log("Successfully obtained credential:", credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("An error occurred:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
import open from "open";
|
import open, { apps } from "open";
|
||||||
|
|
||||||
async function openBrowser(url) {
|
async function openBrowser(url) {
|
||||||
try {
|
try {
|
||||||
await open(url);
|
await open(url, {
|
||||||
|
wait: false,
|
||||||
|
app: {
|
||||||
|
name: apps.edge
|
||||||
|
}
|
||||||
|
});
|
||||||
console.log(`Browser opened to ${url}`);
|
console.log(`Browser opened to ${url}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to open browser: ${error}`);
|
console.error(`Failed to open browser: ${error}`);
|
||||||
|
|||||||
92
src/azure.js
92
src/azure.js
@@ -1,29 +1,68 @@
|
|||||||
// auth.js
|
// azure.js
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import { URL } from "node:url";
|
import { URL } from "node:url";
|
||||||
import open from "open";
|
import open, { apps } from "open";
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { PublicClientApplication } from "@azure/msal-node";
|
|
||||||
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) {
|
||||||
|
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 {
|
||||||
beforeCacheAccess: async (ctx) => {
|
beforeCacheAccess: async (ctx) => {
|
||||||
if (fs.existsSync(cachePath)) {
|
if (fs.existsSync(cachePath)) {
|
||||||
ctx.tokenCache.deserialize(
|
ctx.tokenCache.deserialize(fs.readFileSync(cachePath, "utf8"));
|
||||||
fs.readFileSync(cachePath, "utf8")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
afterCacheAccess: async (ctx) => {
|
afterCacheAccess: async (ctx) => {
|
||||||
if (ctx.cacheHasChanged) {
|
if (ctx.cacheHasChanged) {
|
||||||
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(cachePath, ctx.tokenCache.serialize());
|
||||||
cachePath,
|
|
||||||
ctx.tokenCache.serialize()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -39,16 +78,22 @@ function generatePkce() {
|
|||||||
return { verifier, challenge, challengeMethod: "S256" };
|
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 (!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/azure-node-playground",
|
`.config/${sanitizedAppName}`,
|
||||||
`${clientId}-token-cache.json`
|
`${clientId}-token-cache.json`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const pca = new PublicClientApplication({
|
const pca = new PublicClientApplication({
|
||||||
@@ -109,7 +154,9 @@ export async function loginInteractive({ tenantId, clientId, scopes }) {
|
|||||||
|
|
||||||
resolve(token);
|
resolve(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try { server.close(); } catch {}
|
try {
|
||||||
|
server.close();
|
||||||
|
} catch {}
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -127,12 +174,21 @@ export async function loginInteractive({ tenantId, clientId, scopes }) {
|
|||||||
codeChallengeMethod: pkce.challengeMethod,
|
codeChallengeMethod: pkce.challengeMethod,
|
||||||
});
|
});
|
||||||
|
|
||||||
try { await open(authUrl, { wait: false }); } catch {}
|
try {
|
||||||
console.log("If the browser didn't open, visit:\n" + authUrl);
|
await open(authUrl, {
|
||||||
|
wait: false,
|
||||||
|
app: {
|
||||||
|
name: apps.edge, // Enforce using Microsoft Edge browser
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
console.log("Visit:\n" + authUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try { server.close(); } catch {}
|
try {
|
||||||
|
server.close();
|
||||||
|
} catch {}
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
70
src/graph.js
Normal file
70
src/graph.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Client } from "@microsoft/microsoft-graph-client";
|
||||||
|
import { loginInteractive } from "./azure.js";
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user