Photoblog: CSRF protection

This commit is contained in:
Meutel 2017-07-30 11:05:20 +02:00
parent f0bf40f412
commit 2daba7c575
5 changed files with 51 additions and 14 deletions

View File

@ -13,6 +13,7 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
) )
@ -29,15 +30,17 @@ type AuthCookie struct {
Store *sessions.CookieStore Store *sessions.CookieStore
DataDir *os.File DataDir *os.File
PasswordSecret string PasswordSecret string
CsrfSecret string
} }
// Constructor AuthCookie // Constructor AuthCookie
func NewAuthCookie(tpl *template.Template, sessionSecret, passwordSecret string, data *os.File) *AuthCookie { func NewAuthCookie(tpl *template.Template, sessionSecret, passwordSecret, csrfSecret string, data *os.File) *AuthCookie {
app := &AuthCookie{ app := &AuthCookie{
Templates: tpl, Templates: tpl,
Store: sessions.NewCookieStore([]byte(sessionSecret)), Store: sessions.NewCookieStore([]byte(sessionSecret)),
DataDir: data, DataDir: data,
PasswordSecret: passwordSecret, PasswordSecret: passwordSecret,
CsrfSecret: csrfSecret,
} }
app.Store.Options = &sessions.Options{ app.Store.Options = &sessions.Options{
Secure: true, Secure: true,
@ -47,6 +50,18 @@ func NewAuthCookie(tpl *template.Template, sessionSecret, passwordSecret string,
return app return app
} }
type LoginModel struct {
Username string
UsernameError error
PasswordError error
CsrfToken string
}
// Csrf handler wrapper
func (app *AuthCookie) Protect() func(http.Handler) http.Handler {
return csrf.Protect([]byte(app.CsrfSecret), csrf.Secure(true), csrf.HttpOnly(true))
}
// Verify Username // Verify Username
func (app *AuthCookie) VerifyUsername(username string) error { func (app *AuthCookie) VerifyUsername(username string) error {
if username == "" { if username == "" {
@ -130,18 +145,20 @@ func (app *AuthCookie) RedirectHome(res http.ResponseWriter, req *http.Request)
// login: form // login: form
func (app *AuthCookie) LoginPage(res http.ResponseWriter, req *http.Request) { func (app *AuthCookie) LoginPage(res http.ResponseWriter, req *http.Request) {
formErr := make(map[string]error) model := LoginModel{
CsrfToken: csrf.Token(req),
}
if req.Method == http.MethodPost { if req.Method == http.MethodPost {
username := req.FormValue(USER) model.Username = req.FormValue(USER)
formErr[USER] = app.VerifyUsername(username) model.UsernameError = app.VerifyUsername(model.Username)
formErr[PWD] = app.VerifyPassword(username, req.FormValue(PWD), res, req) model.PasswordError = app.VerifyPassword(model.Username, req.FormValue(PWD), res, req)
if formErr[USER] == nil && formErr[PWD] == nil { if model.UsernameError == nil && model.PasswordError == nil {
app.SaveUsername(username, res, req) app.SaveUsername(model.Username, res, req)
app.RedirectHome(res, req) app.RedirectHome(res, req)
return return
} }
} }
app.Templates.ExecuteTemplate(res, "login.html", formErr) app.Templates.ExecuteTemplate(res, "login.html", model)
} }
// logout: delete session and redirect to home // logout: delete session and redirect to home

View File

@ -17,6 +17,7 @@ import (
type PhotoBlogConfig struct { type PhotoBlogConfig struct {
SessionSecret string `json:"session-secret"` SessionSecret string `json:"session-secret"`
PasswordSecret string `json:"password-secret"` PasswordSecret string `json:"password-secret"`
CsrfSecret string `json:"csrf-secret"`
DataDir string `json:"data-dir"` DataDir string `json:"data-dir"`
} }
@ -58,7 +59,7 @@ func main() {
} }
app := photo.PhotoBlog{ app := photo.PhotoBlog{
*admin.NewAuthCookie(tpl, conf.SessionSecret, conf.PasswordSecret, data), *admin.NewAuthCookie(tpl, conf.SessionSecret, conf.PasswordSecret, conf.CsrfSecret, data),
} }
http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
@ -68,8 +69,9 @@ func main() {
app.HomePage(res, req) app.HomePage(res, req)
} }
}) })
CSRF := app.Protect()
http.HandleFunc("/upload", app.UploadPage) http.HandleFunc("/upload", app.UploadPage)
http.HandleFunc("/login", app.LoginPage) http.HandleFunc("/login", app.LoginPage)
http.HandleFunc("/logout", app.LogoutPage) http.HandleFunc("/logout", app.LogoutPage)
http.ListenAndServeTLS(":9443", "../cert.pem", "../key.pem", context.ClearHandler(http.DefaultServeMux)) http.ListenAndServeTLS(":9443", "../cert.pem", "../key.pem", CSRF(context.ClearHandler(http.DefaultServeMux)))
} }

View File

@ -11,6 +11,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/gorilla/csrf"
"meutel.net/meutel/go-examples/photoblog/admin" "meutel.net/meutel/go-examples/photoblog/admin"
) )
@ -57,6 +59,20 @@ func NewPhotoHomeModel(username string, photos []string) *PhotoHomeModel {
} }
} }
// Upload page model
type PhotoUploadModel struct {
PhotoModel
CsrfToken string
}
// Constructor PhotoUploadModel
func NewPhotoUploadModel(username, tok string) *PhotoUploadModel {
return &PhotoUploadModel{
PhotoModel: *NewPhotoModel(username),
CsrfToken: tok,
}
}
// File and date of last modification // File and date of last modification
type TimedFile struct { type TimedFile struct {
Mod time.Time Mod time.Time
@ -164,7 +180,7 @@ func (app *PhotoBlog) UploadPage(res http.ResponseWriter, req *http.Request) {
app.RedirectHome(res, req) app.RedirectHome(res, req)
return return
} }
data := NewPhotoModel(app.Username(res, req)) data := NewPhotoUploadModel(app.Username(res, req), csrf.Token(req))
if req.Method == http.MethodPost { if req.Method == http.MethodPost {
message, err := app.AddPhoto(res, req) message, err := app.AddPhoto(res, req)
data.Err = err data.Err = err

View File

@ -2,13 +2,14 @@
<h1>Authentification</h1> <h1>Authentification</h1>
<div class="form-container"> <div class="form-container">
<form action="/login" method="POST"> <form action="/login" method="POST">
<input type="hidden" name="gorilla.csrf.Token" value="{{ .CsrfToken }}">
<label> <label>
Username: {{ if .username }}<b class="error">{{ .username }}</b>{{ end }} Username: {{ if .UsernameError }}<b class="error">{{ .UsernameError }}</b>{{ end }}
<input type="text" name="username" placeholder="login..."> <input type="text" name="username" value="{{ .Username }}" placeholder="login...">
</label> </label>
<br> <br>
<label> <label>
Password: {{ if .password }}<b class="error">{{ .password }}</b>{{ end }} Password: {{ if .PasswordError }}<b class="error">{{ .PasswordError }}</b>{{ end }}
<input type="password" name="password" placeholder="password..."> <input type="password" name="password" placeholder="password...">
</label> </label>
<br> <br>

View File

@ -2,6 +2,7 @@
<h1>Send photo</h1> <h1>Send photo</h1>
<div class="form-container"> <div class="form-container">
<form action="/upload" method="POST" enctype="multipart/form-data"> <form action="/upload" method="POST" enctype="multipart/form-data">
<input type="hidden" name="gorilla.csrf.Token" value="{{ .CsrfToken }}">
{{ if .Message }}<p class="message">{{ .Message }}</p>{{ end }} {{ if .Message }}<p class="message">{{ .Message }}</p>{{ end }}
{{ if .Err }}<p class="error">{{ .Err }}</p>{{ end }} {{ if .Err }}<p class="error">{{ .Err }}</p>{{ end }}
<label> <label>