mirror of https://github.com/golang-jwt/jwt.git
`v5` Pre-Release (#234)
Co-authored-by: Micah Parks <66095735+MicahParks@users.noreply.github.com> Co-authored-by: Michael Fridman <mf192@icloud.com>
This commit is contained in:
parent
4fd5621d8d
commit
148d710109
|
@ -25,7 +25,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: [1.17, 1.18, 1.19]
|
go: ["1.18", "1.19", "1.20"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
|
@ -1,4 +1,82 @@
|
||||||
## Migration Guide (v4.0.0)
|
# Migration Guide (v5.0.0)
|
||||||
|
|
||||||
|
Version `v5` contains a major rework of core functionalities in the `jwt-go` library. This includes support for several
|
||||||
|
validation options as well as a re-design of the `Claims` interface. Lastly, we reworked how errors work under the hood,
|
||||||
|
which should provide a better overall developer experience.
|
||||||
|
|
||||||
|
Starting from [v5.0.0](https://github.com/golang-jwt/jwt/releases/tag/v5.0.0), the import path will be:
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
|
For most users, changing the import path *should* suffice. However, since we intentionally changed and cleaned some of
|
||||||
|
the public API, existing programs might need to be adopted. The following paragraphs go through the individual changes
|
||||||
|
and make suggestions how to change existing programs.
|
||||||
|
|
||||||
|
## Parsing and Validation Options
|
||||||
|
|
||||||
|
Under the hood, a new `validator` struct takes care of validating the claims. A long awaited feature has been the option
|
||||||
|
to fine-tune the validation of tokens. This is now possible with several `ParserOption` functions that can be appended
|
||||||
|
to most `Parse` functions, such as `ParseWithClaims`. The most important options and changes are:
|
||||||
|
* `WithLeeway`, which can be used to specific leeway that is taken into account when validating time-based claims, such as `exp` or `nbf`.
|
||||||
|
* The new default behavior now disables checking the `iat` claim by default. Usage of this claim is OPTIONAL according to the JWT RFC. The claim itself is also purely informational according to the RFC, so a strict validation failure is not recommended. If you want to check for sensible values in these claims, please use the `WithIssuedAt` parser option.
|
||||||
|
* New options have also been added to check for expected `aud`, `sub` and `iss`, namely `WithAudience`, `WithSubject` and `WithIssuer`.
|
||||||
|
|
||||||
|
## Changes to the `Claims` interface
|
||||||
|
|
||||||
|
### Complete Restructuring
|
||||||
|
|
||||||
|
Previously, the claims interface was satisfied with an implementation of a `Valid() error` function. This had several issues:
|
||||||
|
* The different claim types (struct claims, map claims, etc.) then contained similar (but not 100 % identical) code of how this validation was done. This lead to a lot of (almost) duplicate code and was hard to maintain
|
||||||
|
* It was not really semantically close to what a "claim" (or a set of claims) really is; which is a list of defined key/value pairs with a certain semantic meaning.
|
||||||
|
|
||||||
|
Since all the validation functionality is now extracted into the validator, all `VerifyXXX` and `Valid` functions have been removed from the `Claims` interface. Instead, the interface now represents a list of getters to retrieve values with a specific meaning. This allows us to completely decouple the validation logic with the underlying storage representation of the claim, which could be a struct, a map or even something stored in a database.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Claims interface {
|
||||||
|
GetExpirationTime() (*NumericDate, error)
|
||||||
|
GetIssuedAt() (*NumericDate, error)
|
||||||
|
GetNotBefore() (*NumericDate, error)
|
||||||
|
GetIssuer() (string, error)
|
||||||
|
GetSubject() (string, error)
|
||||||
|
GetAudience() (ClaimStrings, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Claim Types and Removal of `StandardClaims`
|
||||||
|
|
||||||
|
The two standard claim types supported by this library, `MapClaims` and `RegisteredClaims` both implement the necessary functions of this interface. The old `StandardClaims` struct, which has already been deprecated in `v4` is now removed.
|
||||||
|
|
||||||
|
Users using custom claims, in most cases, will not experience any changes in the behavior as long as they embedded
|
||||||
|
`RegisteredClaims`. If they created a new claim type from scratch, they now need to implemented the proper getter
|
||||||
|
functions.
|
||||||
|
|
||||||
|
### Migrating Application Specific Logic of the old `Valid`
|
||||||
|
|
||||||
|
Previously, users could override the `Valid` method in a custom claim, for example to extend the validation with application-specific claims. However, this was always very dangerous, since once could easily disable the standard validation and signature checking.
|
||||||
|
|
||||||
|
In order to avoid that, while still supporting the use-case, a new `ClaimsValidator` interface has been introduced. This interface consists of the `Validate() error` function. If the validator sees, that a `Claims` struct implements this interface, the errors returned to the `Validate` function will be *appended* to the regular standard validation. It is not possible to disable the standard validation anymore (even only by accident).
|
||||||
|
|
||||||
|
Usage examples can be found in [example_test.go](./example_test.go), to build claims structs like the following.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// MyCustomClaims includes all registered claims, plus Foo.
|
||||||
|
type MyCustomClaims struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate can be used to execute additional application-specific claims
|
||||||
|
// validation.
|
||||||
|
func (m MyCustomClaims) Validate() error {
|
||||||
|
if m.Foo != "bar" {
|
||||||
|
return errors.New("must be foobar")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Migration Guide (v4.0.0)
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
|
@ -8,7 +86,7 @@ The `/v4` version will be backwards compatible with existing `v3.x.y` tags in th
|
||||||
`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:
|
||||||
|
|
||||||
|
@ -17,6 +95,6 @@ go get github.com/golang-jwt/jwt/v4
|
||||||
go mod tidy
|
go mod tidy
|
||||||
```
|
```
|
||||||
|
|
||||||
## Older releases (before v3.2.0)
|
# Older releases (before v3.2.0)
|
||||||
|
|
||||||
The original migration guide for older releases can be found at https://github.com/dgrijalva/jwt-go/blob/master/MIGRATION_GUIDE.md.
|
The original migration guide for older releases can be found at https://github.com/dgrijalva/jwt-go/blob/master/MIGRATION_GUIDE.md.
|
||||||
|
|
26
README.md
26
README.md
|
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
## `jwt-go` Version History
|
# `jwt-go` Version History
|
||||||
|
|
||||||
#### 4.0.0
|
The following version history is kept for historic purposes. To retrieve the current changes of each version, please refer to the change-log of the specific release versions on https://github.com/golang-jwt/jwt/releases.
|
||||||
|
|
||||||
|
## 4.0.0
|
||||||
|
|
||||||
* Introduces support for Go modules. The `v4` version will be backwards compatible with `v3.x.y`.
|
* Introduces support for Go modules. The `v4` version will be backwards compatible with `v3.x.y`.
|
||||||
|
|
||||||
#### 3.2.2
|
## 3.2.2
|
||||||
|
|
||||||
* Starting from this release, we are adopting the policy to support the most 2 recent versions of Go currently available. By the time of this release, this is Go 1.15 and 1.16 ([#28](https://github.com/golang-jwt/jwt/pull/28)).
|
* Starting from this release, we are adopting the policy to support the most 2 recent versions of Go currently available. By the time of this release, this is Go 1.15 and 1.16 ([#28](https://github.com/golang-jwt/jwt/pull/28)).
|
||||||
* Fixed a potential issue that could occur when the verification of `exp`, `iat` or `nbf` was not required and contained invalid contents, i.e. non-numeric/date. Thanks for @thaJeztah for making us aware of that and @giorgos-f3 for originally reporting it to the formtech fork ([#40](https://github.com/golang-jwt/jwt/pull/40)).
|
* Fixed a potential issue that could occur when the verification of `exp`, `iat` or `nbf` was not required and contained invalid contents, i.e. non-numeric/date. Thanks for @thaJeztah for making us aware of that and @giorgos-f3 for originally reporting it to the formtech fork ([#40](https://github.com/golang-jwt/jwt/pull/40)).
|
||||||
* Added support for EdDSA / ED25519 ([#36](https://github.com/golang-jwt/jwt/pull/36)).
|
* Added support for EdDSA / ED25519 ([#36](https://github.com/golang-jwt/jwt/pull/36)).
|
||||||
* Optimized allocations ([#33](https://github.com/golang-jwt/jwt/pull/33)).
|
* Optimized allocations ([#33](https://github.com/golang-jwt/jwt/pull/33)).
|
||||||
|
|
||||||
#### 3.2.1
|
## 3.2.1
|
||||||
|
|
||||||
* **Import Path Change**: See MIGRATION_GUIDE.md for tips on updating your code
|
* **Import Path Change**: See MIGRATION_GUIDE.md for tips on updating your code
|
||||||
* Changed the import path from `github.com/dgrijalva/jwt-go` to `github.com/golang-jwt/jwt`
|
* Changed the import path from `github.com/dgrijalva/jwt-go` to `github.com/golang-jwt/jwt`
|
||||||
|
@ -117,17 +119,17 @@ It is likely the only integration change required here will be to change `func(t
|
||||||
* Refactored the RSA implementation to be easier to read
|
* Refactored the RSA implementation to be easier to read
|
||||||
* Exposed helper methods `ParseRSAPrivateKeyFromPEM` and `ParseRSAPublicKeyFromPEM`
|
* Exposed helper methods `ParseRSAPrivateKeyFromPEM` and `ParseRSAPublicKeyFromPEM`
|
||||||
|
|
||||||
#### 1.0.2
|
## 1.0.2
|
||||||
|
|
||||||
* Fixed bug in parsing public keys from certificates
|
* Fixed bug in parsing public keys from certificates
|
||||||
* Added more tests around the parsing of keys for RS256
|
* Added more tests around the parsing of keys for RS256
|
||||||
* Code refactoring in RS256 implementation. No functional changes
|
* Code refactoring in RS256 implementation. No functional changes
|
||||||
|
|
||||||
#### 1.0.1
|
## 1.0.1
|
||||||
|
|
||||||
* Fixed panic if RS256 signing method was passed an invalid key
|
* Fixed panic if RS256 signing method was passed an invalid key
|
||||||
|
|
||||||
#### 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
* First versioned release
|
* First versioned release
|
||||||
* API stabilized
|
* API stabilized
|
||||||
|
|
277
claims.go
277
claims.go
|
@ -1,269 +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`, `sub` 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 {
|
||||||
Valid() error
|
GetExpirationTime() (*NumericDate, error)
|
||||||
}
|
GetIssuedAt() (*NumericDate, error)
|
||||||
|
GetNotBefore() (*NumericDate, error)
|
||||||
// RegisteredClaims are a structured version of the JWT Claims Set,
|
GetIssuer() (string, error)
|
||||||
// restricted to Registered Claim Names, as referenced at
|
GetSubject() (string, error)
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
|
GetAudience() (ClaimStrings, error)
|
||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
return subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -17,7 +17,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
107
errors.go
107
errors.go
|
@ -2,111 +2,48 @@ package jwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error constants
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidKey = errors.New("key is invalid")
|
ErrInvalidKey = errors.New("key is invalid")
|
||||||
ErrInvalidKeyType = errors.New("key is of invalid type")
|
ErrInvalidKeyType = errors.New("key is of invalid type")
|
||||||
ErrHashUnavailable = errors.New("the requested hash function is unavailable")
|
ErrHashUnavailable = errors.New("the requested hash function is unavailable")
|
||||||
|
|
||||||
ErrTokenMalformed = errors.New("token is malformed")
|
ErrTokenMalformed = errors.New("token is malformed")
|
||||||
ErrTokenUnverifiable = errors.New("token is unverifiable")
|
ErrTokenUnverifiable = errors.New("token is unverifiable")
|
||||||
ErrTokenSignatureInvalid = errors.New("token signature is invalid")
|
ErrTokenSignatureInvalid = errors.New("token signature is invalid")
|
||||||
|
ErrTokenRequiredClaimMissing = errors.New("token is missing required claim")
|
||||||
ErrTokenInvalidAudience = errors.New("token has invalid audience")
|
ErrTokenInvalidAudience = errors.New("token has invalid audience")
|
||||||
ErrTokenExpired = errors.New("token is expired")
|
ErrTokenExpired = errors.New("token is expired")
|
||||||
ErrTokenUsedBeforeIssued = errors.New("token used before issued")
|
ErrTokenUsedBeforeIssued = errors.New("token used before issued")
|
||||||
ErrTokenInvalidIssuer = errors.New("token has invalid issuer")
|
ErrTokenInvalidIssuer = errors.New("token has invalid issuer")
|
||||||
|
ErrTokenInvalidSubject = errors.New("token has invalid subject")
|
||||||
ErrTokenNotValidYet = errors.New("token is not valid yet")
|
ErrTokenNotValidYet = errors.New("token is not valid yet")
|
||||||
ErrTokenInvalidId = errors.New("token has invalid id")
|
ErrTokenInvalidId = errors.New("token has invalid id")
|
||||||
ErrTokenInvalidClaims = errors.New("token has invalid claims")
|
ErrTokenInvalidClaims = errors.New("token has invalid claims")
|
||||||
|
ErrInvalidType = errors.New("invalid type for claim")
|
||||||
)
|
)
|
||||||
|
|
||||||
// The errors that might occur when parsing and validating a token
|
// joinedError is an error type that works similar to what [errors.Join]
|
||||||
const (
|
// produces, with the exception that it has a nice error string; mainly its
|
||||||
ValidationErrorMalformed uint32 = 1 << iota // Token is malformed
|
// error messages are concatenated using a comma, rather than a newline.
|
||||||
ValidationErrorUnverifiable // Token could not be verified because of signing problems
|
type joinedError struct {
|
||||||
ValidationErrorSignatureInvalid // Signature validation failed
|
errs []error
|
||||||
|
|
||||||
// Standard Claim validation errors
|
|
||||||
ValidationErrorAudience // AUD validation failed
|
|
||||||
ValidationErrorExpired // EXP validation failed
|
|
||||||
ValidationErrorIssuedAt // IAT validation failed
|
|
||||||
ValidationErrorIssuer // ISS validation failed
|
|
||||||
ValidationErrorNotValidYet // NBF validation failed
|
|
||||||
ValidationErrorId // JTI validation failed
|
|
||||||
ValidationErrorClaimsInvalid // Generic claims validation error
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewValidationError is a helper for constructing a ValidationError with a string error message
|
|
||||||
func NewValidationError(errorText string, errorFlags uint32) *ValidationError {
|
|
||||||
return &ValidationError{
|
|
||||||
text: errorText,
|
|
||||||
Errors: errorFlags,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidationError represents an error from Parse if token is not valid
|
func (je joinedError) Error() string {
|
||||||
type ValidationError struct {
|
msg := []string{}
|
||||||
Inner error // stores the error returned by external dependencies, i.e.: KeyFunc
|
for _, err := range je.errs {
|
||||||
Errors uint32 // bitfield. see ValidationError... constants
|
msg = append(msg, err.Error())
|
||||||
text string // errors that do not have a valid error just have text
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error is the implementation of the err interface.
|
|
||||||
func (e ValidationError) Error() string {
|
|
||||||
if e.Inner != nil {
|
|
||||||
return e.Inner.Error()
|
|
||||||
} else if e.text != "" {
|
|
||||||
return e.text
|
|
||||||
} else {
|
|
||||||
return "token is invalid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap gives errors.Is and errors.As access to the inner error.
|
|
||||||
func (e *ValidationError) Unwrap() error {
|
|
||||||
return e.Inner
|
|
||||||
}
|
|
||||||
|
|
||||||
// No errors
|
|
||||||
func (e *ValidationError) valid() bool {
|
|
||||||
return e.Errors == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Is checks if this ValidationError is of the supplied error. We are first checking for the exact error message
|
|
||||||
// by comparing the inner error message. If that fails, we compare using the error flags. This way we can use
|
|
||||||
// custom error messages (mainly for backwards compatability) and still leverage errors.Is using the global error variables.
|
|
||||||
func (e *ValidationError) Is(err error) bool {
|
|
||||||
// Check, if our inner error is a direct match
|
|
||||||
if errors.Is(errors.Unwrap(e), err) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, we need to match using our error flags
|
return strings.Join(msg, ", ")
|
||||||
switch err {
|
}
|
||||||
case ErrTokenMalformed:
|
|
||||||
return e.Errors&ValidationErrorMalformed != 0
|
// joinErrors joins together multiple errors. Useful for scenarios where
|
||||||
case ErrTokenUnverifiable:
|
// multiple errors next to each other occur, e.g., in claims validation.
|
||||||
return e.Errors&ValidationErrorUnverifiable != 0
|
func joinErrors(errs ...error) error {
|
||||||
case ErrTokenSignatureInvalid:
|
return &joinedError{
|
||||||
return e.Errors&ValidationErrorSignatureInvalid != 0
|
errs: errs,
|
||||||
case ErrTokenInvalidAudience:
|
}
|
||||||
return e.Errors&ValidationErrorAudience != 0
|
|
||||||
case ErrTokenExpired:
|
|
||||||
return e.Errors&ValidationErrorExpired != 0
|
|
||||||
case ErrTokenUsedBeforeIssued:
|
|
||||||
return e.Errors&ValidationErrorIssuedAt != 0
|
|
||||||
case ErrTokenInvalidIssuer:
|
|
||||||
return e.Errors&ValidationErrorIssuer != 0
|
|
||||||
case ErrTokenNotValidYet:
|
|
||||||
return e.Errors&ValidationErrorNotValidYet != 0
|
|
||||||
case ErrTokenInvalidId:
|
|
||||||
return e.Errors&ValidationErrorId != 0
|
|
||||||
case ErrTokenInvalidClaims:
|
|
||||||
return e.Errors&ValidationErrorClaimsInvalid != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
//go:build go1.20
|
||||||
|
// +build go1.20
|
||||||
|
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unwrap implements the multiple error unwrapping for this error type, which is
|
||||||
|
// possible in Go 1.20.
|
||||||
|
func (je joinedError) Unwrap() []error {
|
||||||
|
return je.errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// newError creates a new error message with a detailed error message. The
|
||||||
|
// message will be prefixed with the contents of the supplied error type.
|
||||||
|
// Additionally, more errors, that provide more context can be supplied which
|
||||||
|
// will be appended to the message. This makes use of Go 1.20's possibility to
|
||||||
|
// include more than one %w formatting directive in [fmt.Errorf].
|
||||||
|
//
|
||||||
|
// For example,
|
||||||
|
//
|
||||||
|
// newError("no keyfunc was provided", ErrTokenUnverifiable)
|
||||||
|
//
|
||||||
|
// will produce the error string
|
||||||
|
//
|
||||||
|
// "token is unverifiable: no keyfunc was provided"
|
||||||
|
func newError(message string, err error, more ...error) error {
|
||||||
|
var format string
|
||||||
|
var args []any
|
||||||
|
if message != "" {
|
||||||
|
format = "%w: %s"
|
||||||
|
args = []any{err, message}
|
||||||
|
} else {
|
||||||
|
format = "%w"
|
||||||
|
args = []any{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range more {
|
||||||
|
format += ": %w"
|
||||||
|
args = append(args, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fmt.Errorf(format, args...)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
//go:build !go1.20
|
||||||
|
// +build !go1.20
|
||||||
|
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Is implements checking for multiple errors using [errors.Is], since multiple
|
||||||
|
// error unwrapping is not possible in versions less than Go 1.20.
|
||||||
|
func (je joinedError) Is(err error) bool {
|
||||||
|
for _, e := range je.errs {
|
||||||
|
if errors.Is(e, err) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrappedErrors is a workaround for wrapping multiple errors in environments
|
||||||
|
// where Go 1.20 is not available. It basically uses the already implemented
|
||||||
|
// functionatlity of joinedError to handle multiple errors with supplies a
|
||||||
|
// custom error message that is identical to the one we produce in Go 1.20 using
|
||||||
|
// multiple %w directives.
|
||||||
|
type wrappedErrors struct {
|
||||||
|
msg string
|
||||||
|
joinedError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the stored error string
|
||||||
|
func (we wrappedErrors) Error() string {
|
||||||
|
return we.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// newError creates a new error message with a detailed error message. The
|
||||||
|
// message will be prefixed with the contents of the supplied error type.
|
||||||
|
// Additionally, more errors, that provide more context can be supplied which
|
||||||
|
// will be appended to the message. Since we cannot use of Go 1.20's possibility
|
||||||
|
// to include more than one %w formatting directive in [fmt.Errorf], we have to
|
||||||
|
// emulate that.
|
||||||
|
//
|
||||||
|
// For example,
|
||||||
|
//
|
||||||
|
// newError("no keyfunc was provided", ErrTokenUnverifiable)
|
||||||
|
//
|
||||||
|
// will produce the error string
|
||||||
|
//
|
||||||
|
// "token is unverifiable: no keyfunc was provided"
|
||||||
|
func newError(message string, err error, more ...error) error {
|
||||||
|
// We cannot wrap multiple errors here with %w, so we have to be a little
|
||||||
|
// bit creative. Basically, we are using %s instead of %w to produce the
|
||||||
|
// same error message and then throw the result into a custom error struct.
|
||||||
|
var format string
|
||||||
|
var args []any
|
||||||
|
if message != "" {
|
||||||
|
format = "%s: %s"
|
||||||
|
args = []any{err, message}
|
||||||
|
} else {
|
||||||
|
format = "%s"
|
||||||
|
args = []any{err}
|
||||||
|
}
|
||||||
|
errs := []error{err}
|
||||||
|
|
||||||
|
for _, e := range more {
|
||||||
|
format += ": %s"
|
||||||
|
args = append(args, e)
|
||||||
|
errs = append(errs, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = &wrappedErrors{
|
||||||
|
msg: fmt.Sprintf(format, args...),
|
||||||
|
joinedError: joinedError{errs: errs},
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_joinErrors(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
errs []error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErrors []error
|
||||||
|
wantMessage string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "multiple errors",
|
||||||
|
args: args{
|
||||||
|
errs: []error{ErrTokenNotValidYet, ErrTokenExpired},
|
||||||
|
},
|
||||||
|
wantErrors: []error{ErrTokenNotValidYet, ErrTokenExpired},
|
||||||
|
wantMessage: "token is not valid yet, token is expired",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := joinErrors(tt.args.errs...)
|
||||||
|
for _, wantErr := range tt.wantErrors {
|
||||||
|
if !errors.Is(err, wantErr) {
|
||||||
|
t.Errorf("joinErrors() error = %v, does not contain %v", err, wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Error() != tt.wantMessage {
|
||||||
|
t.Errorf("joinErrors() error.Error() = %v, wantMessage %v", err, tt.wantMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newError(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
message string
|
||||||
|
err error
|
||||||
|
more []error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantErrors []error
|
||||||
|
wantMessage string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single error",
|
||||||
|
args: args{message: "something is wrong", err: ErrTokenMalformed},
|
||||||
|
wantMessage: "token is malformed: something is wrong",
|
||||||
|
wantErrors: []error{ErrTokenMalformed},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two errors",
|
||||||
|
args: args{message: "something is wrong", err: ErrTokenMalformed, more: []error{io.ErrUnexpectedEOF}},
|
||||||
|
wantMessage: "token is malformed: something is wrong: unexpected EOF",
|
||||||
|
wantErrors: []error{ErrTokenMalformed},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two errors, no detail",
|
||||||
|
args: args{message: "", err: ErrTokenInvalidClaims, more: []error{ErrTokenExpired}},
|
||||||
|
wantMessage: "token has invalid claims: token is expired",
|
||||||
|
wantErrors: []error{ErrTokenInvalidClaims, ErrTokenExpired},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two errors, no detail and join error",
|
||||||
|
args: args{message: "", err: ErrTokenInvalidClaims, more: []error{joinErrors(ErrTokenExpired, ErrTokenNotValidYet)}},
|
||||||
|
wantMessage: "token has invalid claims: token is expired, token is not valid yet",
|
||||||
|
wantErrors: []error{ErrTokenInvalidClaims, ErrTokenExpired, ErrTokenNotValidYet},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := newError(tt.args.message, tt.args.err, tt.args.more...)
|
||||||
|
for _, wantErr := range tt.wantErrors {
|
||||||
|
if !errors.Is(err, wantErr) {
|
||||||
|
t.Errorf("newError() error = %v, does not contain %v", err, wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Error() != tt.wantMessage {
|
||||||
|
t.Errorf("newError() error.Error() = %v, wantMessage %v", err, tt.wantMessage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte("AllYourBase"), nil
|
||||||
|
}, jwt.WithLeeway(5*time.Second))
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate can be used to execute additional application-specific claims
|
||||||
|
// validation.
|
||||||
|
func (m MyCustomClaims) Validate() 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"
|
||||||
|
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte("AllYourBase"), nil
|
||||||
|
}, jwt.WithLeeway(5*time.Second))
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -1,7 +1,3 @@
|
||||||
module github.com/golang-jwt/jwt/v4
|
module github.com/golang-jwt/jwt/v5
|
||||||
|
|
||||||
go 1.16
|
go 1.18
|
||||||
|
|
||||||
retract (
|
|
||||||
v4.4.0 // Contains a backwards incompatible change to the Claims interface.
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
186
map_claims.go
186
map_claims.go
|
@ -2,150 +2,108 @@ package jwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"fmt"
|
||||||
"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
|
||||||
// This is the default claims type if you don't supply one
|
// decoding. 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, error) {
|
||||||
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, error) {
|
||||||
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, 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 {
|
if !ok {
|
||||||
return !req
|
return nil, 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, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req)
|
return newNumericDateFromSeconds(exp), nil
|
||||||
case json.Number:
|
case json.Number:
|
||||||
v, _ := exp.Float64()
|
v, _ := exp.Float64()
|
||||||
|
|
||||||
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req)
|
return newNumericDateFromSeconds(v), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return nil, newError(fmt.Sprintf("%s is invalid", key), ErrInvalidType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, error) {
|
||||||
cmpTime := time.Unix(cmp, 0)
|
var cs []string
|
||||||
|
switch v := m[key].(type) {
|
||||||
v, ok := m["iat"]
|
case string:
|
||||||
|
cs = append(cs, v)
|
||||||
|
case []string:
|
||||||
|
cs = v
|
||||||
|
case []interface{}:
|
||||||
|
for _, a := range v {
|
||||||
|
vs, ok := a.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return !req
|
return nil, newError(fmt.Sprintf("%s is invalid", key), ErrInvalidType)
|
||||||
|
}
|
||||||
|
cs = append(cs, vs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch iat := v.(type) {
|
return cs, nil
|
||||||
case float64:
|
|
||||||
if iat == 0 {
|
|
||||||
return verifyIat(nil, cmpTime, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req)
|
|
||||||
case json.Number:
|
|
||||||
v, _ := iat.Float64()
|
|
||||||
|
|
||||||
return verifyIat(&newNumericDateFromSeconds(v).Time, cmpTime, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
|
// parseString tries to parse a key in the map claims type as a [string] type.
|
||||||
// If req is false, it will return true, if nbf is unset.
|
// If the key does not exist, an empty string is returned. If the key has the
|
||||||
func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool {
|
// wrong type, an error is returned.
|
||||||
cmpTime := time.Unix(cmp, 0)
|
func (m MapClaims) parseString(key string) (string, error) {
|
||||||
|
var (
|
||||||
v, ok := m["nbf"]
|
ok bool
|
||||||
|
raw interface{}
|
||||||
|
iss string
|
||||||
|
)
|
||||||
|
raw, ok = m[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return !req
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch nbf := v.(type) {
|
iss, ok = raw.(string)
|
||||||
case float64:
|
if !ok {
|
||||||
if nbf == 0 {
|
return "", newError(fmt.Sprintf("%s is invalid", key), ErrInvalidType)
|
||||||
return verifyNbf(nil, cmpTime, req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req)
|
return iss, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"},
|
{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
|
// 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{}
|
// []interface{}
|
||||||
{Name: "Empty []interface{} Aud without match required", MapClaims: MapClaims{"aud": nilListInterface}, Expected: true, Required: false, Comparison: "example.com"},
|
{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 {
|
for _, test := range tests {
|
||||||
t.Run(test.Name, func(t *testing.T) {
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
got := test.MapClaims.VerifyAudience(test.Comparison, test.Required)
|
var opts []ParserOption
|
||||||
|
|
||||||
if got != test.Expected {
|
if test.Required {
|
||||||
t.Errorf("Expected %v, got %v", test.Expected, got)
|
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",
|
"iat": "foo",
|
||||||
}
|
}
|
||||||
want := false
|
want := false
|
||||||
got := mapClaims.VerifyIssuedAt(0, false)
|
got := newValidator(WithIssuedAt()).Validate(mapClaims)
|
||||||
if want != got {
|
if want != (got == nil) {
|
||||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,9 +88,9 @@ func TestMapclaimsVerifyNotBeforeInvalidTypeString(t *testing.T) {
|
||||||
"nbf": "foo",
|
"nbf": "foo",
|
||||||
}
|
}
|
||||||
want := false
|
want := false
|
||||||
got := mapClaims.VerifyNotBefore(0, false)
|
got := newValidator().Validate(mapClaims)
|
||||||
if want != got {
|
if want != (got == nil) {
|
||||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,32 +99,91 @@ func TestMapclaimsVerifyExpiresAtInvalidTypeString(t *testing.T) {
|
||||||
"exp": "foo",
|
"exp": "foo",
|
||||||
}
|
}
|
||||||
want := false
|
want := false
|
||||||
got := mapClaims.VerifyExpiresAt(0, false)
|
got := newValidator().Validate(mapClaims)
|
||||||
|
|
||||||
if want != got {
|
if want != (got == nil) {
|
||||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) {
|
func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) {
|
||||||
exp := time.Now().Unix()
|
exp := time.Now()
|
||||||
mapClaims := MapClaims{
|
mapClaims := MapClaims{
|
||||||
"exp": float64(exp),
|
"exp": float64(exp.Unix()),
|
||||||
}
|
}
|
||||||
want := false
|
want := false
|
||||||
got := mapClaims.VerifyExpiresAt(exp, true)
|
got := newValidator(WithTimeFunc(func() time.Time {
|
||||||
if want != got {
|
return exp
|
||||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
})).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)
|
got = newValidator(WithTimeFunc(func() time.Time {
|
||||||
if want != got {
|
return exp.Add(1 * time.Second)
|
||||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
})).Validate(mapClaims)
|
||||||
|
if want != (got == nil) {
|
||||||
|
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
want = true
|
want = true
|
||||||
got = mapClaims.VerifyExpiresAt(exp-1, true)
|
got = newValidator(WithTimeFunc(func() time.Time {
|
||||||
if want != got {
|
return exp.Add(-1 * time.Second)
|
||||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
})).Validate(mapClaims)
|
||||||
|
if want != (got == nil) {
|
||||||
|
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapClaims_parseString(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
m MapClaims
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing key",
|
||||||
|
m: MapClaims{},
|
||||||
|
args: args{
|
||||||
|
key: "mykey",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong key type",
|
||||||
|
m: MapClaims{"mykey": 4},
|
||||||
|
args: args{
|
||||||
|
key: "mykey",
|
||||||
|
},
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "correct key type",
|
||||||
|
m: MapClaims{"mykey": "mystring"},
|
||||||
|
args: args{
|
||||||
|
key: "mykey",
|
||||||
|
},
|
||||||
|
want: "mystring",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.m.parseString(tt.args.key)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("MapClaims.parseString() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("MapClaims.parseString() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
none.go
7
none.go
|
@ -13,7 +13,7 @@ type unsafeNoneMagicConstant string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
SigningMethodNone = &signingMethodNone{}
|
SigningMethodNone = &signingMethodNone{}
|
||||||
NoneSignatureTypeDisallowedError = NewValidationError("'none' signature type is not allowed", ValidationErrorSignatureInvalid)
|
NoneSignatureTypeDisallowedError = newError("'none' signature type is not allowed", ErrTokenUnverifiable)
|
||||||
|
|
||||||
RegisterSigningMethod(SigningMethodNone.Alg(), func() SigningMethod {
|
RegisterSigningMethod(SigningMethodNone.Alg(), func() SigningMethod {
|
||||||
return SigningMethodNone
|
return SigningMethodNone
|
||||||
|
@ -33,10 +33,7 @@ func (m *signingMethodNone) Verify(signingString, signature string, key interfac
|
||||||
}
|
}
|
||||||
// If signing method is none, signature must be an empty string
|
// If signing method is none, signature must be an empty string
|
||||||
if signature != "" {
|
if signature != "" {
|
||||||
return NewValidationError(
|
return newError("'none' signing method with non-empty signature", ErrTokenUnverifiable)
|
||||||
"'none' signing method with non-empty signature",
|
|
||||||
ValidationErrorSignatureInvalid,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept 'none' signing method.
|
// Accept 'none' signing method.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
91
parser.go
91
parser.go
|
@ -9,26 +9,24 @@ import (
|
||||||
|
|
||||||
type Parser struct {
|
type Parser struct {
|
||||||
// If populated, only these methods will be considered valid.
|
// If populated, only these methods will be considered valid.
|
||||||
//
|
validMethods []string
|
||||||
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
|
|
||||||
ValidMethods []string
|
|
||||||
|
|
||||||
// Use JSON Number format in JSON decoder.
|
// Use JSON Number format in JSON decoder.
|
||||||
//
|
useJSONNumber bool
|
||||||
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
|
|
||||||
UseJSONNumber bool
|
|
||||||
|
|
||||||
// Skip claims validation during token parsing.
|
// Skip claims validation during token parsing.
|
||||||
//
|
skipClaimsValidation bool
|
||||||
// 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
|
// NewParser creates a new Parser with the specified options
|
||||||
func NewParser(options ...ParserOption) *Parser {
|
func NewParser(options ...ParserOption) *Parser {
|
||||||
p := &Parser{}
|
p := &Parser{
|
||||||
|
validator: &validator{},
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
|
@ -56,10 +54,10 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify signing method is in the required set
|
// Verify signing method is in the required set
|
||||||
if p.ValidMethods != nil {
|
if p.validMethods != nil {
|
||||||
var signingMethodValid = false
|
var signingMethodValid = false
|
||||||
var alg = token.Method.Alg()
|
var alg = token.Method.Alg()
|
||||||
for _, m := range p.ValidMethods {
|
for _, m := range p.validMethods {
|
||||||
if m == alg {
|
if m == alg {
|
||||||
signingMethodValid = true
|
signingMethodValid = true
|
||||||
break
|
break
|
||||||
|
@ -67,7 +65,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||||
}
|
}
|
||||||
if !signingMethodValid {
|
if !signingMethodValid {
|
||||||
// signing method is not in the listed set
|
// signing method is not in the listed set
|
||||||
return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid)
|
return token, newError(fmt.Sprintf("signing method %v is invalid", alg), ErrTokenSignatureInvalid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,45 +73,34 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||||
var key interface{}
|
var key interface{}
|
||||||
if keyFunc == nil {
|
if keyFunc == nil {
|
||||||
// keyFunc was not provided. short circuiting validation
|
// keyFunc was not provided. short circuiting validation
|
||||||
return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable)
|
return token, newError("no keyfunc was provided", ErrTokenUnverifiable)
|
||||||
}
|
}
|
||||||
if key, err = keyFunc(token); err != nil {
|
if key, err = keyFunc(token); err != nil {
|
||||||
// keyFunc returned an error
|
return token, newError("error while executing keyfunc", ErrTokenUnverifiable, err)
|
||||||
if ve, ok := err.(*ValidationError); ok {
|
|
||||||
return token, ve
|
|
||||||
}
|
|
||||||
return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vErr := &ValidationError{}
|
// Perform signature validation
|
||||||
|
|
||||||
// Validate Claims
|
|
||||||
if !p.SkipClaimsValidation {
|
|
||||||
if err := token.Claims.Valid(); 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 {
|
|
||||||
vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid}
|
|
||||||
} else {
|
|
||||||
vErr = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform validation
|
|
||||||
token.Signature = parts[2]
|
token.Signature = parts[2]
|
||||||
if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil {
|
if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil {
|
||||||
vErr.Inner = err
|
return token, newError("", ErrTokenSignatureInvalid, err)
|
||||||
vErr.Errors |= ValidationErrorSignatureInvalid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if vErr.valid() {
|
// Validate Claims
|
||||||
|
if !p.skipClaimsValidation {
|
||||||
|
// Make sure we have at least a default validator
|
||||||
|
if p.validator == nil {
|
||||||
|
p.validator = newValidator()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.validator.Validate(claims); err != nil {
|
||||||
|
return token, newError("", ErrTokenInvalidClaims, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No errors so far, token is valid.
|
||||||
token.Valid = true
|
token.Valid = true
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return token, vErr
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseUnverified parses the token but doesn't validate the signature.
|
// ParseUnverified parses the token but doesn't validate the signature.
|
||||||
|
@ -125,7 +112,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||||
func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
|
func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
|
||||||
parts = strings.Split(tokenString, ".")
|
parts = strings.Split(tokenString, ".")
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
|
return nil, parts, newError("token contains an invalid number of segments", ErrTokenMalformed)
|
||||||
}
|
}
|
||||||
|
|
||||||
token = &Token{Raw: tokenString}
|
token = &Token{Raw: tokenString}
|
||||||
|
@ -134,12 +121,12 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke
|
||||||
var headerBytes []byte
|
var headerBytes []byte
|
||||||
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
|
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
|
||||||
if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
|
if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
|
||||||
return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)
|
return token, parts, newError("tokenstring should not contain 'bearer '", ErrTokenMalformed)
|
||||||
}
|
}
|
||||||
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
return token, parts, newError("could not base64 decode header", ErrTokenMalformed, err)
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
|
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
|
||||||
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
return token, parts, newError("could not JSON decode header", ErrTokenMalformed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse Claims
|
// parse Claims
|
||||||
|
@ -147,10 +134,10 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke
|
||||||
token.Claims = claims
|
token.Claims = claims
|
||||||
|
|
||||||
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
|
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
|
||||||
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
return token, parts, newError("could not base64 decode claim", ErrTokenMalformed, err)
|
||||||
}
|
}
|
||||||
dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
|
dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
|
||||||
if p.UseJSONNumber {
|
if p.useJSONNumber {
|
||||||
dec.UseNumber()
|
dec.UseNumber()
|
||||||
}
|
}
|
||||||
// JSON Decode. Special case for map type to avoid weird pointer behavior
|
// JSON Decode. Special case for map type to avoid weird pointer behavior
|
||||||
|
@ -161,16 +148,16 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke
|
||||||
}
|
}
|
||||||
// Handle decode error
|
// Handle decode error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
|
return token, parts, newError("could not JSON decode claim", ErrTokenMalformed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup signature method
|
// Lookup signature method
|
||||||
if method, ok := token.Header["alg"].(string); ok {
|
if method, ok := token.Header["alg"].(string); ok {
|
||||||
if token.Method = GetSigningMethod(method); token.Method == nil {
|
if token.Method = GetSigningMethod(method); token.Method == nil {
|
||||||
return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)
|
return token, parts, newError("signing method (alg) is unavailable", ErrTokenUnverifiable)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)
|
return token, parts, newError("signing method (alg) is unspecified", ErrTokenUnverifiable)
|
||||||
}
|
}
|
||||||
|
|
||||||
return token, parts, nil
|
return token, parts, nil
|
||||||
|
|
|
@ -1,29 +1,101 @@
|
||||||
package jwt
|
package jwt
|
||||||
|
|
||||||
// ParserOption is used to implement functional-style options that modify the behavior of the parser. To add
|
import "time"
|
||||||
// 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.
|
// ParserOption is used to implement functional-style options that modify the
|
||||||
|
// behavior of the parser. 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 ParserOption func(*Parser)
|
type ParserOption func(*Parser)
|
||||||
|
|
||||||
// WithValidMethods is an option to supply algorithm methods that the parser will check. Only those methods will be considered valid.
|
// WithValidMethods is an option to supply algorithm methods that the parser
|
||||||
// It is heavily encouraged to use this option in order to prevent attacks such as https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/.
|
// will check. Only those methods will be considered valid. It is heavily
|
||||||
|
// encouraged to use this option in order to prevent attacks such as
|
||||||
|
// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/.
|
||||||
func WithValidMethods(methods []string) ParserOption {
|
func WithValidMethods(methods []string) ParserOption {
|
||||||
return func(p *Parser) {
|
return func(p *Parser) {
|
||||||
p.ValidMethods = methods
|
p.validMethods = methods
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithJSONNumber is an option to configure the underlying JSON parser with UseNumber
|
// WithJSONNumber is an option to configure the underlying JSON parser with
|
||||||
|
// UseNumber.
|
||||||
func WithJSONNumber() ParserOption {
|
func WithJSONNumber() ParserOption {
|
||||||
return func(p *Parser) {
|
return func(p *Parser) {
|
||||||
p.UseJSONNumber = true
|
p.useJSONNumber = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithoutClaimsValidation is an option to disable claims validation. This option should only be used if you exactly know
|
// WithoutClaimsValidation is an option to disable claims validation. This
|
||||||
// what you are doing.
|
// option should only be used if you exactly know what you are doing.
|
||||||
func WithoutClaimsValidation() ParserOption {
|
func WithoutClaimsValidation() ParserOption {
|
||||||
return func(p *Parser) {
|
return func(p *Parser) {
|
||||||
p.SkipClaimsValidation = true
|
p.skipClaimsValidation = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLeeway returns the ParserOption for specifying the leeway window.
|
||||||
|
func WithLeeway(leeway time.Duration) ParserOption {
|
||||||
|
return func(p *Parser) {
|
||||||
|
p.validator.leeway = leeway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTimeFunc returns the ParserOption 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) ParserOption {
|
||||||
|
return func(p *Parser) {
|
||||||
|
p.validator.timeFunc = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIssuedAt returns the ParserOption to enable verification
|
||||||
|
// of issued-at.
|
||||||
|
func WithIssuedAt() ParserOption {
|
||||||
|
return func(p *Parser) {
|
||||||
|
p.validator.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 in 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,
|
||||||
|
// if an audience is expected.
|
||||||
|
func WithAudience(aud string) ParserOption {
|
||||||
|
return func(p *Parser) {
|
||||||
|
p.validator.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 in 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,
|
||||||
|
// if an issuer is expected.
|
||||||
|
func WithIssuer(iss string) ParserOption {
|
||||||
|
return func(p *Parser) {
|
||||||
|
p.validator.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 in 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,
|
||||||
|
// if a subject is expected.
|
||||||
|
func WithSubject(sub string) ParserOption {
|
||||||
|
return func(p *Parser) {
|
||||||
|
p.validator.expectedSub = sub
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
166
parser_test.go
166
parser_test.go
|
@ -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")
|
||||||
|
@ -51,18 +51,46 @@ var jwtTestData = []struct {
|
||||||
keyfunc jwt.Keyfunc
|
keyfunc jwt.Keyfunc
|
||||||
claims jwt.Claims
|
claims jwt.Claims
|
||||||
valid bool
|
valid bool
|
||||||
errors uint32
|
|
||||||
err []error
|
err []error
|
||||||
parser *jwt.Parser
|
parser *jwt.Parser
|
||||||
signingMethod jwt.SigningMethod // The method to sign the JWT token for test purpose
|
signingMethod jwt.SigningMethod // The method to sign the JWT token for test purpose
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
"invalid JWT",
|
||||||
|
"thisisnotreallyajwt",
|
||||||
|
defaultKeyFunc,
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
[]error{jwt.ErrTokenMalformed},
|
||||||
|
nil,
|
||||||
|
jwt.SigningMethodRS256,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid JSON claim",
|
||||||
|
"eyJhbGciOiJSUzI1NiIsInppcCI6IkRFRiJ9.eNqqVkqtKFCyMjQ1s7Q0sbA0MtFRyk3NTUot8kxRslIKLbZQggn4JeamAoUcfRz99HxcXRWeze172tr4bFq7Ui0AAAD__w.jBXD4LT4aq4oXTgDoPkiV6n4QdSZPZI1Z4J8MWQC42aHK0oXwcovEU06dVbtB81TF-2byuu0-qi8J0GUttODT67k6gCl6DV_iuCOV7gczwTcvKslotUvXzoJ2wa0QuujnjxLEE50r0p6k0tsv_9OIFSUZzDksJFYNPlJH2eFG55DROx4TsOz98az37SujZi9GGbTc9SLgzFHPrHMrovRZ5qLC_w4JrdtsLzBBI11OQJgRYwV8fQf4O8IsMkHtetjkN7dKgUkJtRarNWOk76rpTPppLypiLU4_J0-wrElLMh1TzUVZW6Fz2cDHDDBACJgMmKQ2pOFEDK_vYZN74dLCF5GiTZV6DbXhNxO7lqT7JUN4a3p2z96G7WNRjblf2qZeuYdQvkIsiK-rCbSIE836XeY5gaBgkOzuEvzl_tMrpRmb5Oox1ibOfVT2KBh9Lvqsb1XbQjCio2CLE2ViCLqoe0AaRqlUyrk3n8BIG-r0IW4dcw96CEryEMIjsjVp9mtPXamJzf391kt8Rf3iRBqwv3zP7Plg1ResXbmsFUgOflAUPcYmfLug4W3W52ntcUlTHAKXrNfaJL9QQiYAaDukG-ZHDytsOWTuuXw7lVxjt-XYi1VbRAIjh1aIYSELEmEpE4Ny74htQtywYXMQNfJpB0nNn8IiWakgcYYMJ0TmKM",
|
||||||
|
defaultKeyFunc,
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
[]error{jwt.ErrTokenMalformed},
|
||||||
|
nil,
|
||||||
|
jwt.SigningMethodRS256,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bearer in JWT",
|
||||||
|
"bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||||
|
defaultKeyFunc,
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
[]error{jwt.ErrTokenMalformed},
|
||||||
|
nil,
|
||||||
|
jwt.SigningMethodRS256,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"basic",
|
"basic",
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar"},
|
jwt.MapClaims{"foo": "bar"},
|
||||||
true,
|
true,
|
||||||
0,
|
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
|
@ -73,7 +101,6 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
|
jwt.MapClaims{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorExpired,
|
|
||||||
[]error{jwt.ErrTokenExpired},
|
[]error{jwt.ErrTokenExpired},
|
||||||
nil,
|
nil,
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
|
@ -84,7 +111,6 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
|
jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorNotValidYet,
|
|
||||||
[]error{jwt.ErrTokenNotValidYet},
|
[]error{jwt.ErrTokenNotValidYet},
|
||||||
nil,
|
nil,
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
|
@ -95,8 +121,7 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)},
|
jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
|
[]error{jwt.ErrTokenNotValidYet, jwt.ErrTokenExpired},
|
||||||
[]error{jwt.ErrTokenNotValidYet},
|
|
||||||
nil,
|
nil,
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
|
@ -106,7 +131,6 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar"},
|
jwt.MapClaims{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorSignatureInvalid,
|
|
||||||
[]error{jwt.ErrTokenSignatureInvalid, rsa.ErrVerification},
|
[]error{jwt.ErrTokenSignatureInvalid, rsa.ErrVerification},
|
||||||
nil,
|
nil,
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
|
@ -117,7 +141,6 @@ var jwtTestData = []struct {
|
||||||
nilKeyFunc,
|
nilKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar"},
|
jwt.MapClaims{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorUnverifiable,
|
|
||||||
[]error{jwt.ErrTokenUnverifiable},
|
[]error{jwt.ErrTokenUnverifiable},
|
||||||
nil,
|
nil,
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
|
@ -128,7 +151,6 @@ var jwtTestData = []struct {
|
||||||
emptyKeyFunc,
|
emptyKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar"},
|
jwt.MapClaims{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorSignatureInvalid,
|
|
||||||
[]error{jwt.ErrTokenSignatureInvalid},
|
[]error{jwt.ErrTokenSignatureInvalid},
|
||||||
nil,
|
nil,
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
|
@ -139,7 +161,6 @@ var jwtTestData = []struct {
|
||||||
errorKeyFunc,
|
errorKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar"},
|
jwt.MapClaims{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorUnverifiable,
|
|
||||||
[]error{jwt.ErrTokenUnverifiable, errKeyFuncError},
|
[]error{jwt.ErrTokenUnverifiable, errKeyFuncError},
|
||||||
nil,
|
nil,
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
|
@ -150,9 +171,8 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar"},
|
jwt.MapClaims{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorSignatureInvalid,
|
|
||||||
[]error{jwt.ErrTokenSignatureInvalid},
|
[]error{jwt.ErrTokenSignatureInvalid},
|
||||||
&jwt.Parser{ValidMethods: []string{"HS256"}},
|
jwt.NewParser(jwt.WithValidMethods([]string{"HS256"})),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -161,9 +181,8 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar"},
|
jwt.MapClaims{"foo": "bar"},
|
||||||
true,
|
true,
|
||||||
0,
|
|
||||||
nil,
|
nil,
|
||||||
&jwt.Parser{ValidMethods: []string{"RS256", "HS256"}},
|
jwt.NewParser(jwt.WithValidMethods([]string{"RS256", "HS256"})),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -172,9 +191,8 @@ var jwtTestData = []struct {
|
||||||
ecdsaKeyFunc,
|
ecdsaKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar"},
|
jwt.MapClaims{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorSignatureInvalid,
|
|
||||||
[]error{jwt.ErrTokenSignatureInvalid},
|
[]error{jwt.ErrTokenSignatureInvalid},
|
||||||
&jwt.Parser{ValidMethods: []string{"RS256", "HS256"}},
|
jwt.NewParser(jwt.WithValidMethods([]string{"RS256", "HS256"})),
|
||||||
jwt.SigningMethodES256,
|
jwt.SigningMethodES256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -183,9 +201,8 @@ var jwtTestData = []struct {
|
||||||
ecdsaKeyFunc,
|
ecdsaKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar"},
|
jwt.MapClaims{"foo": "bar"},
|
||||||
true,
|
true,
|
||||||
0,
|
|
||||||
nil,
|
nil,
|
||||||
&jwt.Parser{ValidMethods: []string{"HS256", "ES256"}},
|
jwt.NewParser(jwt.WithValidMethods([]string{"HS256", "ES256"})),
|
||||||
jwt.SigningMethodES256,
|
jwt.SigningMethodES256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -194,22 +211,8 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": json.Number("123.4")},
|
jwt.MapClaims{"foo": json.Number("123.4")},
|
||||||
true,
|
true,
|
||||||
0,
|
|
||||||
nil,
|
nil,
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
jwt.NewParser(jwt.WithJSONNumber()),
|
||||||
jwt.SigningMethodRS256,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Standard Claims",
|
|
||||||
"",
|
|
||||||
defaultKeyFunc,
|
|
||||||
&jwt.StandardClaims{
|
|
||||||
ExpiresAt: time.Now().Add(time.Second * 10).Unix(),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
0,
|
|
||||||
nil,
|
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -218,9 +221,8 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar", "exp": json.Number(fmt.Sprintf("%v", time.Now().Unix()-100))},
|
jwt.MapClaims{"foo": "bar", "exp": json.Number(fmt.Sprintf("%v", time.Now().Unix()-100))},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorExpired,
|
|
||||||
[]error{jwt.ErrTokenExpired},
|
[]error{jwt.ErrTokenExpired},
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
jwt.NewParser(jwt.WithJSONNumber()),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -229,9 +231,8 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100))},
|
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100))},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorNotValidYet,
|
|
||||||
[]error{jwt.ErrTokenNotValidYet},
|
[]error{jwt.ErrTokenNotValidYet},
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
jwt.NewParser(jwt.WithJSONNumber()),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -240,9 +241,8 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100)), "exp": json.Number(fmt.Sprintf("%v", time.Now().Unix()-100))},
|
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100)), "exp": json.Number(fmt.Sprintf("%v", time.Now().Unix()-100))},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
|
[]error{jwt.ErrTokenNotValidYet, jwt.ErrTokenExpired},
|
||||||
[]error{jwt.ErrTokenNotValidYet},
|
jwt.NewParser(jwt.WithJSONNumber()),
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -251,9 +251,8 @@ var jwtTestData = []struct {
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100))},
|
jwt.MapClaims{"foo": "bar", "nbf": json.Number(fmt.Sprintf("%v", time.Now().Unix()+100))},
|
||||||
true,
|
true,
|
||||||
0,
|
|
||||||
nil,
|
nil,
|
||||||
&jwt.Parser{UseJSONNumber: true, SkipClaimsValidation: true},
|
jwt.NewParser(jwt.WithJSONNumber(), jwt.WithoutClaimsValidation()),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -264,9 +263,8 @@ var jwtTestData = []struct {
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 10)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 10)),
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
0,
|
|
||||||
nil,
|
nil,
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
jwt.NewParser(jwt.WithJSONNumber()),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -277,9 +275,8 @@ var jwtTestData = []struct {
|
||||||
Audience: jwt.ClaimStrings{"test"},
|
Audience: jwt.ClaimStrings{"test"},
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
0,
|
|
||||||
nil,
|
nil,
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
jwt.NewParser(jwt.WithJSONNumber()),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -290,9 +287,8 @@ var jwtTestData = []struct {
|
||||||
Audience: jwt.ClaimStrings{"test", "test"},
|
Audience: jwt.ClaimStrings{"test", "test"},
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
0,
|
|
||||||
nil,
|
nil,
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
jwt.NewParser(jwt.WithJSONNumber()),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -303,9 +299,8 @@ var jwtTestData = []struct {
|
||||||
Audience: nil, // because of the unmarshal error, this will be empty
|
Audience: nil, // because of the unmarshal error, this will be empty
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorMalformed,
|
|
||||||
[]error{jwt.ErrTokenMalformed},
|
[]error{jwt.ErrTokenMalformed},
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
jwt.NewParser(jwt.WithJSONNumber()),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -316,9 +311,28 @@ var jwtTestData = []struct {
|
||||||
Audience: nil, // because of the unmarshal error, this will be empty
|
Audience: nil, // because of the unmarshal error, this will be empty
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorMalformed,
|
|
||||||
[]error{jwt.ErrTokenMalformed},
|
[]error{jwt.ErrTokenMalformed},
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
jwt.NewParser(jwt.WithJSONNumber()),
|
||||||
|
jwt.SigningMethodRS256,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"RFC7519 Claims - nbf with 60s skew",
|
||||||
|
"", // autogen
|
||||||
|
defaultKeyFunc,
|
||||||
|
&jwt.RegisteredClaims{NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second * 100))},
|
||||||
|
false,
|
||||||
|
[]error{jwt.ErrTokenNotValidYet},
|
||||||
|
jwt.NewParser(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,
|
||||||
|
nil,
|
||||||
|
jwt.NewParser(jwt.WithLeeway(2 * time.Minute)),
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -350,24 +364,23 @@ func TestParser_Parse(t *testing.T) {
|
||||||
|
|
||||||
// Parse the token
|
// Parse the token
|
||||||
var token *jwt.Token
|
var token *jwt.Token
|
||||||
var ve *jwt.ValidationError
|
|
||||||
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)
|
||||||
|
case nil:
|
||||||
|
token, err = parser.ParseWithClaims(data.tokenString, nil, data.keyfunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify result matches expectation
|
// Verify result matches expectation
|
||||||
if !reflect.DeepEqual(data.claims, token.Claims) {
|
if data.claims != nil && !reflect.DeepEqual(data.claims, token.Claims) {
|
||||||
t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims)
|
t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,27 +392,13 @@ func TestParser_Parse(t *testing.T) {
|
||||||
t.Errorf("[%v] Invalid token passed validation", data.name)
|
t.Errorf("[%v] Invalid token passed validation", data.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err == nil && !token.Valid) || (err != nil && token.Valid) {
|
// Since the returned token is nil in the ErrTokenMalformed, we
|
||||||
|
// cannot make the comparison here
|
||||||
|
if !errors.Is(err, jwt.ErrTokenMalformed) &&
|
||||||
|
((err == nil && !token.Valid) || (err != nil && token.Valid)) {
|
||||||
t.Errorf("[%v] Inconsistent behavior between returned error and token.Valid", data.name)
|
t.Errorf("[%v] Inconsistent behavior between returned error and token.Valid", data.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.errors != 0 {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("[%v] Expecting error. Didn't get one.", data.name)
|
|
||||||
} else {
|
|
||||||
if errors.As(err, &ve) {
|
|
||||||
// compare the bitfield part of the error
|
|
||||||
if e := ve.Errors; e != data.errors {
|
|
||||||
t.Errorf("[%v] Errors don't match expectation. %v != %v", data.name, e, data.errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err.Error() == errKeyFuncError.Error() && ve.Inner != errKeyFuncError {
|
|
||||||
t.Errorf("[%v] Inner error does not match expectation. %v != %v", data.name, ve.Inner, errKeyFuncError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.err != nil {
|
if data.err != nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("[%v] Expecting error(s). Didn't get one.", data.name)
|
t.Errorf("[%v] Expecting error(s). Didn't get one.", data.name)
|
||||||
|
@ -433,7 +432,7 @@ func TestParser_ParseUnverified(t *testing.T) {
|
||||||
// Iterate over test data set and run tests
|
// Iterate over test data set and run tests
|
||||||
for _, data := range jwtTestData {
|
for _, data := range jwtTestData {
|
||||||
// Skip test data, that intentionally contains malformed tokens, as they would lead to an error
|
// Skip test data, that intentionally contains malformed tokens, as they would lead to an error
|
||||||
if data.errors&jwt.ValidationErrorMalformed != 0 {
|
if len(data.err) == 1 && errors.Is(data.err[0], jwt.ErrTokenMalformed) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -454,8 +453,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{})
|
||||||
}
|
}
|
||||||
|
@ -655,8 +652,7 @@ func TestSetPadding(t *testing.T) {
|
||||||
// Parse the token
|
// Parse the token
|
||||||
var token *jwt.Token
|
var token *jwt.Token
|
||||||
var err error
|
var err error
|
||||||
parser := new(jwt.Parser)
|
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||||
parser.SkipClaimsValidation = true
|
|
||||||
|
|
||||||
// Figure out correct claims type
|
// Figure out correct claims type
|
||||||
token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc)
|
token, err = parser.ParseWithClaims(data.tokenString, jwt.MapClaims{}, data.keyfunc)
|
||||||
|
@ -695,9 +691,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{})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
92
token.go
92
token.go
|
@ -4,50 +4,51 @@ 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
|
||||||
// states that the tokens will utilize a Base64url encoding with no padding. Unfortunately, some implementations
|
// respectively. Note that the JWS RFC7515 states that the tokens will utilize a
|
||||||
// of JWT are producing non-standard tokens, and thus require support for decoding. Note that this is a global
|
// Base64url encoding with no padding. Unfortunately, some implementations of
|
||||||
// variable, and updating it will change the behavior on a package level, and is also NOT go-routine safe.
|
// JWT are producing non-standard tokens, and thus require support for decoding.
|
||||||
// To use the non-recommended decoding, set this boolean to `true` prior to using this package.
|
// Note that this is a global variable, and updating it will change the behavior
|
||||||
|
// on a package level, and is also NOT go-routine safe. To use the
|
||||||
|
// non-recommended decoding, set this boolean to `true` prior to using this
|
||||||
|
// package.
|
||||||
var DecodePaddingAllowed bool
|
var DecodePaddingAllowed bool
|
||||||
|
|
||||||
// DecodeStrict will switch the codec used for decoding JWTs into strict mode.
|
// DecodeStrict will switch the codec used for decoding JWTs into strict mode.
|
||||||
// In this mode, the decoder requires that trailing padding bits are zero, as described in RFC 4648 section 3.5.
|
// In this mode, the decoder requires that trailing padding bits are zero, as
|
||||||
// Note that this is a global variable, and updating it will change the behavior on a package level, and is also NOT go-routine safe.
|
// described in RFC 4648 section 3.5. Note that this is a global variable, and
|
||||||
// To use strict decoding, set this boolean to `true` prior to using this package.
|
// updating it will change the behavior on a package level, and is also NOT
|
||||||
|
// go-routine safe. To use strict decoding, set this boolean to `true` prior to
|
||||||
|
// using this package.
|
||||||
var DecodeStrict bool
|
var DecodeStrict 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
|
||||||
// but unverified Token. This allows you to use properties in the
|
// Token. This allows you to use properties in the Header of the token (such as
|
||||||
// Header of the token (such as `kid`) to identify which key to use.
|
// `kid`) to identify which key to use.
|
||||||
type Keyfunc func(*Token) (interface{}, error)
|
type Keyfunc func(*Token) (interface{}, error)
|
||||||
|
|
||||||
// Token represents a JWT Token. Different fields will be used depending on whether you're
|
// Token represents a JWT Token. Different fields will be used depending on
|
||||||
// creating or parsing/verifying a token.
|
// whether you're creating or parsing/verifying a token.
|
||||||
type Token struct {
|
type Token struct {
|
||||||
Raw string // The raw token. Populated when you Parse a token
|
Raw string // Raw contains the raw token. Populated when you [Parse] a token
|
||||||
Method SigningMethod // The signing method used or to be used
|
Method SigningMethod // Method is the signing method used or to be used
|
||||||
Header map[string]interface{} // The first segment of the token
|
Header map[string]interface{} // Header is the first segment of the token
|
||||||
Claims Claims // The second segment of the token
|
Claims Claims // Claims is the second segment of the token
|
||||||
Signature string // The third segment of the token. Populated when you Parse a token
|
Signature string // Signature is the third segment of the token. Populated when you Parse a token
|
||||||
Valid bool // Is the token valid? Populated when you Parse/Verify a token
|
Valid bool // Valid specifies if the token is valid. Populated when you Parse/Verify a token
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Token with the specified signing method and an empty map of claims.
|
// New creates a new [Token] with the specified signing method and an empty map of
|
||||||
|
// claims.
|
||||||
func New(method SigningMethod) *Token {
|
func New(method SigningMethod) *Token {
|
||||||
return NewWithClaims(method, MapClaims{})
|
return NewWithClaims(method, MapClaims{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWithClaims creates a new Token with the specified signing method and claims.
|
// NewWithClaims creates a new [Token] with the specified signing method and
|
||||||
|
// claims.
|
||||||
func NewWithClaims(method SigningMethod, claims Claims) *Token {
|
func NewWithClaims(method SigningMethod, claims Claims) *Token {
|
||||||
return &Token{
|
return &Token{
|
||||||
Header: map[string]interface{}{
|
Header: map[string]interface{}{
|
||||||
|
@ -59,8 +60,8 @@ func NewWithClaims(method SigningMethod, claims Claims) *Token {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignedString creates and returns a complete, signed JWT.
|
// SignedString creates and returns a complete, signed JWT. The token is signed
|
||||||
// The token is signed using the SigningMethod specified in the token.
|
// using the SigningMethod specified in the token.
|
||||||
func (t *Token) SignedString(key interface{}) (string, error) {
|
func (t *Token) SignedString(key interface{}) (string, error) {
|
||||||
var sig, sstr string
|
var sig, sstr string
|
||||||
var err error
|
var err error
|
||||||
|
@ -73,10 +74,9 @@ func (t *Token) SignedString(key interface{}) (string, error) {
|
||||||
return strings.Join([]string{sstr, sig}, "."), nil
|
return strings.Join([]string{sstr, sig}, "."), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SigningString generates the signing string. This is the
|
// SigningString generates the signing string. This is the most expensive part
|
||||||
// most expensive part of the whole deal. Unless you
|
// of the whole deal. Unless you need this for something special, just go
|
||||||
// need this for something special, just go straight for
|
// straight for the SignedString.
|
||||||
// the SignedString.
|
|
||||||
func (t *Token) SigningString() (string, error) {
|
func (t *Token) SigningString() (string, error) {
|
||||||
var err error
|
var err error
|
||||||
var jsonValue []byte
|
var jsonValue []byte
|
||||||
|
@ -96,36 +96,38 @@ func (t *Token) SigningString() (string, error) {
|
||||||
|
|
||||||
// Parse parses, validates, verifies the signature and returns the parsed token.
|
// Parse parses, validates, verifies the signature and returns the parsed token.
|
||||||
// keyFunc will receive the parsed token and should return the cryptographic key
|
// keyFunc will receive the parsed token and should return the cryptographic key
|
||||||
// for verifying the signature.
|
// for verifying the signature. The caller is strongly encouraged to set the
|
||||||
// The caller is strongly encouraged to set the WithValidMethods option to
|
// WithValidMethods option to validate the 'alg' claim in the token matches the
|
||||||
// validate the 'alg' claim in the token matches the expected algorithm.
|
// expected algorithm. For more details about the importance of validating the
|
||||||
// For more details about the importance of validating the 'alg' claim,
|
// 'alg' claim, see
|
||||||
// see https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
|
// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
|
||||||
func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token, error) {
|
func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token, error) {
|
||||||
return NewParser(options...).Parse(tokenString, keyFunc)
|
return NewParser(options...).Parse(tokenString, keyFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseWithClaims is a shortcut for NewParser().ParseWithClaims().
|
// ParseWithClaims is a shortcut for NewParser().ParseWithClaims().
|
||||||
//
|
//
|
||||||
// Note: If you provide a custom claim implementation that embeds one of the standard claims (such as RegisteredClaims),
|
// Note: If you provide a custom claim implementation that embeds one of the
|
||||||
// make sure that a) you either embed a non-pointer version of the claims or b) if you are using a pointer, allocate the
|
// standard claims (such as RegisteredClaims), make sure that a) you either
|
||||||
// proper memory for it before passing in the overall claims, otherwise you might run into a panic.
|
// embed a non-pointer version of the claims or b) if you are using a pointer,
|
||||||
|
// allocate the proper memory for it before passing in the overall claims,
|
||||||
|
// otherwise you might run into a panic.
|
||||||
func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc, options ...ParserOption) (*Token, error) {
|
func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc, options ...ParserOption) (*Token, error) {
|
||||||
return NewParser(options...).ParseWithClaims(tokenString, claims, keyFunc)
|
return NewParser(options...).ParseWithClaims(tokenString, claims, keyFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeSegment encodes a JWT specific base64url encoding with padding stripped
|
// EncodeSegment encodes a JWT specific base64url encoding with padding stripped
|
||||||
//
|
//
|
||||||
// Deprecated: In a future release, we will demote this function to a non-exported function, since it
|
// Deprecated: In a future release, we will demote this function to a
|
||||||
// should only be used internally
|
// non-exported function, since it should only be used internally
|
||||||
func EncodeSegment(seg []byte) string {
|
func EncodeSegment(seg []byte) string {
|
||||||
return base64.RawURLEncoding.EncodeToString(seg)
|
return base64.RawURLEncoding.EncodeToString(seg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeSegment decodes a JWT specific base64url encoding with padding stripped
|
// DecodeSegment decodes a JWT specific base64url encoding with padding stripped
|
||||||
//
|
//
|
||||||
// Deprecated: In a future release, we will demote this function to a non-exported function, since it
|
// Deprecated: In a future release, we will demote this function to a
|
||||||
// should only be used internally
|
// non-exported function, since it should only be used internally
|
||||||
func DecodeSegment(seg string) ([]byte, error) {
|
func DecodeSegment(seg string) ([]byte, error) {
|
||||||
encoding := base64.RawURLEncoding
|
encoding := base64.RawURLEncoding
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
43
types.go
43
types.go
|
@ -9,22 +9,23 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TimePrecision sets the precision of times and dates within this library.
|
// TimePrecision sets the precision of times and dates within this library. This
|
||||||
// This has an influence on the precision of times when comparing expiry or
|
// has an influence on the precision of times when comparing expiry or other
|
||||||
// other related time fields. Furthermore, it is also the precision of times
|
// related time fields. Furthermore, it is also the precision of times when
|
||||||
// when serializing.
|
// serializing.
|
||||||
//
|
//
|
||||||
// For backwards compatibility the default precision is set to seconds, so that
|
// For backwards compatibility the default precision is set to seconds, so that
|
||||||
// no fractional timestamps are generated.
|
// no fractional timestamps are generated.
|
||||||
var TimePrecision = time.Second
|
var TimePrecision = time.Second
|
||||||
|
|
||||||
// MarshalSingleStringAsArray modifies the behaviour of the ClaimStrings type, especially
|
// MarshalSingleStringAsArray modifies the behavior of the ClaimStrings type,
|
||||||
// its MarshalJSON function.
|
// especially its MarshalJSON function.
|
||||||
//
|
//
|
||||||
// If it is set to true (the default), it will always serialize the type as an
|
// If it is set to true (the default), it will always serialize the type as an
|
||||||
// array of strings, even if it just contains one element, defaulting to the behaviour
|
// array of strings, even if it just contains one element, defaulting to the
|
||||||
// of the underlying []string. If it is set to false, it will serialize to a single
|
// behavior of the underlying []string. If it is set to false, it will serialize
|
||||||
// string, if it contains one element. Otherwise, it will serialize to an array of strings.
|
// to a single string, if it contains one element. Otherwise, it will serialize
|
||||||
|
// to an array of strings.
|
||||||
var MarshalSingleStringAsArray = true
|
var MarshalSingleStringAsArray = true
|
||||||
|
|
||||||
// NumericDate represents a JSON numeric date value, as referenced at
|
// NumericDate represents a JSON numeric date value, as referenced at
|
||||||
|
@ -58,9 +59,10 @@ func (date NumericDate) MarshalJSON() (b []byte, err error) {
|
||||||
// For very large timestamps, UnixNano would overflow an int64, but this
|
// For very large timestamps, UnixNano would overflow an int64, but this
|
||||||
// function requires nanosecond level precision, so we have to use the
|
// function requires nanosecond level precision, so we have to use the
|
||||||
// following technique to get round the issue:
|
// following technique to get round the issue:
|
||||||
|
//
|
||||||
// 1. Take the normal unix timestamp to form the whole number part of the
|
// 1. Take the normal unix timestamp to form the whole number part of the
|
||||||
// output,
|
// output,
|
||||||
// 2. Take the result of the Nanosecond function, which retuns the offset
|
// 2. Take the result of the Nanosecond function, which returns the offset
|
||||||
// within the second of the particular unix time instance, to form the
|
// within the second of the particular unix time instance, to form the
|
||||||
// decimal part of the output
|
// decimal part of the output
|
||||||
// 3. Concatenate them to produce the final result
|
// 3. Concatenate them to produce the final result
|
||||||
|
@ -72,9 +74,10 @@ func (date NumericDate) MarshalJSON() (b []byte, err error) {
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON is an implementation of the json.RawMessage interface and deserializses a
|
// UnmarshalJSON is an implementation of the json.RawMessage interface and
|
||||||
// NumericDate from a JSON representation, i.e. a json.Number. This number represents an UNIX epoch
|
// deserializes a [NumericDate] from a JSON representation, i.e. a
|
||||||
// with either integer or non-integer seconds.
|
// [json.Number]. This number represents an UNIX epoch with either integer or
|
||||||
|
// non-integer seconds.
|
||||||
func (date *NumericDate) UnmarshalJSON(b []byte) (err error) {
|
func (date *NumericDate) UnmarshalJSON(b []byte) (err error) {
|
||||||
var (
|
var (
|
||||||
number json.Number
|
number json.Number
|
||||||
|
@ -95,8 +98,9 @@ func (date *NumericDate) UnmarshalJSON(b []byte) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClaimStrings is basically just a slice of strings, but it can be either serialized from a string array or just a string.
|
// ClaimStrings is basically just a slice of strings, but it can be either
|
||||||
// This type is necessary, since the "aud" claim can either be a single string or an array.
|
// serialized from a string array or just a string. This type is necessary,
|
||||||
|
// since the "aud" claim can either be a single string or an array.
|
||||||
type ClaimStrings []string
|
type ClaimStrings []string
|
||||||
|
|
||||||
func (s *ClaimStrings) UnmarshalJSON(data []byte) (err error) {
|
func (s *ClaimStrings) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
@ -133,10 +137,11 @@ func (s *ClaimStrings) UnmarshalJSON(data []byte) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s ClaimStrings) MarshalJSON() (b []byte, err error) {
|
func (s ClaimStrings) MarshalJSON() (b []byte, err error) {
|
||||||
// This handles a special case in the JWT RFC. If the string array, e.g. used by the "aud" field,
|
// This handles a special case in the JWT RFC. If the string array, e.g.
|
||||||
// only contains one element, it MAY be serialized as a single string. This may or may not be
|
// used by the "aud" field, only contains one element, it MAY be serialized
|
||||||
// desired based on the ecosystem of other JWT library used, so we make it configurable by the
|
// as a single string. This may or may not be desired based on the ecosystem
|
||||||
// variable MarshalSingleStringAsArray.
|
// of other JWT library used, so we make it configurable by the variable
|
||||||
|
// MarshalSingleStringAsArray.
|
||||||
if len(s) == 1 && !MarshalSingleStringAsArray {
|
if len(s) == 1 && !MarshalSingleStringAsArray {
|
||||||
return json.Marshal(s[0])
|
return json.Marshal(s[0])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -0,0 +1,301 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClaimsValidator is an interface that can be implemented by custom claims who
|
||||||
|
// wish to execute any additional claims validation based on
|
||||||
|
// application-specific logic. The Validate function is then executed in
|
||||||
|
// addition to the regular claims validation and any error returned is appended
|
||||||
|
// to the final validation result.
|
||||||
|
//
|
||||||
|
// type MyCustomClaims struct {
|
||||||
|
// Foo string `json:"foo"`
|
||||||
|
// jwt.RegisteredClaims
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m MyCustomClaims) Validate() error {
|
||||||
|
// if m.Foo != "bar" {
|
||||||
|
// return errors.New("must be foobar")
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
type ClaimsValidator interface {
|
||||||
|
Claims
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// validator is the core of the new Validation API. It is automatically used by
|
||||||
|
// a [Parser] during parsing and can be modified with various parser options.
|
||||||
|
//
|
||||||
|
// Note: This struct is intentionally not exported (yet) as we want to
|
||||||
|
// internally finalize its API. In the future, we might make it publicly
|
||||||
|
// available.
|
||||||
|
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
|
||||||
|
|
||||||
|
// expectedSub contains the subject this token expects. Supplying an empty
|
||||||
|
// string will disable sub checking.
|
||||||
|
expectedSub string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newValidator can be used to create a stand-alone validator with the supplied
|
||||||
|
// options. This validator can then be used to validate already parsed claims.
|
||||||
|
func newValidator(opts ...ParserOption) *validator {
|
||||||
|
p := NewParser(opts...)
|
||||||
|
return p.validator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given claims. It will also perform any custom
|
||||||
|
// validation if claims implements the [ClaimsValidator] interface.
|
||||||
|
func (v *validator) Validate(claims Claims) error {
|
||||||
|
var (
|
||||||
|
now time.Time
|
||||||
|
errs []error = make([]error, 0, 6)
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 usage of the claim
|
||||||
|
// itself is OPTIONAL.
|
||||||
|
if err = v.verifyExpiresAt(claims, now, false); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We always need to check not-before, but usage of the claim itself is
|
||||||
|
// OPTIONAL.
|
||||||
|
if err = v.verifyNotBefore(claims, now, false); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check issued-at if the option is enabled
|
||||||
|
if v.verifyIat {
|
||||||
|
if err = v.verifyIssuedAt(claims, now, false); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have an expected audience, we also require the audience claim
|
||||||
|
if v.expectedAud != "" {
|
||||||
|
if err = v.verifyAudience(claims, v.expectedAud, true); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have an expected issuer, we also require the issuer claim
|
||||||
|
if v.expectedIss != "" {
|
||||||
|
if err = v.verifyIssuer(claims, v.expectedIss, true); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have an expected subject, we also require the subject claim
|
||||||
|
if v.expectedSub != "" {
|
||||||
|
if err = v.verifySubject(claims, v.expectedSub, true); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we want to give the claim itself some possibility to do some
|
||||||
|
// additional custom validation based on a custom Validate function.
|
||||||
|
cvt, ok := claims.(ClaimsValidator)
|
||||||
|
if ok {
|
||||||
|
if err := cvt.Validate(); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return joinErrors(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyExpiresAt compares the exp claim in claims against cmp. This function
|
||||||
|
// will succeed if cmp < exp. Additional leeway is taken into account.
|
||||||
|
//
|
||||||
|
// If exp is not set, it will succeed if the claim is not required,
|
||||||
|
// otherwise ErrTokenRequiredClaimMissing will be returned.
|
||||||
|
//
|
||||||
|
// Additionally, if any error occurs while retrieving the claim, e.g., when its
|
||||||
|
// the wrong type, an ErrTokenUnverifiable error will be returned.
|
||||||
|
func (v *validator) verifyExpiresAt(claims Claims, cmp time.Time, required bool) error {
|
||||||
|
exp, err := claims.GetExpirationTime()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exp == nil {
|
||||||
|
return errorIfRequired(required, "exp")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorIfFalse(cmp.Before((exp.Time).Add(+v.leeway)), ErrTokenExpired)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyIssuedAt compares the iat claim in claims against cmp. This function
|
||||||
|
// will succeed if cmp >= iat. Additional leeway is taken into account.
|
||||||
|
//
|
||||||
|
// If iat is not set, it will succeed if the claim is not required,
|
||||||
|
// otherwise ErrTokenRequiredClaimMissing will be returned.
|
||||||
|
//
|
||||||
|
// Additionally, if any error occurs while retrieving the claim, e.g., when its
|
||||||
|
// the wrong type, an ErrTokenUnverifiable error will be returned.
|
||||||
|
func (v *validator) verifyIssuedAt(claims Claims, cmp time.Time, required bool) error {
|
||||||
|
iat, err := claims.GetIssuedAt()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if iat == nil {
|
||||||
|
return errorIfRequired(required, "iat")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorIfFalse(!cmp.Before(iat.Add(-v.leeway)), ErrTokenUsedBeforeIssued)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyNotBefore compares the nbf claim in claims against cmp. This function
|
||||||
|
// will return true if cmp >= nbf. Additional leeway is taken into account.
|
||||||
|
//
|
||||||
|
// If nbf is not set, it will succeed if the claim is not required,
|
||||||
|
// otherwise ErrTokenRequiredClaimMissing will be returned.
|
||||||
|
//
|
||||||
|
// Additionally, if any error occurs while retrieving the claim, e.g., when its
|
||||||
|
// the wrong type, an ErrTokenUnverifiable error will be returned.
|
||||||
|
func (v *validator) verifyNotBefore(claims Claims, cmp time.Time, required bool) error {
|
||||||
|
nbf, err := claims.GetNotBefore()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if nbf == nil {
|
||||||
|
return errorIfRequired(required, "nbf")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorIfFalse(!cmp.Before(nbf.Add(-v.leeway)), ErrTokenNotValidYet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyAudience compares the aud claim against cmp.
|
||||||
|
//
|
||||||
|
// If aud is not set or an empty list, it will succeed if the claim is not required,
|
||||||
|
// otherwise ErrTokenRequiredClaimMissing will be returned.
|
||||||
|
//
|
||||||
|
// Additionally, if any error occurs while retrieving the claim, e.g., when its
|
||||||
|
// the wrong type, an ErrTokenUnverifiable error will be returned.
|
||||||
|
func (v *validator) verifyAudience(claims Claims, cmp string, required bool) error {
|
||||||
|
aud, err := claims.GetAudience()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(aud) == 0 {
|
||||||
|
return errorIfRequired(required, "aud")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 errorIfRequired(required, "aud")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorIfFalse(result, ErrTokenInvalidAudience)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyIssuer compares the iss claim in claims against cmp.
|
||||||
|
//
|
||||||
|
// If iss is not set, it will succeed if the claim is not required,
|
||||||
|
// otherwise ErrTokenRequiredClaimMissing will be returned.
|
||||||
|
//
|
||||||
|
// Additionally, if any error occurs while retrieving the claim, e.g., when its
|
||||||
|
// the wrong type, an ErrTokenUnverifiable error will be returned.
|
||||||
|
func (v *validator) verifyIssuer(claims Claims, cmp string, required bool) error {
|
||||||
|
iss, err := claims.GetIssuer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if iss == "" {
|
||||||
|
return errorIfRequired(required, "iss")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorIfFalse(iss == cmp, ErrTokenInvalidIssuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifySubject compares the sub claim against cmp.
|
||||||
|
//
|
||||||
|
// If sub is not set, it will succeed if the claim is not required,
|
||||||
|
// otherwise ErrTokenRequiredClaimMissing will be returned.
|
||||||
|
//
|
||||||
|
// Additionally, if any error occurs while retrieving the claim, e.g., when its
|
||||||
|
// the wrong type, an ErrTokenUnverifiable error will be returned.
|
||||||
|
func (v *validator) verifySubject(claims Claims, cmp string, required bool) error {
|
||||||
|
sub, err := claims.GetSubject()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if sub == "" {
|
||||||
|
return errorIfRequired(required, "sub")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorIfFalse(sub == cmp, ErrTokenInvalidSubject)
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorIfFalse returns the error specified in err, if the value is true.
|
||||||
|
// Otherwise, nil is returned.
|
||||||
|
func errorIfFalse(value bool, err error) error {
|
||||||
|
if value {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorIfRequired returns an ErrTokenRequiredClaimMissing error if required is
|
||||||
|
// true. Otherwise, nil is returned.
|
||||||
|
func errorIfRequired(required bool, claim string) error {
|
||||||
|
if required {
|
||||||
|
return newError(fmt.Sprintf("%s claim is required", claim), ErrTokenRequiredClaimMissing)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,261 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrFooBar = errors.New("must be foobar")
|
||||||
|
|
||||||
|
type MyCustomClaims struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MyCustomClaims) Validate() error {
|
||||||
|
if m.Foo != "bar" {
|
||||||
|
return ErrFooBar
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validator_Validate(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
leeway time.Duration
|
||||||
|
timeFunc func() time.Time
|
||||||
|
verifyIat bool
|
||||||
|
expectedAud string
|
||||||
|
expectedIss string
|
||||||
|
expectedSub string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
claims Claims
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "expected iss mismatch",
|
||||||
|
fields: fields{expectedIss: "me"},
|
||||||
|
args: args{RegisteredClaims{Issuer: "not_me"}},
|
||||||
|
wantErr: ErrTokenInvalidIssuer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expected iss is missing",
|
||||||
|
fields: fields{expectedIss: "me"},
|
||||||
|
args: args{RegisteredClaims{}},
|
||||||
|
wantErr: ErrTokenRequiredClaimMissing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expected sub mismatch",
|
||||||
|
fields: fields{expectedSub: "me"},
|
||||||
|
args: args{RegisteredClaims{Subject: "not-me"}},
|
||||||
|
wantErr: ErrTokenInvalidSubject,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expected sub is missing",
|
||||||
|
fields: fields{expectedSub: "me"},
|
||||||
|
args: args{RegisteredClaims{}},
|
||||||
|
wantErr: ErrTokenRequiredClaimMissing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom validator",
|
||||||
|
fields: fields{},
|
||||||
|
args: args{MyCustomClaims{Foo: "not-bar"}},
|
||||||
|
wantErr: ErrFooBar,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
expectedSub: tt.fields.expectedSub,
|
||||||
|
}
|
||||||
|
if err := v.Validate(tt.args.claims); (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||||
|
t.Errorf("validator.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validator_verifyExpiresAt(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
leeway time.Duration
|
||||||
|
timeFunc func() time.Time
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
claims Claims
|
||||||
|
cmp time.Time
|
||||||
|
required bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "good claim",
|
||||||
|
fields: fields{timeFunc: time.Now},
|
||||||
|
args: args{claims: RegisteredClaims{ExpiresAt: NewNumericDate(time.Now().Add(10 * time.Minute))}},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claims with invalid type",
|
||||||
|
fields: fields{},
|
||||||
|
args: args{claims: MapClaims{"exp": "string"}},
|
||||||
|
wantErr: ErrInvalidType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
v := &validator{
|
||||||
|
leeway: tt.fields.leeway,
|
||||||
|
timeFunc: tt.fields.timeFunc,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := v.verifyExpiresAt(tt.args.claims, tt.args.cmp, tt.args.required)
|
||||||
|
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||||
|
t.Errorf("validator.verifyExpiresAt() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validator_verifyIssuer(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
expectedIss string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
claims Claims
|
||||||
|
cmp string
|
||||||
|
required bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "good claim",
|
||||||
|
fields: fields{expectedIss: "me"},
|
||||||
|
args: args{claims: MapClaims{"iss": "me"}, cmp: "me"},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claims with invalid type",
|
||||||
|
fields: fields{expectedIss: "me"},
|
||||||
|
args: args{claims: MapClaims{"iss": 1}, cmp: "me"},
|
||||||
|
wantErr: ErrInvalidType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
v := &validator{
|
||||||
|
expectedIss: tt.fields.expectedIss,
|
||||||
|
}
|
||||||
|
err := v.verifyIssuer(tt.args.claims, tt.args.cmp, tt.args.required)
|
||||||
|
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||||
|
t.Errorf("validator.verifyIssuer() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validator_verifySubject(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
expectedSub string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
claims Claims
|
||||||
|
cmp string
|
||||||
|
required bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "good claim",
|
||||||
|
fields: fields{expectedSub: "me"},
|
||||||
|
args: args{claims: MapClaims{"sub": "me"}, cmp: "me"},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "claims with invalid type",
|
||||||
|
fields: fields{expectedSub: "me"},
|
||||||
|
args: args{claims: MapClaims{"sub": 1}, cmp: "me"},
|
||||||
|
wantErr: ErrInvalidType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
v := &validator{
|
||||||
|
expectedSub: tt.fields.expectedSub,
|
||||||
|
}
|
||||||
|
err := v.verifySubject(tt.args.claims, tt.args.cmp, tt.args.required)
|
||||||
|
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||||
|
t.Errorf("validator.verifySubject() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validator_verifyIssuedAt(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
leeway time.Duration
|
||||||
|
timeFunc func() time.Time
|
||||||
|
verifyIat bool
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
claims Claims
|
||||||
|
cmp time.Time
|
||||||
|
required bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "good claim without iat",
|
||||||
|
fields: fields{verifyIat: true},
|
||||||
|
args: args{claims: MapClaims{}, required: false},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "good claim with iat",
|
||||||
|
fields: fields{verifyIat: true},
|
||||||
|
args: args{
|
||||||
|
claims: RegisteredClaims{IssuedAt: NewNumericDate(time.Now())},
|
||||||
|
cmp: time.Now().Add(10 * time.Minute),
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
if err := v.verifyIssuedAt(tt.args.claims, tt.args.cmp, tt.args.required); (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||||
|
t.Errorf("validator.verifyIssuedAt() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue