Interactive login first phase.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
# Ignore node modules and config files
|
# Ignore node modules and config files
|
||||||
node_modules
|
node_modules
|
||||||
config.js
|
*config.js
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# MacOS system files
|
# MacOS system files
|
||||||
|
|||||||
12
bin/interactive-login.js
Normal file
12
bin/interactive-login.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { loginInteractive } from "../src/auth.js";
|
||||||
|
import { config } from "../public-config.js";
|
||||||
|
const scopes = ["https://management.azure.com/.default"];
|
||||||
|
|
||||||
|
const token = await loginInteractive({
|
||||||
|
tenantId: config.tenantId,
|
||||||
|
clientId: config.clientId,
|
||||||
|
scopes,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Access token acquired:");
|
||||||
|
console.log(token.accessToken);
|
||||||
13
bin/open-browser.js
Normal file
13
bin/open-browser.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import open from "open";
|
||||||
|
|
||||||
|
async function openBrowser(url) {
|
||||||
|
try {
|
||||||
|
await open(url);
|
||||||
|
console.log(`Browser opened to ${url}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to open browser: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlToOpen = "https://jmespath-playground.koszewscy.waw.pl";
|
||||||
|
openBrowser(urlToOpen);
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { exec, execSync, spawnSync } from "child_process";
|
import { exec, execSync, spawnSync } from "child_process";
|
||||||
import { writeFileSync } from "fs";
|
import { writeFileSync } from "fs";
|
||||||
|
import { env } from "process";
|
||||||
import { parseArgs } from "util";
|
import { parseArgs } from "util";
|
||||||
|
|
||||||
const args = parseArgs({
|
const args = parseArgs({
|
||||||
@@ -9,11 +10,18 @@ const args = parseArgs({
|
|||||||
"app-name": { type: "string", short: "a" },
|
"app-name": { type: "string", short: "a" },
|
||||||
help: { type: "boolean", short: "h" },
|
help: { type: "boolean", short: "h" },
|
||||||
"generate-client-secret": { type: "boolean", short: "s" },
|
"generate-client-secret": { type: "boolean", short: "s" },
|
||||||
"write-config": { type: "boolean", short: "w" },
|
"config": { type: "string", short: "c", default: "config.js" },
|
||||||
"write-env": { type: "boolean", short: "e" },
|
"write-config": { type: "string", short: "w" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (args.values["write-config"] && !["js", "env", "both"].includes(args.values["write-config"])) {
|
||||||
|
console.error(
|
||||||
|
"Invalid value for --write-config. Allowed values are: js, env, both.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const config = {};
|
const config = {};
|
||||||
|
|
||||||
if (args.values["app-name"]) {
|
if (args.values["app-name"]) {
|
||||||
@@ -134,14 +142,17 @@ if (args.values["generate-client-secret"]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.values["write-env"] || args.values["generate-client-secret"]) {
|
let envContent = `AZ_APP_NAME="${config.appName}"
|
||||||
// Write the APP_ID to the .env file
|
|
||||||
const envContent = `AZ_APP_NAME="${config.appName}"
|
|
||||||
ARM_TENANT_ID=${config.tenantId}
|
ARM_TENANT_ID=${config.tenantId}
|
||||||
ARM_CLIENT_ID=${config.clientId}
|
ARM_CLIENT_ID=${config.clientId}
|
||||||
ARM_CLIENT_SECRET=${config.clientSecret || ""}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
if (config.clientSecret) {
|
||||||
|
envContent += `ARM_CLIENT_SECRET=${config.clientSecret}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["env", "both"].includes(args.values["write-config"])) {
|
||||||
|
// Write the APP_ID to the .env file
|
||||||
writeFileSync(".env", envContent);
|
writeFileSync(".env", envContent);
|
||||||
try {
|
try {
|
||||||
execSync("chmod 600 .env");
|
execSync("chmod 600 .env");
|
||||||
@@ -151,23 +162,29 @@ ARM_CLIENT_SECRET=${config.clientSecret || ""}
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log(".env file created with application configuration.");
|
console.log(".env file created with application configuration.");
|
||||||
|
} else {
|
||||||
|
console.log("\nThe .env file for the application:");
|
||||||
|
console.log(envContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.values["write-config"] || args.values["generate-client-secret"]) {
|
if (["js", "both"].includes(args.values["write-config"])) {
|
||||||
// Save the config to the 'config.js' file.
|
// Save the config to the 'config.js' file.
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
"config.js",
|
args.values["config"],
|
||||||
`export const config = ${JSON.stringify(config, null, 4)};\n`,
|
`export const config = ${JSON.stringify(config, null, 4)};\n`,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
execSync("chmod 600 config.js");
|
execSync(`chmod 600 ${args.values["config"]}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Could not set file permissions for config.js. Please ensure it is secured appropriately.",
|
`Could not set file permissions for ${args.values["config"]}. Please ensure it is secured appropriately.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log("config.js file created.");
|
console.log(`${args.values["config"]} file created.`);
|
||||||
|
} else {
|
||||||
|
console.log(`The ${args.values["config"]} file content:`);
|
||||||
|
console.log(
|
||||||
|
`export const config = ${JSON.stringify(config, null, 4)};\n`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Setup complete.");
|
|
||||||
|
|||||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -8,7 +8,8 @@
|
|||||||
"name": "azure-node-playground",
|
"name": "azure-node-playground",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/identity": "^4.13.0"
|
"@azure/identity": "^4.13.0",
|
||||||
|
"@azure/msal-node": "^5.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
@@ -124,6 +125,20 @@
|
|||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@azure/identity/node_modules/@azure/msal-node": {
|
||||||
|
"version": "3.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.6.tgz",
|
||||||
|
"integrity": "sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/msal-common": "15.14.1",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"uuid": "^8.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@azure/logger": {
|
"node_modules/@azure/logger": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
|
||||||
@@ -159,17 +174,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/msal-node": {
|
"node_modules/@azure/msal-node": {
|
||||||
"version": "3.8.6",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.0.2.tgz",
|
||||||
"integrity": "sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w==",
|
"integrity": "sha512-3tHeJghckgpTX98TowJoXOjKGuds0L+FKfeHJtoZFl2xvwE6RF65shZJzMQ5EQZWXzh3sE1i9gE+m3aRMachjA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-common": "15.14.1",
|
"@azure/msal-common": "16.0.2",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"uuid": "^8.3.0"
|
"uuid": "^8.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@azure/msal-node/node_modules/@azure/msal-common": {
|
||||||
|
"version": "16.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.0.2.tgz",
|
||||||
|
"integrity": "sha512-ZJ/UR7lyqIntURrIJCyvScwJFanM9QhJYcJCheB21jZofGKpP9QxWgvADANo7UkresHKzV+6YwoeZYP7P7HvUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typespec/ts-http-runtime": {
|
"node_modules/@typespec/ts-http-runtime": {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/identity": "^4.13.0"
|
"@azure/identity": "^4.13.0",
|
||||||
|
"@azure/msal-node": "^5.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/auth.js
Normal file
91
src/auth.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// auth.js
|
||||||
|
import http from "node:http";
|
||||||
|
import { URL } from "node:url";
|
||||||
|
import open from "open";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import { PublicClientApplication } from "@azure/msal-node";
|
||||||
|
|
||||||
|
function generatePkce() {
|
||||||
|
const verifier = crypto.randomBytes(32).toString("base64url"); // 43 chars, valid
|
||||||
|
const challenge = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(verifier)
|
||||||
|
.digest("base64url");
|
||||||
|
|
||||||
|
return { verifier, challenge, challengeMethod: "S256" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginInteractive({ tenantId, clientId, scopes }) {
|
||||||
|
if (!tenantId) throw new Error("tenantId is required");
|
||||||
|
if (!clientId) throw new Error("clientId is required");
|
||||||
|
if (!Array.isArray(scopes) || scopes.length === 0)
|
||||||
|
throw new Error("scopes[] is required");
|
||||||
|
|
||||||
|
const pca = new PublicClientApplication({
|
||||||
|
auth: {
|
||||||
|
clientId,
|
||||||
|
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pkce = generatePkce();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let redirectUri;
|
||||||
|
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
||||||
|
|
||||||
|
if (url.pathname !== "/callback") {
|
||||||
|
res.writeHead(404).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = url.searchParams.get("code");
|
||||||
|
if (!code) {
|
||||||
|
res.writeHead(400).end("Missing authorization code");
|
||||||
|
server.close();
|
||||||
|
reject(new Error("Missing authorization code"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end("Authentication complete. You may close this tab.");
|
||||||
|
server.close();
|
||||||
|
|
||||||
|
const token = await pca.acquireTokenByCode({
|
||||||
|
code,
|
||||||
|
scopes,
|
||||||
|
redirectUri,
|
||||||
|
codeVerifier: pkce.verifier,
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(token);
|
||||||
|
} catch (e) {
|
||||||
|
try { server.close(); } catch {}
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(0, "127.0.0.1", async () => {
|
||||||
|
try {
|
||||||
|
const { port } = server.address();
|
||||||
|
redirectUri = `http://localhost:${port}/callback`;
|
||||||
|
console.log("Using redirectUri:", redirectUri);
|
||||||
|
|
||||||
|
const authUrl = await pca.getAuthCodeUrl({
|
||||||
|
scopes,
|
||||||
|
redirectUri,
|
||||||
|
codeChallenge: pkce.challenge,
|
||||||
|
codeChallengeMethod: pkce.challengeMethod,
|
||||||
|
});
|
||||||
|
|
||||||
|
try { await open(authUrl, { wait: false }); } catch {}
|
||||||
|
console.log("If the browser didn't open, visit:\n" + authUrl);
|
||||||
|
} catch (e) {
|
||||||
|
try { server.close(); } catch {}
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
24
src/pkce.js
Normal file
24
src/pkce.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// pkce.js
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
function base64UrlEncode(buf) {
|
||||||
|
return buf
|
||||||
|
.toString("base64")
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RFC7636 PKCE (S256)
|
||||||
|
* @returns {{ verifier: string, challenge: string, challengeMethod: "S256" }}
|
||||||
|
*/
|
||||||
|
export function generatePkce() {
|
||||||
|
// 32 bytes -> ~43 chars base64url, within RFC7636 43..128 range
|
||||||
|
const verifier = base64UrlEncode(crypto.randomBytes(32));
|
||||||
|
const challenge = base64UrlEncode(
|
||||||
|
crypto.createHash("sha256").update(verifier).digest()
|
||||||
|
);
|
||||||
|
|
||||||
|
return { verifier, challenge, challengeMethod: "S256" };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user