Compare commits

...

8 Commits

Author SHA1 Message Date
Christian Banse 0b5f240626 ++ 2022-08-28 11:51:21 +02:00
Christian Banse cc30155791 Compat test 2022-08-28 11:48:19 +02:00
Christian Banse 5735b9c09c Added option for audience check 2022-08-27 13:42:01 +02:00
Christian Banse 93dcd2e886 Added timeFunc, made iat optional 2022-08-27 13:36:45 +02:00
Christian Banse 1d88540186 GetExpiresAt() -> GetExpirationTime() 2022-08-27 13:07:17 +02:00
Christian Banse 3098ea8845 Fixed linting errors 2022-08-27 12:59:15 +02:00
Christian Banse 1b437d8467 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.
2022-08-27 12:51:49 +02:00
Christian Banse 895749e449 Starting `v5` development
This commit serves as the basis for further `v5` developments. It will introduce some API-breaking changes, especially to the way tokens are validated. This will allow us to provide some long-wanted features with regards to the validation API. We are aiming to do this as smoothly as possible, however, with any major version. please expect that you might need to adapt your code.

The actual development will be done in the course of the next week, if time permits. It will be done in seperate PRs that will use this PR as a base. Afterwards, we will probably merge this and release an initial 5.0.0-alpha1 or similar.
2022-08-27 11:40:20 +02:00
29 changed files with 520 additions and 465 deletions

View File

@ -2,18 +2,18 @@
Starting from [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0), the import path will be: Starting from [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0), the import path will be:
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
The `/v4` version will be backwards compatible with existing `v3.x.y` tags in this repo, as well as The `/v4` version will be backwards compatible with existing `v3.x.y` tags in this repo, as well as
`github.com/dgrijalva/jwt-go`. For most users this should be a drop-in replacement, if you're having `github.com/dgrijalva/jwt-go`. For most users this should be a drop-in replacement, if you're having
troubles migrating, please open an issue. troubles migrating, please open an issue.
You can replace all occurrences of `github.com/dgrijalva/jwt-go` or `github.com/golang-jwt/jwt` with `github.com/golang-jwt/jwt/v4`, either manually or by using tools such as `sed` or `gofmt`. You can replace all occurrences of `github.com/dgrijalva/jwt-go` or `github.com/golang-jwt/jwt` with `github.com/golang-jwt/jwt/v5`, either manually or by using tools such as `sed` or `gofmt`.
And then you'd typically run: And then you'd typically run:
``` ```
go get github.com/golang-jwt/jwt/v4 go get github.com/golang-jwt/jwt/v5
go mod tidy go mod tidy
``` ```

View File

