forked from mirror/jwt
Compare commits
13 Commits
main
...
pattern-ma
Author | SHA1 | Date |
---|---|---|
Christian Banse | e8c10437a0 | |
Christian Banse | 5a65c47732 | |
Christian Banse | 5d57c292ea | |
Christian Banse | 2281dd9079 | |
Christian Banse | 06a12c108b | |
Christian Banse | 91f51d0f6b | |
Christian Banse | eedf3ebe01 | |
Christian Banse | 4990d2cdf3 | |
Christian Banse | 0e79f91215 | |
Christian Banse | 066f850043 | |
Christian Banse | dc52415cf7 | |
Christian Banse | 7e82f33cee | |
Christian Banse | 895749e449 |
|
@ -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:
|
||||
|
||||
"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
|
||||
`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.
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
go get github.com/golang-jwt/jwt/v4
|
||||
go get github.com/golang-jwt/jwt/v5
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
|
|
28
README.md
28
README.md
|
@ -1,12 +1,12 @@
|
|||
# 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)
|
||||
[![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).
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
```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:
|
||||
|
||||
```go
|
||||
import "github.com/golang-jwt/jwt/v4"
|
||||
import "github.com/golang-jwt/jwt/v5"
|
||||
```
|
||||
|
||||
## 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 building and signing a token](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#example-New-Hmac)
|
||||
* [Directory of Examples](https://pkg.go.dev/github.com/golang-jwt/jwt/v4#pkg-examples)
|
||||
* [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/v5#example-New-Hmac)
|
||||
* [Directory of Examples](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#pkg-examples)
|
||||
|
||||
## 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 |
|
||||
| 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
|
||||
|
||||
|
@ -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:
|
||||
|
||||
* 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 [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 [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 [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 [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/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/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/v5#SigningMethodEd25519) (`Ed25519`) expect `ed25519.PrivateKey` for signing and `ed25519.PublicKey` for validation
|
||||
|
||||
### JWT and OAuth
|
||||
|
||||
|
@ -131,7 +131,7 @@ This library uses descriptive error messages whenever possible. If you are not g
|
|||
|
||||
## 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.
|
||||
|
||||
|
|
281
claims.go
281
claims.go
|
@ -1,273 +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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
GetExpirationTime() (*NumericDate, error)
|
||||
GetIssuedAt() (*NumericDate, error)
|
||||
GetNotBefore() (*NumericDate, error)
|
||||
GetIssuer() (string, error)
|
||||
GetSubject() (string, error)
|
||||
GetAudience() (ClaimStrings, error)
|
||||
}
|
||||
|
|
|
@ -16,4 +16,4 @@ To simply display a token, use:
|
|||
|
||||
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
|
|
@ -17,7 +17,7 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var ecdsaTestData = []struct {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var ed25519TestData = []struct {
|
||||
|
|
|
@ -18,6 +18,7 @@ var (
|
|||
ErrTokenExpired = errors.New("token is expired")
|
||||
ErrTokenUsedBeforeIssued = errors.New("token used before issued")
|
||||
ErrTokenInvalidIssuer = errors.New("token has invalid issuer")
|
||||
ErrTokenInvalidSubject = errors.New("token has invalid subject")
|
||||
ErrTokenNotValidYet = errors.New("token is not valid yet")
|
||||
ErrTokenInvalidId = errors.New("token has invalid id")
|
||||
ErrTokenInvalidClaims = errors.New("token has invalid claims")
|
||||
|
@ -29,11 +30,12 @@ const (
|
|||
ValidationErrorUnverifiable // Token could not be verified because of signing problems
|
||||
ValidationErrorSignatureInvalid // Signature validation failed
|
||||
|
||||
// Standard Claim validation errors
|
||||
// Registered Claim validation errors
|
||||
ValidationErrorAudience // AUD validation failed
|
||||
ValidationErrorExpired // EXP validation failed
|
||||
ValidationErrorIssuedAt // IAT validation failed
|
||||
ValidationErrorIssuer // ISS validation failed
|
||||
ValidationErrorSubject // SUB validation failed
|
||||
ValidationErrorNotValidYet // NBF validation failed
|
||||
ValidationErrorId // JTI validation failed
|
||||
ValidationErrorClaimsInvalid // Generic claims validation error
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
"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.
|
||||
|
@ -70,7 +70,7 @@ func ExampleNewWithClaims_customClaimsType() {
|
|||
//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.
|
||||
func ExampleParseWithClaims_customClaimsType() {
|
||||
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
|
||||
|
@ -93,7 +93,65 @@ func ExampleParseWithClaims_customClaimsType() {
|
|||
// 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() {
|
||||
// Token from another example. This token is expired
|
||||
var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
|
||||
|
|
6
go.mod
6
go.mod
|
@ -1,7 +1,3 @@
|
|||
module github.com/golang-jwt/jwt/v4
|
||||
module github.com/golang-jwt/jwt/v5
|
||||
|
||||
go 1.16
|
||||
|
||||
retract (
|
||||
v4.4.0 // Contains a backwards incompatible change to the Claims interface.
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"os"
|
||||
"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
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var hmacTestData = []struct {
|
||||
|
|
|
@ -16,8 +16,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v4/request"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/golang-jwt/jwt/v5/request"
|
||||
)
|
||||
|
||||
// location of the files used for signing and verification
|
||||
|
|
182
map_claims.go
182
map_claims.go
|
@ -3,149 +3,109 @@ package jwt
|
|||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
// "fmt"
|
||||
)
|
||||
|
||||
// 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
|
||||
type MapClaims map[string]interface{}
|
||||
|
||||
// VerifyAudience Compares the aud claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
func (m MapClaims) VerifyAudience(cmp string, req bool) bool {
|
||||
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)
|
||||
var ErrInvalidType = errors.New("invalid type for claim")
|
||||
|
||||
// GetExpirationTime implements the Claims interface.
|
||||
func (m MapClaims) GetExpirationTime() (*NumericDate, error) {
|
||||
return m.ParseNumericDate("exp")
|
||||
}
|
||||
|
||||
// VerifyExpiresAt compares the exp claim against cmp (cmp <= exp).
|
||||
// If req is false, it will return true, if exp is unset.
|
||||
func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool {
|
||||
cmpTime := time.Unix(cmp, 0)
|
||||
// GetNotBefore implements the Claims interface.
|
||||
func (m MapClaims) GetNotBefore() (*NumericDate, error) {
|
||||
return m.ParseNumericDate("nbf")
|
||||
}
|
||||
|
||||
v, ok := m["exp"]
|
||||
// GetIssuedAt implements the Claims interface.
|
||||
func (m MapClaims) GetIssuedAt() (*NumericDate, error) {
|
||||
return m.ParseNumericDate("iat")
|
||||
}
|
||||
|
||||
// GetAudience implements the Claims interface.
|
||||
func (m MapClaims) GetAudience() (ClaimStrings, error) {
|
||||
return m.ParseClaimsString("aud")
|
||||
}
|
||||
|
||||
// GetIssuer implements the Claims interface.
|
||||
func (m MapClaims) GetIssuer() (string, error) {
|
||||
return m.ParseString("iss")
|
||||
}
|
||||
|
||||
// GetSubject implements the Claims interface.
|
||||
func (m MapClaims) GetSubject() (string, error) {
|
||||
return m.ParseString("sub")
|
||||
}
|
||||
|
||||
// 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, error) {
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return !req
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch exp := v.(type) {
|
||||
case float64:
|
||||
if exp == 0 {
|
||||
return verifyExp(nil, cmpTime, req)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req)
|
||||
return newNumericDateFromSeconds(exp), nil
|
||||
case json.Number:
|
||||
v, _ := exp.Float64()
|
||||
|
||||
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req)
|
||||
return newNumericDateFromSeconds(v), nil
|
||||
}
|
||||
|
||||
return false
|
||||
return nil, ErrInvalidType
|
||||
}
|
||||
|
||||
// VerifyIssuedAt compares the exp claim against cmp (cmp >= iat).
|
||||
// If req is false, it will return true, if iat is unset.
|
||||
func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool {
|
||||
cmpTime := time.Unix(cmp, 0)
|
||||
|
||||
v, ok := m["iat"]
|
||||
if !ok {
|
||||
return !req
|
||||
}
|
||||
|
||||
switch iat := v.(type) {
|
||||
case float64:
|
||||
if iat == 0 {
|
||||
return verifyIat(nil, cmpTime, req)
|
||||
// ParseClaimsString tries to parse a key in the map claims type as a
|
||||
// [ClaimsStrings] type, which can either be a string or an array of string.
|
||||
func (m MapClaims) ParseClaimsString(key string) (ClaimStrings, error) {
|
||||
var cs []string
|
||||
switch v := m[key].(type) {
|
||||
case string:
|
||||
cs = append(cs, v)
|
||||
case []string:
|
||||
cs = v
|
||||
case []interface{}:
|
||||
for _, a := range v {
|
||||
vs, ok := a.(string)
|
||||
if !ok {
|
||||
return nil, ErrInvalidType
|
||||
}
|
||||
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, nil
|
||||
}
|
||||
|
||||
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
|
||||
// If req is false, it will return true, if nbf is unset.
|
||||
func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool {
|
||||
cmpTime := time.Unix(cmp, 0)
|
||||
|
||||
v, ok := m["nbf"]
|
||||
// ParseString tries to parse a key in the map claims type as a [string] type.
|
||||
// If the key does not exist, an empty string is returned. If the key has the
|
||||
// wrong type, an error is returned.
|
||||
func (m MapClaims) ParseString(key string) (string, error) {
|
||||
var (
|
||||
ok bool
|
||||
raw interface{}
|
||||
iss string
|
||||
)
|
||||
raw, ok = m[key]
|
||||
if !ok {
|
||||
return !req
|
||||
return "", nil
|
||||
}
|
||||
|
||||
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)
|
||||
iss, ok = raw.(string)
|
||||
if !ok {
|
||||
return "", ErrInvalidType
|
||||
}
|
||||
|
||||
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
|
||||
return iss, nil
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ func TestVerifyAud(t *testing.T) {
|
|||
{Name: "[]String Aud without match not required", MapClaims: MapClaims{"aud": []string{"not.example.com", "example.example.com"}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
|
||||
// Required = false
|
||||
{Name: "Empty []String Aud without match required", MapClaims: MapClaims{"aud": []string{""}}, Expected: false, Required: true, Comparison: "example.com"},
|
||||
{Name: "Empty []String Aud without match required", MapClaims: MapClaims{"aud": []string{""}}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
|
||||
// []interface{}
|
||||
{Name: "Empty []interface{} Aud without match required", MapClaims: MapClaims{"aud": nilListInterface}, Expected: true, Required: false, Comparison: "example.com"},
|
||||
|
@ -56,10 +56,17 @@ func TestVerifyAud(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
got := test.MapClaims.VerifyAudience(test.Comparison, test.Required)
|
||||
var opts []ValidatorOption
|
||||
|
||||
if got != test.Expected {
|
||||
t.Errorf("Expected %v, got %v", test.Expected, got)
|
||||
if test.Required {
|
||||
opts = append(opts, WithAudience(test.Comparison))
|
||||
}
|
||||
|
||||
validator := NewValidator(opts...)
|
||||
got := validator.Validate(test.MapClaims)
|
||||
|
||||
if (got == nil) != test.Expected {
|
||||
t.Errorf("Expected %v, got %v", test.Expected, (got == nil))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -70,9 +77,9 @@ func TestMapclaimsVerifyIssuedAtInvalidTypeString(t *testing.T) {
|
|||
"iat": "foo",
|
||||
}
|
||||
want := false
|
||||
got := mapClaims.VerifyIssuedAt(0, false)
|
||||
if want != got {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
got := NewValidator(WithIssuedAt()).Validate(mapClaims)
|
||||
if want != (got == nil) {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,9 +88,9 @@ func TestMapclaimsVerifyNotBeforeInvalidTypeString(t *testing.T) {
|
|||
"nbf": "foo",
|
||||
}
|
||||
want := false
|
||||
got := mapClaims.VerifyNotBefore(0, false)
|
||||
if want != got {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
got := NewValidator().Validate(mapClaims)
|
||||
if want != (got == nil) {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,32 +99,38 @@ func TestMapclaimsVerifyExpiresAtInvalidTypeString(t *testing.T) {
|
|||
"exp": "foo",
|
||||
}
|
||||
want := false
|
||||
got := mapClaims.VerifyExpiresAt(0, false)
|
||||
got := NewValidator().Validate(mapClaims)
|
||||
|
||||
if want != got {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
if want != (got == nil) {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) {
|
||||
exp := time.Now().Unix()
|
||||
exp := time.Now()
|
||||
mapClaims := MapClaims{
|
||||
"exp": float64(exp),
|
||||
"exp": float64(exp.Unix()),
|
||||
}
|
||||
want := false
|
||||
got := mapClaims.VerifyExpiresAt(exp, true)
|
||||
if want != got {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
got := NewValidator(WithTimeFunc(func() time.Time {
|
||||
return exp
|
||||
})).Validate(mapClaims)
|
||||
if want != (got == nil) {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||
}
|
||||
|
||||
got = mapClaims.VerifyExpiresAt(exp+1, true)
|
||||
if want != got {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
got = NewValidator(WithTimeFunc(func() time.Time {
|
||||
return exp.Add(1 * time.Second)
|
||||
})).Validate(mapClaims)
|
||||
if want != (got == nil) {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||
}
|
||||
|
||||
want = true
|
||||
got = mapClaims.VerifyExpiresAt(exp-1, true)
|
||||
if want != got {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||
got = NewValidator(WithTimeFunc(func() time.Time {
|
||||
return exp.Add(-1 * time.Second)
|
||||
})).Validate(mapClaims)
|
||||
if want != (got == nil) {
|
||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var noneTestData = []struct {
|
||||
|
|
10
parser.go
10
parser.go
|
@ -22,13 +22,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.
|
||||
SkipClaimsValidation bool
|
||||
|
||||
validator *Validator
|
||||
}
|
||||
|
||||
// NewParser creates a new Parser with the specified options
|
||||
func NewParser(options ...ParserOption) *Parser {
|
||||
p := &Parser{}
|
||||
|
||||
// loop through our parsing options and apply them
|
||||
// Loop through our parsing options and apply them
|
||||
for _, option := range options {
|
||||
option(p)
|
||||
}
|
||||
|
@ -82,8 +84,12 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
|||
|
||||
// Validate Claims
|
||||
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 = NewValidator()
|
||||
}
|
||||
|
||||
if err := p.validator.Validate(claims); err != nil {
|
||||
// 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 e, ok := err.(*ValidationError); !ok {
|
||||
|
|
|
@ -27,3 +27,10 @@ func WithoutClaimsValidation() ParserOption {
|
|||
p.SkipClaimsValidation = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithValidator is an option to include a claims validator.
|
||||
func WithValidator(v *Validator) ParserOption {
|
||||
return func(p *Parser) {
|
||||
p.validator = v
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v4/test"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/golang-jwt/jwt/v5/test"
|
||||
)
|
||||
|
||||
var errKeyFuncError error = fmt.Errorf("error loading key")
|
||||
|
@ -199,19 +199,6 @@ var jwtTestData = []struct {
|
|||
&jwt.Parser{UseJSONNumber: true},
|
||||
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",
|
||||
"", // autogen
|
||||
|
@ -321,6 +308,28 @@ var jwtTestData = []struct {
|
|||
&jwt.Parser{UseJSONNumber: true},
|
||||
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.
|
||||
|
@ -354,14 +363,12 @@ func TestParser_Parse(t *testing.T) {
|
|||
var err error
|
||||
var parser = data.parser
|
||||
if parser == nil {
|
||||
parser = new(jwt.Parser)
|
||||
parser = jwt.NewParser()
|
||||
}
|
||||
// Figure out correct claims type
|
||||
switch data.claims.(type) {
|
||||
case jwt.MapClaims:
|
||||
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:
|
||||
token, err = parser.ParseWithClaims(data.tokenString, &jwt.RegisteredClaims{}, data.keyfunc)
|
||||
}
|
||||
|
@ -454,8 +461,6 @@ func TestParser_ParseUnverified(t *testing.T) {
|
|||
switch data.claims.(type) {
|
||||
case jwt.MapClaims:
|
||||
token, _, err = parser.ParseUnverified(data.tokenString, jwt.MapClaims{})
|
||||
case *jwt.StandardClaims:
|
||||
token, _, err = parser.ParseUnverified(data.tokenString, &jwt.StandardClaims{})
|
||||
case *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) {
|
||||
benchmarkParsing(b, parser, data.tokenString, jwt.MapClaims{})
|
||||
})
|
||||
case *jwt.StandardClaims:
|
||||
b.Run("standard_claims", func(b *testing.B) {
|
||||
benchmarkParsing(b, parser, data.tokenString, &jwt.StandardClaims{})
|
||||
case *jwt.RegisteredClaims:
|
||||
b.Run("registered_claims", func(b *testing.B) {
|
||||
benchmarkParsing(b, parser, data.tokenString, &jwt.RegisteredClaims{})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
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, error) {
|
||||
return c.ExpiresAt, nil
|
||||
}
|
||||
|
||||
// GetNotBefore implements the Claims interface.
|
||||
func (c RegisteredClaims) GetNotBefore() (*NumericDate, error) {
|
||||
return c.NotBefore, nil
|
||||
}
|
||||
|
||||
// GetIssuedAt implements the Claims interface.
|
||||
func (c RegisteredClaims) GetIssuedAt() (*NumericDate, error) {
|
||||
return c.IssuedAt, nil
|
||||
}
|
||||
|
||||
// GetAudience implements the Claims interface.
|
||||
func (c RegisteredClaims) GetAudience() (ClaimStrings, error) {
|
||||
return c.Audience, nil
|
||||
}
|
||||
|
||||
// GetIssuer implements the Claims interface.
|
||||
func (c RegisteredClaims) GetIssuer() (string, error) {
|
||||
return c.Issuer, nil
|
||||
}
|
||||
|
||||
// GetSubject implements the Claims interface.
|
||||
func (c RegisteredClaims) GetSubject() (string, error) {
|
||||
return c.Subject, nil
|
||||
}
|
|
@ -3,7 +3,7 @@ package request
|
|||
import (
|
||||
"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.
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v4/test"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/golang-jwt/jwt/v5/test"
|
||||
)
|
||||
|
||||
var requestTestData = []struct {
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v4/test"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/golang-jwt/jwt/v5/test"
|
||||
)
|
||||
|
||||
var rsaPSSTestData = []struct {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var rsaTestData = []struct {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"crypto/rsa"
|
||||
"os"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey {
|
||||
|
|
6
token.go
6
token.go
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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.
|
||||
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
|
||||
// the key for verification. The function receives the parsed,
|
||||
// but unverified Token. This allows you to use properties in the
|
||||
|
|
|
@ -3,7 +3,7 @@ package jwt_test
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func TestToken_SigningString(t1 *testing.T) {
|
||||
|
@ -30,7 +30,7 @@ func TestToken_SigningString(t1 *testing.T) {
|
|||
"typ": "JWT",
|
||||
"alg": jwt.SigningMethodHS256.Alg(),
|
||||
},
|
||||
Claims: jwt.StandardClaims{},
|
||||
Claims: jwt.RegisteredClaims{},
|
||||
Signature: "",
|
||||
Valid: false,
|
||||
},
|
||||
|
@ -67,7 +67,7 @@ func BenchmarkToken_SigningString(b *testing.B) {
|
|||
"typ": "JWT",
|
||||
"alg": jwt.SigningMethodHS256.Alg(),
|
||||
},
|
||||
Claims: jwt.StandardClaims{},
|
||||
Claims: jwt.RegisteredClaims{},
|
||||
}
|
||||
b.Run("BenchmarkToken_SigningString", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func TestNumericDate(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Validator is the core of the new Validation API. It can either be used to
|
||||
// modify the validation used during parsing with the [WithValidator] parser
|
||||
// option or used standalone to validate an already parsed [Claim]. It can be
|
||||
// further customized with a range of specified [ValidatorOption]s.
|
||||
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 audience this token expects. Supplying an empty
|
||||
// string will disable aud checking.
|
||||
expectedAud string
|
||||
|
||||
// expectedIss contains the issuer this token expects. Supplying an empty
|
||||
// string will disable iss checking.
|
||||
expectedIss string
|
||||
|
||||
expectedSubPattern PatternFunc
|
||||
}
|
||||
|
||||
// CustomClaims represents a custom claims interface, which can be built upon the integrated
|
||||
// claim types, such as map claims or registered claims.
|
||||
type CustomClaims interface {
|
||||
// CustomValidation can be implemented by a user-specific claim to support
|
||||
// additional validation steps in addition to the regular validation.
|
||||
CustomValidation() error
|
||||
}
|
||||
|
||||
func NewValidator(opts ...ValidatorOption) *Validator {
|
||||
v := &Validator{}
|
||||
|
||||
// Apply the validator options
|
||||
for _, o := range opts {
|
||||
o(v)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// Validate validates the given claims. It will also perform any custom validation if claims implements the CustomValidator interface.
|
||||
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()
|
||||
}
|
||||
|
||||
// We always need to check the expiration time, but the claim itself is OPTIONAL
|
||||
if !v.VerifyExpiresAt(claims, now, false) {
|
||||
vErr.Inner = ErrTokenExpired
|
||||
vErr.Errors |= ValidationErrorExpired
|
||||
}
|
||||
|
||||
// We always need to check not-before, but the claim itself is OPTIONAL
|
||||
if !v.VerifyNotBefore(claims, now, false) {
|
||||
vErr.Inner = ErrTokenNotValidYet
|
||||
vErr.Errors |= ValidationErrorNotValidYet
|
||||
}
|
||||
|
||||
// Check issued-at if the option is enabled
|
||||
if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) {
|
||||
vErr.Inner = ErrTokenUsedBeforeIssued
|
||||
vErr.Errors |= ValidationErrorIssuedAt
|
||||
}
|
||||
|
||||
// If we have an expected audience, we also require the audience claim
|
||||
if v.expectedAud != "" && !v.VerifyAudience(claims, v.expectedAud, true) {
|
||||
vErr.Inner = ErrTokenInvalidAudience
|
||||
vErr.Errors |= ValidationErrorAudience
|
||||
}
|
||||
|
||||
// If we have an expected issuer, we also require the issuer claim
|
||||
if v.expectedIss != "" && !v.VerifyIssuer(claims, v.expectedIss, true) {
|
||||
vErr.Inner = ErrTokenInvalidIssuer
|
||||
vErr.Errors |= ValidationErrorIssuer
|
||||
}
|
||||
|
||||
// If we have an expected subject, we also require the subject claim
|
||||
if v.expectedSubPattern != nil && !v.VerifySubject(claims, v.expectedSubPattern, true) {
|
||||
vErr.Inner = ErrTokenInvalidSubject
|
||||
vErr.Errors |= ValidationErrorSubject
|
||||
}
|
||||
|
||||
// Finally, we want to give the claim itself some possibility to do some
|
||||
// additional custom validation based on their custom claims
|
||||
cvt, ok := claims.(CustomClaims)
|
||||
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 {
|
||||
aud, err := claims.GetAudience()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return verifyAud(aud, 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 {
|
||||
var time *time.Time = nil
|
||||
|
||||
exp, err := claims.GetExpirationTime()
|
||||
if err != nil {
|
||||
return false
|
||||
} else if exp != nil {
|
||||
time = &exp.Time
|
||||
}
|
||||
|
||||
return verifyExp(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 {
|
||||
var time *time.Time = nil
|
||||
|
||||
iat, err := claims.GetIssuedAt()
|
||||
if err != nil {
|
||||
return false
|
||||
} else if iat != nil {
|
||||
time = &iat.Time
|
||||
}
|
||||
|
||||
return verifyIat(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 {
|
||||
var time *time.Time = nil
|
||||
|
||||
nbf, err := claims.GetNotBefore()
|
||||
if err != nil {
|
||||
return false
|
||||
} else if nbf != nil {
|
||||
time = &nbf.Time
|
||||
}
|
||||
|
||||
return verifyNbf(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 {
|
||||
iss, err := claims.GetIssuer()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return verifyIss(iss, cmp, req)
|
||||
}
|
||||
|
||||
// VerifySubject compares the sub claim against cmp.
|
||||
// If required is false, this method will return true if the value matches or is unset
|
||||
func (v *Validator) VerifySubject(claims Claims, cmp PatternFunc, req bool) bool {
|
||||
iss, err := claims.GetSubject()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return verifySub(iss, 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 stringClaims == "" {
|
||||
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.Before(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.Before(t)
|
||||
}
|
||||
|
||||
func verifyIss(iss string, cmp string, required bool) bool {
|
||||
if iss == "" {
|
||||
return !required
|
||||
}
|
||||
|
||||
return iss == cmp
|
||||
}
|
||||
|
||||
func verifySub(sub string, cmp PatternFunc, required bool) bool {
|
||||
if sub == "" {
|
||||
return !required
|
||||
}
|
||||
|
||||
return cmp(sub)
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"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)
|
||||
|
||||
type PatternFunc func(s string) bool
|
||||
|
||||
func HasPrefix(prefix string) PatternFunc {
|
||||
return func(s string) bool {
|
||||
return strings.HasPrefix(s, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
func Equals(cmp string) PatternFunc {
|
||||
return func(s string) bool {
|
||||
return cmp == s
|
||||
}
|
||||
}
|
||||
|
||||
// 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 configures the validator to require the specified audience in
|
||||
// the `aud` claim. Validation will fail if the audience is not listed in the
|
||||
// token or the `aud` claim is missing.
|
||||
//
|
||||
// NOTE: While the `aud` claim is OPTIONAL is a JWT, the handling of it is
|
||||
// application-specific. Since this validation API is helping developers in
|
||||
// writing secure application, we decided to REQUIRE the existence of the claim.
|
||||
func WithAudience(aud string) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.expectedAud = aud
|
||||
}
|
||||
}
|
||||
|
||||
// WithIssuer configures the validator to require the specified issuer in the
|
||||
// `iss` claim. Validation will fail if a different issuer is specified in the
|
||||
// token or the `iss` claim is missing.
|
||||
//
|
||||
// NOTE: While the `iss` claim is OPTIONAL is a JWT, the handling of it is
|
||||
// application-specific. Since this validation API is helping developers in
|
||||
// writing secure application, we decided to REQUIRE the existence of the claim.
|
||||
func WithIssuer(iss string) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.expectedIss = iss
|
||||
}
|
||||
}
|
||||
|
||||
// WithSubject configures the validator to require the specified subject in the
|
||||
// `sub` claim. Validation will fail if a different subject is specified in the
|
||||
// token or the `sub` claim is missing.
|
||||
//
|
||||
// NOTE: While the `sub` claim is OPTIONAL is a JWT, the handling of it is
|
||||
// application-specific. Since this validation API is helping developers in
|
||||
// writing secure application, we decided to REQUIRE the existence of the claim.
|
||||
func WithSubject(sub string) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.expectedSubPattern = Equals(sub)
|
||||
}
|
||||
}
|
||||
|
||||
func WithSubjectPattern(pattern PatternFunc) ValidatorOption {
|
||||
return func(v *Validator) {
|
||||
v.expectedSubPattern = pattern
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package jwt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValidator_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
leeway time.Duration
|
||||
timeFunc func() time.Time
|
||||
verifyIat bool
|
||||
expectedAud string
|
||||
expectedIss string
|
||||
expectedSubPattern PatternFunc
|
||||
}
|
||||
type args struct {
|
||||
claims Claims
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "with subject pattern",
|
||||
fields: fields{
|
||||
expectedSubPattern: HasPrefix("My"),
|
||||
},
|
||||
args: args{
|
||||
claims: RegisteredClaims{Subject: "MyUser"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &Validator{
|
||||
leeway: tt.fields.leeway,
|
||||
timeFunc: tt.fields.timeFunc,
|
||||
verifyIat: tt.fields.verifyIat,
|
||||
expectedAud: tt.fields.expectedAud,
|
||||
expectedIss: tt.fields.expectedIss,
|
||||
expectedSubPattern: tt.fields.expectedSubPattern,
|
||||
}
|
||||
if err := v.Validate(tt.args.claims); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validator.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue