Skip to content

Commit 1467221

Browse files
committed
fix: resolve CSRF token issues in authentication forms
- Add hidden CSRF token fields to login, register, and logout forms - Fix form binding by adding form: struct tags to LoginRequest and RegisterRequest - Enhance JavaScript CSRF handling with automatic token population and rotation - Improve error handling with specific status code messages and token refresh - Update CSRF token initialization to use safer endpoint for token fetching
1 parent c052aad commit 1467221

File tree

5 files changed

+66
-19
lines changed

5 files changed

+66
-19
lines changed

cookies.jar

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Netscape HTTP Cookie File
2+
# https://curl.se/docs/http-cookies.html
3+
# This file was generated by libcurl! Edit at your own risk.
4+
5+
#HttpOnly_localhost FALSE / FALSE 1754705477 _csrf 22e337af7343f1f0167d8033f151725a03ee3177c8d721b6c87a5bfb0d76d5e5

cookies.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Netscape HTTP Cookie File
2+
# https://curl.se/docs/http-cookies.html
3+
# This file was generated by libcurl! Edit at your own risk.
4+
5+
#HttpOnly_localhost FALSE / FALSE 1754705155 _csrf b296543fc6637c439c9cd027996129ca4cb9a00933d851157a91683d7a4b168d

internal/handler/auth.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@ func NewAuthHandler(s *store.Store, authService *middleware.AuthService) *AuthHa
2626

2727
// LoginRequest represents a login request
2828
type LoginRequest struct {
29-
Email string `json:"email" validate:"required,email"`
30-
Password string `json:"password" validate:"required,min=1"`
29+
Email string `json:"email" form:"email" validate:"required,email"`
30+
Password string `json:"password" form:"password" validate:"required,min=1"`
3131
}
3232

3333
// RegisterRequest represents a registration request
3434
type RegisterRequest struct {
35-
Email string `json:"email" validate:"required,email"`
36-
Name string `json:"name" validate:"required,min=2,max=100"`
37-
Password string `json:"password" validate:"required,password"`
38-
ConfirmPassword string `json:"confirm_password" validate:"required"`
39-
Bio string `json:"bio,omitempty" validate:"max=500"`
40-
AvatarURL string `json:"avatar_url,omitempty" validate:"omitempty,url"`
35+
Email string `json:"email" form:"email" validate:"required,email"`
36+
Name string `json:"name" form:"name" validate:"required,min=2,max=100"`
37+
Password string `json:"password" form:"password" validate:"required,password"`
38+
ConfirmPassword string `json:"confirm_password" form:"confirm_password" validate:"required"`
39+
Bio string `json:"bio,omitempty" form:"bio" validate:"max=500"`
40+
AvatarURL string `json:"avatar_url,omitempty" form:"avatar_url" validate:"omitempty,url"`
4141
}
4242

4343
// Validate implements custom validation for RegisterRequest

