Created
February 21, 2026 09:43
-
-
Save jangaraj/407b7dbbd12dfef71f452c897ed35cd0 to your computer and use it in GitHub Desktop.
LogicMonitor Edwin AI Dexda custom ("PKCE") auth
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
| /** | |
| * 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