-
-
Save commercebuild/7d726d0999d50c9b436d98a3248603ca to your computer and use it in GitHub Desktop.
Cloudflare 2FA Worker
This file contains 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
addEventListener('fetch', event => { | |
event.respondWith(handleRequest(event.request, event)); | |
}); | |
async function handleRequest(request, event) { | |
const ipAddress = request.headers.get('CF-Connecting-IP'); | |
const cacheKey = `https://www.auth-${ipAddress}`; | |
const cache = await caches.open('auth-cache'); | |
const cachedResponse = await cache.match(cacheKey); | |
const cachedTimestamp = cachedResponse ? await cachedResponse.text() : null; | |
if (cachedTimestamp) { | |
const currentTime = Date.now(); | |
const cachedTime = parseInt(cachedTimestamp); | |
if ((currentTime - cachedTime) <= 3600000) { | |
const originalResponse = await fetch(request); | |
const responseClone = new Response(originalResponse.body, originalResponse); | |
responseClone.headers.set('2fa-passed-at', cachedTimestamp); | |
return responseClone; | |
} | |
} | |
const token = request.headers.get('x-2fa-token'); | |
if (!token) { | |
let msg = "Please use your 2FA application to enter the 2FA code. Thank you."; | |
return get2FAHtmlForm(msg); | |
} | |
const secret = 'CZOK7ANZAISSEYSM'; // REPLACE WITH A BASE32 ENCODED STRING | |
const isValidToken = await validateTotp(token, secret); | |
if (!isValidToken) { | |
return new Response(JSON.stringify({ success: false, msg: "Invalid 2FA Code" }), { | |
headers: { 'Content-Type': 'application/json' } | |
}); | |
} | |
const currentTime = Date.now().toString(); | |
const authStatus = new Response(currentTime); | |
event.waitUntil(cache.put(cacheKey, authStatus)); | |
return new Response(null, { | |
status: 302, | |
statusText: 'Found', | |
headers: { | |
'Location': '/admin', | |
'2fa-passed-at': currentTime | |
} | |
}); | |
} | |
function get2FAHtmlForm(msg) { | |
const html = ` | |
<html> | |
<head> | |
<style> | |
body { | |
background-color: #602da5; | |
color: #FFFFFF; | |
font-family: Arial, sans-serif; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
height: 100vh; | |
margin: 0; | |
} | |
form { | |
background-color: #FFFFFF; | |
color: #602da5; | |
padding: 20px; | |
border-radius: 10px; | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
p { | |
background-color: #f2f2f2; | |
padding: 10px; | |
border-radius: 5px; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
margin-bottom: 20px; | |
} | |
label { | |
font-weight: bold; | |
} | |
input { | |
width: 100%; | |
padding: 10px; | |
margin-top: 5px; | |
margin-bottom: 20px; | |
border: 1px solid #602da5; | |
border-radius: 5px; | |
} | |
button { | |
background-color: #602da5; | |
color: #FFFFFF; | |
padding: 10px 20px; | |
border: none; | |
border-radius: 5px; | |
cursor: pointer; | |
} | |
button:hover { | |
background-color: #4a2380; | |
} | |
</style> | |
</head> | |
<body> | |
<form id="2faForm" onsubmit="submitToken(event)"> | |
<p id="message">${msg}</p> | |
<label for="token">Enter 2FA Token:</label> | |
<input type="text" id="token" name="token"> | |
<button type="submit">Submit</button> | |
</form> | |
<script> | |
function submitToken(event) { | |
event.preventDefault(); | |
const token = document.getElementById('token').value; | |
fetch(location.href, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'x-2fa-token': token | |
}, | |
body: JSON.stringify({ token: token }) | |
}).then(response => { | |
if (response.headers.get('Content-Type').includes('application/json')) { | |
return response.json(); | |
} else { | |
location.reload(); | |
} | |
}).then(data => { | |
if (data && data.success) { | |
location.reload(); | |
} else { | |
document.getElementById('message').innerText = data.msg; | |
} | |
}); | |
} | |
</script> | |
</body> | |
</html> | |
`; | |
return new Response(html, { headers: { 'Content-Type': 'text/html' } }); | |
} | |
async function validateTotp(token, secret) { | |
const timeStep = 30; | |
const tolerance = 1; | |
const currentTime = Math.floor(Date.now() / 1000); | |
for (let i = -tolerance; i <= tolerance; i++) { | |
let timeCounter = Math.floor((currentTime + i * timeStep) / timeStep); | |
let generatedToken = await generateToken(timeCounter, secret); | |
if (generatedToken === token.toString()) { | |
return true; | |
} | |
} | |
return false; | |
} | |
async function generateToken(timeCounter, secret) { | |
const timeCounterHex = timeCounter.toString(16).padStart(16, '0'); | |
const keyHex = base32tohex(secret); | |
const hmacHex = await jsSHA1HMAC(keyHex, timeCounterHex); | |
const offset = parseInt(hmacHex.slice(-1), 16); | |
const truncatedHash = parseInt(hmacHex.slice(offset * 2, offset * 2 + 8), 16) & 0x7fffffff; | |
let generatedToken = (truncatedHash % 1000000).toString(); | |
generatedToken = generatedToken.padStart(6, "0"); | |
return generatedToken; | |
} | |
async function jsSHA1HMAC(keyHex, messageHex) { | |
const key = new Uint8Array(keyHex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))); | |
const message = new Uint8Array(messageHex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))); | |
const cryptoKey = await crypto.subtle.importKey( | |
'raw', | |
key, | |
{ name: 'HMAC', hash: 'SHA-1' }, | |
false, | |
['sign'] | |
); | |
const signature = await crypto.subtle.sign('HMAC', cryptoKey, message); | |
const signatureArray = new Uint8Array(signature); | |
const hmacHex = Array.from(signatureArray).map(b => b.toString(16).padStart(2, '0')).join(''); | |
return hmacHex; | |
} | |
function base32tohex(base32) { | |
var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | |
var bits = ""; | |
var hex = ""; | |
for (var i = 0; i < base32.length; i++) { | |
var val = base32chars.indexOf(base32.charAt(i).toUpperCase()); | |
bits += leftpad(val.toString(2), 5, '0'); | |
} | |
for (var i = 0; i+4 <= bits.length; i+=4) { | |
var chunk = bits.substr(i, 4); | |
hex = hex + parseInt(chunk, 2).toString(16); | |
} | |
return hex; | |
} | |
function leftpad(str, len, pad) { | |
if (len + 1 >= str.length) { | |
str = Array(len + 1 - str.length).join(pad) + str; | |
} | |
return str; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment