Last active
November 16, 2025 20:57
-
-
Save gartnera/e190154e8eaf8fee79bb9bac0bba48c7 to your computer and use it in GitHub Desktop.
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
| // ==UserScript== | |
| // @name AWS Console Session Reset Fixes | |
| // @namespace https://agartner.com | |
| // @version 1.1 | |
| // @description Automatically refreshes the AWS console page when the 'Sign in again' modal appears. Ensures that the 'Sign in again' button correctly redirects to the the SSO portal. | |
| // @author Alex Gartner | |
| // @match *://*.console.aws.amazon.com/* | |
| // @match *://*.signin.aws.amazon.com/* | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // @run-at document-end | |
| // @noframes | |
| // ==/UserScript== | |
| // Configuration - can be customized through Tampermonkey menu | |
| const DEFAULT_SSO_LOGIN_URL = "https://agtonomy.awsapps.com/start/#/console"; | |
| const ssoLoginUrlBase = GM_getValue("ssoLoginUrl", DEFAULT_SSO_LOGIN_URL); | |
| // Register menu command to configure SSO URL | |
| GM_registerMenuCommand("Configure SSO URL", function () { | |
| const newUrl = prompt("Enter SSO Login URL:", ssoLoginUrlBase); | |
| if (newUrl !== null && newUrl.trim() !== "") { | |
| GM_setValue("ssoLoginUrl", newUrl.trim()); | |
| alert( | |
| "SSO URL updated! Please refresh the page for changes to take effect.", | |
| ); | |
| } | |
| }); | |
| /** | |
| * Strips the AWS multisession prefix from a console URL. | |
| * Tolerates URLs without a prefix, non-AWS URLs, and invalid inputs. | |
| * | |
| * @param {string} urlString The URL to process. | |
| * @returns {string} The URL with the prefix removed, or the original URL if no prefix was found. | |
| */ | |
| function stripAwsMultisessionPrefix(urlString) { | |
| const CORE_DOMAIN = "console.aws.amazon.com"; | |
| let url; | |
| try { | |
| // Use the URL API for robust parsing. | |
| url = new URL(urlString); | |
| } catch (error) { | |
| // If the URL is invalid, return the original string. | |
| return urlString; | |
| } | |
| // Only proceed if it's a valid AWS console domain. | |
| if (!url.hostname.endsWith(CORE_DOMAIN)) { | |
| return urlString; | |
| } | |
| const hostnameParts = url.hostname.split("."); | |
| const coreDomainParts = CORE_DOMAIN.split("."); // ['console', 'aws', 'amazon', 'com'] | |
| // Isolate the subdomains before 'console.aws.amazon.com' | |
| const subdomains = hostnameParts.slice( | |
| 0, | |
| hostnameParts.length - coreDomainParts.length, | |
| ); | |
| // The pattern for a multisession prefix is: <prefix>.<region> | |
| // So, we're looking for exactly two subdomains. | |
| if (subdomains.length === 2) { | |
| const potentialRegion = subdomains[1]; | |
| // A simple regex to check if the second part looks like a region (e.g., 'us-west-2'). | |
| const isRegion = /^[a-z]{2}-[a-z]+-\d$/.test(potentialRegion); | |
| if (isRegion) { | |
| // It matches the multisession pattern. Rebuild the hostname without the prefix. | |
| const newHostname = [potentialRegion, ...coreDomainParts].join("."); | |
| url.hostname = newHostname; | |
| return url.toString(); | |
| } | |
| } | |
| // If the pattern doesn't match, return the original URL. | |
| return urlString; | |
| } | |
| function redirectToSSO(accountId, role, destination) { | |
| const ssoLoginUrl = new URL(ssoLoginUrlBase); | |
| ssoLoginUrl.searchParams.set("account_id", accountId); | |
| ssoLoginUrl.searchParams.set("role_name", role); | |
| ssoLoginUrl.searchParams.set( | |
| "destination", | |
| stripAwsMultisessionPrefix(destination), | |
| ); | |
| document.location = ssoLoginUrl.toString(); | |
| } | |
| // initiateSSOLogin will try to initiate sso login redirecting back to the current page with the same role | |
| function initiateSSOLogin() { | |
| const infoTile = document.querySelector( | |
| "[data-testid='awsc-account-info-tile']", | |
| ); | |
| const infoText = infoTile.innerText; | |
| // 2. Define regular expressions to find the patterns | |
| const accountNumberRegex = /\((\d{4}-\d{4}-\d{4})\)/; | |
| const roleRegex = /\n(.*?)\//; | |
| const accountId = infoText | |
| .match(accountNumberRegex)[1] | |
| .trim() | |
| .replaceAll("-", ""); | |
| const role = infoText.match(roleRegex)[1]; | |
| redirectToSSO(accountId, role, document.location); | |
| } | |
| function getCookie(name) { | |
| const cookieString = document.cookie; | |
| // Find the cookie string, trim it, and check if it starts with the name | |
| const cookie = cookieString.split(";").find((cookie) => { | |
| return cookie.trim().startsWith(name + "="); | |
| }); | |
| if (cookie) { | |
| // If found, split by '=' and return the decoded value | |
| // We use .slice(1) to handle cases where the value has an '=' | |
| return decodeURIComponent(cookie.split("=").slice(1).join("=")); | |
| } | |
| return null; | |
| } | |
| function handleIAMLoginRedirect() { | |
| const rawUserInfo = getCookie("aws-userInfo"); | |
| if (!rawUserInfo) { | |
| console.error("unable to get awsUserInfo cookie"); | |
| return; | |
| } | |
| const userInfo = JSON.parse(rawUserInfo); | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const redirectUri = urlParams.get("redirect_uri"); | |
| if (!redirectUri) { | |
| console.error("no redirect_uri set"); | |
| return; | |
| } | |
| // Parse ARN: arn:aws:sts::ACCOUNT_ID:assumed-role/ROLE_NAME/USER | |
| const arnParts = userInfo.arn.split(":"); | |
| const accountId = arnParts[4]; | |
| const roleInfo = arnParts[5].split("/"); | |
| const roleName = roleInfo[1]; | |
| // Extract the role name without the AWSReservedSSO_ prefix and hash suffix | |
| const role = roleName | |
| .replace(/^AWSReservedSSO_/, "") | |
| .replace(/_[a-f0-9]+$/, ""); | |
| redirectToSSO(accountId, role, redirectUri); | |
| } | |
| // aws-consoleInfo is for single session | |
| // aws-session-id is for multi-session support | |
| function hasUnexpiredCookie() { | |
| return document.cookie | |
| .split(";") | |
| .map((a) => a.split("=")[0]) | |
| .find((a) => a.includes("aws-consoleInfo") || a.includes("aws-session-id")); | |
| } | |
| (function () { | |
| "use strict"; | |
| const targetNodeId = "awsc-nav-signin-again-modal-root"; | |
| // Callback function to execute when mutations are observed | |
| const callback = (mutationsList, observer) => { | |
| // We only care if the modal now exists on the page | |
| if (!document.getElementById(targetNodeId)) { | |
| return; | |
| } | |
| console.log(`💡 Session modal (${targetNodeId}) detected`); | |
| // Disconnect the observer to prevent it from firing again during reload | |
| observer.disconnect(); | |
| if (hasUnexpiredCookie()) { | |
| // wait a bit for all the cookies to be reset | |
| setTimeout(() => { | |
| console.log( | |
| "token is unexpired and seems to be valid after wait. reloading page.", | |
| ); | |
| window.location.reload(); | |
| }, 500); | |
| return; | |
| } | |
| // replace the sign in button with one that correctly redirects to the sso page | |
| const submitButtonOriginal = document.querySelector( | |
| "#awsc-nav-signin-again-modal-root [type='submit']", | |
| ); | |
| const submitButtonClone = submitButtonOriginal.cloneNode(true); | |
| submitButtonOriginal.parentNode.replaceChild( | |
| submitButtonClone, | |
| submitButtonOriginal, | |
| ); | |
| submitButtonClone.onclick = initiateSSOLogin; | |
| // if you login on another tab and the cookie becomes valid again, we should refresh the page | |
| let expiryInterval = null; | |
| expiryInterval = setInterval(() => { | |
| if (!hasUnexpiredCookie()) { | |
| return; | |
| } | |
| clearInterval(expiryInterval); | |
| // wait a bit for all the cookies to be reset | |
| setTimeout(() => { | |
| console.log( | |
| "token is unexpired and seems to be valid after wait. reloading page.", | |
| ); | |
| window.location.reload(); | |
| }, 500); | |
| return; | |
| }, 500); | |
| }; | |
| if (document.location.origin.includes("signin.aws.amazon.com")) { | |
| console.log("handing IAM signin redirect"); | |
| handleIAMLoginRedirect(); | |
| return; | |
| } | |
| // only run on the top level console page | |
| if (!document.getElementById("consoleNavHeader")) { | |
| return; | |
| } | |
| // Log to the console to confirm the script is running | |
| console.log("🚀 AWS Auto-Refresh script active, watching for session modal."); | |
| // Create a new observer instance linked to the callback function | |
| const observer = new MutationObserver(callback); | |
| // Configuration for the observer: | |
| // We want to know if child elements are added or removed from the entire document | |
| const config = { | |
| childList: true, | |
| subtree: true, | |
| }; | |
| // Start observing the document body for configured mutations | |
| observer.observe(document.body, config); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment