-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcsrf.go
More file actions
231 lines (203 loc) · 6.68 KB
/
Copy pathcsrf.go
File metadata and controls
231 lines (203 loc) · 6.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package form
import (
"context"
"errors"
"net/http"
"time"
"github.com/donseba/go-form/v2/csrf"
)
// CSRF errors
var (
DefaultCSRFField = "_csrf"
)
// CSRFOptions configures how the CSRF middleware behaves
type CSRFOptions struct {
// ErrorHandler lets you customize error handling instead of returning HTTP errors
ErrorHandler func(w http.ResponseWriter, r *http.Request, err error)
}
// DefaultCSRFOptions returns the default options for CSRF protection
func DefaultCSRFOptions() CSRFOptions {
return CSRFOptions{
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, csrf.ErrTokenMismatch):
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
case errors.Is(err, csrf.ErrTokenExpired):
http.Error(w, "CSRF token expired", http.StatusForbidden)
case errors.Is(err, csrf.ErrKeyOrTokenEmpty):
http.Error(w, "CSRF token or session ID is empty", http.StatusBadRequest)
case errors.Is(err, csrf.ErrTokenNotFound):
http.Error(w, "CSRF token not found", http.StatusBadRequest)
default:
http.Error(w, "CSRF validation error: "+err.Error(), http.StatusBadRequest)
}
},
}
}
// CSRFMiddleware creates middleware for CSRF protection with default options
func (f *Form) CSRFMiddleware() func(next http.Handler) http.Handler {
return f.CSRFMiddlewareWithOptions(DefaultCSRFOptions())
}
// CSRFMiddlewareWithOptions creates middleware for CSRF protection with custom options
func (f *Form) CSRFMiddlewareWithOptions(options CSRFOptions) func(next http.Handler) http.Handler {
if !f.HasCSRFStore() {
f.SetCSRFStore(csrf.NewMemoryCSRFStore())
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get session key (from cookie or create one)
sessionID, err := getOrCreateSessionID(w, r)
if err != nil {
if options.ErrorHandler != nil {
options.ErrorHandler(w, r, err)
return
}
http.Error(w, "Session error", http.StatusInternalServerError)
return
}
// Store the session ID in the context
ctx := context.WithValue(r.Context(), csrf.SessionIDContextKey, sessionID)
r = r.WithContext(ctx)
// For safe methods (GET, HEAD), generate and store a token
if r.Method == http.MethodGet || r.Method == http.MethodHead {
token, err := csrf.GenerateCSRFToken()
if err != nil {
if options.ErrorHandler != nil {
options.ErrorHandler(w, r, err)
return
}
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
return
}
// Store the token
err = f.GetCSRFStore().Store(sessionID, token)
if err != nil {
if options.ErrorHandler != nil {
options.ErrorHandler(w, r, err)
return
}
http.Error(w, "Failed to store CSRF token", http.StatusInternalServerError)
return
}
// Add token to context
ctx = context.WithValue(r.Context(), csrf.CSRFTokenContextKey, token)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// For unsafe methods, validate the token
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete || r.Method == http.MethodPatch {
submittedToken := r.FormValue(DefaultCSRFField)
if submittedToken == "" {
if options.ErrorHandler != nil {
options.ErrorHandler(w, r, errors.New("token not found"))
return
}
http.Error(w, "Missing CSRF token", http.StatusBadRequest)
return
}
// Validate the token
err := f.GetCSRFStore().Validate(sessionID, submittedToken)
if err != nil {
if options.ErrorHandler != nil {
options.ErrorHandler(w, r, err)
return
}
if errors.Is(err, csrf.ErrTokenMismatch) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
} else {
http.Error(w, "CSRF validation error", http.StatusInternalServerError)
}
return
}
// Generate a fresh token for the next request
token, err := csrf.GenerateCSRFToken()
if err != nil {
if options.ErrorHandler != nil {
options.ErrorHandler(w, r, err)
return
}
http.Error(w, "Failed to generate CSRF token", http.StatusInternalServerError)
return
}
// Store the token
err = f.GetCSRFStore().Store(sessionID, token)
if err != nil {
if options.ErrorHandler != nil {
options.ErrorHandler(w, r, err)
return
}
http.Error(w, "Failed to store CSRF token", http.StatusInternalServerError)
return
}
// Add token to context
ctx = context.WithValue(r.Context(), csrf.CSRFTokenContextKey, token)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// For other methods, just pass through
next.ServeHTTP(w, r)
})
}
}
// Helper function to get or create a session ID
func getOrCreateSessionID(w http.ResponseWriter, r *http.Request) (string, error) {
// Check for existing session cookie
cookie, err := r.Cookie(csrf.DefaultSessionID)
if err == nil && cookie.Value != "" {
return cookie.Value, nil
}
// Create a new session ID
sessionID, err := csrf.GenerateCSRFToken()
if err != nil {
return "", err
}
// Set the cookie
http.SetCookie(w, &http.Cookie{
Name: csrf.DefaultSessionID,
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: r.TLS != nil, // Set secure if using HTTPS
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(csrf.DefaultExpirationTime),
})
return sessionID, nil
}
// getSessionID retrieves the session ID from the request context or cookie
func getSessionID(r *http.Request) (string, error) {
// First check if sessionID is in context
if sessionID, ok := r.Context().Value(csrf.SessionIDContextKey).(string); ok && sessionID != "" {
return sessionID, nil
}
// Then check for cookie
cookie, err := r.Cookie(csrf.DefaultSessionID)
if err != nil || cookie.Value == "" {
return "", errors.New("session ID not found")
}
return cookie.Value, nil
}
// GetCSRFToken retrieves the CSRF token from the request context
func GetCSRFToken(r *http.Request) (string, bool) {
token, ok := r.Context().Value(csrf.CSRFTokenContextKey).(string)
return token, ok
}
// InjectCSRFToken adds the CSRF token to the form Info struct
func InjectCSRFToken(r *http.Request, info *Info) {
if r == nil {
return
}
InjectCSRFTokenContext(r.Context(), info)
}
// InjectCSRFTokenContext adds the CSRF token from ctx to the form Info struct.
// It is useful when rendering happens below the HTTP controller layer.
func InjectCSRFTokenContext(ctx context.Context, info *Info) {
if ctx == nil || info == nil {
return
}
if token, ok := ctx.Value(csrf.CSRFTokenContextKey).(string); ok && token != "" {
info.CsrfValue = token
if info.CsrfField == "" {
info.CsrfField = DefaultCSRFField
}
}
}