Skip to content

Instantly share code, notes, and snippets.

@tayyebi
Created February 10, 2026 08:23
Show Gist options
  • Select an option

  • Save tayyebi/49c88c975da96539fd438ea4448c271a to your computer and use it in GitHub Desktop.

Select an option

Save tayyebi/49c88c975da96539fd438ea4448c271a to your computer and use it in GitHub Desktop.
A lite website to share your 2FA tokens with Google Apps Script
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
<style>
html,
body {
height: 100%;
margin: 0;
font-family: 'Roboto', sans-serif;
scroll-behavior: smooth;
}
section {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
}
#publicSection {
background: #f5f5f5;
}
#privateSection {
background: #e8f0fe;
display: none;
padding: 20px;
box-sizing: border-box;
}
input {
padding: 12px;
font-size: 18px;
width: 220px;
text-align: center;
border-radius: 6px;
border: 1px solid #ccc;
margin-bottom: 12px;
}
button {
padding: 12px 24px;
font-size: 16px;
border-radius: 6px;
border: none;
background-color: #1976d2;
color: white;
cursor: pointer;
margin: 6px;
}
button:hover {
background-color: #1565c0;
}
.message {
margin-top: 12px;
font-size: 50px;
color: green;
}
.error {
color: red;
}
table {
border-collapse: collapse;
width: 90%;
max-width: 900px;
margin-top: 20px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
table th,
table td {
border: 1px solid #ccc;
padding: 10px;
text-align: center;
}
table th {
background: #1976d2;
color: white;
font-weight: 500;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
min-width: 320px;
text-align: center;
}
</style>
</head>
<body>
<!-- PUBLIC SECTION -->
<section id="publicSection">
<h2>Enter Passphrase to Get 2FA</h2>
<input id="tokenInput" placeholder="Passphrase"/>
<button onclick="submitPassphrase()">Get 2FA</button>
<div id="result" class="message"></div>
<!-- Admin login link -->
<p style="margin-top:20px;">
<a href="#" id="adminLoginLink" onclick="showAdminLogin(); return false;">Admin Login</a>
</p>
<!-- Hidden admin login form -->
<div id="adminLoginForm" style="display:none; margin-top:10px;">
<input id="adminPasswordInput" type="password" placeholder="Admin Password"/>
<button onclick="submitAdminLogin()">Login</button>
</div>
<div id="shortLink">
Use <a href="https://bit.ly/tayyebi-2fa">this link</a> to access this webpage.
</div>
</section>
<section id="privateSection">
<h2>Admin Panel</h2>
<div class="add-entity">
<input id="entityName" placeholder="Entity Name"/>
<input id="entitySecret" placeholder="Base32 Secret"/>
<button onclick="addEntity()">Add Entity</button>
<button onclick="logoutAdmin()">Logout</button>
</div>
<table id="entitiesTable">
<thead>
<tr>
<th>Entity</th>
<th>Passphrases (24h TTL)</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</section>
<div id="modal" class="modal">
<div class="modal-content">
<p id="modalText"></p>
<button onclick="closeModal()">Close</button>
</div>
</div>
<script>
function setCookie(name, value, hours) {
const d = new Date();
d.setTime(d.getTime() + (hours*60*60*1000));
document.cookie = `${name}=${value};expires=${d.toUTCString()};path=/`;
}
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
function deleteCookie(name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/`;
}
// ---------- PUBLIC ----------
function submitPassphrase() {
const passphrase = document.getElementById('tokenInput').value.trim();
if(!passphrase) return alert('Enter passphrase');
google.script.run.withSuccessHandler(code=>{
document.getElementById('result').textContent = code;
}).get2FAByPassphrase(passphrase);
}
// ---------- ADMIN ----------
function addEntity() {
const name = document.getElementById('entityName').value.trim();
const secret = document.getElementById('entitySecret').value.trim();
if(!name || !secret) { alert('Name and Secret required'); return; }
google.script.run.withSuccessHandler(loadEntities)
.withFailureHandler(e=>alert(e.message))
.createEntity(name, secret);
document.getElementById('entityName').value = '';
document.getElementById('entitySecret').value = '';
}
function loadEntities() {
google.script.run.withSuccessHandler(entities=>{
const tbody = document.querySelector('#entitiesTable tbody');
tbody.innerHTML = '';
entities.forEach(e=>{
const tr = document.createElement('tr');
const passCell = document.createElement('td');
passCell.innerHTML = e.passphrases.map(p=>{
const age = Math.floor((Date.now() - p.createdAt)/1000/60);
return `${p.value} (${age} min) <button onclick="revokePass('${e.id}','${p.value}')">Revoke</button>`;
}).join('<br/>');
const actionCell = document.createElement('td');
actionCell.innerHTML = `<button onclick="createPass('${e.id}')">Create Passphrase</button>`;
tr.innerHTML = `<td>${e.name}</td>`;
tr.appendChild(passCell);
tr.appendChild(actionCell);
tbody.appendChild(tr);
});
}).getEntities();
}
function createPass(entityId) { google.script.run.withSuccessHandler(loadEntities).createPassphrase(entityId); }
function revokePass(entityId, passphrase) { google.script.run.withSuccessHandler(loadEntities).revokePassphrase(entityId, passphrase); }
function closeModal(){ document.getElementById('modal').style.display='none'; }
function showAdminLogin() {
document.getElementById('adminLoginForm').style.display = 'block';
document.getElementById('adminPasswordInput').focus();
}
function submitAdminLogin() {
const password = document.getElementById('adminPasswordInput').value.trim();
if(!password) return alert('Enter admin password');
google.script.run.withSuccessHandler(success=>{
if(success){
document.getElementById('privateSection').style.display='flex';
setCookie('adminLoggedIn','true',24);
loadEntities();
document.getElementById('privateSection').scrollIntoView({behavior:'smooth'});
document.getElementById('adminLoginForm').style.display='none';
document.getElementById('adminPasswordInput').value='';
} else {
alert('Wrong password');
}
}).loginAdmin(password);
}
function login() {
const password = prompt('Enter admin password:');
google.script.run.withSuccessHandler(success=>{
if(success){
document.getElementById('privateSection').style.display='flex';
setCookie('adminLoggedIn','true',24);
loadEntities();
document.getElementById('privateSection').scrollIntoView({behavior:'smooth'});
} else alert('Wrong password');
}).loginAdmin(password);
}
function logoutAdmin(){
deleteCookie('adminLoggedIn');
document.getElementById('privateSection').style.display='none';
document.getElementById('publicSection').scrollIntoView({behavior:'smooth'});
}
window.addEventListener('load', ()=>{
if(getCookie('adminLoggedIn')==='true'){
document.getElementById('privateSection').style.display='flex';
loadEntities();
document.getElementById('privateSection').scrollIntoView({behavior:'smooth'});
}
});
</script>
</body>
</html>
const SCRIPT_PROPERTIES = PropertiesService.getScriptProperties();
const ADMIN_PASSWORD_KEY = 'Avh23FSO0fTy'; // Change to your admin password
// ---------- WEB APP ENTRY ----------
function doGet(e) {
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('2FA Passphrase App')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// ---------- ENTITY STORAGE ----------
function getEntities() {
const json = SCRIPT_PROPERTIES.getProperty('entities');
return json ? JSON.parse(json) : [];
}
function saveEntities(entities) {
SCRIPT_PROPERTIES.setProperty('entities', JSON.stringify(entities));
}
// ---------- ENTITY MANAGEMENT ----------
function createEntity(name, secret) {
if (!name) throw new Error('Entity name is required');
if (!secret) throw new Error('Secret is required');
if (!isValidSecret(secret)) throw new Error('Secret must be valid Base32 (A-Z,2-7)');
const entities = getEntities();
const id = Math.random().toString(36).substring(2, 8).toUpperCase();
entities.push({
id,
name,
secret,
passphrases: []
});
saveEntities(entities);
return entities;
}
// ---------- PASSPHRASE MANAGEMENT ----------
function createPassphrase(entityId) {
const entities = getEntities();
const entity = entities.find(e => e.id === entityId);
if (!entity) return null;
const passphrase = Math.random().toString(36).substring(2, 8).toUpperCase();
const now = Date.now();
if(!entity.passphrases) entity.passphrases = [];
entity.passphrases.push({ value: passphrase, createdAt: now });
saveEntities(entities);
Logger.log(`Passphrase created for entity ${entity.name}: ${passphrase}`);
return passphrase;
}
function revokePassphrase(entityId, passphraseValue) {
const entities = getEntities();
const entity = entities.find(e => e.id === entityId);
if (!entity || !entity.passphrases) return false;
entity.passphrases = entity.passphrases.filter(p => p.value !== passphraseValue);
saveEntities(entities);
Logger.log(`Passphrase revoked for entity ${entity.name}: ${passphraseValue}`);
return true;
}
// ---------- PUBLIC 2FA ----------
function get2FAByPassphrase(passphrase) {
const entities = getEntities();
const now = Date.now();
for (const entity of entities) {
if (!entity.passphrases) continue;
for (const p of entity.passphrases) {
if (p.value === passphrase && (now - p.createdAt) <= 24 * 60 * 60 * 1000) {
Logger.log(`2FA requested for entity ${entity.name} using passphrase ${passphrase}`);
return generateTOTP(entity.secret, now / 1000, 6, 30);
}
}
}
return 'Invalid or expired passphrase';
}
// ---------- ADMIN LOGIN ----------
function loginAdmin(password) {
return password === ADMIN_PASSWORD_KEY;
}
// ---------- TOTP IMPLEMENTATION ----------
function base32Decode(base32) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const clean = base32.toUpperCase().replace(/=+$/, '');
let bits = '';
for (let i = 0; i < clean.length; i++) {
const val = alphabet.indexOf(clean.charAt(i));
if (val < 0) throw new Error('Invalid base32 character');
bits += val.toString(2).padStart(5, '0');
}
const bytes = [];
for (let i = 0; i + 8 <= bits.length; i += 8)
bytes.push(parseInt(bits.substring(i, i + 8), 2));
return bytes;
}
function sha1Bytes(messageBytes) {
function rotl(n, b) { return ((n << b) | (n >>> (32 - b))) >>> 0; }
const words = [];
for (let i = 0; i < messageBytes.length * 8; i += 8)
words[i >> 5] |= (messageBytes[i / 8] & 0xff) << (24 - i % 32);
const bitLen = messageBytes.length * 8;
words[bitLen >> 5] |= 0x80 << (24 - bitLen % 32);
words[(((bitLen + 64) >> 9) << 4) + 15] = bitLen;
let h0 = 0x67452301, h1 = 0xefcdab89, h2 = 0x98badcfe, h3 = 0x10325476, h4 = 0xc3d2e1f0;
for (let i = 0; i < words.length; i += 16) {
const w = new Array(80);
for (let t = 0; t < 16; t++) w[t] = words[i + t] | 0;
for (let t = 16; t < 80; t++) w[t] = rotl(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1);
let a = h0, b = h1, c = h2, d = h3, e = h4;
for (let t = 0; t < 80; t++) {
let f, k;
if (t < 20) { f = (b & c) | ((~b) & d); k = 0x5a827999; }
else if (t < 40) { f = b ^ c ^ d; k = 0x6ed9eba1; }
else if (t < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8f1bbcdc; }
else { f = b ^ c ^ d; k = 0xca62c1d6; }
const temp = (rotl(a, 5) + f + e + k + w[t]) >>> 0;
e = d; d = c; c = rotl(b, 30) >>> 0; b = a; a = temp;
}
h0 = (h0 + a) >>> 0;
h1 = (h1 + b) >>> 0;
h2 = (h2 + c) >>> 0;
h3 = (h3 + d) >>> 0;
h4 = (h4 + e) >>> 0;
}
const hash = [h0, h1, h2, h3, h4];
const out = [];
for (let i = 0; i < hash.length; i++) {
out.push((hash[i] >>> 24) & 0xff);
out.push((hash[i] >>> 16) & 0xff);
out.push((hash[i] >>> 8) & 0xff);
out.push(hash[i] & 0xff);
}
return out;
}
function hmacSha1(keyBytes, messageBytes) {
const blockSize = 64;
if (keyBytes.length > blockSize) keyBytes = sha1Bytes(keyBytes);
while (keyBytes.length < blockSize) keyBytes.push(0x00);
const oKeyPad = keyBytes.map(b => b ^ 0x5c);
const iKeyPad = keyBytes.map(b => b ^ 0x36);
const inner = sha1Bytes(iKeyPad.concat(messageBytes));
return sha1Bytes(oKeyPad.concat(inner));
}
function generateTOTP(secretBase32, time = Date.now() / 1000, digits = 6, period = 30) {
const keyBytes = base32Decode(secretBase32);
let counter = Math.floor(time / period);
const counterBytes = new Array(8).fill(0);
for (let i = 7; i >= 0; i--) {
counterBytes[i] = counter & 0xff;
counter >>>= 8;
}
const hmac = hmacSha1(keyBytes, counterBytes);
const offset = hmac[hmac.length - 1] & 0xf;
const binary = ((hmac[offset] & 0x7f) << 24) |
((hmac[offset + 1] & 0xff) << 16) |
((hmac[offset + 2] & 0xff) << 8) |
(hmac[offset + 3] & 0xff);
const otp = binary % Math.pow(10, digits);
return otp.toString().padStart(digits, '0');
}
function isValidSecret(secret) {
return /^[A-Z2-7]+=*$/.test(secret);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment