feat(security): #286 use csrf token for delete request
All checks were successful
Build Docker Image / Build-Docker-Image (push) Successful in 45s
Build and Push Docker Image / Build-And-Push-Docker-Image (push) Successful in 50s

This commit was merged in pull request #304.
This commit is contained in:
2024-12-11 15:47:29 +01:00
parent 8cf2210aaf
commit 12d7c13b02
4 changed files with 82 additions and 87 deletions

View File

@@ -86,16 +86,7 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc {
return nil, err return nil, err
} }
cookie := http.Cookie{ cookie := middleware.CreateSessionCookie(session.Id)
Name: "id",
Value: session.Id,
MaxAge: 60 * 60 * 8, // 8 hours
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
return session.User, nil return session.User, nil

View File

@@ -30,6 +30,7 @@ func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
if err == nil { if err == nil {
csrfField := fmt.Sprintf(`<input type="hidden" name="csrf-token" value="%s">`, csrfToken) csrfField := fmt.Sprintf(`<input type="hidden" name="csrf-token" value="%s">`, csrfToken)
dataStr = strings.ReplaceAll(dataStr, "</form>", csrfField+"</form>") dataStr = strings.ReplaceAll(dataStr, "</form>", csrfField+"</form>")
dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", csrfToken)
} }
} }
@@ -52,30 +53,21 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler
r.Method == http.MethodPatch { r.Method == http.MethodPatch {
csrfToken := r.FormValue("csrf-token") csrfToken := r.FormValue("csrf-token")
if csrfToken == "" {
csrfToken = r.Header.Get("csrf-token")
}
if csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) { if csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) {
http.Error(w, "", http.StatusForbidden) http.Error(w, "", http.StatusForbidden)
return return
} }
} }
if session == nil { if session == nil && (strings.Contains(r.RequestURI, "/auth/signup") || strings.Contains(r.RequestURI, "/auth/signin")) {
var err error session, _ = auth.SignInAnonymous()
session, err = auth.SignInAnonymous()
if err != nil { cookie := CreateSessionCookie(session.Id)
http.Error(w, "", http.StatusInternalServerError) http.SetCookie(w, &cookie)
return
}
} }
cookie := http.Cookie{
Name: "id",
Value: session.Id,
MaxAge: 60 * 60 * 8, // 8 hours
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
http.SetCookie(w, &cookie)
responseWriter := newCsrfResponseWriter(w, auth, session) responseWriter := newCsrfResponseWriter(w, auth, session)
next.ServeHTTP(responseWriter, r) next.ServeHTTP(responseWriter, r)

View File

@@ -0,0 +1,15 @@
package middleware
import "net/http"
func CreateSessionCookie(sessionId string) http.Cookie {
return http.Cookie{
Name: "id",
Value: sessionId,
MaxAge: 60 * 60 * 8, // 8 hours
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
}

View File

@@ -1,73 +1,70 @@
package workout package workout
templ WorkoutComp(currentDate string) { templ WorkoutComp(currentDate string) {
<main class="mx-2"> <main class="mx-2">
<form <form class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10" hx-post="/api/workout"
class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10" hx-target="#workout-placeholder" hx-swap="outerHTML">
hx-post="/api/workout" <h2 class="text-4xl mb-8">Track your workout</h2>
hx-target="#workout-placeholder" <input id="date" type="date" class="input input-bordered" value={ currentDate } name="date" />
hx-swap="outerHTML" <select class="select select-bordered w-full" name="type">
> <option>Push Ups</option>
<h2 class="text-4xl mb-8">Track your workout</h2> <option>Pull Ups</option>
<input id="date" type="date" class="input input-bordered" value={ currentDate } name="date"/> </select>
<select class="select select-bordered w-full" name="type"> <input type="number" class="input input-bordered" placeholder="Sets" name="sets" />
<option>Push Ups</option> <input type="number" class="input input-bordered" placeholder="Reps" name="reps" />
<option>Pull Ups</option> <button class="btn btn-primary self-end">Save</button>
</select> </form>
<input type="number" class="input input-bordered" placeholder="Sets" name="sets"/> <div hx-get="/api/workout" hx-trigger="load"></div>
<input type="number" class="input input-bordered" placeholder="Reps" name="reps"/> </main>
<button class="btn btn-primary self-end">Save</button>
</form>
<div hx-get="/api/workout" hx-trigger="load"></div>
</main>
} }
type Workout struct { type Workout struct {
Id string Id string
Date string Date string
Type string Type string
Sets string Sets string
Reps string Reps string
} }
templ WorkoutListComp(workouts []Workout) { templ WorkoutListComp(workouts []Workout) {
<div class="overflow-x-auto mx-auto max-w-screen-lg"> <div class="overflow-x-auto mx-auto max-w-screen-lg">
<h2 class="text-4xl mt-14 mb-8">Workout history</h2> <h2 class="text-4xl mt-14 mb-8">Workout history</h2>
<table class="table table-auto max-w-full"> <table class="table table-auto max-w-full">
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Date</th>
<th>Type</th> <th>Type</th>
<th>Sets</th> <th>Sets</th>
<th>Reps</th> <th>Reps</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr class="hidden" id="workout-placeholder"></tr> <tr class="hidden" id="workout-placeholder"></tr>
for _,w := range workouts { for _,w := range workouts {
@WorkoutItemComp(w, false) @WorkoutItemComp(w, false)
} }
</tbody> </tbody>
</table> </table>
</div> </div>
} }
templ WorkoutItemComp(w Workout, includePlaceholder bool) { templ WorkoutItemComp(w Workout, includePlaceholder bool) {
if includePlaceholder { if includePlaceholder {
<tr class="hidden" id="workout-placeholder"></tr> <tr class="hidden" id="workout-placeholder"></tr>
} }
<tr> <tr>
<th>{ w.Date }</th> <th>{ w.Date }</th>
<th>{ w.Type }</th> <th>{ w.Type }</th>
<th>{ w.Sets }</th> <th>{ w.Sets }</th>
<th>{ w.Reps }</th> <th>{ w.Reps }</th>
<th> <th>
<div class="tooltip" data-tip="Delete Entry"> <div class="tooltip" data-tip="Delete Entry">
<button hx-delete={ "api/workout/" + w.Id } hx-target="closest tr"> <button hx-headers='{"csrf-token": "CSRF_TOKEN"}' hx-delete={ "api/workout/" + w.Id } hx-target="closest tr"
Delete type="submit">
</button> Delete
</div> </button>
</th> </div>
</tr> </th>
</tr>
} }