-
Notifications
You must be signed in to change notification settings - Fork 347
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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 <66095735+MicahParks@users.noreply.github.com>
- Loading branch information
1 parent
6e66008
commit 1ef0fe8
Showing
11 changed files
with
631 additions
and
348 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,173 +1,16 @@ | ||
package jwt | ||
|
||
import ( | ||
"crypto/subtle" | ||
"fmt" | ||
"time" | ||
) | ||
|
||
// Claims must just have a Valid method that determines | ||
// if the token is invalid for any supported reason | ||
// Claims represent any form of a JWT Claims Set according to | ||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4. In order to have a | ||
// common basis for validation, it is required that an implementation is able to | ||
// supply at least the claim names provided in | ||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 namely `exp`, | ||
// `iat`, `nbf`, `iss` and `aud`. | ||
type Claims interface { | ||
Valid() error | ||
} | ||
|
||
// RegisteredClaims are a structured version of the JWT Claims Set, | ||
// restricted to Registered Claim Names, as referenced at | ||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 | ||
// | ||
// This type can be used on its own, but then additional private and | ||
// public claims embedded in the JWT will not be parsed. The typical usecase | ||
// therefore is to embedded this in a user-defined claim type. | ||
// | ||
// See examples for how to use this with your own claim types. | ||
type RegisteredClaims struct { | ||
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 | ||
Issuer string `json:"iss,omitempty"` | ||
|
||
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2 | ||
Subject string `json:"sub,omitempty"` | ||
|
||
// the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 | ||
Audience ClaimStrings `json:"aud,omitempty"` | ||
|
||
// the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 | ||
ExpiresAt *NumericDate `json:"exp,omitempty"` | ||
|
||
// the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5 | ||
NotBefore *NumericDate `json:"nbf,omitempty"` | ||
|
||
// the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 | ||
IssuedAt *NumericDate `json:"iat,omitempty"` | ||
|
||
// the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7 | ||
ID string `json:"jti,omitempty"` | ||
} | ||
|
||
// Valid validates time based claims "exp, iat, nbf". | ||
// There is no accounting for clock skew. | ||
// As well, if any of the above claims are not in the token, it will still | ||
// be considered a valid claim. | ||
func (c RegisteredClaims) Valid() error { | ||
vErr := new(ValidationError) | ||
now := TimeFunc() | ||
|
||
// The claims below are optional, by default, so if they are set to the | ||
// default value in Go, let's not fail the verification for them. | ||
if !c.VerifyExpiresAt(now, false) { | ||
delta := now.Sub(c.ExpiresAt.Time) | ||
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta) | ||
vErr.Errors |= ValidationErrorExpired | ||
} | ||
|
||
if !c.VerifyIssuedAt(now, false) { | ||
vErr.Inner = ErrTokenUsedBeforeIssued | ||
vErr.Errors |= ValidationErrorIssuedAt | ||
} | ||
|
||
if !c.VerifyNotBefore(now, false) { | ||
vErr.Inner = ErrTokenNotValidYet | ||
vErr.Errors |= ValidationErrorNotValidYet | ||
} | ||
|
||
if vErr.valid() { | ||
return nil | ||
} | ||
|
||
return vErr | ||
} | ||
|
||
// VerifyAudience compares the aud claim against cmp. | ||
// If required is false, this method will return true if the value matches or is unset | ||
func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool { | ||
return verifyAud(c.Audience, cmp, req) | ||
} | ||
|
||
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp). | ||
// If req is false, it will return true, if exp is unset. | ||
func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool { | ||
if c.ExpiresAt == nil { | ||
return verifyExp(nil, cmp, req) | ||
} | ||
|
||
return verifyExp(&c.ExpiresAt.Time, cmp, req) | ||
} | ||
|
||
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat). | ||
// If req is false, it will return true, if iat is unset. | ||
func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool { | ||
if c.IssuedAt == nil { | ||
return verifyIat(nil, cmp, req) | ||
} | ||
|
||
return verifyIat(&c.IssuedAt.Time, cmp, req) | ||
} | ||
|
||
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). | ||
// If req is false, it will return true, if nbf is unset. | ||
func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool { | ||
if c.NotBefore == nil { | ||
return verifyNbf(nil, cmp, req) | ||
} | ||
|
||
return verifyNbf(&c.NotBefore.Time, cmp, req) | ||
} | ||
|
||
// VerifyIssuer compares the iss claim against cmp. | ||
// If required is false, this method will return true if the value matches or is unset | ||
func (c *RegisteredClaims) VerifyIssuer(cmp string, req bool) bool { | ||
return verifyIss(c.Issuer, cmp, req) | ||
} | ||
|
||
// ----- helpers | ||
|
||
func verifyAud(aud []string, cmp string, required bool) bool { | ||
if len(aud) == 0 { | ||
return !required | ||
} | ||
// use a var here to keep constant time compare when looping over a number of claims | ||
result := false | ||
|
||
var stringClaims string | ||
for _, a := range aud { | ||
if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 { | ||
result = true | ||
} | ||
stringClaims = stringClaims + a | ||
} | ||
|
||
// case where "" is sent in one or many aud claims | ||
if len(stringClaims) == 0 { | ||
return !required | ||
} | ||
|
||
return result | ||
} | ||
|
||
func verifyExp(exp *time.Time, now time.Time, required bool) bool { | ||
if exp == nil { | ||
return !required | ||
} | ||
return now.Before(*exp) | ||
} | ||
|
||
func verifyIat(iat *time.Time, now time.Time, required bool) bool { | ||
if iat == nil { | ||
return !required | ||
} | ||
return now.After(*iat) || now.Equal(*iat) | ||
} | ||
|
||
func verifyNbf(nbf *time.Time, now time.Time, required bool) bool { | ||
if nbf == nil { | ||
return !required | ||
} | ||
return now.After(*nbf) || now.Equal(*nbf) | ||
} | ||
|
||
func verifyIss(iss string, cmp string, required bool) bool { | ||
if iss == "" { | ||
return !required | ||
} | ||
return subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 | ||
GetExpirationTime() (*NumericDate, error) | ||
GetIssuedAt() (*NumericDate, error) | ||
GetNotBefore() (*NumericDate, error) | ||
GetIssuer() (string, error) | ||
GetSubject() (string, error) | ||
GetAudience() (ClaimStrings, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.