Skip to content

Instantly share code, notes, and snippets.

@jangaraj
Created February 21, 2026 09:43
Show Gist options
  • Select an option

  • Save jangaraj/407b7dbbd12dfef71f452c897ed35cd0 to your computer and use it in GitHub Desktop.

Select an option

Save jangaraj/407b7dbbd12dfef71f452c897ed35cd0 to your computer and use it in GitHub Desktop.
LogicMonitor Edwin AI Dexda custom ("PKCE") auth
/**
* LogicMonitor Custom PKCE OAuth & JWT Decoder Flow
* * STANDARD PKCE (RFC 7636):
* 1. Verifier = Random String
* 2. Challenge = Base64UrlEncode(SHA256(Verifier))
* * LOGICMONITOR CUSTOM PKCE:
* 1. Verifier = 56-character Hexadecimal String
* 2. Challenge = Base64Encode(HexEncode(SHA256(Verifier)))
* Note: It also omits standard parameters like 'code_challenge_method'
* and 'grant_type'.
*/
var runCustomLMFlow = async function() {
try {
// ==========================================
// STEP 1: GENERATE THE CUSTOM PKCE SECRETS
// ==========================================
// 1a. Generate a 28-byte array of cryptographically strong random numbers.
var verifierBytes = new Uint8Array(28);
window.crypto.getRandomValues(verifierBytes);
// 1b. Convert those bytes into a 56-character Hexadecimal string.
// This is our 'code_verifier'. We will send this in the final token exchange.
var verifier = Array.from(verifierBytes).map(b => b.toString(16).padStart(2, '0')).join('');
// 1c. Prepare the verifier to be hashed by encoding it as UTF-8 bytes.
var encoder = new TextEncoder();
var data = encoder.encode(verifier);
// 1d. Hash the verifier using SHA-256. (Returns a raw binary buffer).
var hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
// 1e. LOGICMONITOR QUIRK: Instead of Base64Url-encoding the raw binary hash,
// we must first convert the binary hash back into a Hexadecimal string.
var hashHex = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
// 1f. LOGICMONITOR QUIRK: Standard Base64 encode the Hex string (leaves padding '=' intact).
// This is our 'code_challenge'. We send this in the very first request.
var challenge = btoa(hashHex);
console.log("Verifier (Hex Secret):", verifier);
console.log("Challenge (Hashed & Encoded):", challenge);
// ==========================================
// STEP 2: REQUEST THE AUTHORIZATION CODE
// ==========================================
// Construct the URL parameters for the /authorize endpoint.
var authParams = new URLSearchParams({
'client_id': 'DEXDA', // The app requesting access
'code_challenge': challenge, // The hashed secret we just made
'state': Math.random().toString(36).substring(2), // Random CSRF protection string
'prompt': 'none', // Silent auth (relies on existing LogicMonitor cookies)
'scope': 'admin' // The permissions we are requesting
// Notice: 'code_challenge_method=S256' is intentionally missing to match LM's custom flow
});
console.log("Step 1: Requesting Auth Code...");
// Make the request. 'credentials: include' is MANDATORY here because prompt=none
// requires the browser to send your existing LogicMonitor session cookies.
var authRes = await fetch('https://company.logicmonitor.com/santaba/oauth2/v1/authorize?company=company', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
credentials: 'include',
body: authParams
});
// Parse the response. If successful, LogicMonitor returns a short-lived 'code'.
var authData = await authRes.json();
if (!authData.code) throw new Error("No code found. Your LogicMonitor session may have expired.");
console.log("Received Short-Lived Code:", authData.code);
// ==========================================
// STEP 3: EXCHANGE THE CODE FOR A TOKEN
// ==========================================
// Construct the payload to trade the short-lived code for the actual access token.
var tokenBody = new URLSearchParams({
'client_id': 'DEXDA',
'code': authData.code, // The code we just received
'code_verifier': verifier // The original un-hashed Hex secret from Step 1b
// Notice: 'grant_type=authorization_code' is intentionally missing to match LM's flow
});
console.log("Step 2: Exchanging Code for Token...");
// Make the request. 'credentials' are NOT needed here because the 'code' + 'verifier'
// mathematically prove who we are.
var tokenRes = await fetch('https://company.logicmonitor.com/santaba/oauth2/v1/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenBody
});
var tokenData = await tokenRes.json();
// Check if LogicMonitor rejected our mathematical proof (the verifier didn't match the challenge).
if (tokenData.error) {
console.error("Token Error Details:", tokenData);
return;
}
console.log("--- SUCCESS! Raw Token Data Received ---");
console.dir(tokenData);
// ==========================================
// STEP 4: DECODE THE JSON WEB TOKEN (JWT)
// ==========================================
// If we got an access_token, decode its payload so humans can read the permissions/expiry.
if (tokenData.access_token) {
console.log("--- Decoded JWT Payload ---");
console.dir(decodeJWTPayload(tokenData.access_token));
}
} catch (err) {
console.error("Flow failed:", err.message);
}
};
/**
* Helper function to safely decode a JWT payload in the browser.
* A JWT looks like this: Header.Payload.Signature (all Base64Url encoded).
* We only care about the middle part (Payload).
*/
function decodeJWTPayload(token) {
try {
// 1. Split the token by '.' and grab the middle section (index 1)
var base64Url = token.split('.')[1];
// 2. Convert Base64Url to standard Base64 (replace - with +, _ with /)
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
// 3. Decode the Base64 string.
// We use this complex mapping to ensure special characters (like accents)
// don't break the standard browser window.atob() function.
var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
// 4. Parse the resulting JSON string into a usable JavaScript object.
return JSON.parse(jsonPayload);
} catch (e) {
return { error: "Could not decode token", details: e.message };
}
}
// Kick off the whole process
runCustomLMFlow();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment