(function()()

in tampermonkey.js [24:371]


(function() {
  'use strict';
  const STS = new AWS.STS();


  /**
   * The SESSION_DURATION_OVERRIDES property can be used to override the 
   * session duration of access keys for a specific IAM Role.  To specify a 
   * session duration, put the role ARN as the key and desired duration 
   * (in seconds) as the value.
   *
   * Each IAM Role has a defined MaxSessionDuration (between 15 minutes and 
   * 12 hours). The requested duration cannot exceed the allowed duration set 
   * on the role. If you see the message "The requested DurationSeconds exceeds
   * the MaxSessionDuration set for this role", then this is why.
   */
  const SESSION_DURATION_OVERRIDES = {
    'arn:aws:iam::123456789012:role/RoleName': 14400, // Example: 4 hours
  };


  $('#signin_button').text('Console Access');
  // Inject Button for Programmatic Access
  const btn = $('<a id="accesskeys_button">Programmatic Access</a>')
    .attr({ class: 'css3button', href: '#', alt: 'Access Keys', value: 'Access Keys' });
  btn.click(getCredentials);
  $('#input_signin_button').append(btn);


  /**
   * Read the Selected Role and SAML Token from the form, then 
   * call sts.assumeRoleWithSAML to get temporarry AWS Access Keys
   *
   * TODO: The below code could take a second or two to execute,
   * consider showing some kind of spinner during this time
   */
  function getCredentials() {
    const encodedSAML = $('input[name="SAMLResponse"]').val();
    const role = $('input[name="roleIndex"]:checked').val();
    if(!role) return; // Nothing was selected

    // Parse SAML token into an XML DOM we can use
    const rawSAML = atob(encodedSAML);
    const parsedSAML = $.parseXML(rawSAML);
    const $saml = $(parsedSAML);

    const idp = getIDPForRole($saml, role);
    const duration = getSessionDurationForRole($saml, role);

    // Request temporary access keys from AWS STS
    return STS.assumeRoleWithSAML({
      PrincipalArn: idp,
      RoleArn: role,
      SAMLAssertion: encodedSAML, /* SAML Token Base64 encoded */
      DurationSeconds: duration
    }).promise()
      .then((data)=> {
        const accountId = role.substr(13, 12);
        const roleName = role.substr(role.lastIndexOf('/')+1);

        // Render popup with the new credentials
        displayCredentials(accountId, roleName,
          data.Credentials.AccessKeyId,
          data.Credentials.SecretAccessKey,
          data.Credentials.SessionToken,
          data.Credentials.Expiration);
      })
      .catch((err) => {
        const message = err.message ? err.message : JSON.stringify(err, null, 2);
        alert(message);
      });
  }

  /**
   * Find the ARN of the IDP associated with the specified role
   *
   * The SAML XML contains a list of IDP and Role ARNs inside an attribute tag
   * https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html
   */
  function getIDPForRole($saml, role) {
    const $attribute = $saml.find('[Name=\'https://aws.amazon.com/SAML/Attributes/Role\']')
      .filter((idx, element) => {
        return element.localName === 'Attribute' && element.namespaceURI === 'urn:oasis:names:tc:SAML:2.0:assertion';
      });
    const $value = $attribute.find(':contains(\''+role+'\')')
      .filter((idx, element) => {
        return element.localName === 'AttributeValue' && element.namespaceURI === 'urn:oasis:names:tc:SAML:2.0:assertion';
      });

    if(!$value || !$value.length) {
      throw new Error('Failed to find IDP ARN for selected role: '+role);
    }

    // IDP is usually the first part, but not always
    var parts = $value.text().split(',');
    if(parts[0].indexOf(':saml-provider/') !== -1) {
      return parts[0];
    } else {
      return parts[1];
    }
  }

  /**
   * Find the Session Duration to use for temporary AWS access keys
   * Note: There is no easy way for this code to know exactly what duration the
   * role allows.  Selecting a duration that is too long gives an error
   *
   * Values are selected with the following preference:
   * 1) Override value specified in SESSION_DURATION_OVERRIDES above
   * 2) SessionDuration attribute specified within SAML token
   * 3) Default value of 1 hour
   */
  function getSessionDurationForRole($saml, role) {

    // Override value specified in SESSION_DURATION_OVERRIDES above
    if( SESSION_DURATION_OVERRIDES[role] ) {
      return SESSION_DURATION_OVERRIDES[role];
    }

    // SessionDuration attribute specified within SAML token
    try {

      const $attribute = $saml.find('[Name=\'https://aws.amazon.com/SAML/Attributes/SessionDuration\']')
      .filter((idx, element) => {
        return element.localName === 'Attribute' && element.namespaceURI === 'urn:oasis:names:tc:SAML:2.0:assertion';
      });
      
      if( $attribute && $attribute.length ) {
        const duration = $attribute.children(':first').text();
        if(duration >= 900 && duration <= 43200) { // allowed values are between 15 minutes and 12 hours
          return duration;
        }
      }
    } catch(err) {
      console.warn('Was not able to read SessionDuration attribute from SAML token');
      // Fall through to default value below
    }

    // Default value of 1 hour
    return 3600;
  }


  /**
   * Render a modal popup to display the AWS Access keys
   */
  function displayCredentials(accountNumber, roleName, accessKey, secretAccessKey, sessionToken, expiration) {
    const expireTime = new Date(expiration).toLocaleTimeString();

    // HTML that forms the popup UI.  Note that styles are added via GM_addStyle further down
    const popupHTML = `
      <div id="backdrop">
        <div id="credentials">
          <div class="title">AWS Credentials for ${roleName}</div>
          <div class="expiration">Credentials will expire at <b>${expireTime}</b></div>
          <div class="option">Option 1: Set AWS environment variables</div>
          <p>Paste the following commands in your command line to set the AWS environment variables.
            <a href="https://docs.aws.amazon.com/console/singlesignon/user-portal/aws-accounts/command-line/get-credentials/option1">Learn More</a>
          </p>
          <div id="tabs">
            <ul>
              <li><a href="#linux">MacOS or Linux</a></li>
              <li><a href="#windows">Windows CMD</a></li>
              <li><a href="#powershell">PowerShell</a></li>
            </ul>
            <div class="codepanel" id="linux">
              <textarea class="code-box" rows="3" spellcheck="false">export AWS_ACCESS_KEY_ID="${accessKey}"
export AWS_SECRET_ACCESS_KEY="${secretAccessKey}"
export AWS_SESSION_TOKEN="${sessionToken}"</textarea>
            </div>
            <div class="codepanel" id="windows">
              <textarea class="code-box" rows="3" spellcheck="false">set AWS_ACCESS_KEY_ID="${accessKey}"
set AWS_SECRET_ACCESS_KEY="${secretAccessKey}"
set AWS_SESSION_TOKEN="${sessionToken}"</textarea>
            </div>
            <div class="codepanel" id="powershell">
              <textarea class="code-box" rows="3" spellcheck="false">Set-AWSCredential -AccessKey "${accessKey}" \`
-SecretKey "${secretAccessKey}" \`
-SessionToken "${sessionToken}"</textarea>
            </div>
          </div>
          <div class="option">Option 2: Add a profile to your AWS credentials file</div>
          <p>Paste the following text into your AWS credentials file (typically found at ~/.aws/credentials).
            <a href="https://docs.aws.amazon.com/console/singlesignon/user-portal/aws-accounts/command-line/get-credentials/option2">Learn More</a>
          </p>
          <div class="codepanel">
            <textarea class="code-box" rows="4" spellcheck="false">[${accountNumber}-${roleName}]
aws_access_key_id = ${accessKey}
aws_secret_access_key = ${secretAccessKey}
aws_session_token = ${sessionToken}</textarea>
          </div>
          <div class="option">Option 3: Use individual values in your AWS service client</div>
          <div id="raw-values">
            <span>AWS Access Key Id</span>
            <input type="text" value="${accessKey}" readonly spellcheck="false">
            <span>AWS Secret Access Key</span>
            <input type="text" value="${secretAccessKey}" readonly spellcheck="false">
            <span>AWS Session Token</span>
            <input type="text" value="${sessionToken}" readonly spellcheck="false">
          </div>
        </div>
      </div>`;

    // Remember which platform option the user has selected
    const TAB_SELECTION_KEY = 'envvar/tab/selected';
    const storage = window.localStorage;

    let activeTab = 0;
    try {
      activeTab = storage.getItem(TAB_SELECTION_KEY);
    } catch(err) {} // Not a big deal if we can't recall the user's selection
    function onTabChange(event, ui) {
      var tabIndex = ui.newTab.parent().children().index(ui.newTab);
      storage.setItem(TAB_SELECTION_KEY, tabIndex);
    }

    const popup = $(popupHTML);
    $('body').append(popup);
    $('#tabs').tabs({active: activeTab, activate: onTabChange});
    $('#credentials').click((event)=> { event.stopPropagation(); });
    $('#backdrop').scroll((event)=> { event.stopPropagation(); });
    $('#backdrop').click(()=> { $('#backdrop').remove(); });
  }


  // Load Custom Styles required by the credentials popup
  GM_addStyle( GM_getResourceText('JQUI-CSS'));
  GM_addStyle ( `
    #backdrop {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background-color: #333333DD;
    }
    #tabs a:focus {
      outline: 0;
    }
    #credentials {
      max-height: max-content;
      position: fixed;
      overflow-y: scroll;
      top: 20px;
      left: 50%;
      bottom: 20px;
      min-height: 200px;
      transform: translate(-50%,0);
      -ms-transform: translate(-50%,0);
      margin: 20px auto;
      background: #FFF;
      border: 1px solid #BBB;
      padding: 20px 10px;
      font-size: 14px;
    }
    #credentials > .title {
      font-size: 20px;
      font-weight: bold;
      margin-bottom: 20px;
      text-align: center;
    }
    #credentials > .expiration {
      text-align: center;
    }
    #credentials > .option {
      padding-top: 20px;
      font-size: 16px;
      font-weight: bold;
      border-top: 1px solid #CCC;
      margin-top: 10px;
    }

    #tabs {
      background: transparent;
      border: none;
      padding-top: 0px;
      margin-top: -10px;
    }
    #tabs .ui-widget-header {
      background: transparent;
      border: none;
      -moz-border-radius: 0px;
      -webkit-border-radius: 0px;
      border-radius: 0px;
    }
    #tabs .ui-tabs-nav .ui-state-default {
      background: transparent;
      border: none;
    }
    #tabs .ui-tabs-nav .ui-state-active {
      background: transparent;
      border: none;
    }
    #tabs .ui-tabs-nav .ui-state-default a {
      color: #c0c0c0;
    }
    #tabs .ui-tabs-nav .ui-state-active a {
      color: #FF9900;
    }
    #tabs .ui-tabs-panel {
      padding: 0;
    }

    #raw-values {
      display: grid;
      grid-template-columns: max-content auto;
      grid-row-gap: 8px;
      grid-column-gap: 6px;
      padding-top: 10px;
    }
    #raw-values span {
      padding-top: 10px;
    }
    #raw-values input {
      background: #FAFAFA;
      padding: 4px 10px;
      border-left: solid 3px #FF9900;
      border-top: 1px solid #c0c0c0;
      margin: 0px;
      unicode-bidi: embed;
      font-family: monospace !important;
      white-space: pre;
      font-size: 1em;
    }

    .code-box {
      unicode-bidi: embed;
      font-family: monospace !important;
      white-space: pre;
      font-size: 1em;
      width: 675px;
      height: auto;
      resize: none;
      border: 0;
      background: #FAFAFA;
      padding: 10px;
      border-left: solid 3px #FF9900;
      border-top: 1px solid #c0c0c0;
      margin: 0px;
      overflow-x: hidden;
    }

    .codepanel {
      position: relative;
      padding-left: 10px;
    }`);

})();