Skip to content

Commit 1ef0fe8

Browse files
oxistoMicahParks
andcommitted
New validation API (#236)
* New Validation API Some guidelines in designing the new validation API * Previously, the `Valid` method was placed on the claim, which was always not entirely semantically correct, since the validity is concerning the token, not the claims. Although the validity of the token is based on the processing of the claims (such as `exp`). Therefore, the function `Valid` was removed from the `Claims` interface and the single canonical way to retrieve the validity of the token is to retrieve the `Valid` property of the `Token` struct. * The previous fact was enhanced by the fact that most claims implementations had additional exported `VerifyXXX` functions, which are now removed * All validation errors should be comparable with `errors.Is` to determine, why a particular validation has failed * Developers want to adjust validation options. Popular options include: * Leeway when processing exp, nbf, iat * Not verifying `iat`, since this is actually just an informational claim. When purely looking at the standard, this should probably the default * Verifying `aud` by default, which actually the standard sort of demands. We need to see how strong we want to enforce this * Developers want to create their own claim types, mostly by embedding one of the existing types such as `RegisteredClaims`. * Sometimes there is the need to further tweak the validation of a token by checking the value of a custom claim. Previously, this was possibly by overriding `Valid`. However, this was error-prone, e.g., if the original `Valid` was not called. Therefore, we should provide an easy way for *additional* checks, without by-passing the necessary validations This leads to the following two major changes: * The `Claims` interface now represents a set of functions that return the mandatory claims represented in a token, rather than just a `Valid` function. This is also more semantically correct. * All validation tasks are offloaded to a new (optional) `validator`, which can also be configured with appropriate options. If no custom validator was supplied, a default one is used. Co-authored-by: Micah Parks <[email protected]>
1 parent 6e66008 commit 1ef0fe8

11 files changed

+631
-348
lines changed

claims.go

+12-169
Original file line numberDiff line numberDiff line change
@@ -1,173 +1,16 @@
11
package jwt
22

3-
import (
4-
"crypto/subtle"
5-
"fmt"
6-
"time"
7-
)
8-
9-
// Claims must just have a Valid method that determines
10-
// if the token is invalid for any supported reason
3+
// Claims represent any form of a JWT Claims Set according to
4+
// https://datatracker.ietf.org/doc/html/rfc7519#section-4. In order to have a
5+
// common basis for validation, it is required that an implementation is able to
6+
// supply at least the claim names provided in
7+
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 namely `exp`,
8+
// `iat`, `nbf`, `iss` and `aud`.
119
type Claims interface {
12-
Valid() error
13-
}
14-
15-
// RegisteredClaims are a structured version of the JWT Claims Set,
16-
// restricted to Registered Claim Names, as referenced at
17-
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
18-
//
19-
// This type can be used on its own, but then additional private and
20-
// public claims embedded in the JWT will not be parsed. The typical usecase
21-
// therefore is to embedded this in a user-defined claim type.
22-
//
23-
// See examples for how to use this with your own claim types.
24-
type RegisteredClaims struct {
25-
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
26-
Issuer string `json:"iss,omitempty"`
27-
28-
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
29-
Subject string `json:"sub,omitempty"`
30-
31-
// the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
32-
Audience ClaimStrings `json:"aud,omitempty"`
33-
34-
// the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
35-
ExpiresAt *NumericDate `json:"exp,omitempty"`
36-
37-
// the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
38-
NotBefore *NumericDate `json:"nbf,omitempty"`
39-
40-
// the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
41-
IssuedAt *NumericDate `json:"iat,omitempty"`
42-
43-
// the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
44-
ID string `json:"jti,omitempty"`
45-
}
46-
47-
// Valid validates time based claims "exp, iat, nbf".
48-
// There is no accounting for clock skew.
49-
// As well, if any of the above claims are not in the token, it will still
50-
// be considered a valid claim.
51-
func (c RegisteredClaims) Valid() error {
52-
vErr := new(ValidationError)
53-
now := TimeFunc()
54-
55-
// The claims below are optional, by default, so if they are set to the
56-
// default value in Go, let's not fail the verification for them.
57-
if !c.VerifyExpiresAt(now, false) {
58-
delta := now.Sub(c.ExpiresAt.Time)
59-
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
60-
vErr.Errors |= ValidationErrorExpired
61-
}
62-
63-
if !c.VerifyIssuedAt(now, false) {
64-
vErr.Inner = ErrTokenUsedBeforeIssued
65-
vErr.Errors |= ValidationErrorIssuedAt
66-
}
67-
68-
if !c.VerifyNotBefore(now, false) {
69-
vErr.Inner = ErrTokenNotValidYet
70-
vErr.Errors |= ValidationErrorNotValidYet
71-
}
72-
73-
if vErr.valid() {
74-
return nil
75-
}
76-
77-
return vErr
78-
}
79-
80-
// VerifyAudience compares the aud claim against cmp.
81-
// If required is false, this method will return true if the value matches or is unset
82-
func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool {
83-
return verifyAud(c.Audience, cmp, req)
84-
}
85-
86-
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
87-
// If req is false, it will return true, if exp is unset.
88-
func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool {
89-
if c.ExpiresAt == nil {
90-
return verifyExp(nil, cmp, req)
91-
}
92-
93-
return verifyExp(&c.ExpiresAt.Time, cmp, req)
94-
}
95-
96-
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
97-
// If req is false, it will return true, if iat is unset.
98-
func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool {
99-
if c.IssuedAt == nil {
100-
return verifyIat(nil, cmp, req)
101-
}
102-
103-
return verifyIat(&c.IssuedAt.Time, cmp, req)
104-
}
105-
106-
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
107-
// If req is false, it will return true, if nbf is unset.
108-
func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool {
109-
if c.NotBefore == nil {
110-
return verifyNbf(nil, cmp, req)
111-
}
112-
113-
return verifyNbf(&c.NotBefore.Time, cmp, req)
114-
}
115-
116-
// VerifyIssuer compares the iss claim against cmp.
117-
// If required is false, this method will return true if the value matches or is unset
118-
func (c *RegisteredClaims) VerifyIssuer(cmp string, req bool) bool {
119-
return verifyIss(c.Issuer, cmp, req)
120-
}
121-
122-
// ----- helpers
123-
124-
func verifyAud(aud []string, cmp string, required bool) bool {
125-
if len(aud) == 0 {
126-
return !required
127-
}
128-
// use a var here to keep constant time compare when looping over a number of claims
129-
result := false
130-
131-
var stringClaims string
132-
for _, a := range aud {
133-
if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 {
134-
result = true
135-
}
136-
stringClaims = stringClaims + a
137-
}
138-
139-
// case where "" is sent in one or many aud claims
140-
if len(stringClaims) == 0 {
141-
return !required
142-
}
143-
144-
return result
145-
}
146-
147-
func verifyExp(exp *time.Time, now time.Time, required bool) bool {
148-
if exp == nil {
149-
return !required
150-
}
151-
return now.Before(*exp)
152-
}
153-
154-
func verifyIat(iat *time.Time, now time.Time, required bool) bool {
155-
if iat == nil {
156-
return !required
157-
}
158-
return now.After(*iat) || now.Equal(*iat)
159-
}
160-
161-
func verifyNbf(nbf *time.Time, now time.Time, required bool) bool {
162-
if nbf == nil {
163-
return !required
164-
}
165-
return now.After(*nbf) || now.Equal(*nbf)
166-
}
167-
168-
func verifyIss(iss string, cmp string, required bool) bool {
169-
if iss == "" {
170-
return !required
171-
}
172-
return subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0
10+
GetExpirationTime() (*NumericDate, error)
11+
GetIssuedAt() (*NumericDate, error)
12+
GetNotBefore() (*NumericDate, error)
13+
GetIssuer() (string, error)
14+
GetSubject() (string, error)
15+
GetAudience() (ClaimStrings, error)
17316
}

errors.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var (
1818
ErrTokenExpired = errors.New("token is expired")
1919
ErrTokenUsedBeforeIssued = errors.New("token used before issued")
2020
ErrTokenInvalidIssuer = errors.New("token has invalid issuer")
21+
ErrTokenInvalidSubject = errors.New("token has invalid subject")
2122
ErrTokenNotValidYet = errors.New("token is not valid yet")
2223
ErrTokenInvalidId = errors.New("token has invalid id")
2324
ErrTokenInvalidClaims = errors.New("token has invalid claims")
@@ -29,11 +30,12 @@ const (
2930
ValidationErrorUnverifiable // Token could not be verified because of signing problems
3031
ValidationErrorSignatureInvalid // Signature validation failed
3132

32-
// Standard Claim validation errors
33+
// Registered Claim validation errors
3334
ValidationErrorAudience // AUD validation failed
3435
ValidationErrorExpired // EXP validation failed
3536
ValidationErrorIssuedAt // IAT validation failed
3637
ValidationErrorIssuer // ISS validation failed
38+
ValidationErrorSubject // SUB validation failed
3739
ValidationErrorNotValidYet // NBF validation failed
3840
ValidationErrorId // JTI validation failed
3941
ValidationErrorClaimsInvalid // Generic claims validation error

example_test.go

+58-2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func ExampleNewWithClaims_customClaimsType() {
7070
//Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM <nil>
7171
}
7272

73-
// Example creating a token using a custom claims type. The StandardClaim is embedded
73+
// Example creating a token using a custom claims type. The RegisteredClaims is embedded
7474
// in the custom type to allow for easy encoding, parsing and validation of standard claims.
7575
func ExampleParseWithClaims_customClaimsType() {
7676
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
@@ -93,7 +93,63 @@ func ExampleParseWithClaims_customClaimsType() {
9393
// Output: bar test
9494
}
9595

96-
// An example of parsing the error types using bitfield checks
96+
// Example creating a token using a custom claims type and validation options. The RegisteredClaims is embedded
97+
// in the custom type to allow for easy encoding, parsing and validation of standard claims.
98+
func ExampleParseWithClaims_validationOptions() {
99+
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
100+
101+
type MyCustomClaims struct {
102+
Foo string `json:"foo"`
103+
jwt.RegisteredClaims
104+
}
105+
106+
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
107+
return []byte("AllYourBase"), nil
108+
}, jwt.WithLeeway(5*time.Second))
109+
110+
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
111+
fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
112+
} else {
113+
fmt.Println(err)
114+
}
115+
116+
// Output: bar test
117+
}
118+
119+
type MyCustomClaims struct {
120+
Foo string `json:"foo"`
121+
jwt.RegisteredClaims
122+
}
123+
124+
func (m MyCustomClaims) CustomValidation() error {
125+
if m.Foo != "bar" {
126+
return errors.New("must be foobar")
127+
}
128+
129+
return nil
130+
}
131+
132+
// Example creating a token using a custom claims type and validation options.
133+
// The RegisteredClaims is embedded in the custom type to allow for easy
134+
// encoding, parsing and validation of standard claims and the function
135+
// CustomValidation is implemented.
136+
func ExampleParseWithClaims_customValidation() {
137+
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
138+
139+
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
140+
return []byte("AllYourBase"), nil
141+
}, jwt.WithLeeway(5*time.Second))
142+
143+
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
144+
fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
145+
} else {
146+
fmt.Println(err)
147+
}
148+
149+
// Output: bar test
150+
}
151+
152+
// An example of parsing the error types using errors.Is.
97153
func ExampleParse_errorChecking() {
98154
// Token from another example. This token is expired
99155
var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"

0 commit comments

Comments
 (0)