forked from johnsto/go-passwordless
-
Notifications
You must be signed in to change notification settings - Fork 0
/
store_session.go
196 lines (170 loc) · 4.96 KB
/
store_session.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
package passwordless
import (
"crypto/subtle"
"errors"
"fmt"
"net/http"
"time"
"context"
"github.com/golang-jwt/jwt"
"github.com/gorilla/securecookie"
)
var (
ErrNoResponseWriter = errors.New("Context passed to CookieStore.Store " +
"does not contain a ResponseWriter")
ErrInvalidTokenUID = errors.New("invalid UID in token")
ErrInvalidTokenPIN = errors.New("invalid PIN in token")
ErrWrongTokenUID = errors.New("wrong UID in token")
)
// CookieStore stores tokens in a encrypted cookie on the user's browser.
// This token is then decrypted and checked against the provided value to
// determine of the token is valid.
type CookieStore struct {
sk []byte
cs *securecookie.SecureCookie
Path string
Key string
}
// NewCookieStore creates a new signed and encrypted CookieStore.
func NewCookieStore(signingKey, authKey, encrKey []byte) *CookieStore {
return &CookieStore{
Path: "/",
Key: "passwordless",
sk: signingKey,
cs: securecookie.New(authKey, encrKey),
}
}
// Store encrypts and writes the token to the curent response.
//
// The cookie is set with an expiry equal to that of the token, but the token
// expiry *must* be validated on receipt.
//
// This function requires that a ResponseWriter is present in the context.
func (s *CookieStore) Store(ctx context.Context, token, uid string, ttl time.Duration) error {
rw, _ := fromContext(ctx)
if rw == nil {
return ErrNoResponseWriter
}
// Create signed token
exp := time.Now().Add(ttl)
tokString, err := s.newToken(token, uid, exp)
if err != nil {
return err
}
// Encode and encrypt cookie value
encoded, err := s.cs.Encode(s.Key, tokString)
if err != nil {
return err
}
// Emit cookie into response
cookie := &http.Cookie{
Expires: exp,
MaxAge: int(ttl / time.Second),
Name: s.Key,
Value: encoded,
Path: s.Path,
}
http.SetCookie(rw, cookie)
return nil
}
func (s *CookieStore) Exists(ctx context.Context, uid string) (bool, time.Time, error) {
// Read cookie
_, req := fromContext(ctx)
var cookie *http.Cookie
var err error
if cookie, err = req.Cookie(s.Key); err != nil {
return false, time.Time{}, err
}
// Read JWT string from cookie
var tokString string
if err = s.cs.Decode(s.Key, cookie.Value, &tokString); err != nil {
return false, time.Time{}, err
}
// Parse JWT string
tok, claims, err := s.parseToken(tokString)
// Reject invalid JWTs
if err != nil || !tok.Valid {
return false, time.Time{}, err
}
// Check token is for the same UID
if u, ok := claims["uid"].(string); !ok {
// Token contains bad UID
return false, time.Time{}, ErrInvalidTokenUID
} else if u != uid {
// Token is for a different UID
return false, time.Time{}, ErrWrongTokenUID
}
exp := time.Unix(int64(claims["exp"].(float64)), 0)
return true, exp, nil
}
// Verify reads the cookie from the request and verifies it against the
// provided values, returning true on success.
func (s *CookieStore) Verify(ctx context.Context, pin, uid string) (bool, error) {
_, req := fromContext(ctx)
var cookie *http.Cookie
var err error
if cookie, err = req.Cookie(s.Key); err != nil {
return false, err
}
var tokString string
if err = s.cs.Decode(s.Key, cookie.Value, &tokString); err != nil {
return false, err
}
return s.verifyToken(tokString, pin, uid)
}
// Delete deletes the cookie.
//
// This function requires that a ResponseWriter is present in the context.
func (s *CookieStore) Delete(ctx context.Context, uid string) error {
rw, _ := fromContext(ctx)
if rw == nil {
return ErrNoResponseWriter
}
cookie := &http.Cookie{
MaxAge: 0,
Name: s.Key,
Path: s.Path,
}
http.SetCookie(rw, cookie)
return nil
}
// newToken creates and returns a new *unencrypted* JWT token containing the
// pin and user ID.
func (s *CookieStore) newToken(pin, uid string, exp time.Time) (string, error) {
tok := jwt.New(jwt.SigningMethodHS256)
tok.Claims = jwt.MapClaims{
"exp": exp.Unix(),
"uid": uid,
"pin": pin,
}
return tok.SignedString(s.sk)
}
// parseToken parses the token stored in the given strinng.
func (s *CookieStore) parseToken(t string) (*jwt.Token, jwt.MapClaims, error) {
claims := jwt.MapClaims{}
tok, err := jwt.ParseWithClaims(t, claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("verifyToken: unexpected signing method %s", token.Header["alg"])
}
return s.sk, nil
})
return tok, claims, err
}
// verifyToken verifies an *unencrypted* JWT token.
func (s *CookieStore) verifyToken(t, pin, uid string) (bool, error) {
tok, claims, err := s.parseToken(t)
// Reject invalid JWTs
if err != nil || !tok.Valid {
return false, err
}
// Check token matches supplied data.
if u, ok := claims["uid"].(string); !ok {
return false, ErrInvalidTokenUID
} else if p, ok := claims["pin"].(string); !ok {
return false, ErrInvalidTokenPIN
} else {
validUID := (u == uid)
validPIN := (1 == subtle.ConstantTimeCompare([]byte(p), []byte(pin)))
return validUID && validPIN, nil
}
}