Update logout userPrincipalName
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/identity": "^4.13.0",
|
"@azure/identity": "^4.13.0",
|
||||||
"@azure/msal-node": "^5.0.3",
|
"@azure/msal-node": "^5.0.3",
|
||||||
|
"@azure/msal-node-extensions": "^1.2.0",
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||||
"azure-devops-node-api": "^15.1.2"
|
"azure-devops-node-api": "^15.1.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,39 +1,56 @@
|
|||||||
|
|
||||||
import http from "node:http";
|
|
||||||
import { URL } from "node:url";
|
|
||||||
import open, { apps } from "open";
|
import open, { apps } from "open";
|
||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import os from "node:os";
|
|
||||||
|
|
||||||
import { PublicClientApplication } from "@azure/msal-node";
|
import { PublicClientApplication } from "@azure/msal-node";
|
||||||
|
import {
|
||||||
|
DataProtectionScope,
|
||||||
|
Environment,
|
||||||
|
PersistenceCachePlugin,
|
||||||
|
PersistenceCreator,
|
||||||
|
} from "@azure/msal-node-extensions";
|
||||||
|
|
||||||
function fileCachePlugin(cachePath) {
|
async function createPca({ tenantId, clientId }) {
|
||||||
return {
|
const cachePath = path.join(
|
||||||
beforeCacheAccess: async (ctx) => {
|
Environment.getUserRootDirectory(),
|
||||||
if (fs.existsSync(cachePath)) {
|
"sk-az-tools",
|
||||||
ctx.tokenCache.deserialize(fs.readFileSync(cachePath, "utf8"));
|
`${clientId}-msal.cache`,
|
||||||
}
|
);
|
||||||
|
|
||||||
|
const persistence = await PersistenceCreator.createPersistence({
|
||||||
|
cachePath,
|
||||||
|
dataProtectionScope: DataProtectionScope.CurrentUser,
|
||||||
|
serviceName: "sk-az-tools",
|
||||||
|
accountName: "msal-cache",
|
||||||
|
usePlaintextFileOnLinux: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new PublicClientApplication({
|
||||||
|
auth: {
|
||||||
|
clientId,
|
||||||
|
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||||
},
|
},
|
||||||
afterCacheAccess: async (ctx) => {
|
cache: {
|
||||||
if (ctx.cacheHasChanged) {
|
cachePlugin: new PersistenceCachePlugin(persistence),
|
||||||
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
||||||
fs.writeFileSync(cachePath, ctx.tokenCache.serialize());
|
|
||||||
fs.chmodSync(cachePath, 0o600); // Owner read/write only
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePkce() {
|
async function acquireTokenWithCache({ pca, scopes }) {
|
||||||
const verifier = crypto.randomBytes(32).toString("base64url"); // 43 chars, valid
|
const accounts = await pca.getTokenCache().getAllAccounts();
|
||||||
const challenge = crypto
|
|
||||||
.createHash("sha256")
|
|
||||||
.update(verifier)
|
|
||||||
.digest("base64url");
|
|
||||||
|
|
||||||
return { verifier, challenge, challengeMethod: "S256" };
|
if (accounts.length > 0) {
|
||||||
|
try {
|
||||||
|
return await pca.acquireTokenSilent({
|
||||||
|
account: accounts[0],
|
||||||
|
scopes,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// proceed to interactive/device code login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loginInteractive({ tenantId, clientId, scopes }) {
|
export async function loginInteractive({ tenantId, clientId, scopes }) {
|
||||||
@@ -42,105 +59,71 @@ export async function loginInteractive({ tenantId, clientId, scopes }) {
|
|||||||
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");
|
||||||
|
|
||||||
const cachePath = path.join(
|
const pca = await createPca({ tenantId, clientId });
|
||||||
os.homedir(),
|
|
||||||
`.config/sk-az-tools`,
|
|
||||||
`${clientId}-token-cache.json`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const pca = new PublicClientApplication({
|
const cached = await acquireTokenWithCache({ pca, scopes });
|
||||||
auth: {
|
if (cached) return cached;
|
||||||
clientId,
|
|
||||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
return await pca.acquireTokenInteractive({
|
||||||
},
|
scopes,
|
||||||
cache: {
|
openBrowser: async (url) => {
|
||||||
cachePlugin: fileCachePlugin(cachePath),
|
try {
|
||||||
|
await open(url, { wait: false });
|
||||||
|
// To enforce Microsoft Edge instead of the default browser, use:
|
||||||
|
// await open(url, {
|
||||||
|
// wait: false,
|
||||||
|
// app: {
|
||||||
|
// name: apps.edge,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
} catch {
|
||||||
|
// If auto-open fails, provide URL for manual copy/paste.
|
||||||
|
console.log("Visit:\n" + url);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginDeviceCode({ 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 = await createPca({ tenantId, clientId });
|
||||||
|
|
||||||
|
const cached = await acquireTokenWithCache({ pca, scopes });
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
return await pca.acquireTokenByDeviceCode({
|
||||||
|
scopes,
|
||||||
|
deviceCodeCallback: (response) => {
|
||||||
|
console.log(response.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout({ tenantId, clientId, userPrincipalName }) {
|
||||||
|
if (!tenantId) throw new Error("tenantId is required");
|
||||||
|
if (!clientId) throw new Error("clientId is required");
|
||||||
|
if (!userPrincipalName)
|
||||||
|
throw new Error("userPrincipalName is required");
|
||||||
|
|
||||||
|
const pca = await createPca({ tenantId, clientId });
|
||||||
|
|
||||||
const accounts = await pca.getTokenCache().getAllAccounts();
|
const accounts = await pca.getTokenCache().getAllAccounts();
|
||||||
|
const accountToSignOut = accounts.find(
|
||||||
|
(acct) =>
|
||||||
|
acct.username?.toLowerCase() === userPrincipalName.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
if (accounts.length > 0) {
|
if (!accountToSignOut) return false;
|
||||||
try {
|
|
||||||
const silentResult = await pca.acquireTokenSilent({
|
|
||||||
account: accounts[0],
|
|
||||||
scopes,
|
|
||||||
});
|
|
||||||
return silentResult;
|
|
||||||
} catch (e) {
|
|
||||||
// proceed to interactive login
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pkce = generatePkce();
|
pca.signOut({ account: accountToSignOut }).then(() => {
|
||||||
|
console.log(`Signed out ${userPrincipalName}`);
|
||||||
return new Promise((resolve, reject) => {
|
return true;
|
||||||
let redirectUri;
|
}).catch((err) => {
|
||||||
|
console.error(`Failed to sign out ${userPrincipalName}:`, err);
|
||||||
const server = http.createServer(async (req, res) => {
|
return false;
|
||||||
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,
|
|
||||||
app: {
|
|
||||||
name: apps.edge, // Enforce using Microsoft Edge browser
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
console.log("Visit:\n" + authUrl);
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
server.close();
|
|
||||||
} catch {}
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user