-
Notifications
You must be signed in to change notification settings - Fork 1
/
south.go
213 lines (179 loc) · 6.15 KB
/
south.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
// Copyright 2015 Ayke van Laethem. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
// Package south provides stateless HTTP authentication using cookies.
//
// It works by saving the user ID and creation time to a cookie, signed with
// HMAC-256. This cookie can later be verified.
// Note: this package only signs the cookie, it doesn't encrypt it. Therefore,
// the user ID and the creation time (in seconds) will be visible.
//
// The user ID must be able to fit in a cookie value and not contain a colon.
// This means simple identifiers (including numbers) are allowed, but also
// e-mail adresses as defined by the HTML5 spec:
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address.
package south
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"net/http"
"strconv"
"strings"
"time"
)
var (
ErrInvalidId = errors.New("south: user ID contains invalid characters")
ErrInvalidToken = errors.New("south: invalid token")
ErrExpiredToken = errors.New("south: token expired")
ErrKeySize = errors.New("south: key does not have the right size")
)
// KeySize is the minimum size of the HMAC-SHA256 key.
const KeySize = sha256.Size
// DefaultDuration is the default session duration for a session store.
const DefaultDuration = 7 * 86400 // seven days
const DefaultCookieName = "sessionToken"
// Store saves authentication tokens inside cookies.
type Store struct {
// The time after which tokens will expire, defaulting to DefaultDuration.
Duration int
// CookieName is the cookie name returned by Token.Cookie(), defaulting to
// DefaultCookieName.
CookieName string
// cookiePath is the path for the cookie returned by Token.Cookie().
cookiePath string
// HMAC key
key []byte
}
// Token is a single authentication token for one user ID.
type Token struct {
auth *Store
id string
}
// GenerateKey returns a new key of the right size for use by the session store.
func GenerateKey() ([]byte, error) {
key := make([]byte, KeySize)
_, err := rand.Read(key)
if err != nil {
return nil, err
}
return key, nil
}
// New returns a new session store.
// A new key can be generated using GenerateKey().
// Returns an error if the key does not have the right length.
func New(key []byte, path string) (*Store, error) {
// The cookie path must not be left empty
if len(key) != KeySize {
return nil, ErrKeySize
}
return &Store{
Duration: DefaultDuration,
CookieName: DefaultCookieName,
cookiePath: path,
key: key,
}, nil
}
// NewToken returns a new Token for this user ID. An error may be returned if
// the id doesn't adhere to the requirements (see package documentation for
// requirements on token IDs).
func (s *Store) NewToken(id string) (*Token, error) {
if !validId(id) {
return nil, ErrInvalidId
}
return &Token{s, id}, nil
}
// Cookie returns a new cookie that can be appended to a request. You may want
// to regenerate the cookie for each request, to keep the session alive.
//
// The returned cookie is secure by default: the 'secure' and 'httpOnly' flags
// are set. If you want to use this cookie over plain HTTP without SSL (making
// the token vulnerable to interception) or want to read it using JavaScript
// (making the token vulnerable to XSS attacks), you may modify the relevant
// flags inside the returned cookie.
func (t *Token) Cookie() *http.Cookie {
created := time.Now().Unix()
token := t.id + ":" + strconv.FormatInt(created, 10)
mac := signMessage(token, t.auth.key)
token += ":" + base64.URLEncoding.EncodeToString(mac)
return &http.Cookie{Name: t.auth.CookieName, Value: token, Path: t.auth.cookiePath, MaxAge: t.auth.Duration, Secure: true, HttpOnly: true}
}
// Verify verifies the token encapsulated inside the HTTP cookie.
// The error returned can be ErrInvalidToken or ErrExpiredToken for invalid or
// expred tokens, respectively.
//
// ErrExpiredToken will not normally be returned, as cookie tokens should be
// removed by the browser once they expire.
func (s *Store) Verify(c *http.Cookie) (*Token, error) {
fields := strings.Split(c.Value, ":")
if len(fields) != 3 {
return nil, ErrInvalidToken
}
mac1, err := base64.URLEncoding.DecodeString(fields[2])
if err != nil {
// Treat this error just like any other token decode error.
return nil, ErrInvalidToken
}
mac2 := signMessage(strings.Join(fields[:2], ":"), s.key)
if !hmac.Equal(mac1, mac2) {
// It looks like either the token has been tampered with, or the key has
// changed.
return nil, ErrInvalidToken
}
// This is a valid token.
// Now check whether it hasn't expired yet.
created, err := strconv.ParseInt(fields[1], 10, 64)
if err != nil {
// This may be an error on our side: the token has been verified but
// contains invalid data...
return nil, ErrInvalidToken
}
now := time.Now().Unix()
if created+int64(s.Duration) < now {
// This will not happen often in practice, as the cookie will have been
// deleted by the browser already.
return nil, ErrExpiredToken
}
return &Token{s, fields[0]}, nil
}
// Id returns the user ID for this token.
func (t *Token) Id() string {
return t.id
}
// Rerturn true if this user ID string does not contain invalid characters.
func validId(id string) bool {
for _, c := range id {
// See http://tools.ietf.org/html/rfc6265#section-4.1.1 for the allowed
// characters. Luckily, e-mail addresses exactly fit into this
// definition.
if c < '!' || c > '~' {
// Not a printable US-ASCII character.
return false
}
if c == ':' {
// ':' is not allowed as we use it ourselves to separate fields.
// Colons are not allowed in e-mail adresses as defined by the HTML5
// spec.
return false
}
switch c {
case ' ', '"', ',', ';', '\\':
// Not allowed in cookie values (see cookie-octet in the linked
// RFC).
return false
}
}
return true
}
// A helper function for HMAC-SHA256. The key must be of the right length, or
// this function will panic.
func signMessage(message string, key []byte) []byte {
if len(key) != KeySize {
panic("HMAC key is not the right size")
}
signer := hmac.New(sha256.New, key)
signer.Write([]byte(message))
return signer.Sum(nil)
}