Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add token generator protected by Cloudflare Access #43

Merged
merged 10 commits into from
Jun 20, 2024
51 changes: 51 additions & 0 deletions functions-src/secure-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const encoder = new TextEncoder();

async function generateHash(input) {
return [
...new Uint8Array(
await crypto.subtle.digest(
{ name: 'SHA-256' },
encoder.encode(input)
)
)
].map(x => x.toString(16).padStart(2, '0')).join('')
}

async function generateSecureToken({
expiry,
name,
secret,
}) {
const hash = (await generateHash(`${name}/${expiry}/${secret}`)).substring(0, 8);
return `${name}/${expiry}/${hash}`;
}

async function verifySecureToken({
secret,
token,
}) {
const [name, expiry, hash] = token.split("/");

const expiryDate = new Date(expiry).addDays(1);
if (expiryDate < new Date()) {
return false;
}

const expectedHash = (await generateHash(`${name}/${expiry}/${secret}`)).substring(0, 8);
if (hash != expectedHash) {
return false;
}

return true;
}

Date.prototype.addDays = function(days) {
var date = new Date(this.valueOf());
date.setDate(date.getDate() + days);
return date;
}

export {
generateSecureToken,
verifySecureToken,
}
15 changes: 15 additions & 0 deletions functions/api/token-verify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { verifySecureToken } from "../../functions-src/secure-token.js"

export async function onRequest(context) {
if (context.request.method !== "POST") {
return new Response("Invalid request method", { status: 405 });
}

const json = await context.request.json();
const secret = context.env.TOKEN_GENERATOR_SECRET;
const ok = await verifySecureToken({
token: json.token,
secret,
});
return Response.json({ ok });
}
16 changes: 16 additions & 0 deletions functions/secure/api/token-generate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { generateSecureToken } from "../../../functions-src/secure-token.js"

export async function onRequest(context) {
if (context.request.method !== "POST") {
return new Response("Invalid request method", { status: 405 });
}

const json = await context.request.json();
const secret = context.env.TOKEN_GENERATOR_SECRET;
const token = await generateSecureToken({
name: json.name,
expiry: json.expiry,
secret,
});
return Response.json({ token });
}
6 changes: 6 additions & 0 deletions secure/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
layout: default
title: "Secure area: index"
---

- [Token generator (for urgent contact form)](token-generator)
73 changes: 73 additions & 0 deletions secure/token-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
layout: default
title: Token Generator
---

Use this form to generate a token for the urgent contact form.

<form id="token-generation-form">
<fieldset style="margin-bottom:1em">
<label for="name" style="display:inline-block; margin-bottom:0.5em">Token name:</label>
<input type="text" name="name" id="name" placeholder="Token name" style="box-sizing:border-box; width:100%; max-width:20em" required>
</fieldset>
<fieldset style="margin-bottom:1em">
<label for="expiry" style="display:inline-block; margin-bottom:0.5em">Expiry date:</label>
<input type="date" name="expiry" id="expiry" style="box-sizing:border-box; width:100%; max-width:20em" required>
</fieldset>
<button type="submit" style="margin-bottom:1em">Generate Token</button>
</form>
<p>Your token is:</p>
<pre style="display:inline" id="token-value">...</pre>

## Token Verifier

<form id="token-verification-form">
<fieldset style="margin-bottom:1em">
<label for="name" style="display:inline-block; margin-bottom:0.5em">Token:</label>
<input type="text" name="token" id="token" placeholder="Token" style="box-sizing:border-box; width:100%; max-width:20em" required>
</fieldset>
<button type="submit" style="margin-bottom:1em">Verify Token</button>
</form>
<p>Token status: <span id="token-verify-output">?</span></p>

<script>
document.getElementById("token-generation-form").addEventListener("submit", event => {
event.preventDefault()
const formData = new FormData(event.target);
const name = formData.get("name");
const expiry = formData.get("expiry");
fetch('/secure/api/token-generate', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, expiry })
})
.then(res => res.json())
.then(res => document.getElementById("token-value").innerHTML = res.token);
});

document.getElementById("token-verification-form").addEventListener("submit", event => {
event.preventDefault()
const formData = new FormData(event.target);
const token = formData.get("token");
fetch('/api/token-verify', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ token })
})
.then(res => res.json())
.then(res => {
const element = document.getElementById("token-verify-output");
if (res.ok) {
element.innerHTML = "ok";
} else {
element.innerHTML = "BAD";
}
});
});
</script>