feat: add CSRF tokens to templates and JS fetch calls

This commit is contained in:
Johan Lundberg 2026-02-19 14:03:34 +01:00
parent d1f2b39cb6
commit 9e5773f52f
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
6 changed files with 14 additions and 4 deletions

View file

@ -14,6 +14,11 @@ function bytesToBase64url(bytes) {
return btoa(raw).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); return btoa(raw).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
} }
function getCsrfToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
}
async function beginRegistration() { async function beginRegistration() {
const statusEl = document.getElementById('webauthn-status'); const statusEl = document.getElementById('webauthn-status');
@ -21,7 +26,7 @@ async function beginRegistration() {
// Step 1: Get options from server // Step 1: Get options from server
const beginRes = await fetch('/manage/credentials/webauthn/begin', { const beginRes = await fetch('/manage/credentials/webauthn/begin', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() },
}); });
if (!beginRes.ok) { if (!beginRes.ok) {
if (statusEl) statusEl.innerHTML = '<div role="alert">Failed to start registration</div>'; if (statusEl) statusEl.innerHTML = '<div role="alert">Failed to start registration</div>';
@ -57,7 +62,7 @@ async function beginRegistration() {
// Step 5: Send to server // Step 5: Send to server
const completeRes = await fetch('/manage/credentials/webauthn/complete', { const completeRes = await fetch('/manage/credentials/webauthn/complete', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
@ -115,7 +120,7 @@ async function beginAuthentication() {
// Step 5: Send to server // Step 5: Send to server
const completeRes = await fetch('/login/webauthn/complete', { const completeRes = await fetch('/login/webauthn/complete', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });

View file

@ -3,11 +3,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token_processor(request) }}">
<title>{% block title %}Porchlight{% endblock %}</title> <title>{% block title %}Porchlight{% endblock %}</title>
<link rel="icon" type="image/png" href="/static/favicon.png"> <link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
</head> </head>
<body> <body hx-headers='{"X-CSRF-Token": "{{ csrf_token_processor(request) }}"}'>
<a class="skip-link" href="#main">Skip to content</a> <a class="skip-link" href="#main">Skip to content</a>
<header class="site-header"> <header class="site-header">
<a href="/"> <a href="/">

View file

@ -8,6 +8,7 @@
<p>This application is requesting access to your account.</p> <p>This application is requesting access to your account.</p>
<form method="post" action="/consent"> <form method="post" action="/consent">
<input type="hidden" name="csrf_token" value="{{ csrf_token_processor(request) }}">
<fieldset> <fieldset>
<legend>Permissions requested</legend> <legend>Permissions requested</legend>
<ul class="scope-list" role="list"> <ul class="scope-list" role="list">

View file

@ -10,6 +10,7 @@
<section> <section>
<h2>Password</h2> <h2>Password</h2>
<form hx-post="/login/password" hx-target="#login-error" hx-swap="innerHTML"> <form hx-post="/login/password" hx-target="#login-error" hx-swap="innerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token_processor(request) }}">
<div> <div>
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username"> <input type="text" id="username" name="username" required autocomplete="username">

View file

@ -39,6 +39,7 @@
<p>No password set.</p> <p>No password set.</p>
{% endif %} {% endif %}
<form hx-post="/manage/credentials/password" hx-target="#password-section" hx-swap="innerHTML"> <form hx-post="/manage/credentials/password" hx-target="#password-section" hx-swap="innerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token_processor(request) }}">
<div> <div>
<label for="password">{{ "New password" if has_password else "Set password" }}</label> <label for="password">{{ "New password" if has_password else "Set password" }}</label>
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password"> <input type="password" id="password" name="password" required minlength="8" autocomplete="new-password">

View file

@ -14,6 +14,7 @@
<h2>Personal information</h2> <h2>Personal information</h2>
<div id="profile-section"> <div id="profile-section">
<form hx-post="/manage/profile" hx-target="#profile-status" hx-swap="innerHTML"> <form hx-post="/manage/profile" hx-target="#profile-status" hx-swap="innerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token_processor(request) }}">
<div> <div>
<label for="given_name">Given name</label> <label for="given_name">Given name</label>
<input type="text" id="given_name" name="given_name" value="{{ user.given_name or '' }}" maxlength="255" autocomplete="given-name"> <input type="text" id="given_name" name="given_name" value="{{ user.given_name or '' }}" maxlength="255" autocomplete="given-name">