Last active
March 23, 2026 21:43
-
-
Save gaieges/b1a6f9f2b495124aae0d364b6038b4d4 to your computer and use it in GitHub Desktop.
Revoke Active Suitecloud M2m credentials via devtools script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // this is a simple script that gets pasted into the console of the M2M certificate management page | |
| // that will to revoke all certificates that are for "SuiteCloud Development Integration" and have a revoke button available. | |
| // It collects the certificateIds, then makes a POST request to revoke each one sequentially, | |
| // and finally prints out the results to console.log. | |
| // | |
| // Instructions: | |
| // - go to the m2m page: /app/oauth2/clientcredentials/setup.nl | |
| // - open up browser dev tools (chrome is ideal) | |
| // - update this script to enable / disable dryRun mode | |
| // - paste this entire script into the console and hit enter | |
| const targetAppName = "SuiteCloud Development Integration"; | |
| const results = []; | |
| // Set to false to actually perform the revocations, true will just simulate and log the actions without making network requests | |
| const dryRun = true; | |
| // Phase 1: Fetch all client credentials and select the ones we want to revoke. | |
| // This avoids any reliance on the page’s DOM / infinite scroll. | |
| const getCredentialsToRevoke = async () => { | |
| const listUrl = `${window.location.origin}/app/oauth2/clientcredentials/dataservice.nl?action=getClientCredentials`; | |
| const response = await fetch(listUrl, { | |
| method: "GET", | |
| headers: { | |
| Accept: "application/json", | |
| }, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status} ${response.statusText} ${text}`); | |
| } | |
| const payload = await response.json(); | |
| if (payload?.status !== "ok") { | |
| throw new Error( | |
| `Unexpected payload status from getClientCredentials: ${JSON.stringify( | |
| payload, | |
| )}`, | |
| ); | |
| } | |
| const rows = Array.isArray(payload?.data) ? payload.data : []; | |
| return ( | |
| rows | |
| .filter((row) => row?.application?.name === targetAppName) | |
| // already revoked = no-op | |
| .filter((row) => row?.revoked === false) | |
| // ensure we have the data we need to revoke | |
| .filter( | |
| (row) => Boolean(row?.application?.id) && Boolean(row?.certificateId), | |
| ) | |
| .map((row) => ({ | |
| appId: row.application.id, | |
| certificateId: row.certificateId, | |
| applicationName: row.application.name, | |
| entityName: row?.entity?.name, | |
| roleName: row?.role?.name, | |
| validUntil: row?.validUntil, | |
| })) | |
| ); | |
| }; | |
| // Phase 2: Perform fetch requests for each collected certificateId | |
| const buildRevokeBody = ({ appId, certificateId }) => | |
| `action=revokeClientCredentials&appid=${encodeURIComponent( | |
| String(appId), | |
| )}&certificateId=${encodeURIComponent(String(certificateId))}`; | |
| const revokeCertificatesSequentially = async (credentialsToRevoke) => { | |
| const revokeUrl = `${window.location.origin}/app/oauth2/clientcredentials/dataservice.nl`; | |
| const revokeHeaders = { | |
| "Content-Type": "application/x-www-form-urlencoded", | |
| }; | |
| for (const credential of credentialsToRevoke) { | |
| const { appId, certificateId } = credential; | |
| const body = buildRevokeBody(credential); | |
| try { | |
| const response = await fetch(revokeUrl, { | |
| method: "POST", | |
| headers: revokeHeaders, | |
| body: body, | |
| }); | |
| if (response.ok) { | |
| const responseText = await response.text(); // Or .json() if the response is JSON | |
| results.push({ | |
| appId, | |
| certificateId, | |
| status: "success", | |
| response: responseText, | |
| }); | |
| console.log( | |
| `Successfully revoked certificateId=${certificateId} appid=${appId}: ${responseText}`, | |
| ); | |
| } else { | |
| const errorText = await response.text(); | |
| results.push({ | |
| appId, | |
| certificateId, | |
| status: "failed", | |
| error: `HTTP error! Status: ${response.status} - ${errorText}`, | |
| }); | |
| console.error( | |
| `Failed to revoke certificateId=${certificateId} appid=${appId}: HTTP error! Status: ${response.status} - ${errorText}`, | |
| ); | |
| } | |
| } catch (error) { | |
| results.push({ | |
| appId, | |
| certificateId, | |
| status: "failed", | |
| error: `Network error: ${error.message}`, | |
| }); | |
| console.error( | |
| `Failed to revoke certificateId=${certificateId} appid=${appId}: Network error - ${error.message}`, | |
| ); | |
| } | |
| } | |
| }; | |
| // We need to define an async IIFE (Immediately Invoked Function Expression) to use await in the global scope | |
| (async () => { | |
| const credentialsToRevoke = await getCredentialsToRevoke(); | |
| if (dryRun) { | |
| console.log( | |
| `[dryRun] Would revoke ${credentialsToRevoke.length} certificate(s):`, | |
| credentialsToRevoke, | |
| ); | |
| } else { | |
| await revokeCertificatesSequentially(credentialsToRevoke); | |
| // After all fetches are complete, assign the results to the top-level data variable | |
| // This will be the return value of the executeJavaScript function | |
| console.log("All operations complete. Results:", results); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment