|
// ==UserScript== |
|
// @name UH MFA Auto-Advance + Skip Setup |
|
// @namespace https://alexey-fedorov.com |
|
// @version 1.0 |
|
// @description Speeds up the University of Houston Microsoft 365 sign-in flow: auto-advances the "set up another verification method" proof-up prompt after a cancellable 2s countdown, and auto-skips the Microsoft Authenticator install page. This only DEFERS/skips the optional MFA setup prompts — it does NOT disable, weaken, or bypass multi-factor authentication. |
|
// @author Alexey Fedorov (alexey-max-fedorov) |
|
// @homepageURL https://alexey-fedorov.com |
|
// @supportURL https://github.com/alexey-max-fedorov |
|
// @match https://login.microsoftonline.com/* |
|
// @match https://mysignins.microsoft.com/* |
|
// @grant none |
|
// @license MIT |
|
// ==/UserScript== |
|
|
|
(function () { |
|
'use strict'; |
|
|
|
const TAG = '[UH MFA]'; |
|
|
|
function fireClick(el) { |
|
['mousedown', 'mouseup', 'click'].forEach(type => { |
|
el.dispatchEvent(new MouseEvent(type, { |
|
bubbles: true, |
|
cancelable: true, |
|
view: window, |
|
})); |
|
}); |
|
} |
|
|
|
// ============================================================ |
|
// STEP 2 — mysignins.microsoft.com: click "Skip setup" |
|
// ============================================================ |
|
function trySkip() { |
|
// Guard: only proceed if the heading says "Install Microsoft Authenticator" |
|
const heading = [...document.querySelectorAll('h2')].find( |
|
h => h.textContent.trim() === 'Install Microsoft Authenticator' |
|
); |
|
if (!heading) return false; |
|
|
|
const skipBtn = [...document.querySelectorAll('button')].find( |
|
b => b.textContent.trim() === 'Skip setup' |
|
); |
|
if (skipBtn) { |
|
console.log(`${TAG} Heading confirmed + skip button found, clicking...`); |
|
fireClick(skipBtn); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
// ============================================================ |
|
// STEP 1 — login.microsoftonline.com: auto-advance "Next" |
|
// with a cancellable 2s countdown. |
|
// ============================================================ |
|
const NEXT_ID = 'idSubmit_ProofUp_Redirect'; |
|
const NOTE_ID = 'uh-mfa-autoadvance-note'; |
|
|
|
let armed = false; |
|
let cancelled = false; |
|
let originalValue = 'Next'; |
|
const timers = []; |
|
|
|
function upsertNote(text) { |
|
const boiler = document.getElementById('idBoilerPlateText'); |
|
if (!boiler) return; // best-effort — countdown still runs without it |
|
let note = document.getElementById(NOTE_ID); |
|
if (!note) { |
|
note = document.createElement('p'); |
|
note.id = NOTE_ID; |
|
const b = document.createElement('b'); |
|
note.appendChild(b); |
|
boiler.insertBefore(note, boiler.firstChild); // prepend as top child |
|
} |
|
note.firstChild.textContent = text; |
|
} |
|
|
|
function onUserInput(e) { cancelCountdown(e.type); } |
|
|
|
function addCancelListeners() { |
|
window.addEventListener('keydown', onUserInput, true); |
|
window.addEventListener('mousedown', onUserInput, true); |
|
} |
|
function removeCancelListeners() { |
|
window.removeEventListener('keydown', onUserInput, true); |
|
window.removeEventListener('mousedown', onUserInput, true); |
|
} |
|
|
|
function cancelCountdown(reason) { |
|
if (!armed || cancelled) return; |
|
cancelled = true; |
|
timers.forEach(clearTimeout); |
|
removeCancelListeners(); |
|
const btn = document.getElementById(NEXT_ID); |
|
if (btn) btn.value = originalValue; // restore "Next" |
|
upsertNote('Auto-continue cancelled — click Next yourself, or choose "Use a different account".'); |
|
console.log(`${TAG} countdown cancelled via ${reason}`); |
|
} |
|
|
|
function armCountdown() { |
|
if (armed) return true; |
|
const btn = document.getElementById(NEXT_ID); |
|
if (!btn || btn.offsetParent === null) return false; // not present / not visible yet |
|
armed = true; |
|
originalValue = btn.value || 'Next'; |
|
|
|
upsertNote('Auto-continuing in 2s — press any key or click anywhere to cancel (e.g. to use a different account).'); |
|
addCancelListeners(); |
|
|
|
btn.value = 'Next (2)'; |
|
console.log(`${TAG} Next detected — auto-advancing in 2s (2)...`); |
|
|
|
timers.push(setTimeout(() => { |
|
if (cancelled) return; |
|
btn.value = 'Next (1)'; |
|
console.log(`${TAG} ...(1)`); |
|
}, 1000)); |
|
|
|
timers.push(setTimeout(() => { |
|
if (cancelled) return; |
|
removeCancelListeners(); // remove BEFORE our synthetic click so it doesn't self-cancel |
|
btn.value = originalValue; // restore label, then click |
|
console.log(`${TAG} countdown elapsed — auto-clicking Next`); |
|
fireClick(btn); |
|
}, 2000)); |
|
|
|
return true; |
|
} |
|
|
|
// ============================================================ |
|
// Shared observer — a page is only ever one of the two hosts, |
|
// so only the matching handler will ever fire. |
|
// ============================================================ |
|
const observer = new MutationObserver(() => { |
|
if (trySkip() || armCountdown()) observer.disconnect(); |
|
}); |
|
|
|
observer.observe(document.body, { childList: true, subtree: true }); |
|
|
|
// Initial attempt in case the target is already in the DOM at run time. |
|
if (trySkip() || armCountdown()) observer.disconnect(); |
|
})(); |