-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathvalidator.go
170 lines (140 loc) · 4.89 KB
/
validator.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
// (c) 2022-present, LDC Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package cwt
import (
"fmt"
"math"
"time"
"github.com/ldclabs/cose/iana"
)
const (
cwtMaxClockSkewMinutes = 10
)
// ValidatorOpts defines validation options for CWT validators.
type ValidatorOpts struct {
ExpectedIssuer string
ExpectedAudience string
AllowMissingExpiration bool
ExpectIssuedInThePast bool
ClockSkew time.Duration
FixedNow time.Time
}
// Validator defines how CBOR Web Tokens (CWT) should be validated.
type Validator struct {
opts ValidatorOpts
}
// NewValidator creates a new CWT Validator.
func NewValidator(opts *ValidatorOpts) (*Validator, error) {
if opts == nil {
return nil, fmt.Errorf("cose/cwt: NewValidator: nil ValidatorOpts")
}
if opts.ClockSkew.Minutes() > cwtMaxClockSkewMinutes {
return nil, fmt.Errorf("cose/cwt: NewValidator: clock skew too large, expected <= %d minutes, got %f",
cwtMaxClockSkewMinutes, opts.ClockSkew.Minutes())
}
return &Validator{
opts: *opts,
}, nil
}
// Validate validates a *Claims according to the options provided.
func (v *Validator) Validate(claims *Claims) error {
if claims == nil {
return fmt.Errorf("cose/cwt: Validator.Validate: nil Claims")
}
now := time.Now()
if !v.opts.FixedNow.IsZero() {
now = v.opts.FixedNow
}
if claims.Expiration == 0 && !v.opts.AllowMissingExpiration {
return fmt.Errorf("cose/cwt: Validator.Validate: token doesn't have an expiration set")
}
if claims.Expiration > 0 {
if !toTime(claims.Expiration).After(now.Add(-v.opts.ClockSkew)) {
return fmt.Errorf("cose/cwt: Validator.Validate: token has expired")
}
}
if claims.NotBefore > 0 {
if t := toTime(claims.NotBefore); t.IsZero() || t.After(now.Add(v.opts.ClockSkew)) {
return fmt.Errorf("cose/cwt: Validator.Validate: token cannot be used yet")
}
}
if claims.IssuedAt > 0 && v.opts.ExpectIssuedInThePast {
if t := toTime(claims.IssuedAt); t.IsZero() || t.After(now.Add(v.opts.ClockSkew)) {
return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid iat claim in the future")
}
}
if v.opts.ExpectedIssuer != "" && v.opts.ExpectedIssuer != claims.Issuer {
return fmt.Errorf("cose/cwt: Validator.Validate: issuer mismatch, expected %q, got %q",
v.opts.ExpectedIssuer, claims.Issuer)
}
if v.opts.ExpectedAudience != "" && v.opts.ExpectedAudience != claims.Audience {
return fmt.Errorf("cose/cwt: Validator.Validate: audience mismatch, expected %q, got %q",
v.opts.ExpectedAudience, claims.Audience)
}
return nil
}
// ValidateMap validates a ClaimsMap according to the options provided.
func (v *Validator) ValidateMap(claims ClaimsMap) error {
if claims == nil {
return fmt.Errorf("cose/cwt: Validator.Validate: nil ClaimsMap")
}
now := time.Now()
if !v.opts.FixedNow.IsZero() {
now = v.opts.FixedNow
}
if !claims.Has(iana.CWTClaimExp) && !v.opts.AllowMissingExpiration {
return fmt.Errorf("cose/cwt: Validator.Validate: token doesn't have an expiration set")
}
if claims.Has(iana.CWTClaimExp) {
exp, err := claims.GetUint64(iana.CWTClaimExp)
if err != nil {
return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid exp claim, %w", err)
}
if !toTime(exp).After(now.Add(-v.opts.ClockSkew)) {
return fmt.Errorf("cose/cwt: Validator.Validate: token has expired")
}
}
if claims.Has(iana.CWTClaimNbf) {
nbf, err := claims.GetUint64(iana.CWTClaimNbf)
if err != nil {
return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid nbf claim, %w", err)
}
if t := toTime(nbf); t.IsZero() || t.After(now.Add(v.opts.ClockSkew)) {
return fmt.Errorf("cose/cwt: Validator.Validate: token cannot be used yet")
}
}
if claims.Has(iana.CWTClaimIat) {
iat, err := claims.GetUint64(iana.CWTClaimIat)
if err != nil {
return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid iat claim, %w", err)
}
if iat > 0 && v.opts.ExpectIssuedInThePast {
if t := toTime(iat); t.IsZero() || t.After(now.Add(v.opts.ClockSkew)) {
return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid iat claim in the future")
}
}
}
iss, err := claims.GetString(iana.CWTClaimIss)
if err != nil {
return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid iss claim, %w", err)
}
if v.opts.ExpectedIssuer != "" && v.opts.ExpectedIssuer != iss {
return fmt.Errorf("cose/cwt: Validator.Validate: issuer mismatch, expected %q, got %q",
v.opts.ExpectedIssuer, iss)
}
aud, err := claims.GetString(iana.CWTClaimAud)
if err != nil {
return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid aud claim, %w", err)
}
if v.opts.ExpectedAudience != "" && v.opts.ExpectedAudience != aud {
return fmt.Errorf("cose/cwt: Validator.Validate: audience mismatch, expected %q, got %q",
v.opts.ExpectedAudience, aud)
}
return nil
}
func toTime(u uint64) time.Time {
if u >= math.MaxInt64 {
return time.Time{}
}
return time.Unix(int64(u), 0)
}