Interactive login first phase.
This commit is contained in:
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