internal/view/auth.templ

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ templ LoginContent() {
2626
<p>Welcome back! Please sign in to your account.</p>
2727
</hgroup>
2828
<form hx-post="/auth/login" hx-swap="none" hx-indicator="#login-spinner">
29+
<input type="hidden" name="csrf_token" id="csrf-token-login"/>
2930
<div>
3031
<label for="email">Email Address</label>
3132
<input
@@ -82,6 +83,7 @@ templ RegisterContent() {
8283
<p>Join us! Create your account to get started.</p>
8384
</hgroup>
8485
<form hx-post="/auth/register" hx-swap="none" hx-indicator="#register-spinner">
86+
<input type="hidden" name="csrf_token" id="csrf-token-register"/>
8587
<div>
8688
<label for="name">Full Name</label>
8789
<input
@@ -198,14 +200,18 @@ templ ProfileContent(user middleware.User) {
198200
<footer>
199201
<div role="group">
200202
<button class="secondary outline">Edit Profile</button>
201-
<button
202-
hx-post="/auth/logout"
203-
hx-swap="none"
204-
class="outline"
205-
hx-confirm="Are you sure you want to log out?"
206-
>
207-
Logout
208-
</button>
203+
<form style="display: inline;">
204+
<input type="hidden" name="csrf_token" id="csrf-token-logout"/>
205+
<button
206+
hx-post="/auth/logout"
207+
hx-swap="none"
208+
class="outline"
209+
hx-confirm="Are you sure you want to log out?"
210+
type="submit"
211+
>
212+
Logout
213+
</button>
214+
</form>
209215
</div>
210216
</footer>
211217
</article>

internal/view/layout/base.templ

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,17 +145,26 @@ templ BaseWithCSRF(title, csrfToken string) {
145145
// Track current CSRF token
146146
let currentCSRFToken = null;
147147

148+
// Update hidden CSRF token fields
149+
const updateCSRFTokenFields = (token) => {
150+
const csrfFields = document.querySelectorAll('input[name="csrf_token"]');
151+
csrfFields.forEach(field => {
152+
field.value = token;
153+
});
154+
};
155+
148156
// Initialize CSRF token from page load or fetch it
149157
const initializeCSRFToken = () => {
150158
// First try to get token from a meta tag (set by server)
151159
const metaToken = document.querySelector('meta[name="csrf-token"]');
152160
if (metaToken) {
153161
currentCSRFToken = metaToken.getAttribute('content');
162+
updateCSRFTokenFields(currentCSRFToken);
154163
return;
155164
}
156165

157-
// If no meta token, make a request to get one
158-
fetch('/users', {
166+
// If no meta token, make a request to get one from a safe endpoint
167+
fetch('/', {
159168
method: 'GET',
160169
headers: {
161170
'X-Requested-With': 'XMLHttpRequest'
@@ -164,6 +173,7 @@ templ BaseWithCSRF(title, csrfToken string) {
164173
const token = response.headers.get('X-CSRF-Token');
165174
if (token) {
166175
currentCSRFToken = token;
176+
updateCSRFTokenFields(currentCSRFToken);
167177
}
168178
}).catch(e => {
169179
console.warn('Failed to initialize CSRF token:', e);
@@ -185,6 +195,7 @@ templ BaseWithCSRF(title, csrfToken string) {
185195
const newToken = evt.detail.xhr.getResponseHeader('X-CSRF-Token');
186196
if (newToken) {
187197
currentCSRFToken = newToken;
198+
updateCSRFTokenFields(currentCSRFToken);
188199
}
189200
});
190201

@@ -228,13 +239,33 @@ templ BaseWithCSRF(title, csrfToken string) {
228239
// Re-initialize theme
229240
const savedTheme = localStorage.getItem('preferred-theme') || 'dark';
230241
setTheme(savedTheme);
242+
243+
// Update CSRF tokens in new content
244+
if (currentCSRFToken) {
245+
updateCSRFTokenFields(currentCSRFToken);
246+
}
231247
}
232248
});
233249

234250
// Handle errors
235251
document.body.addEventListener('htmx:responseError', function(evt) {
236252
pageLoading.classList.remove('active');
237-
showFlash('Failed to load page. Please try again.', 'error');
253+
let errorMessage = 'Request failed. Please try again.';
254+
255+
// Handle specific error codes
256+
if (evt.detail.xhr.status === 403) {
257+
errorMessage = 'Access forbidden. Please refresh the page and try again.';
258+
// Try to refresh CSRF token
259+
initializeCSRFToken();
260+
} else if (evt.detail.xhr.status === 400) {
261+
errorMessage = 'Invalid request. Please check your input and try again.';
262+
} else if (evt.detail.xhr.status === 401) {
263+
errorMessage = 'Authentication required. Please log in.';
264+
} else if (evt.detail.xhr.status >= 500) {
265+
errorMessage = 'Server error. Please try again later.';
266+
}
267+
268+
showFlash(errorMessage, 'error');
238269
});
239270

240271
document.body.addEventListener('htmx:timeout', function(evt) {

0 commit comments

Comments
 (0)