-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathvalidators.go
More file actions
137 lines (123 loc) · 4.51 KB
/
Copy pathvalidators.go
File metadata and controls
137 lines (123 loc) · 4.51 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
package jwt
import (
"errors"
"time"
)
// ErrMissingExpiry indicates that a token carries no "exp" claim and the caller asked for
// one through RequireExpiry.
var ErrMissingExpiry = errors.New("jwt: token has no expiry")
// RequireExpiry rejects any token without an "exp" claim.
//
// Nothing in RFC 7519 makes "exp" mandatory, so a token without one verifies and stays
// valid until the signing key is retired. That is rarely what anyone wants from a bearer
// credential, and it is easy to arrive at by accident: MaxAge returns NoMaxAge for any
// duration of a second or less, and a KeyConfiguration with no MaxAge set skips the option
// entirely, so a zero value in a configuration file produces permanent tokens with no
// error anywhere.
//
// Blocklist has the same trouble from the other side. A revocation for a token with no
// expiry can never be collected, because there is no point at which it stops mattering.
//
// Pass it wherever you pass any other validator:
//
// verifiedToken, err := jwt.Verify(jwt.HS256, key, token, jwt.RequireExpiry)
//
// It returns ErrMissingExpiry when the claim is absent. A token whose "exp" has passed
// fails earlier with ErrExpired, so this validator only ever sees the missing case.
var RequireExpiry = TokenValidatorFunc(func(token []byte, standardClaims Claims, err error) error {
if err != nil {
return err
}
if standardClaims.Expiry == 0 {
return ErrMissingExpiry
}
return nil
})
// Skew tolerates a clock that runs ahead of ours, for both "nbf" and "iat".
//
// Future only ever rescued "iat", and validateClaims checks "nbf" first and returns as soon
// as it fails, so a token whose issuer set "nbf" equal to "iat" could not be rescued at all.
// Callers reaching for clock-skew tolerance were reliably reaching for the wrong tool: four
// call sites across two services depended on Future and worked only because the identity
// provider in question happens to omit "nbf".
//
// Validator order is load-bearing and easy to get wrong. Verify stops at the first
// validator that returns an error, so a rescuing validator has to come before any stricter
// one:
//
// jwt.Verify(alg, key, token, jwt.Skew(2*time.Minute), jwt.Expected{Issuer: "us"})
//
// Reversed, the stricter validator never runs. Tolerance in the other direction, for a
// token that has just expired, is not offered: extending the life of an expired credential
// is a policy decision, not a clock correction.
func Skew(d time.Duration) TokenValidatorFunc {
return func(_ []byte, standardClaims Claims, err error) error {
if err == nil {
return nil
}
if d <= 0 {
return err
}
now := Clock().Round(time.Second).Add(d).Unix()
switch {
case errors.Is(err, ErrNotValidYet):
if now >= standardClaims.NotBefore {
return nil
}
case errors.Is(err, ErrIssuedInTheFuture):
if now >= standardClaims.IssuedAt {
return nil
}
}
return err
}
}
// Validators collects token validators of mixed types into one slice.
//
// Go will not convert a []TokenValidatorFunc into a []TokenValidator, so anything holding
// a slice of one and calling a function that wants the other copies it element by element
// at every call site. It accepts TokenValidator and TokenValidatorFunc values, and drops
// nils, which is the other half of that same loop.
//
// jwt.Verify(alg, key, token, jwt.Validators(app.Validators, jwt.Skew(time.Minute))...)
//
// Order is preserved, and order matters: see Skew.
func Validators(values ...any) []TokenValidator {
out := make([]TokenValidator, 0, len(values))
var add func(v any)
add = func(v any) {
switch value := v.(type) {
case nil:
return
// TokenValidatorFunc has a ValidateToken method, so it satisfies TokenValidator and
// this case has to come first. Behind the interface case it is unreachable, and the
// nil check goes with it: a nil TokenValidatorFunc held in a TokenValidator is not a
// nil interface, because the interface still carries a type, so it would be collected
// and Verify would call a nil func. Compared as a func type here, it is caught.
case TokenValidatorFunc:
if value != nil {
out = append(out, value)
}
case TokenValidator:
if value != nil {
out = append(out, value)
}
case func(token []byte, standardClaims Claims, err error) error:
if value != nil {
out = append(out, TokenValidatorFunc(value))
}
case []TokenValidator:
for _, item := range value {
add(item)
}
case []TokenValidatorFunc:
for _, item := range value {
add(item)
}
}
}
for _, v := range values {
add(v)
}
return out
}