138 lines
3.5 KiB
JavaScript
138 lines
3.5 KiB
JavaScript
// 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";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import os from "node:os";
|
|
|
|
function fileCachePlugin(cachePath) {
|
|
return {
|
|
beforeCacheAccess: async (ctx) => {
|
|
if (fs.existsSync(cachePath)) {
|
|
ctx.tokenCache.deserialize(
|
|
fs.readFileSync(cachePath, "utf8")
|
|
);
|
|
}
|
|
},
|
|
afterCacheAccess: async (ctx) => {
|
|
if (ctx.cacheHasChanged) {
|
|
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
fs.writeFileSync(
|
|
cachePath,
|
|
ctx.tokenCache.serialize()
|
|
);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
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 cachePath = path.join(
|
|
os.homedir(),
|
|
".config/azure-node-playground",
|
|
`${clientId}-token-cache.json`
|
|
);
|
|
|
|
const pca = new PublicClientApplication({
|
|
auth: {
|
|
clientId,
|
|
authority: `https://login.microsoftonline.com/${tenantId}`,
|
|
},
|
|
cache: {
|
|
cachePlugin: fileCachePlugin(cachePath),
|
|
},
|
|
});
|
|
|
|
const accounts = await pca.getTokenCache().getAllAccounts();
|
|
|
|
if (accounts.length > 0) {
|
|
try {
|
|
const silentResult = await pca.acquireTokenSilent({
|
|
account: accounts[0],
|
|
scopes,
|
|
});
|
|
return silentResult;
|
|
} catch (e) {
|
|
// proceed to interactive login
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
} |