-
Notifications
You must be signed in to change notification settings - Fork 43
/
jwt.go
559 lines (467 loc) · 16.3 KB
/
jwt.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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
// much of the architecture for this package was taken from https://github.com/unrolled/secure
// thanks!
package jwt
import (
"crypto/rsa"
"errors"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/adam-hanna/randomstrings"
jwtGo "github.com/dgrijalva/jwt-go"
)
type ClaimsType struct {
// Standard claims are the standard jwt claims from the ietf standard
// https://tools.ietf.org/html/rfc7519
jwtGo.StandardClaims
Csrf string
CustomClaims map[string]interface{}
}
// Options is a struct for specifying configuration options
type Options struct {
PrivateKeyLocation string
PublicKeyLocation string
RefreshTokenValidTime time.Duration
AuthTokenValidTime time.Duration
Debug bool
TokenClaims ClaimsType
}
const defaultRefreshTokenValidTime = 72 * time.Hour
const defaultAuthTokenValidTime = 15 * time.Minute
func defaultTokenRevoker(tokenId string) error {
return nil
}
type TokenRevoker func(tokenId string) error
func defaultCheckTokenId(tokenId string) bool {
// return true if the token id is valid (has not been revoked). False for otherwise
return true
}
type TokenIdChecker func(tokenId string) bool
func defaultErrorHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal Server Error", 500)
}
func defaultUnauthorizedHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Unauthorized", 401)
}
// Auth is a middleware that provides jwt based authentication.
type Auth struct {
signKey *rsa.PrivateKey
verifyKey *rsa.PublicKey
options Options
// Handlers for when an error occurs
errorHandler http.Handler
unauthorizedHandler http.Handler
// funcs for certain actions
revokeRefreshToken TokenRevoker
checkTokenId TokenIdChecker
}
// New constructs a new Auth instance with supplied options.
func New(auth *Auth, options ...Options) error {
var o Options
if len(options) == 0 {
o = Options{}
} else {
o = options[0]
}
// check to make sure the provided options are valid
if o.PrivateKeyLocation == "" || o.PublicKeyLocation == "" {
return errors.New("Private and public key locations are required!")
}
// check if durations have been provided for auth and refresh token exp
// if not, set them equal to the default
if o.RefreshTokenValidTime <= 0 {
o.RefreshTokenValidTime = defaultRefreshTokenValidTime
}
if o.AuthTokenValidTime <= 0 {
o.AuthTokenValidTime = defaultAuthTokenValidTime
}
// read the key files
signBytes, err := ioutil.ReadFile(o.PrivateKeyLocation)
if err != nil {
return err
}
signKey, err := jwtGo.ParseRSAPrivateKeyFromPEM(signBytes)
if err != nil {
return err
}
verifyBytes, err := ioutil.ReadFile(o.PublicKeyLocation)
if err != nil {
return err
}
verifyKey, err := jwtGo.ParseRSAPublicKeyFromPEM(verifyBytes)
if err != nil {
return err
}
auth.signKey = signKey
auth.verifyKey = verifyKey
auth.options = o
auth.errorHandler = http.HandlerFunc(defaultErrorHandler)
auth.unauthorizedHandler = http.HandlerFunc(defaultUnauthorizedHandler)
auth.revokeRefreshToken = TokenRevoker(defaultTokenRevoker)
auth.checkTokenId = TokenIdChecker(defaultCheckTokenId)
return nil
}
// add methods to allow the changing of default functions
func (a *Auth) SetErrorHandler(handler http.Handler) {
a.errorHandler = handler
}
func (a *Auth) SetUnauthorizedHandler(handler http.Handler) {
a.unauthorizedHandler = handler
}
func (a *Auth) SetRevokeTokenFunction(revoker TokenRevoker) {
a.revokeRefreshToken = revoker
}
func (a *Auth) SetCheckTokenIdFunction(checker TokenIdChecker) {
a.checkTokenId = checker
}
// Handler implements the http.HandlerFunc for integration with the standard net/http lib.
func (a *Auth) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
err := a.Process(w, r)
// If there was an error, do not continue.
if err != nil {
return
}
h.ServeHTTP(w, r)
})
}
// HandlerFuncWithNext is a special implementation for Negroni, but could be used elsewhere.
func (a *Auth) HandlerFuncWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
err := a.Process(w, r)
// If there was an error, do not call next.
if err == nil && next != nil {
next(w, r)
}
}
// Process runs the actual checks and returns an error if the middleware chain should stop.
func (a *Auth) Process(w http.ResponseWriter, r *http.Request) error {
// cookies aren't included with options, so simply pass through
if r.Method == "OPTIONS" {
a.myLog("Method is OPTIONS")
return nil
}
// read cookies
AuthCookie, authErr := r.Cookie("AuthToken")
if authErr == http.ErrNoCookie {
a.myLog("Unauthorized attempt! No auth cookie")
a.NullifyTokenCookies(&w, r)
a.unauthorizedHandler.ServeHTTP(w, r)
return errors.New("Unauthorized")
} else if authErr != nil {
a.myLog(authErr)
a.NullifyTokenCookies(&w, r)
a.errorHandler.ServeHTTP(w, r)
return errors.New("Internal Server Error")
}
RefreshCookie, refreshErr := r.Cookie("RefreshToken")
if refreshErr == http.ErrNoCookie {
a.myLog("Unauthorized attempt! No refresh cookie")
a.NullifyTokenCookies(&w, r)
a.unauthorizedHandler.ServeHTTP(w, r)
return errors.New("Unauthorized")
} else if refreshErr != nil {
a.myLog(refreshErr)
a.NullifyTokenCookies(&w, r)
a.errorHandler.ServeHTTP(w, r)
return errors.New("Internal Server Error")
}
// grab the csrf token
requestCsrfToken := grabCsrfFromReq(r)
// check the jwt's for validity
authTokenString, refreshTokenString, csrfSecret, err := a.checkAndRefreshTokens(AuthCookie.Value, RefreshCookie.Value, requestCsrfToken)
if err != nil {
if err.Error() == "Unauthorized" {
a.myLog("Unauthorized attempt! JWT's not valid!")
a.unauthorizedHandler.ServeHTTP(w, r)
return errors.New("Unauthorized")
} else {
// @adam-hanna: do we 401 or 500, here?
// it could be 401 bc the token they provided was messed up
// or it could be 500 bc there was some error on our end
a.myLog(err)
a.errorHandler.ServeHTTP(w, r)
return errors.New("Internal Server Error")
}
}
a.myLog("Successfully checked / refreshed jwts")
// if we've made it this far, everything is valid!
// And tokens have been refreshed if need-be
setAuthAndRefreshCookies(&w, authTokenString, refreshTokenString)
w.Header().Set("X-CSRF-Token", csrfSecret)
return nil
}
func (a *Auth) NullifyTokenCookies(w *http.ResponseWriter, r *http.Request) {
authCookie := http.Cookie{
Name: "AuthToken",
Value: "",
Expires: time.Now().Add(-1000 * time.Hour),
HttpOnly: true,
}
http.SetCookie(*w, &authCookie)
refreshCookie := http.Cookie{
Name: "RefreshToken",
Value: "",
Expires: time.Now().Add(-1000 * time.Hour),
HttpOnly: true,
}
http.SetCookie(*w, &refreshCookie)
// if present, revoke the refresh cookie from our db
RefreshCookie, refreshErr := r.Cookie("RefreshToken")
if refreshErr == http.ErrNoCookie {
// do nothing, there is no refresh cookie present
return
} else if refreshErr != nil {
a.myLog(refreshErr)
http.Error(*w, http.StatusText(500), 500)
} else {
a.revokeRefreshToken(RefreshCookie.Value)
}
return
}
func setAuthAndRefreshCookies(w *http.ResponseWriter, authTokenString string, refreshTokenString string) {
authCookie := http.Cookie{
Name: "AuthToken",
Value: authTokenString,
HttpOnly: true,
}
http.SetCookie(*w, &authCookie)
refreshCookie := http.Cookie{
Name: "RefreshToken",
Value: refreshTokenString,
HttpOnly: true,
}
http.SetCookie(*w, &refreshCookie)
}
func grabCsrfFromReq(r *http.Request) string {
csrfFromFrom := r.FormValue("X-CSRF-Token")
if csrfFromFrom != "" {
return csrfFromFrom
} else {
return r.Header.Get("X-CSRF-Token")
}
}
// and also modify create refresh and auth token functions!
func (a *Auth) IssueNewTokens(w http.ResponseWriter, claims ClaimsType) (err error) {
// generate the csrf secret
csrfSecret, err := randomstrings.GenerateRandomString(32)
if err != nil {
return
}
w.Header().Set("X-CSRF-Token", csrfSecret)
// generate the refresh token
refreshTokenString, err := a.createRefreshTokenString(claims, csrfSecret)
// generate the auth token
authTokenString, err := a.createAuthTokenString(claims, csrfSecret)
if err != nil {
return
}
setAuthAndRefreshCookies(&w, authTokenString, refreshTokenString)
// don't need to check for err bc we're returning everything anyway
return
}
// @adam-hanna: check if refreshToken["sub"] == authToken["sub"]?
// I don't think this is necessary bc a valid refresh token will always generate
// a valid auth token of the same "sub"
func (a *Auth) checkAndRefreshTokens(oldAuthTokenString string, oldRefreshTokenString string, oldCsrfSecret string) (newAuthTokenString, newRefreshTokenString, newCsrfSecret string, err error) {
// first, check that a csrf token was provided
if oldCsrfSecret == "" {
a.myLog("No CSRF token in request!")
err = errors.New("Unauthorized")
return
}
// now, check that it matches what's in the auth token claims
authToken, err := jwtGo.ParseWithClaims(oldAuthTokenString, &ClaimsType{}, func(token *jwtGo.Token) (interface{}, error) {
if _, ok := token.Method.(*jwtGo.SigningMethodRSA); !ok {
a.myLog("Incorrect singing method on auth token")
return nil, errors.New("Incorrect singing method on auth token")
}
return a.verifyKey, nil
})
if err != nil {
return
}
authTokenClaims, ok := authToken.Claims.(*ClaimsType)
if !ok {
return
}
if oldCsrfSecret != authTokenClaims.Csrf {
a.myLog("CSRF token doesn't match jwt!")
err = errors.New("Unauthorized")
return
}
// next, check the auth token in a stateless manner
if authToken.Valid {
a.myLog("Auth token is valid")
// auth token has not expired
// we need to return the csrf secret bc that's what the function calls for
newCsrfSecret = authTokenClaims.Csrf
// update the exp of refresh token string, but don't save to the db
// we don't need to check if our refresh token is valid here
// because we aren't renewing the auth token, the auth token is already valid
newRefreshTokenString, err = a.updateRefreshTokenExp(oldRefreshTokenString)
newAuthTokenString = oldAuthTokenString
return
} else if ve, ok := err.(*jwtGo.ValidationError); ok {
a.myLog("Auth token is not valid")
if ve.Errors&(jwtGo.ValidationErrorExpired) != 0 {
a.myLog("Auth token is expired")
// auth token is expired
// fyi - refresh token is checked in the update auth func
newAuthTokenString, newCsrfSecret, err = a.updateAuthTokenString(oldRefreshTokenString, oldAuthTokenString)
if err != nil {
return
}
// update the exp of refresh token string
newRefreshTokenString, err = a.updateRefreshTokenExp(oldRefreshTokenString)
if err != nil {
return
}
// update the csrf string of the refresh token
newRefreshTokenString, err = a.updateRefreshTokenCsrf(newRefreshTokenString, newCsrfSecret)
return
} else {
a.myLog("Error in auth token")
err = errors.New("Error in auth token")
return
}
} else {
a.myLog("Error in auth token")
err = errors.New("Error in auth token")
return
}
}
func (a *Auth) createRefreshTokenString(claims ClaimsType, csrfString string) (refreshTokenString string, err error) {
refreshTokenExp := time.Now().Add(a.options.RefreshTokenValidTime).Unix()
if err != nil {
return
}
claims.StandardClaims.ExpiresAt = refreshTokenExp
claims.Csrf = csrfString
// create a signer for rsa 256
refreshJwt := jwtGo.NewWithClaims(jwtGo.GetSigningMethod("RS256"), claims)
// generate the refresh token string
refreshTokenString, err = refreshJwt.SignedString(a.signKey)
return
}
func (a *Auth) createAuthTokenString(claims ClaimsType, csrfSecret string) (authTokenString string, err error) {
authTokenExp := time.Now().Add(a.options.AuthTokenValidTime).Unix()
claims.StandardClaims.ExpiresAt = authTokenExp
claims.Csrf = csrfSecret
// create a signer for rsa 256
authJwt := jwtGo.NewWithClaims(jwtGo.GetSigningMethod("RS256"), claims)
// generate the auth token string
authTokenString, err = authJwt.SignedString(a.signKey)
return
}
func (a *Auth) updateRefreshTokenExp(oldRefreshTokenString string) (string, error) {
refreshToken, _ := jwtGo.ParseWithClaims(oldRefreshTokenString, &ClaimsType{}, func(token *jwtGo.Token) (interface{}, error) {
// no need verify refresh token alg because it was verified at `updateAuthTokenString`
return a.verifyKey, nil
})
oldRefreshTokenClaims, ok := refreshToken.Claims.(*ClaimsType)
if !ok {
return "", errors.New("Error parsing claims")
}
refreshTokenExp := time.Now().Add(a.options.RefreshTokenValidTime).Unix()
oldRefreshTokenClaims.StandardClaims.ExpiresAt = refreshTokenExp
// create a signer for rsa 256
refreshJwt := jwtGo.NewWithClaims(jwtGo.GetSigningMethod("RS256"), oldRefreshTokenClaims)
// generate the refresh token string
return refreshJwt.SignedString(a.signKey)
}
func (a *Auth) updateAuthTokenString(refreshTokenString string, oldAuthTokenString string) (newAuthTokenString, csrfSecret string, err error) {
refreshToken, err := jwtGo.ParseWithClaims(refreshTokenString, &ClaimsType{}, func(token *jwtGo.Token) (interface{}, error) {
if _, ok := token.Method.(*jwtGo.SigningMethodRSA); !ok {
a.myLog("Incorrect singing method on auth token")
return nil, errors.New("Incorrect singing method on auth token")
}
return a.verifyKey, nil
})
if err != nil {
return
}
refreshTokenClaims, ok := refreshToken.Claims.(*ClaimsType)
if !ok {
err = errors.New("Error reading jwt claims")
return
}
// check if the refresh token has been revoked
if a.checkTokenId(refreshTokenClaims.StandardClaims.Id) {
a.myLog("Refresh token has not been revoked")
// the refresh token has not been revoked
// has it expired?
if refreshToken.Valid {
a.myLog("Refresh token is not expired")
// nope, the refresh token has not expired
// issue a new auth token
// our policy is to regenerate the csrf secret for each new auth token
csrfSecret, err = randomstrings.GenerateRandomString(32)
if err != nil {
return
}
newAuthTokenString, err = a.createAuthTokenString(*refreshTokenClaims, csrfSecret)
// fyi - updating of refreshtoken csrf and exp is done after calling this func
// so we can simply return
return
} else {
a.myLog("Refresh token has expired!")
// the refresh token has expired! Require the user to re-authenticate
// @adam-hanna: Do we want to revoke the token in our db?
// I don't think we need to because it has expired and we can simply check the
// exp. No need to update the db.
err = errors.New("Unauthorized")
return
}
} else {
a.myLog("Refresh token has been revoked!")
// the refresh token has been revoked!
err = errors.New("Unauthorized")
return
}
}
func (a *Auth) updateRefreshTokenCsrf(oldRefreshTokenString string, newCsrfString string) (string, error) {
refreshToken, _ := jwtGo.ParseWithClaims(oldRefreshTokenString, &ClaimsType{}, func(token *jwtGo.Token) (interface{}, error) {
// no need verify refresh token alg because it was verified at `updateAuthTokenString`
return a.verifyKey, nil
})
oldRefreshTokenClaims, ok := refreshToken.Claims.(*ClaimsType)
if !ok {
return "", errors.New("Error parsing claims")
}
oldRefreshTokenClaims.Csrf = newCsrfString
// create a signer for rsa 256
refreshJwt := jwtGo.NewWithClaims(jwtGo.GetSigningMethod("RS256"), oldRefreshTokenClaims)
// generate the refresh token string
return refreshJwt.SignedString(a.signKey)
}
func (a *Auth) GrabTokenClaims(w http.ResponseWriter, r *http.Request) (ClaimsType, error) {
// read cookies
AuthCookie, authErr := r.Cookie("AuthToken")
if authErr == http.ErrNoCookie {
a.myLog("Unauthorized attempt! No auth cookie")
a.NullifyTokenCookies(&w, r)
a.unauthorizedHandler.ServeHTTP(w, r)
return ClaimsType{}, errors.New("Unauthorized")
} else if authErr != nil {
a.myLog(authErr)
a.NullifyTokenCookies(&w, r)
a.errorHandler.ServeHTTP(w, r)
return ClaimsType{}, errors.New("Unauthorized")
}
token, _ := jwtGo.ParseWithClaims(AuthCookie.Value, &ClaimsType{}, func(token *jwtGo.Token) (interface{}, error) {
return ClaimsType{}, errors.New("Error processing token string claims")
})
tokenClaims, ok := token.Claims.(*ClaimsType)
if !ok {
return ClaimsType{}, errors.New("Error processing token string claims")
}
return *tokenClaims, nil
}
func (a *Auth) myLog(stoofs interface{}) {
if a.options.Debug {
log.Println(stoofs)
}
}