@ -1,12 +1,12 @@
# jwt-go # jwt-go
[![build](https://github.com/golang-jwt/jwt/actions/workflows/build.yml/badge.svg)](https://github.com/golang-jwt/jwt/actions/workflows/build.yml) [![build](https://github.com/golang-jwt/jwt/actions/workflows/build.yml/badge.svg)](https://github.com/golang-jwt/jwt/actions/workflows/build.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/golang-jwt/jwt/v4.svg)](https://pkg.go.dev/github.com/golang-jwt/jwt/v4) [![Go Reference](https://pkg.go.dev/badge/github.com/golang-jwt/jwt/v5.svg)](https://pkg.go.dev/github.com/golang-jwt/jwt/v5)
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519). A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](https://datatracker.ietf.org/doc/html/rfc7519).
Starting with [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0) this project adds Go module support, but maintains backwards compatibility with older `v3.x.y` tags and upstream `github.com/dgrijalva/jwt-go`. Starting with [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0) this project adds Go module support, but maintains backwards compatibility with older `v3.x.y` tags and upstream `github.com/dgrijalva/jwt-go`.
See the [`MIGRATION_GUIDE.md`](./MIGRATION_GUIDE.md) for more information. See the [`MIGRATION_GUIDE.md`](./MIGRATION_GUIDE.md) for more information. Version v5.0.0 introduces major improvements to the validation of tokens, but is not entirely backwards compatible.
> After the original author of the library suggested migrating the maintenance of `jwt-go`, a dedicated team of open source maintainers decided to clone the existing library into this repository. See [dgrijalva/jwt-go#462](https://github.com/dgrijalva/jwt-go/issues/462) for a detailed discussion on this topic. > After the original author of the library suggested migrating the maintenance of `jwt-go`, a dedicated team of open source maintainers decided to clone the existing library into this repository. See [dgrijalva/jwt-go#462](https://github.com/dgrijalva/jwt-go/issues/462) for a detailed discussion on this topic.
@ -41,22 +41,22 @@ This library supports the parsing and verification as well as the generation and
1. To install the jwt package, you first need to have [Go](https://go.dev/doc/install) installed, then you can use the command below to add `jwt-go` as a dependency in your Go program. 1. To install the jwt package, you first need to have [Go](https://go.dev/doc/install) installed, then you can use the command below to add `jwt-go` as a dependency in your Go program.
```sh ```sh
go get -u github.com/golang-jwt/jwt/v4 go get -u github.com/golang-jwt/jwt/v5
``` ```
2. Import it in your code: 2. Import it in your code:
```go ```go
import "github.com/golang-jwt/jwt/v4" import "github.com/golang-jwt/jwt/v5"
``` ```
## Examples ## Examples
See [the project documentation](https://pkg.go.dev/github.com/golang-jwt/jwt/v4) for examples of usage: See [the project documentation](https://pkg.go.dev/github.com/golang-jwt/jwt/v5) for examples of usage:
* [Simple example of parsing and validating a token](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#example-Parse-Hmac) * [Simple example of parsing and validating a token](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-Parse-Hmac)
* [Simple example of building and signing a token](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#example-New-Hmac) * [Simple example of building and signing a token](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-New-Hmac)
* [Directory of Examples](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#pkg-examples) * [Directory of Examples](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#pkg-examples)
## Extensions ## Extensions
@ -68,7 +68,7 @@ A common use case would be integrating with different 3rd party signature provid
| --------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | --------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| GCP | Integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS) | https://github.com/someone1/gcp-jwt-go | | GCP | Integrates with multiple Google Cloud Platform signing tools (AppEngine, IAM API, Cloud KMS) | https://github.com/someone1/gcp-jwt-go |
| AWS | Integrates with AWS Key Management Service, KMS | https://github.com/matelang/jwt-go-aws-kms | | AWS | Integrates with AWS Key Management Service, KMS | https://github.com/matelang/jwt-go-aws-kms |
| JWKS | Provides support for JWKS ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) as a `jwt.Keyfunc` | https://github.com/MicahParks/keyfunc | | JWKS | Provides support for JWKS ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) as a `jwt.Keyfunc` | https://github.com/MicahParks/keyfunc |
*Disclaimer*: Unless otherwise specified, these integrations are maintained by third parties and should not be considered as a primary offer by any of the mentioned cloud providers *Disclaimer*: Unless otherwise specified, these integrations are maintained by third parties and should not be considered as a primary offer by any of the mentioned cloud providers
@ -110,10 +110,10 @@ Asymmetric signing methods, such as RSA, use different keys for signing and veri
Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones: Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones:
* The [HMAC signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation * The [HMAC signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation
* The [RSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation * The [RSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation
* The [ECDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation * The [ECDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation
* The [EdDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#SigningMethodEd25519) (`Ed25519`) expect `ed25519.PrivateKey` for signing and `ed25519.PublicKey` for validation * The [EdDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#SigningMethodEd25519) (`Ed25519`) expect `ed25519.PrivateKey` for signing and `ed25519.PublicKey` for validation
### JWT and OAuth ### JWT and OAuth
@ -131,7 +131,7 @@ This library uses descriptive error messages whenever possible. If you are not g
## More ## More
Documentation can be found [on pkg.go.dev](https://pkg.go.dev/github.com/golang-jwt/jwt/v4). Documentation can be found [on pkg.go.dev](https://pkg.go.dev/github.com/golang-jwt/jwt/v5).
The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in the documentation. The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. You'll also find several implementation examples in the documentation.

279
claims.go
View File

@ -1,273 +1,16 @@
package jwt package jwt
import ( // Claims represent any form of a JWT Claims Set according to
"crypto/subtle" // https://datatracker.ietf.org/doc/html/rfc7519#section-4. In order to have a
"fmt" // common basis for validation, it is required that an implementation is able to
"time" // 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`.
// Claims must just have a Valid method that determines
// if the token is invalid for any supported reason
type Claims interface { type Claims interface {
GetExpirationTime() *NumericDate
GetIssuedAt() *NumericDate
GetNotBefore() *NumericDate
GetIssuer() string
GetAudience() ClaimStrings
Valid() error 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)
}
// StandardClaims are a structured version of the JWT Claims Set, as referenced at
// https://datatracker.ietf.org/doc/html/rfc7519#section-4. They do not follow the
// specification exactly, since they were based on an earlier draft of the
// specification and not updated. The main difference is that they only
// support integer-based date fields and singular audiences. This might lead to
// incompatibilities with other JWT implementations. The use of this is discouraged, instead
// the newer RegisteredClaims struct should be used.
//
// Deprecated: Use RegisteredClaims instead for a forward-compatible way to access registered claims in a struct.
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,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 StandardClaims) Valid() error {
vErr := new(ValidationError)
now := TimeFunc().Unix()
// 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 := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0))
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 *StandardClaims) VerifyAudience(cmp string, req bool) bool {
return verifyAud([]string{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 *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool {
if c.ExpiresAt == 0 {
return verifyExp(nil, time.Unix(cmp, 0), req)
}
t := time.Unix(c.ExpiresAt, 0)
return verifyExp(&t, time.Unix(cmp, 0), req)
}
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
// If req is false, it will return true, if iat is unset.
func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool {
if c.IssuedAt == 0 {
return verifyIat(nil, time.Unix(cmp, 0), req)
}
t := time.Unix(c.IssuedAt, 0)
return verifyIat(&t, time.Unix(cmp, 0), req)
}
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
// If req is false, it will return true, if nbf is unset.
func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
if c.NotBefore == 0 {
return verifyNbf(nil, time.Unix(cmp, 0), req)
}
t := time.Unix(c.NotBefore, 0)
return verifyNbf(&t, time.Unix(cmp, 0), 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 *StandardClaims) 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
}
if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 {
return true
} else {
return false
}
}

View File

@ -16,4 +16,4 @@ To simply display a token, use:
You can install this tool with the following command: You can install this tool with the following command:
go install github.com/golang-jwt/jwt/v4/cmd/jwt go install github.com/golang-jwt/jwt/v5/cmd/jwt

View File

@ -17,7 +17,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
var ( var (

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
var ecdsaTestData = []struct { var ecdsaTestData = []struct {

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
var ed25519TestData = []struct { var ed25519TestData = []struct {

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
// Example (atypical) using the RegisteredClaims type by itself to parse a token. // Example (atypical) using the RegisteredClaims type by itself to parse a token.
@ -70,7 +70,7 @@ func ExampleNewWithClaims_customClaimsType() {
//Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM <nil> //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM <nil>
} }
// Example creating a token using a custom claims type. The StandardClaim is embedded // Example creating a token using a custom claims type. The RegisteredClaims is embedded
// in the custom type to allow for easy encoding, parsing and validation of standard claims. // in the custom type to allow for easy encoding, parsing and validation of standard claims.
func ExampleParseWithClaims_customClaimsType() { func ExampleParseWithClaims_customClaimsType() {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA" tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
@ -93,7 +93,65 @@ func ExampleParseWithClaims_customClaimsType() {
// Output: bar test // Output: bar test
} }
// An example of parsing the error types using bitfield checks // Example creating a token using a custom claims type and validation options. The RegisteredClaims is embedded
// in the custom type to allow for easy encoding, parsing and validation of standard claims.
func ExampleParseWithClaims_validationOptions() {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
type MyCustomClaims struct {
Foo string `json:"foo"`
jwt.RegisteredClaims
}
validator := jwt.NewValidator(jwt.WithLeeway(5 * time.Second))
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte("AllYourBase"), nil
}, jwt.WithValidator(validator))
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
} else {
fmt.Println(err)
}
// Output: bar test
}
type MyCustomClaims struct {
Foo string `json:"foo"`
jwt.RegisteredClaims
}
func (m MyCustomClaims) CustomValidation() error {
if m.Foo != "bar" {
return errors.New("must be foobar")
}
return nil
}
// Example creating a token using a custom claims type and validation options.
// The RegisteredClaims is embedded in the custom type to allow for easy
// encoding, parsing and validation of standard claims and the function
// CustomValidation is implemented.
func ExampleParseWithClaims_customValidation() {
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
validator := jwt.NewValidator(jwt.WithLeeway(5 * time.Second))
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte("AllYourBase"), nil
}, jwt.WithValidator(validator))
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
} else {
fmt.Println(err)
}
// Output: bar test
}
// An example of parsing the error types using errors.Is.
func ExampleParse_errorChecking() { func ExampleParse_errorChecking() {
// Token from another example. This token is expired // Token from another example. This token is expired
var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c" var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"

6
go.mod
View File

@ -1,7 +1,3 @@
module github.com/golang-jwt/jwt/v4 module github.com/golang-jwt/jwt/v5
go 1.16 go 1.16
retract (
v4.4.0 // Contains a backwards incompatible change to the Claims interface.
)

View File

@ -5,7 +5,7 @@ import (
"os" "os"
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
// For HMAC signing method, the key can be any []byte. It is recommended to generate // For HMAC signing method, the key can be any []byte. It is recommended to generate

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
var hmacTestData = []struct { var hmacTestData = []struct {

View File

@ -16,8 +16,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
"github.com/golang-jwt/jwt/v4/request" "github.com/golang-jwt/jwt/v5/request"
) )
// location of the files used for signing and verification // location of the files used for signing and verification

View File

@ -2,150 +2,92 @@ package jwt
import ( import (
"encoding/json" "encoding/json"
"errors"
"time"
// "fmt"
) )
// MapClaims is a claims type that uses the map[string]interface{} for JSON decoding. // MapClaims is a claims type that uses the map[string]interface{} for JSON decoding.
// This is the default claims type if you don't supply one // This is the default claims type if you don't supply one
type MapClaims map[string]interface{} type MapClaims map[string]interface{}
// VerifyAudience Compares the aud claim against cmp. // GetExpirationTime implements the Claims interface.
// If required is false, this method will return true if the value matches or is unset func (m MapClaims) GetExpirationTime() *NumericDate {
func (m MapClaims) VerifyAudience(cmp string, req bool) bool { return m.ParseNumericDate("exp")
var aud []string
switch v := m["aud"].(type) {
case string:
aud = append(aud, v)
case []string:
aud = v
case []interface{}:
for _, a := range v {
vs, ok := a.(string)
if !ok {
return false
}
aud = append(aud, vs)
}
}
return verifyAud(aud, cmp, req)
} }
// VerifyExpiresAt compares the exp claim against cmp (cmp <= exp). // GetNotBefore implements the Claims interface.
// If req is false, it will return true, if exp is unset. func (m MapClaims) GetNotBefore() *NumericDate {
func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { return m.ParseNumericDate("nbf")
cmpTime := time.Unix(cmp, 0) }
v, ok := m["exp"] // GetIssuedAt implements the Claims interface.
func (m MapClaims) GetIssuedAt() *NumericDate {
return m.ParseNumericDate("iat")
}
// GetAudience implements the Claims interface.
func (m MapClaims) GetAudience() ClaimStrings {
return m.ParseClaimsString("aud")
}
// GetIssuer implements the Claims interface.
func (m MapClaims) GetIssuer() string {
return m.ParseString("iss")
}
func (m MapClaims) Valid() error {
return nil
}
// ParseNumericDate tries to parse a key in the map claims type as a number
// date. This will succeed, if the underlying type is either a [float64] or a
// [json.Number]. Otherwise, nil will be returned.
func (m MapClaims) ParseNumericDate(key string) *NumericDate {
v, ok := m[key]
if !ok { if !ok {
return !req return nil
} }
switch exp := v.(type) { switch exp := v.(type) {
case float64: case float64:
if exp == 0 { if exp == 0 {
return verifyExp(nil, cmpTime, req) return nil
} }
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req) return newNumericDateFromSeconds(exp)
case json.Number: case json.Number:
v, _ := exp.Float64() v, _ := exp.Float64()
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req) return newNumericDateFromSeconds(v)
} }
return false return nil
} }
// VerifyIssuedAt compares the exp claim against cmp (cmp >= iat). // ParseClaimsString tries to parse a key in the map claims type as a
// If req is false, it will return true, if iat is unset. // [ClaimsStrings] type, which can either be a string or an array of string.
func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { func (m MapClaims) ParseClaimsString(key string) ClaimStrings {
cmpTime := time.Unix(cmp, 0) var cs []string
switch v := m[key].(type) {
v, ok := m["iat"] case string:
if !ok { cs = append(cs, v)
return !req case []string:
} cs = v
case []interface{}:
switch iat := v.(type) { for _, a := range v {
case float64: vs, ok := a.(string)
if iat == 0 { if !ok {
return verifyIat(nil, cmpTime, req) return nil
}
cs = append(cs, vs)
} }
return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req)
case json.Number:
v, _ := iat.Float64()
return verifyIat(&newNumericDateFromSeconds(v).Time, cmpTime, req)
} }
return false return cs
} }
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). // ParseString tries to parse a key in the map claims type as a
// If req is false, it will return true, if nbf is unset. // [string] type. Otherwise, an empty string is returned.
func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { func (m MapClaims) ParseString(key string) string {
cmpTime := time.Unix(cmp, 0) iss, _ := m[key].(string)
v, ok := m["nbf"] return iss
if !ok {
return !req
}
switch nbf := v.(type) {
case float64:
if nbf == 0 {
return verifyNbf(nil, cmpTime, req)
}
return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req)
case json.Number:
v, _ := nbf.Float64()
return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req)
}
return false
}
// VerifyIssuer compares the iss claim against cmp.
// If required is false, this method will return true if the value matches or is unset
func (m MapClaims) VerifyIssuer(cmp string, req bool) bool {
iss, _ := m["iss"].(string)
return verifyIss(iss, cmp, req)
}
// 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 (m MapClaims) Valid() error {
vErr := new(ValidationError)
now := TimeFunc().Unix()
if !m.VerifyExpiresAt(now, false) {
// TODO(oxisto): this should be replaced with ErrTokenExpired
vErr.Inner = errors.New("Token is expired")
vErr.Errors |= ValidationErrorExpired
}
if !m.VerifyIssuedAt(now, false) {
// TODO(oxisto): this should be replaced with ErrTokenUsedBeforeIssued
vErr.Inner = errors.New("Token used before issued")
vErr.Errors |= ValidationErrorIssuedAt
}
if !m.VerifyNotBefore(now, false) {
// TODO(oxisto): this should be replaced with ErrTokenNotValidYet
vErr.Inner = errors.New("Token is not valid yet")
vErr.Errors |= ValidationErrorNotValidYet
}
if vErr.valid() {
return nil
}
return vErr
} }

View File

@ -1,10 +1,7 @@
package jwt package jwt
import ( /*
"testing" TODO(oxisto): Re-enable tests with validation API
"time"
)
func TestVerifyAud(t *testing.T) { func TestVerifyAud(t *testing.T) {
var nilInterface interface{} var nilInterface interface{}
var nilListInterface []interface{} var nilListInterface []interface{}
@ -121,3 +118,4 @@ func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) {
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got) t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
} }
} }
*/

View File

@ -4,7 +4,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
var noneTestData = []struct { var noneTestData = []struct {

View File

@ -7,6 +7,9 @@ import (
"strings" "strings"
) )
// DefaultValidator is the default validator that is used, if no custom validator is supplied in a Parser.
var DefaultValidator = NewValidator()
type Parser struct { type Parser struct {
// If populated, only these methods will be considered valid. // If populated, only these methods will be considered valid.
// //
@ -22,13 +25,15 @@ type Parser struct {
// //
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead. // Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
SkipClaimsValidation bool SkipClaimsValidation bool
validator *Validator
} }
// NewParser creates a new Parser with the specified options // NewParser creates a new Parser with the specified options
func NewParser(options ...ParserOption) *Parser { func NewParser(options ...ParserOption) *Parser {
p := &Parser{} p := &Parser{}
// loop through our parsing options and apply them // Loop through our parsing options and apply them
for _, option := range options { for _, option := range options {
option(p) option(p)
} }
@ -82,8 +87,12 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
// Validate Claims // Validate Claims
if !p.SkipClaimsValidation { if !p.SkipClaimsValidation {
if err := token.Claims.Valid(); err != nil { // Make sure we have at least a default validator
if p.validator == nil {
p.validator = DefaultValidator
}
if err := p.validator.Validate(claims); err != nil {
// If the Claims Valid returned an error, check if it is a validation error, // If the Claims Valid returned an error, check if it is a validation error,
// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set // If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set
if e, ok := err.(*ValidationError); !ok { if e, ok := err.(*ValidationError); !ok {

View File

@ -27,3 +27,9 @@ func WithoutClaimsValidation() ParserOption {
p.SkipClaimsValidation = true p.SkipClaimsValidation = true
} }
} }
func WithValidator(v *Validator) ParserOption {
return func(p *Parser) {
p.validator = v
}
}

View File

@ -10,8 +10,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
"github.com/golang-jwt/jwt/v4/test" "github.com/golang-jwt/jwt/v5/test"
) )
var errKeyFuncError error = fmt.Errorf("error loading key") var errKeyFuncError error = fmt.Errorf("error loading key")
@ -199,19 +199,6 @@ var jwtTestData = []struct {
&jwt.Parser{UseJSONNumber: true}, &jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256, jwt.SigningMethodRS256,
}, },
{
"Standard Claims",
"",
defaultKeyFunc,
&jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Second * 10).Unix(),
},
true,
0,
nil,
&jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256,
},
{ {
"JSON Number - basic expired", "JSON Number - basic expired",
"", // autogen "", // autogen
@ -321,6 +308,28 @@ var jwtTestData = []struct {
&jwt.Parser{UseJSONNumber: true}, &jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256, jwt.SigningMethodRS256,
}, },
{
"RFC7519 Claims - nbf with 60s skew",
"", // autogen
defaultKeyFunc,
&jwt.RegisteredClaims{NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second * 100))},
false,
jwt.ValidationErrorNotValidYet,
[]error{jwt.ErrTokenNotValidYet},
jwt.NewParser(jwt.WithValidator(jwt.NewValidator(jwt.WithLeeway(time.Minute)))),
jwt.SigningMethodRS256,
},
{
"RFC7519 Claims - nbf with 120s skew",
"", // autogen
defaultKeyFunc,
&jwt.RegisteredClaims{NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second * 100))},
true,
0,
nil,
jwt.NewParser(jwt.WithValidator(jwt.NewValidator(jwt.WithLeeway(2 * time.Minute)))),
jwt.SigningMethodRS256,
},
} }
// signToken creates and returns a signed JWT token using signingMethod. // signToken creates and returns a signed JWT token using signingMethod.
@ -354,14 +363,12 @@ func TestParser_Parse(t *testing.T) {
var err error var err error
var parser = data.parser var parser = data.parser
if parser == nil { if parser == nil {
parser = new(jwt.Parser) parser = jwt.NewParser()
} }
// Figure out correct claims type // Figure out correct claims type
switch data.claims.(type) { switch data.claims.(type) {
case jwt.MapClaims: case jwt.MapClaims:
token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc) token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc)
case *jwt.StandardClaims:
token, err = parser.ParseWithClaims(data.tokenString, &jwt.StandardClaims{}, data.keyfunc)
case *jwt.RegisteredClaims: case *jwt.RegisteredClaims:
token, err = parser.ParseWithClaims(data.tokenString, &jwt.RegisteredClaims{}, data.keyfunc) token, err = parser.ParseWithClaims(data.tokenString, &jwt.RegisteredClaims{}, data.keyfunc)
} }
@ -454,8 +461,6 @@ func TestParser_ParseUnverified(t *testing.T) {
switch data.claims.(type) { switch data.claims.(type) {
case jwt.MapClaims: case jwt.MapClaims:
token, _, err = parser.ParseUnverified(data.tokenString, jwt.MapClaims{}) token, _, err = parser.ParseUnverified(data.tokenString, jwt.MapClaims{})
case *jwt.StandardClaims:
token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{})
case *jwt.RegisteredClaims: case *jwt.RegisteredClaims:
token, _, err = parser.ParseUnverified(data.tokenString, &jwt.RegisteredClaims{}) token, _, err = parser.ParseUnverified(data.tokenString, &jwt.RegisteredClaims{})
} }
@ -605,9 +610,9 @@ func BenchmarkParseUnverified(b *testing.B) {
b.Run("map_claims", func(b *testing.B) { b.Run("map_claims", func(b *testing.B) {
benchmarkParsing(b, parser, data.tokenString, jwt.MapClaims{}) benchmarkParsing(b, parser, data.tokenString, jwt.MapClaims{})
}) })
case *jwt.StandardClaims: case *jwt.RegisteredClaims:
b.Run("standard_claims", func(b *testing.B) { b.Run("registered_claims", func(b *testing.B) {
benchmarkParsing(b, parser, data.tokenString, &jwt.StandardClaims{}) benchmarkParsing(b, parser, data.tokenString, &jwt.RegisteredClaims{})
}) })
} }
} }

62
registered_claims.go Normal file
View File

@ -0,0 +1,62 @@
package jwt
// 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 use-case
// 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"`
}
// GetExpirationTime implements the Claims interface.
func (c RegisteredClaims) GetExpirationTime() *NumericDate {
return c.ExpiresAt
}
// GetNotBefore implements the Claims interface.
func (c RegisteredClaims) GetNotBefore() *NumericDate {
return c.NotBefore
}
// GetIssuedAt implements the Claims interface.
func (c RegisteredClaims) GetIssuedAt() *NumericDate {
return c.IssuedAt
}
// GetAudience implements the Claims interface.
func (c RegisteredClaims) GetAudience() ClaimStrings {
return c.Audience
}
// GetIssuer implements the Claims interface.
func (c RegisteredClaims) GetIssuer() string {
return c.Issuer
}
func (c RegisteredClaims) Valid() error {
return nil
}

View File

@ -3,7 +3,7 @@ package request
import ( import (
"net/http" "net/http"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
// ParseFromRequest extracts and parses a JWT token from an HTTP request. // ParseFromRequest extracts and parses a JWT token from an HTTP request.

View File

@ -8,8 +8,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
"github.com/golang-jwt/jwt/v4/test" "github.com/golang-jwt/jwt/v5/test"
) )
var requestTestData = []struct { var requestTestData = []struct {

View File

@ -10,8 +10,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
"github.com/golang-jwt/jwt/v4/test" "github.com/golang-jwt/jwt/v5/test"
) )
var rsaPSSTestData = []struct { var rsaPSSTestData = []struct {

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
var rsaTestData = []struct { var rsaTestData = []struct {

View File

@ -5,7 +5,7 @@ import (
"crypto/rsa" "crypto/rsa"
"os" "os"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey { func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey {

View File

@ -4,7 +4,6 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"strings" "strings"
"time"
) )
// DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515 // DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515
@ -14,11 +13,6 @@ import (
// To use the non-recommended decoding, set this boolean to `true` prior to using this package. // To use the non-recommended decoding, set this boolean to `true` prior to using this package.
var DecodePaddingAllowed bool var DecodePaddingAllowed bool
// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time).
// You can override it to use another time value. This is useful for testing or if your
// server uses a different time zone than your tokens.
var TimeFunc = time.Now
// Keyfunc will be used by the Parse methods as a callback function to supply // Keyfunc will be used by the Parse methods as a callback function to supply
// the key for verification. The function receives the parsed, // the key for verification. The function receives the parsed,
// but unverified Token. This allows you to use properties in the // but unverified Token. This allows you to use properties in the

View File

@ -3,7 +3,7 @@ package jwt_test
import ( import (
"testing" "testing"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
func TestToken_SigningString(t1 *testing.T) { func TestToken_SigningString(t1 *testing.T) {
@ -30,7 +30,7 @@ func TestToken_SigningString(t1 *testing.T) {
"typ": "JWT", "typ": "JWT",
"alg": jwt.SigningMethodHS256.Alg(), "alg": jwt.SigningMethodHS256.Alg(),
}, },
Claims: jwt.StandardClaims{}, Claims: jwt.RegisteredClaims{},
Signature: "", Signature: "",
Valid: false, Valid: false,
}, },
@ -67,7 +67,7 @@ func BenchmarkToken_SigningString(b *testing.B) {
"typ": "JWT", "typ": "JWT",
"alg": jwt.SigningMethodHS256.Alg(), "alg": jwt.SigningMethodHS256.Alg(),
}, },
Claims: jwt.StandardClaims{}, Claims: jwt.RegisteredClaims{},
} }
b.Run("BenchmarkToken_SigningString", func(b *testing.B) { b.Run("BenchmarkToken_SigningString", func(b *testing.B) {
b.ResetTimer() b.ResetTimer()

View File

@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v5"
) )
func TestNumericDate(t *testing.T) { func TestNumericDate(t *testing.T) {

201
validator.go Normal file
View File

@ -0,0 +1,201 @@
package jwt
import (
"crypto/subtle"
"fmt"
"time"
)
// Validator is the core of the new Validation API. It is
type Validator struct {
// leeway is an optional leeway that can be provided to account for clock skew.
leeway time.Duration
// timeFunc is used to supply the current time that is needed for
// validation. If unspecified, this defaults to time.Now.
timeFunc func() time.Time
// verifyIat specifies whether the iat (Issued At) claim will be verified.
// According to https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6 this
// only specifies the age of the token, but no validation check is
// necessary. However, if wanted, it can be checked if the iat is
// unrealistic, i.e., in the future.
verifyIat bool
// expectedAud contains the audiences this token expects. Supplying an empty
// string will disable aud checking.
expectedAud string
}
type customValidationType interface {
CustomValidation() error
}
func NewValidator(opts ...ValidatorOption) *Validator {
v := &Validator{}
// Apply the validator options
for _, o := range opts {
o(v)
}
return v
}
func (v *Validator) Validate(claims Claims) error {
var now time.Time
vErr := new(ValidationError)
// Check, if we have a time func
if v.timeFunc != nil {
now = v.timeFunc()
} else {
now = time.Now()
}
if !v.VerifyExpiresAt(claims, now, false) {
exp := claims.GetExpirationTime()
delta := now.Sub(exp.Time)
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
vErr.Errors |= ValidationErrorExpired
}
// Check iat if the option is enabled
if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) {
vErr.Inner = ErrTokenUsedBeforeIssued
vErr.Errors |= ValidationErrorIssuedAt
}
if !v.VerifyNotBefore(claims, now, false) {
vErr.Inner = ErrTokenNotValidYet
vErr.Errors |= ValidationErrorNotValidYet
}
if v.expectedAud != "" && !v.VerifyAudience(claims, v.expectedAud, false) {
vErr.Inner = ErrTokenNotValidYet
vErr.Errors |= ValidationErrorNotValidYet
}
// Finally, we want to give the claim itself some possibility to do some
// additional custom validation based on their custom claims
cvt, ok := claims.(customValidationType)
if ok {
if err := cvt.CustomValidation(); err != nil {
vErr.Inner = err
vErr.Errors |= ValidationErrorClaimsInvalid
}
}
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 (v *Validator) VerifyAudience(claims Claims, cmp string, req bool) bool {
return verifyAud(claims.GetAudience(), cmp, req)
}
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
// If req is false, it will return true, if exp is unset.
func (v *Validator) VerifyExpiresAt(claims Claims, cmp time.Time, req bool) bool {
exp := claims.GetExpirationTime()
if exp == nil {
return verifyExp(nil, cmp, req, v.leeway)
}
return verifyExp(&exp.Time, cmp, req, v.leeway)
}
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
// If req is false, it will return true, if iat is unset.
func (v *Validator) VerifyIssuedAt(claims Claims, cmp time.Time, req bool) bool {
iat := claims.GetIssuedAt()
if iat == nil {
return verifyIat(nil, cmp, req, v.leeway)
}
return verifyIat(&iat.Time, cmp, req, v.leeway)
}
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
// If req is false, it will return true, if nbf is unset.
func (v *Validator) VerifyNotBefore(claims Claims, cmp time.Time, req bool) bool {
nbf := claims.GetNotBefore()
if nbf == nil {
return verifyNbf(nil, cmp, req, v.leeway)
}
return verifyNbf(&nbf.Time, cmp, req, v.leeway)
}
// VerifyIssuer compares the iss claim against cmp.
// If required is false, this method will return true if the value matches or is unset
func (v *Validator) VerifyIssuer(claims Claims, cmp string, req bool) bool {
return verifyIss(claims.GetIssuer(), 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, skew time.Duration) bool {
if exp == nil {
return !required
}
return now.Before((*exp).Add(+skew))
}
func verifyIat(iat *time.Time, now time.Time, required bool, skew time.Duration) bool {
if iat == nil {
return !required
}
t := (*iat).Add(-skew)
return now.After(t) || now.Equal(t)
}
func verifyNbf(nbf *time.Time, now time.Time, required bool, skew time.Duration) bool {
if nbf == nil {
return !required
}
t := (*nbf).Add(-skew)
return now.After(t) || now.Equal(t)
}
func verifyIss(iss string, cmp string, required bool) bool {
if iss == "" {
return !required
}
if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 {
return true
} else {
return false
}
}

41
validator_option.go Normal file
View File

@ -0,0 +1,41 @@
package jwt
import "time"
// ValidatorOption is used to implement functional-style options that modify the
// behavior of the validator. To add new options, just create a function
// (ideally beginning with With or Without) that returns an anonymous function
// that takes a *Parser type as input and manipulates its configuration
// accordingly.
type ValidatorOption func(*Validator)
// WithLeeway returns the ValidatorOption for specifying the leeway window.
func WithLeeway(leeway time.Duration) ValidatorOption {
return func(v *Validator) {
v.leeway = leeway
}
}
// WithTimeFunc returns the ValidatorOption for specifying the time func. The
// primary use-case for this is testing. If you are looking for a way to account
// for clock-skew, WithLeeway should be used instead.
func WithTimeFunc(f func() time.Time) ValidatorOption {
return func(v *Validator) {
v.timeFunc = f
}
}
// WithIssuedAt returns the ValidatorOption to enable verification
// of issued-at.
func WithIssuedAt() ValidatorOption {
return func(v *Validator) {
v.verifyIat = true
}
}
// WithAudience returns the ValidatorOption to set the expected audience.
func WithAudience(aud string) ValidatorOption {
return func(v *Validator) {
v.expectedAud = aud
}
}