Skip to content

Commit

Permalink
Add token generator protected by Cloudflare Access (#43)
Browse files Browse the repository at this point in the history
* WIP: token generator

* Fix FE JS

* Add token to DOM, take input from JSON

* Make token preformatted

* Added secure area index

* Added verification functionality

* Add missing await

* Added brackets

* Move token-verify function

* Add heading for token verification form
  • Loading branch information
BenjaminEHowe authored Jun 20, 2024
1 parent ac1219b commit f7996fa
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 0 deletions.
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>

0 comments on commit f7996fa

Please sign in to comment.