-
Notifications
You must be signed in to change notification settings - Fork 17
/
sessions.go
302 lines (271 loc) · 8.26 KB
/
sessions.go
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
package jeff
import (
"context"
"crypto/rand"
"encoding/base64"
"log"
"net/http"
"strings"
"time"
)
// Cookie Format
// SessionToken is already encoded and safe
// CookieName=encode(SessionKey)::SessionToken
const separator = "::"
var defaultRedirect = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
})
type contextKey struct{ name string }
var sessionKey = contextKey{name: "session"}
var now = func() time.Time {
return time.Now()
}
// Jeff holds the metadata needed to handle session management.
type Jeff struct {
s Storage
redir http.Handler
cookieName string
domain string
path string
expires time.Duration
insecure bool
samesite http.SameSite
}
// Domain sets the domain the cookie belongs to. If unset, cookie becomes a
// host-only domain, meaning subdomains won't receive the cookie.
func Domain(d string) func(*Jeff) {
return func(j *Jeff) {
j.domain = d
}
}
// CookieName sets the name of the cookie in the browser. If you want to avoid
// fingerprinting, override it here. defaults to "_gosession"
func CookieName(n string) func(*Jeff) {
return func(j *Jeff) {
j.cookieName = n
}
}
// Redirect sets the handler which gets called when authentication fails. By
// default, this redirects to '/'. It's recommended that you replace this with
// your own.
//
// sessions := jeff.New(store, jeff.Redirect(
// http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// http.Redirect(w, r, "/login", http.StatusFound)
// })))
//
// Setting this is particularly useful if you want to stop a redirect on an
// authenticated route to render a page despite the user not being
// authenticated. For example, say you want to display user information on the
// home page if they're logged in, but otherwise want to ignore the redirect
// and render the page for an anonymous user. You'd define that behavior using
// a custom handler here.
func Redirect(h http.Handler) func(*Jeff) {
return func(j *Jeff) {
j.redir = h
}
}
// Path sets the path attribute of the cookie. Defaults to '/'. You probably
// don't need to change this. See the RFC for details:
// https://tools.ietf.org/html/rfc6265
func Path(p string) func(*Jeff) {
return func(j *Jeff) {
j.path = p
}
}
// Expires sets the cookie lifetime. After logging in, the session will last
// as long as defined here and then expire. If set to 0, then Expiration is
// not set and the cookie will expire when the client closes their user agent.
// Defaults to 30 days.
func Expires(dur time.Duration) func(*Jeff) {
return func(j *Jeff) {
j.expires = dur
}
}
// Insecure unsets the Secure flag for the cookie. This is for development
// only. Doing this in production is an error.
func Insecure(j *Jeff) {
log.Println("ERROR: sessions configured insecurely. for development only")
j.insecure = true
}
// SameSite sets the SameSite attribute for the cookie. If unset, the default
// behavior is to inherit the default behavior of the http package. See the
// docs for details.
// https://pkg.go.dev/net/http?tab=doc#SameSite
func Samesite(s http.SameSite) func(*Jeff) {
return func(j *Jeff) {
j.samesite = s
}
}
// New instantiates a Jeff, applying the options provided.
func New(s Storage, opts ...func(*Jeff)) *Jeff {
j := &Jeff{
s: s,
expires: 30 * 24 * time.Hour,
}
for _, o := range opts {
o(j)
}
j.defaults()
return j
}
// Public wraps the given handler, adding the Session object (if there's an
// active session) to the request context before passing control to the next
// handler.
func (j *Jeff) Public(wrap http.Handler) http.Handler {
return j.wrap(wrap, wrap)
}
// PublicFunc wraps the given handler, adding the Session object (if there's an
// active session) to the request context before passing control to the next
// handler.
func (j *Jeff) PublicFunc(wrap http.HandlerFunc) http.HandlerFunc {
return j.wrap(wrap, wrap).ServeHTTP
}
// Wrap wraps the given handler, authenticating this route and calling the
// redirect handler if session is invalid.
func (j *Jeff) Wrap(wrap http.Handler) http.Handler {
return j.wrap(j.redir, wrap)
}
// WrapFunc wraps the given handler, authenticating this route and calling the
// redirect handler if session is invalid.
func (j *Jeff) WrapFunc(wrap http.HandlerFunc) http.HandlerFunc {
return j.wrap(j.redir, wrap).ServeHTTP
}
func (j *Jeff) wrap(redir, wrap http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie(j.cookieName)
if err != nil {
redir.ServeHTTP(w, r)
return
}
vals := strings.SplitN(c.Value, separator, 2)
if len(vals) != 2 {
redir.ServeHTTP(w, r)
return
}
decoded, err := decode(vals[0])
if err != nil {
redir.ServeHTTP(w, r)
return
}
ctx := r.Context()
s, err := j.loadOne(ctx, decoded, []byte(vals[1]))
if err != nil {
redir.ServeHTTP(w, r)
} else {
r = r.WithContext(context.WithValue(ctx, sessionKey, s))
wrap.ServeHTTP(w, r)
}
})
}
// Set the session cookie on the response. Call after successful
// authentication / login. meta optional parameter sets metadata in the
// session storage.
func (j *Jeff) Set(ctx context.Context, w http.ResponseWriter, key []byte, meta ...[]byte) error {
if len(meta) > 1 {
panic("meta must not be longer than 1")
}
secure := genRandomString(24) // 192 bits
c := &http.Cookie{
Secure: !j.insecure,
HttpOnly: true,
Name: j.cookieName,
Value: strings.Join([]string{encode(key), secure}, separator),
Path: j.path,
Domain: j.domain,
SameSite: j.samesite,
}
var exp time.Time
if j.expires != 0 {
exp = now().Add(j.expires)
c.Expires = exp
} else {
// For session cookies which expire "when the browser closes"
exp = now().Add(30 * 24 * time.Hour)
}
http.SetCookie(w, c)
var m []byte
if len(meta) == 1 {
m = meta[0]
}
return j.store(ctx, Session{
Key: key,
Token: []byte(secure),
Exp: exp,
Meta: m,
})
}
// Clear the session in the context for the given key.
func (j *Jeff) Clear(ctx context.Context, w http.ResponseWriter) error {
s := ActiveSession(ctx)
c := &http.Cookie{
Secure: !j.insecure,
HttpOnly: true,
Name: j.cookieName,
Value: "deleted",
Path: j.path,
Domain: j.domain,
Expires: time.Time{},
}
http.SetCookie(w, c)
if len(s.Key) > 0 {
// TODO: a bit worried about corrupt (empty) tokens.
return j.clear(ctx, s.Key, s.Token)
}
return nil
}
// Delete the session for the given key.
func (j *Jeff) Delete(ctx context.Context, key []byte, tokens ...[]byte) error {
return j.clear(ctx, key, tokens...)
}
// ActiveSession returns the currently active session on the context. If there
// is no active session on the context, it returns an empty session object.
func ActiveSession(ctx context.Context) Session {
if v, ok := ctx.Value(sessionKey).(Session); ok {
return v
}
return Session{}
}
// SessionsForKey returns the list of active sessions that exist in the
// backend for the given key. The result may have stale (expired) sessions.
func (j *Jeff) SessionsForKey(ctx context.Context, key []byte) (SessionList, error) {
return j.load(ctx, key)
}
func (j *Jeff) defaults() {
if j.redir == nil {
j.redir = defaultRedirect
}
if j.cookieName == "" {
j.cookieName = "_gosession"
}
if j.path == "" {
j.path = "/"
}
}
// From: https://blog.questionable.services/article/generating-secure-random-numbers-crypto-rand/
// genRandomString returns a URL-safe, base64 encoded securely generated random
// string. It will return an error if the system's secure random number
// generator fails to function correctly, in which case the caller should not
// continue.
func genRandomString(n int) string {
b := genRandomBytes(n)
return encode(b)
}
// genRandomBytes returns securely generated random bytes. It will return an
// error if the system's secure random number generator fails to function
// correctly, in which case the caller should not continue.
func genRandomBytes(n int) []byte {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err != nil when we fail to read len(b) bytes.
if err != nil {
panic(err)
}
return b
}
func encode(b []byte) string {
return base64.RawURLEncoding.EncodeToString(b)
}
func decode(s string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(s)
}