Skip to content

Instantly share code, notes, and snippets.

@pozil
Created December 8, 2020 17:07
Show Gist options
  • Save pozil/252d4b04ec7308723a88ed4632b24c2c to your computer and use it in GitHub Desktop.
Save pozil/252d4b04ec7308723a88ed4632b24c2c to your computer and use it in GitHub Desktop.
GitHub Auto SSO script for Tampermonkey
// ==UserScript==
// @name GitHub Auto SSO
// @namespace http://pozil.github.io
// @version 1.0
// @description Adds a button that lets you automatically signs-in to all orgs that requires SSO login (instead of clicking 3x per org)
// @author pozil
// @match https://github.com/*
// ==/UserScript==
(function() {
'use strict';
const GITHUB_HOME_URL = 'https://github.com/';
const SSO_URL_REGEX = /https:\/\/github\.com\/orgs\/([a-z\-]+)\/sso/;
const SAML_INITIATE_URL_REGEX = /https:\/\/github\.com\/orgs\/[a-z\-]+\/saml\/initiate/;
const COOKIE_DURATION = 180000; // 3 minutes
const COOKIE_KEY = 'gh-auto-sso';
var context = loadRuntimeContext();
const url = window.location.href;
if (context.isRunning) { // SSO started
if (url === GITHUB_HOME_URL) { // Homepage
continueSsoFromHome();
} else if (SSO_URL_REGEX.test(url)) { // SSO start confirmation page
retry(continueFromSsoPage, 10);
} else if (SAML_INITIATE_URL_REGEX.test(url)) { // SSO end confirmation page
retry(skipSsoOnSamlError, 10);
}
} else if (url === GITHUB_HOME_URL) { // SSO hasn't started yet and we're on home page
retry(addGlobalSsoButton, 10);
}
/**
* Adds a button to start the multi org SSO script
*/
function addGlobalSsoButton() {
// Get orgs that require SSO
const ssoOrgs = getSsoOrgs();
if (ssoOrgs.length === 0) {
return false;
}
context.ssoOrgs = ssoOrgs;
saveRuntimeContext(context);
// Add sign in button to header
const button = document.createElement('button');
button.type = 'button';
button.innerHTML = `Sign in ${ssoOrgs.length} organizations`;
button.className = 'btn btn-sm btn-primary text-white ml-2';
button.onclick = startSsoFromHome;
const header = document.querySelector('.news>h2:first-of-type');
header.appendChild(button);
return true;
}
/**
* Remove global SSO button and start multi org SSO process
*/
function startSsoFromHome(event) {
event.target.remove();
context.isRunning = true;
saveRuntimeContext(context);
continueSsoFromHome();
}
/**
* Resume/start SSO process from the GitHub home page
*/
function continueSsoFromHome() {
if (context.isSigningIn) {
// SSO just succeeded
const org = context.ssoOrgs.shift();
context.ssoSuccessOrgs.push(org);
context.isSigningIn = false;
saveRuntimeContext(context);
}
// Look at the next org
if (context.ssoOrgs.length === 0) {
// Done with all orgs
context.isRunning = false;
saveRuntimeContext(context);
} else {
// Sign in next org
const org = context.ssoOrgs[0];
window.location = `https://github.com/orgs/${org}/sso?return_to=%2F`;
}
return true;
}
/**
* Automatically click on Continue button in SSO transition page
*/
function continueFromSsoPage() {
const continueButton = document.querySelector('button[type=submit]');
if (continueButton) {
context.isSigningIn = true;
saveRuntimeContext(context);
continueButton.click();
return true;
}
return false;
}
/**
* Skip org sign in if a warning is displayed (eg: unauthorized IP)
*/
function skipSsoOnSamlError() {
const warning = document.querySelector('#js-pjax-container svg.octicon-alert');
if (warning) {
// SSO failed
const org = context.ssoOrgs.shift();
context.ssoFailedOrgs.push(org);
context.isSigningIn = false;
saveRuntimeContext(context);
window.location = GITHUB_HOME_URL;
return true;
}
return false;
}
/**
* Runs a function a number of times till it reports success.
* Each attempt is separated by a small break.
* @param callFunction {function} pointer to a function that will be retried. Function must return a boolean that indicates success.
* @param retries {number} number of retries left till we abort
*/
function retry(callFunction, retries) {
const success = callFunction();
if (!success) {
const newRetries = retries -1;
if (newRetries === 0) {
log('Giving up after several retries');
} else {
setTimeout(() => {
retry(callFunction, newRetries);
}, 250);
}
}
}
/**
* Parse the list of orgs that require SSO
* @returns [string] list of org names that require SSO
*/
function getSsoOrgs() {
const ssoOrgs = [];
const links = document.querySelectorAll('.js-recent-activity-container a');
links.forEach(link => {
if (link.textContent === 'Single sign-on') {
const url = link.href;
const orgNameMatches = url.match(SSO_URL_REGEX);
ssoOrgs.push(orgNameMatches[1]);
}
});
return ssoOrgs;
}
/**
* Save script runtime context to a cookie
*/
function saveRuntimeContext(context) {
const d = new Date();
d.setTime(d.getTime() + COOKIE_DURATION);
document.cookie = `${COOKIE_KEY}=${escape(
JSON.stringify(context)
)}; expires=${d.toUTCString()}; path=/`;
};
/**
* Load script runtime context from cookie
*/
function loadRuntimeContext() {
const name = `${COOKIE_KEY}=`;
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return JSON.parse(unescape(c.substring(name.length, c.length)));
}
}
return {
isRunning: false,
isSigningIn: false,
ssoOrgs: [],
ssoSuccessOrgs: [],
ssoFailedOrgs: []
};
};
/**
* Logs a message to the console
* @param message {String}
*/
function log(message) {
console.log(`GitHub auto SSO: ${message}`);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment