forked from mirror/jwt
Compare commits
6 Commits
compat-tes
...
main
Author | SHA1 | Date |
---|---|---|
re | 29918af7f7 | |
Alexander Yastrebov | 9358574a7a | |
Christian Banse | 2f0984a28b | |
Christian Banse | 2101c1f4bc | |
Krouton | 35053d4e20 | |
Jacob Kopczynski | 0c4e387985 |
|
@ -33,6 +33,8 @@ jobs:
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "${{ matrix.go }}"
|
go-version: "${{ matrix.go }}"
|
||||||
|
check-latest: true
|
||||||
|
cache: true
|
||||||
- name: Check Go code formatting
|
- name: Check Go code formatting
|
||||||
run: |
|
run: |
|
||||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
|
@ -42,6 +44,7 @@ jobs:
|
||||||
fi
|
fi
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
go install github.com/mfridman/tparse@latest
|
||||||
go vet ./...
|
go vet ./...
|
||||||
go test -v ./...
|
go test -v -race -count=1 -json -coverpkg=$(go list ./...) ./... | tparse -follow -notests
|
||||||
go build ./...
|
go build ./...
|
||||||
|
|
|
@ -2,18 +2,18 @@
|
||||||
|
|
||||||
Starting from [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0), the import path will be:
|
Starting from [v4.0.0](https://github.com/golang-jwt/jwt/releases/tag/v4.0.0), the import path will be:
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
|
|
||||||
The `/v4` version will be backwards compatible with existing `v3.x.y` tags in this repo, as well as
|
The `/v4` version will be backwards compatible with existing `v3.x.y` tags in this repo, as well as
|
||||||
`github.com/dgrijalva/jwt-go`. For most users this should be a drop-in replacement, if you're having
|
`github.com/dgrijalva/jwt-go`. For most users this should be a drop-in replacement, if you're having
|
||||||
troubles migrating, please open an issue.
|
troubles migrating, please open an issue.
|
||||||
|
|
||||||
You can replace all occurrences of `github.com/dgrijalva/jwt-go` or `github.com/golang-jwt/jwt` with `github.com/golang-jwt/jwt/v5`, 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 `git.internal/re/jwt/v4`, either manually or by using tools such as `sed` or `gofmt`.
|
||||||
|
|
||||||
And then you'd typically run:
|
And then you'd typically run:
|
||||||
|
|
||||||
```
|
```
|
||||||
go get github.com/golang-jwt/jwt/v5
|
go get git.internal/re/jwt/v4
|
||||||
go mod tidy
|
go mod tidy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
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/v5.svg)](https://pkg.go.dev/github.com/golang-jwt/jwt/v5)
|
[![Go Reference](https://pkg.go.dev/badge/git.internal/re/jwt/v4.svg)](https://pkg.go.dev/git.internal/re/jwt/v4)
|
||||||
|
|
||||||
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. Version v5.0.0 introduces major improvements to the validation of tokens, but is not entirely backwards compatible.
|
See the [`MIGRATION_GUIDE.md`](./MIGRATION_GUIDE.md) for more information.
|
||||||
|
|
||||||
> 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/v5
|
go get -u git.internal/re/jwt/v4
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Import it in your code:
|
2. Import it in your code:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "github.com/golang-jwt/jwt/v5"
|
import "git.internal/re/jwt/v4"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
See [the project documentation](https://pkg.go.dev/github.com/golang-jwt/jwt/v5) for examples of usage:
|
See [the project documentation](https://pkg.go.dev/git.internal/re/jwt/v4) for examples of usage:
|
||||||
|
|
||||||
* [Simple example of parsing and validating a token](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-Parse-Hmac)
|
* [Simple example of parsing and validating a token](https://pkg.go.dev/git.internal/re/jwt/v4#example-Parse-Hmac)
|
||||||
* [Simple example of building and signing a token](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#example-New-Hmac)
|
* [Simple example of building and signing a token](https://pkg.go.dev/git.internal/re/jwt/v4#example-New-Hmac)
|
||||||
* [Directory of Examples](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#pkg-examples)
|
* [Directory of Examples](https://pkg.go.dev/git.internal/re/jwt/v4#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/v5#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation
|
* The [HMAC signing method](https://pkg.go.dev/git.internal/re/jwt/v4#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation
|
||||||
* The [RSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation
|
* The [RSA signing method](https://pkg.go.dev/git.internal/re/jwt/v4#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation
|
||||||
* The [ECDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation
|
* The [ECDSA signing method](https://pkg.go.dev/git.internal/re/jwt/v4#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation
|
||||||
* The [EdDSA signing method](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#SigningMethodEd25519) (`Ed25519`) expect `ed25519.PrivateKey` for signing and `ed25519.PublicKey` for validation
|
* The [EdDSA signing method](https://pkg.go.dev/git.internal/re/jwt/v4#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/v5).
|
Documentation can be found [on pkg.go.dev](https://pkg.go.dev/git.internal/re/jwt/v4).
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|
275
claims.go
275
claims.go
|
@ -1,16 +1,269 @@
|
||||||
package jwt
|
package jwt
|
||||||
|
|
||||||
// Claims represent any form of a JWT Claims Set according to
|
import (
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4. In order to have a
|
"crypto/subtle"
|
||||||
// common basis for validation, it is required that an implementation is able to
|
"fmt"
|
||||||
// supply at least the claim names provided in
|
"time"
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 namely `exp`,
|
)
|
||||||
// `iat`, `nbf`, `iss` and `aud`.
|
|
||||||
|
// Claims must just have a Valid method that determines
|
||||||
|
// if the token is invalid for any supported reason
|
||||||
type Claims interface {
|
type Claims interface {
|
||||||
GetExpirationTime() *NumericDate
|
|
||||||
GetIssuedAt() *NumericDate
|
|
||||||
GetNotBefore() *NumericDate
|
|
||||||
GetIssuer() string
|
|
||||||
GetAudience() ClaimStrings
|
|
||||||
Valid() error
|
Valid() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisteredClaims are a structured version of the JWT Claims Set,
|
||||||
|
// restricted to Registered Claim Names, as referenced at
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
|
||||||
|
//
|
||||||
|
// This type can be used on its own, but then additional private and
|
||||||
|
// public claims embedded in the JWT will not be parsed. The typical usecase
|
||||||
|
// therefore is to embedded this in a user-defined claim type.
|
||||||
|
//
|
||||||
|
// See examples for how to use this with your own claim types.
|
||||||
|
type RegisteredClaims struct {
|
||||||
|
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
|
||||||
|
Issuer string `json:"iss,omitempty"`
|
||||||
|
|
||||||
|
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
|
||||||
|
Subject string `json:"sub,omitempty"`
|
||||||
|
|
||||||
|
// the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
|
||||||
|
Audience ClaimStrings `json:"aud,omitempty"`
|
||||||
|
|
||||||
|
// the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
|
||||||
|
ExpiresAt *NumericDate `json:"exp,omitempty"`
|
||||||
|
|
||||||
|
// the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
|
||||||
|
NotBefore *NumericDate `json:"nbf,omitempty"`
|
||||||
|
|
||||||
|
// the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
|
||||||
|
IssuedAt *NumericDate `json:"iat,omitempty"`
|
||||||
|
|
||||||
|
// the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
|
||||||
|
ID string `json:"jti,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid validates time based claims "exp, iat, nbf".
|
||||||
|
// There is no accounting for clock skew.
|
||||||
|
// As well, if any of the above claims are not in the token, it will still
|
||||||
|
// be considered a valid claim.
|
||||||
|
func (c RegisteredClaims) Valid() error {
|
||||||
|
vErr := new(ValidationError)
|
||||||
|
now := TimeFunc()
|
||||||
|
|
||||||
|
// The claims below are optional, by default, so if they are set to the
|
||||||
|
// default value in Go, let's not fail the verification for them.
|
||||||
|
if !c.VerifyExpiresAt(now, false) {
|
||||||
|
delta := now.Sub(c.ExpiresAt.Time)
|
||||||
|
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
|
||||||
|
vErr.Errors |= ValidationErrorExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.VerifyIssuedAt(now, false) {
|
||||||
|
vErr.Inner = ErrTokenUsedBeforeIssued
|
||||||
|
vErr.Errors |= ValidationErrorIssuedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.VerifyNotBefore(now, false) {
|
||||||
|
vErr.Inner = ErrTokenNotValidYet
|
||||||
|
vErr.Errors |= ValidationErrorNotValidYet
|
||||||
|
}
|
||||||
|
|
||||||
|
if vErr.valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return vErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyAudience compares the aud claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool {
|
||||||
|
return verifyAud(c.Audience, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
|
||||||
|
// If req is false, it will return true, if exp is unset.
|
||||||
|
func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool {
|
||||||
|
if c.ExpiresAt == nil {
|
||||||
|
return verifyExp(nil, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyExp(&c.ExpiresAt.Time, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
|
||||||
|
// If req is false, it will return true, if iat is unset.
|
||||||
|
func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool {
|
||||||
|
if c.IssuedAt == nil {
|
||||||
|
return verifyIat(nil, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyIat(&c.IssuedAt.Time, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
|
||||||
|
// If req is false, it will return true, if nbf is unset.
|
||||||
|
func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool {
|
||||||
|
if c.NotBefore == nil {
|
||||||
|
return verifyNbf(nil, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyNbf(&c.NotBefore.Time, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyIssuer compares the iss claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (c *RegisteredClaims) VerifyIssuer(cmp string, req bool) bool {
|
||||||
|
return verifyIss(c.Issuer, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StandardClaims are a structured version of the JWT Claims Set, as referenced at
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc7519#section-4. They do not follow the
|
||||||
|
// specification exactly, since they were based on an earlier draft of the
|
||||||
|
// specification and not updated. The main difference is that they only
|
||||||
|
// support integer-based date fields and singular audiences. This might lead to
|
||||||
|
// incompatibilities with other JWT implementations. The use of this is discouraged, instead
|
||||||
|
// the newer RegisteredClaims struct should be used.
|
||||||
|
//
|
||||||
|
// Deprecated: Use RegisteredClaims instead for a forward-compatible way to access registered claims in a struct.
|
||||||
|
type StandardClaims struct {
|
||||||
|
Audience string `json:"aud,omitempty"`
|
||||||
|
ExpiresAt int64 `json:"exp,omitempty"`
|
||||||
|
Id string `json:"jti,omitempty"`
|
||||||
|
IssuedAt int64 `json:"iat,omitempty"`
|
||||||
|
Issuer string `json:"iss,omitempty"`
|
||||||
|
NotBefore int64 `json:"nbf,omitempty"`
|
||||||
|
Subject string `json:"sub,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid validates time based claims "exp, iat, nbf". There is no accounting for clock skew.
|
||||||
|
// As well, if any of the above claims are not in the token, it will still
|
||||||
|
// be considered a valid claim.
|
||||||
|
func (c StandardClaims) Valid() error {
|
||||||
|
vErr := new(ValidationError)
|
||||||
|
now := TimeFunc().Unix()
|
||||||
|
|
||||||
|
// The claims below are optional, by default, so if they are set to the
|
||||||
|
// default value in Go, let's not fail the verification for them.
|
||||||
|
if !c.VerifyExpiresAt(now, false) {
|
||||||
|
delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0))
|
||||||
|
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
|
||||||
|
vErr.Errors |= ValidationErrorExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.VerifyIssuedAt(now, false) {
|
||||||
|
vErr.Inner = ErrTokenUsedBeforeIssued
|
||||||
|
vErr.Errors |= ValidationErrorIssuedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.VerifyNotBefore(now, false) {
|
||||||
|
vErr.Inner = ErrTokenNotValidYet
|
||||||
|
vErr.Errors |= ValidationErrorNotValidYet
|
||||||
|
}
|
||||||
|
|
||||||
|
if vErr.valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return vErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyAudience compares the aud claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
|
||||||
|
return verifyAud([]string{c.Audience}, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
|
||||||
|
// If req is false, it will return true, if exp is unset.
|
||||||
|
func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool {
|
||||||
|
if c.ExpiresAt == 0 {
|
||||||
|
return verifyExp(nil, time.Unix(cmp, 0), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Unix(c.ExpiresAt, 0)
|
||||||
|
return verifyExp(&t, time.Unix(cmp, 0), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
|
||||||
|
// If req is false, it will return true, if iat is unset.
|
||||||
|
func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool {
|
||||||
|
if c.IssuedAt == 0 {
|
||||||
|
return verifyIat(nil, time.Unix(cmp, 0), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Unix(c.IssuedAt, 0)
|
||||||
|
return verifyIat(&t, time.Unix(cmp, 0), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
|
||||||
|
// If req is false, it will return true, if nbf is unset.
|
||||||
|
func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
|
||||||
|
if c.NotBefore == 0 {
|
||||||
|
return verifyNbf(nil, time.Unix(cmp, 0), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Unix(c.NotBefore, 0)
|
||||||
|
return verifyNbf(&t, time.Unix(cmp, 0), req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyIssuer compares the iss claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool {
|
||||||
|
return verifyIss(c.Issuer, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- helpers
|
||||||
|
|
||||||
|
func verifyAud(aud []string, cmp string, required bool) bool {
|
||||||
|
if len(aud) == 0 {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
// use a var here to keep constant time compare when looping over a number of claims
|
||||||
|
result := false
|
||||||
|
|
||||||
|
var stringClaims string
|
||||||
|
for _, a := range aud {
|
||||||
|
if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 {
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
stringClaims = stringClaims + a
|
||||||
|
}
|
||||||
|
|
||||||
|
// case where "" is sent in one or many aud claims
|
||||||
|
if len(stringClaims) == 0 {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyExp(exp *time.Time, now time.Time, required bool) bool {
|
||||||
|
if exp == nil {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
return now.Before(*exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyIat(iat *time.Time, now time.Time, required bool) bool {
|
||||||
|
if iat == nil {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
return now.After(*iat) || now.Equal(*iat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyNbf(nbf *time.Time, now time.Time, required bool) bool {
|
||||||
|
if nbf == nil {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
return now.After(*nbf) || now.Equal(*nbf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyIss(iss string, cmp string, required bool) bool {
|
||||||
|
if iss == "" {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
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/v5/cmd/jwt
|
go install git.internal/re/jwt/v4/cmd/jwt
|
|
@ -17,7 +17,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ecdsaTestData = []struct {
|
var ecdsaTestData = []struct {
|
||||||
|
@ -90,7 +90,6 @@ func TestECDSASign(t *testing.T) {
|
||||||
toSign := strings.Join(parts[0:2], ".")
|
toSign := strings.Join(parts[0:2], ".")
|
||||||
method := jwt.GetSigningMethod(data.alg)
|
method := jwt.GetSigningMethod(data.alg)
|
||||||
sig, err := method.Sign(toSign, ecdsaKey)
|
sig, err := method.Sign(toSign, ecdsaKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("[%v] Error signing token: %v", data.name, err)
|
t.Errorf("[%v] Error signing token: %v", data.name, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ed25519TestData = []struct {
|
var ed25519TestData = []struct {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Example (atypical) using the RegisteredClaims type by itself to parse a token.
|
// Example (atypical) using the RegisteredClaims type by itself to parse a token.
|
||||||
|
@ -25,7 +25,7 @@ func ExampleNewWithClaims_registeredClaims() {
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
ss, err := token.SignedString(mySigningKey)
|
ss, err := token.SignedString(mySigningKey)
|
||||||
fmt.Printf("%v %v", ss, err)
|
fmt.Printf("%v %v", ss, err)
|
||||||
//Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8 <nil>
|
// Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8 <nil>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example creating a token using a custom claims type. The RegisteredClaims is embedded
|
// Example creating a token using a custom claims type. The RegisteredClaims is embedded
|
||||||
|
@ -67,10 +67,10 @@ func ExampleNewWithClaims_customClaimsType() {
|
||||||
ss, err := token.SignedString(mySigningKey)
|
ss, err := token.SignedString(mySigningKey)
|
||||||
fmt.Printf("%v %v", ss, err)
|
fmt.Printf("%v %v", ss, err)
|
||||||
|
|
||||||
//Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM <nil>
|
// Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM <nil>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example creating a token using a custom claims type. The RegisteredClaims is embedded
|
// Example creating a token using a custom claims type. The StandardClaim 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,68 +93,10 @@ func ExampleParseWithClaims_customClaimsType() {
|
||||||
// Output: bar test
|
// Output: bar test
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example creating a token using a custom claims type and validation options. The RegisteredClaims is embedded
|
// An example of parsing the error types using bitfield checks
|
||||||
// in the custom type to allow for easy encoding, parsing and validation of standard claims.
|
|
||||||
func ExampleParseWithClaims_validationOptions() {
|
|
||||||
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
|
|
||||||
|
|
||||||
type MyCustomClaims struct {
|
|
||||||
Foo string `json:"foo"`
|
|
||||||
jwt.RegisteredClaims
|
|
||||||
}
|
|
||||||
|
|
||||||
validator := jwt.NewValidator(jwt.WithLeeway(5 * time.Second))
|
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
|
||||||
return []byte("AllYourBase"), nil
|
|
||||||
}, jwt.WithValidator(validator))
|
|
||||||
|
|
||||||
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
|
|
||||||
fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
|
|
||||||
} else {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output: bar test
|
|
||||||
}
|
|
||||||
|
|
||||||
type MyCustomClaims struct {
|
|
||||||
Foo string `json:"foo"`
|
|
||||||
jwt.RegisteredClaims
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m MyCustomClaims) CustomValidation() error {
|
|
||||||
if m.Foo != "bar" {
|
|
||||||
return errors.New("must be foobar")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example creating a token using a custom claims type and validation options.
|
|
||||||
// The RegisteredClaims is embedded in the custom type to allow for easy
|
|
||||||
// encoding, parsing and validation of standard claims and the function
|
|
||||||
// CustomValidation is implemented.
|
|
||||||
func ExampleParseWithClaims_customValidation() {
|
|
||||||
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA"
|
|
||||||
|
|
||||||
validator := jwt.NewValidator(jwt.WithLeeway(5 * time.Second))
|
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
|
||||||
return []byte("AllYourBase"), nil
|
|
||||||
}, jwt.WithValidator(validator))
|
|
||||||
|
|
||||||
if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid {
|
|
||||||
fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer)
|
|
||||||
} else {
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output: bar test
|
|
||||||
}
|
|
||||||
|
|
||||||
// An example of parsing the error types using errors.Is.
|
|
||||||
func ExampleParse_errorChecking() {
|
func ExampleParse_errorChecking() {
|
||||||
// Token from another example. This token is expired
|
// Token from another example. This token is expired
|
||||||
var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
|
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c"
|
||||||
|
|
||||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
return []byte("AllYourBase"), nil
|
return []byte("AllYourBase"), nil
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -1,3 +1,7 @@
|
||||||
module github.com/golang-jwt/jwt/v5
|
module git.internal/re/jwt/v4
|
||||||
|
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
|
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/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var hmacTestData = []struct {
|
var hmacTestData = []struct {
|
||||||
|
|
|
@ -16,8 +16,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
"github.com/golang-jwt/jwt/v5/request"
|
"git.internal/re/jwt/v4/request"
|
||||||
)
|
)
|
||||||
|
|
||||||
// location of the files used for signing and verification
|
// location of the files used for signing and verification
|
||||||
|
@ -73,7 +73,7 @@ type CustomerInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomClaimsExample struct {
|
type CustomClaimsExample struct {
|
||||||
*jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
TokenType string
|
TokenType string
|
||||||
CustomerInfo
|
CustomerInfo
|
||||||
}
|
}
|
||||||
|
@ -109,11 +109,10 @@ func Example_getTokenViaHTTP() {
|
||||||
claims := token.Claims.(*CustomClaimsExample)
|
claims := token.Claims.(*CustomClaimsExample)
|
||||||
fmt.Println(claims.CustomerInfo.Name)
|
fmt.Println(claims.CustomerInfo.Name)
|
||||||
|
|
||||||
//Output: test
|
// Output: test
|
||||||
}
|
}
|
||||||
|
|
||||||
func Example_useTokenViaHTTP() {
|
func Example_useTokenViaHTTP() {
|
||||||
|
|
||||||
// Make a sample token
|
// Make a sample token
|
||||||
// In a real world situation, this token will have been acquired from
|
// In a real world situation, this token will have been acquired from
|
||||||
// some other API call (see Example_getTokenViaHTTP)
|
// some other API call (see Example_getTokenViaHTTP)
|
||||||
|
@ -142,7 +141,7 @@ func createToken(user string) (string, error) {
|
||||||
|
|
||||||
// set our claims
|
// set our claims
|
||||||
t.Claims = &CustomClaimsExample{
|
t.Claims = &CustomClaimsExample{
|
||||||
&jwt.RegisteredClaims{
|
jwt.RegisteredClaims{
|
||||||
// set the expire time
|
// set the expire time
|
||||||
// see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
|
// see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 1)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 1)),
|
||||||
|
@ -197,7 +196,6 @@ func restrictedHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// we also only use its public counter part to verify
|
// we also only use its public counter part to verify
|
||||||
return verifyKey, nil
|
return verifyKey, nil
|
||||||
}, request.WithClaims(&CustomClaimsExample{}))
|
}, request.WithClaims(&CustomClaimsExample{}))
|
||||||
|
|
||||||
// If the token is missing or invalid, return error
|
// If the token is missing or invalid, return error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
|
184
map_claims.go
184
map_claims.go
|
@ -2,92 +2,150 @@ package jwt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
// "fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MapClaims is a claims type that uses the map[string]interface{} for JSON decoding.
|
// MapClaims is a claims type that uses the map[string]interface{} for JSON decoding.
|
||||||
// This is the default claims type if you don't supply one
|
// This is the default claims type if you don't supply one
|
||||||
type MapClaims map[string]interface{}
|
type MapClaims map[string]interface{}
|
||||||
|
|
||||||
// GetExpirationTime implements the Claims interface.
|
// VerifyAudience Compares the aud claim against cmp.
|
||||||
func (m MapClaims) GetExpirationTime() *NumericDate {
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
return m.ParseNumericDate("exp")
|
func (m MapClaims) VerifyAudience(cmp string, req bool) bool {
|
||||||
}
|
var aud []string
|
||||||
|
switch v := m["aud"].(type) {
|
||||||
// GetNotBefore implements the Claims interface.
|
case string:
|
||||||
func (m MapClaims) GetNotBefore() *NumericDate {
|
aud = append(aud, v)
|
||||||
return m.ParseNumericDate("nbf")
|
case []string:
|
||||||
}
|
aud = v
|
||||||
|
case []interface{}:
|
||||||
// GetIssuedAt implements the Claims interface.
|
for _, a := range v {
|
||||||
func (m MapClaims) GetIssuedAt() *NumericDate {
|
vs, ok := a.(string)
|
||||||
return m.ParseNumericDate("iat")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAudience implements the Claims interface.
|
|
||||||
func (m MapClaims) GetAudience() ClaimStrings {
|
|
||||||
return m.ParseClaimsString("aud")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIssuer implements the Claims interface.
|
|
||||||
func (m MapClaims) GetIssuer() string {
|
|
||||||
return m.ParseString("iss")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m MapClaims) Valid() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseNumericDate tries to parse a key in the map claims type as a number
|
|
||||||
// date. This will succeed, if the underlying type is either a [float64] or a
|
|
||||||
// [json.Number]. Otherwise, nil will be returned.
|
|
||||||
func (m MapClaims) ParseNumericDate(key string) *NumericDate {
|
|
||||||
v, ok := m[key]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return false
|
||||||
|
}
|
||||||
|
aud = append(aud, vs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verifyAud(aud, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyExpiresAt compares the exp claim against cmp (cmp <= exp).
|
||||||
|
// If req is false, it will return true, if exp is unset.
|
||||||
|
func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool {
|
||||||
|
cmpTime := time.Unix(cmp, 0)
|
||||||
|
|
||||||
|
v, ok := m["exp"]
|
||||||
|
if !ok {
|
||||||
|
return !req
|
||||||
}
|
}
|
||||||
|
|
||||||
switch exp := v.(type) {
|
switch exp := v.(type) {
|
||||||
case float64:
|
case float64:
|
||||||
if exp == 0 {
|
if exp == 0 {
|
||||||
return nil
|
return verifyExp(nil, cmpTime, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newNumericDateFromSeconds(exp)
|
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req)
|
||||||
case json.Number:
|
case json.Number:
|
||||||
v, _ := exp.Float64()
|
v, _ := exp.Float64()
|
||||||
|
|
||||||
return newNumericDateFromSeconds(v)
|
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseClaimsString tries to parse a key in the map claims type as a
|
// VerifyIssuedAt compares the exp claim against cmp (cmp >= iat).
|
||||||
// [ClaimsStrings] type, which can either be a string or an array of string.
|
// If req is false, it will return true, if iat is unset.
|
||||||
func (m MapClaims) ParseClaimsString(key string) ClaimStrings {
|
func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool {
|
||||||
var cs []string
|
cmpTime := time.Unix(cmp, 0)
|
||||||
switch v := m[key].(type) {
|
|
||||||
case string:
|
v, ok := m["iat"]
|
||||||
cs = append(cs, v)
|
|
||||||
case []string:
|
|
||||||
cs = v
|
|
||||||
case []interface{}:
|
|
||||||
for _, a := range v {
|
|
||||||
vs, ok := a.(string)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
return !req
|
||||||
|
}
|
||||||
|
|
||||||
|
switch iat := v.(type) {
|
||||||
|
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).
|
||||||
|
// If req is false, it will return true, if nbf is unset.
|
||||||
|
func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool {
|
||||||
|
cmpTime := time.Unix(cmp, 0)
|
||||||
|
|
||||||
|
v, ok := m["nbf"]
|
||||||
|
if !ok {
|
||||||
|
return !req
|
||||||
|
}
|
||||||
|
|
||||||
|
switch nbf := v.(type) {
|
||||||
|
case float64:
|
||||||
|
if nbf == 0 {
|
||||||
|
return verifyNbf(nil, cmpTime, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req)
|
||||||
|
case json.Number:
|
||||||
|
v, _ := nbf.Float64()
|
||||||
|
|
||||||
|
return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyIssuer compares the iss claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (m MapClaims) VerifyIssuer(cmp string, req bool) bool {
|
||||||
|
iss, _ := m["iss"].(string)
|
||||||
|
return verifyIss(iss, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid validates time based claims "exp, iat, nbf".
|
||||||
|
// There is no accounting for clock skew.
|
||||||
|
// As well, if any of the above claims are not in the token, it will still
|
||||||
|
// be considered a valid claim.
|
||||||
|
func (m MapClaims) Valid() error {
|
||||||
|
vErr := new(ValidationError)
|
||||||
|
now := TimeFunc().Unix()
|
||||||
|
|
||||||
|
if !m.VerifyExpiresAt(now, false) {
|
||||||
|
// TODO(oxisto): this should be replaced with ErrTokenExpired
|
||||||
|
vErr.Inner = errors.New("Token is expired")
|
||||||
|
vErr.Errors |= ValidationErrorExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.VerifyIssuedAt(now, false) {
|
||||||
|
// TODO(oxisto): this should be replaced with ErrTokenUsedBeforeIssued
|
||||||
|
vErr.Inner = errors.New("Token used before issued")
|
||||||
|
vErr.Errors |= ValidationErrorIssuedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.VerifyNotBefore(now, false) {
|
||||||
|
// TODO(oxisto): this should be replaced with ErrTokenNotValidYet
|
||||||
|
vErr.Inner = errors.New("Token is not valid yet")
|
||||||
|
vErr.Errors |= ValidationErrorNotValidYet
|
||||||
|
}
|
||||||
|
|
||||||
|
if vErr.valid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cs = append(cs, vs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cs
|
return vErr
|
||||||
}
|
|
||||||
|
|
||||||
// ParseString tries to parse a key in the map claims type as a
|
|
||||||
// [string] type. Otherwise, an empty string is returned.
|
|
||||||
func (m MapClaims) ParseString(key string) string {
|
|
||||||
iss, _ := m[key].(string)
|
|
||||||
|
|
||||||
return iss
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package jwt
|
package jwt
|
||||||
|
|
||||||
/*
|
import (
|
||||||
TODO(oxisto): Re-enable tests with validation API
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func TestVerifyAud(t *testing.T) {
|
func TestVerifyAud(t *testing.T) {
|
||||||
var nilInterface interface{}
|
var nilInterface interface{}
|
||||||
var nilListInterface []interface{}
|
var nilListInterface []interface{}
|
||||||
|
@ -118,4 +121,3 @@ func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) {
|
||||||
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var noneTestData = []struct {
|
var noneTestData = []struct {
|
||||||
|
|
20
parser.go
20
parser.go
|
@ -7,9 +7,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultValidator is the default validator that is used, if no custom validator is supplied in a Parser.
|
|
||||||
var DefaultValidator = NewValidator()
|
|
||||||
|
|
||||||
type Parser struct {
|
type Parser struct {
|
||||||
// If populated, only these methods will be considered valid.
|
// If populated, only these methods will be considered valid.
|
||||||
//
|
//
|
||||||
|
@ -25,15 +22,13 @@ type Parser struct {
|
||||||
//
|
//
|
||||||
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
|
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
|
||||||
SkipClaimsValidation bool
|
SkipClaimsValidation bool
|
||||||
|
|
||||||
validator *Validator
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewParser creates a new Parser with the specified options
|
// NewParser creates a new Parser with the specified options
|
||||||
func NewParser(options ...ParserOption) *Parser {
|
func NewParser(options ...ParserOption) *Parser {
|
||||||
p := &Parser{}
|
p := &Parser{}
|
||||||
|
|
||||||
// Loop through our parsing options and apply them
|
// loop through our parsing options and apply them
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
option(p)
|
option(p)
|
||||||
}
|
}
|
||||||
|
@ -47,6 +42,13 @@ func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
||||||
return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc)
|
return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseWithClaims parses, validates, and verifies like Parse, but supplies a default object implementing the Claims
|
||||||
|
// interface. This provides default values which can be overridden and allows a caller to use their own type, rather
|
||||||
|
// than the default MapClaims implementation of Claims.
|
||||||
|
//
|
||||||
|
// Note: If you provide a custom claim implementation that embeds one of the standard claims (such as RegisteredClaims),
|
||||||
|
// make sure that a) you either 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 (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
|
func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
|
||||||
token, parts, err := p.ParseUnverified(tokenString, claims)
|
token, parts, err := p.ParseUnverified(tokenString, claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -87,12 +89,8 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
|
||||||
|
|
||||||
// Validate Claims
|
// Validate Claims
|
||||||
if !p.SkipClaimsValidation {
|
if !p.SkipClaimsValidation {
|
||||||
// Make sure we have at least a default validator
|
if err := token.Claims.Valid(); err != nil {
|
||||||
if p.validator == nil {
|
|
||||||
p.validator = DefaultValidator
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.validator.Validate(claims); err != nil {
|
|
||||||
// If the Claims Valid returned an error, check if it is a validation error,
|
// If the Claims Valid returned an error, check if it is a validation error,
|
||||||
// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set
|
// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set
|
||||||
if e, ok := err.(*ValidationError); !ok {
|
if e, ok := err.(*ValidationError); !ok {
|
||||||
|
|
|
@ -27,9 +27,3 @@ func WithoutClaimsValidation() ParserOption {
|
||||||
p.SkipClaimsValidation = true
|
p.SkipClaimsValidation = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithValidator(v *Validator) ParserOption {
|
|
||||||
return func(p *Parser) {
|
|
||||||
p.validator = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
163
parser_test.go
163
parser_test.go
|
@ -10,8 +10,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
"github.com/golang-jwt/jwt/v5/test"
|
"git.internal/re/jwt/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errKeyFuncError error = fmt.Errorf("error loading key")
|
var errKeyFuncError error = fmt.Errorf("error loading key")
|
||||||
|
@ -42,7 +42,6 @@ func init() {
|
||||||
// Load private keys
|
// Load private keys
|
||||||
jwtTestRSAPrivateKey = test.LoadRSAPrivateKeyFromDisk("test/sample_key")
|
jwtTestRSAPrivateKey = test.LoadRSAPrivateKeyFromDisk("test/sample_key")
|
||||||
jwtTestEC256PrivateKey = test.LoadECPrivateKeyFromDisk("test/ec256-private.pem")
|
jwtTestEC256PrivateKey = test.LoadECPrivateKeyFromDisk("test/ec256-private.pem")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var jwtTestData = []struct {
|
var jwtTestData = []struct {
|
||||||
|
@ -199,6 +198,19 @@ var jwtTestData = []struct {
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
&jwt.Parser{UseJSONNumber: true},
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Standard Claims",
|
||||||
|
"",
|
||||||
|
defaultKeyFunc,
|
||||||
|
&jwt.StandardClaims{
|
||||||
|
ExpiresAt: time.Now().Add(time.Second * 10).Unix(),
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
0,
|
||||||
|
nil,
|
||||||
|
&jwt.Parser{UseJSONNumber: true},
|
||||||
|
jwt.SigningMethodRS256,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"JSON Number - basic expired",
|
"JSON Number - basic expired",
|
||||||
"", // autogen
|
"", // autogen
|
||||||
|
@ -308,28 +320,6 @@ var jwtTestData = []struct {
|
||||||
&jwt.Parser{UseJSONNumber: true},
|
&jwt.Parser{UseJSONNumber: true},
|
||||||
jwt.SigningMethodRS256,
|
jwt.SigningMethodRS256,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"RFC7519 Claims - nbf with 60s skew",
|
|
||||||
"", // autogen
|
|
||||||
defaultKeyFunc,
|
|
||||||
&jwt.RegisteredClaims{NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second * 100))},
|
|
||||||
false,
|
|
||||||
jwt.ValidationErrorNotValidYet,
|
|
||||||
[]error{jwt.ErrTokenNotValidYet},
|
|
||||||
jwt.NewParser(jwt.WithValidator(jwt.NewValidator(jwt.WithLeeway(time.Minute)))),
|
|
||||||
jwt.SigningMethodRS256,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"RFC7519 Claims - nbf with 120s skew",
|
|
||||||
"", // autogen
|
|
||||||
defaultKeyFunc,
|
|
||||||
&jwt.RegisteredClaims{NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second * 100))},
|
|
||||||
true,
|
|
||||||
0,
|
|
||||||
nil,
|
|
||||||
jwt.NewParser(jwt.WithValidator(jwt.NewValidator(jwt.WithLeeway(2 * time.Minute)))),
|
|
||||||
jwt.SigningMethodRS256,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// signToken creates and returns a signed JWT token using signingMethod.
|
// signToken creates and returns a signed JWT token using signingMethod.
|
||||||
|
@ -347,11 +337,9 @@ func signToken(claims jwt.Claims, signingMethod jwt.SigningMethod) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParser_Parse(t *testing.T) {
|
func TestParser_Parse(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 {
|
||||||
t.Run(data.name, func(t *testing.T) {
|
t.Run(data.name, func(t *testing.T) {
|
||||||
|
|
||||||
// If the token string is blank, use helper function to generate string
|
// If the token string is blank, use helper function to generate string
|
||||||
if data.tokenString == "" {
|
if data.tokenString == "" {
|
||||||
data.tokenString = signToken(data.claims, data.signingMethod)
|
data.tokenString = signToken(data.claims, data.signingMethod)
|
||||||
|
@ -361,14 +349,16 @@ func TestParser_Parse(t *testing.T) {
|
||||||
var token *jwt.Token
|
var token *jwt.Token
|
||||||
var ve *jwt.ValidationError
|
var ve *jwt.ValidationError
|
||||||
var err error
|
var err error
|
||||||
var parser = data.parser
|
parser := data.parser
|
||||||
if parser == nil {
|
if parser == nil {
|
||||||
parser = jwt.NewParser()
|
parser = new(jwt.Parser)
|
||||||
}
|
}
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
|
@ -411,7 +401,7 @@ func TestParser_Parse(t *testing.T) {
|
||||||
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)
|
||||||
} else {
|
} else {
|
||||||
var all = false
|
all := false
|
||||||
for _, e := range data.err {
|
for _, e := range data.err {
|
||||||
all = errors.Is(err, e)
|
all = errors.Is(err, e)
|
||||||
}
|
}
|
||||||
|
@ -436,7 +426,6 @@ func TestParser_Parse(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParser_ParseUnverified(t *testing.T) {
|
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
|
||||||
|
@ -453,7 +442,7 @@ func TestParser_ParseUnverified(t *testing.T) {
|
||||||
// Parse the token
|
// Parse the token
|
||||||
var token *jwt.Token
|
var token *jwt.Token
|
||||||
var err error
|
var err error
|
||||||
var parser = data.parser
|
parser := data.parser
|
||||||
if parser == nil {
|
if parser == nil {
|
||||||
parser = new(jwt.Parser)
|
parser = new(jwt.Parser)
|
||||||
}
|
}
|
||||||
|
@ -461,6 +450,8 @@ 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{})
|
||||||
}
|
}
|
||||||
|
@ -494,6 +485,7 @@ var setPaddingTestData = []struct {
|
||||||
tokenString string
|
tokenString string
|
||||||
claims jwt.Claims
|
claims jwt.Claims
|
||||||
paddedDecode bool
|
paddedDecode bool
|
||||||
|
strictDecode bool
|
||||||
signingMethod jwt.SigningMethod
|
signingMethod jwt.SigningMethod
|
||||||
keyfunc jwt.Keyfunc
|
keyfunc jwt.Keyfunc
|
||||||
valid bool
|
valid bool
|
||||||
|
@ -552,19 +544,108 @@ var setPaddingTestData = []struct {
|
||||||
keyfunc: paddedKeyFunc,
|
keyfunc: paddedKeyFunc,
|
||||||
valid: true,
|
valid: true,
|
||||||
},
|
},
|
||||||
|
// DecodeStrict tests, DecodePaddingAllowed=false
|
||||||
|
{
|
||||||
|
name: "Validated non-padded token with padding disabled, non-strict decode, non-tweaked signature",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||||
|
"g",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: false,
|
||||||
|
strictDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated non-padded token with padding disabled, non-strict decode, tweaked signature",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||||
|
"h",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: false,
|
||||||
|
strictDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated non-padded token with padding disabled, strict decode, non-tweaked signature",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||||
|
"g",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: false,
|
||||||
|
strictDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error for non-padded token with padding disabled, strict decode, tweaked signature",
|
||||||
|
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
|
||||||
|
"h",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: false,
|
||||||
|
strictDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodRS256,
|
||||||
|
keyfunc: defaultKeyFunc,
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
// DecodeStrict tests, DecodePaddingAllowed=true
|
||||||
|
{
|
||||||
|
name: "Validated padded token with padding enabled, non-strict decode, non-tweaked signature",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||||
|
"w==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: true,
|
||||||
|
strictDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated padded token with padding enabled, non-strict decode, tweaked signature",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||||
|
"x==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: true,
|
||||||
|
strictDecode: false,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Validated padded token with padding enabled, strict decode, non-tweaked signature",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||||
|
"w==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: true,
|
||||||
|
strictDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error for padded token with padding enabled, strict decode, tweaked signature",
|
||||||
|
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
|
||||||
|
"x==",
|
||||||
|
claims: nil,
|
||||||
|
paddedDecode: true,
|
||||||
|
strictDecode: true,
|
||||||
|
signingMethod: jwt.SigningMethodES256,
|
||||||
|
keyfunc: paddedKeyFunc,
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension of Parsing, this is to test out functionality specific to switching codecs with padding.
|
// Extension of Parsing, this is to test out functionality specific to switching codecs with padding.
|
||||||
func TestSetPadding(t *testing.T) {
|
func TestSetPadding(t *testing.T) {
|
||||||
for _, data := range setPaddingTestData {
|
for _, data := range setPaddingTestData {
|
||||||
t.Run(data.name, func(t *testing.T) {
|
t.Run(data.name, func(t *testing.T) {
|
||||||
|
jwt.DecodePaddingAllowed = data.paddedDecode
|
||||||
|
jwt.DecodeStrict = data.strictDecode
|
||||||
|
|
||||||
// If the token string is blank, use helper function to generate string
|
// If the token string is blank, use helper function to generate string
|
||||||
jwt.DecodePaddingAllowed = data.paddedDecode
|
|
||||||
|
|
||||||
if data.tokenString == "" {
|
if data.tokenString == "" {
|
||||||
data.tokenString = signToken(data.claims, data.signingMethod)
|
data.tokenString = signToken(data.claims, data.signingMethod)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the token
|
// Parse the token
|
||||||
|
@ -583,15 +664,13 @@ func TestSetPadding(t *testing.T) {
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
jwt.DecodePaddingAllowed = false
|
jwt.DecodePaddingAllowed = false
|
||||||
|
jwt.DecodeStrict = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkParseUnverified(b *testing.B) {
|
func BenchmarkParseUnverified(b *testing.B) {
|
||||||
|
|
||||||
// Iterate over test data set and run tests
|
// Iterate over test data set and run tests
|
||||||
for _, data := range jwtTestData {
|
for _, data := range jwtTestData {
|
||||||
// If the token string is blank, use helper function to generate string
|
// If the token string is blank, use helper function to generate string
|
||||||
|
@ -600,7 +679,7 @@ func BenchmarkParseUnverified(b *testing.B) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the token
|
// Parse the token
|
||||||
var parser = data.parser
|
parser := data.parser
|
||||||
if parser == nil {
|
if parser == nil {
|
||||||
parser = new(jwt.Parser)
|
parser = new(jwt.Parser)
|
||||||
}
|
}
|
||||||
|
@ -610,9 +689,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.RegisteredClaims:
|
case *jwt.StandardClaims:
|
||||||
b.Run("registered_claims", func(b *testing.B) {
|
b.Run("standard_claims", func(b *testing.B) {
|
||||||
benchmarkParsing(b, parser, data.tokenString, &jwt.RegisteredClaims{})
|
benchmarkParsing(b, parser, data.tokenString, &jwt.StandardClaims{})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
package jwt
|
|
||||||
|
|
||||||
// RegisteredClaims are a structured version of the JWT Claims Set,
|
|
||||||
// restricted to Registered Claim Names, as referenced at
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
|
|
||||||
//
|
|
||||||
// This type can be used on its own, but then additional private and
|
|
||||||
// public claims embedded in the JWT will not be parsed. The typical use-case
|
|
||||||
// therefore is to embedded this in a user-defined claim type.
|
|
||||||
//
|
|
||||||
// See examples for how to use this with your own claim types.
|
|
||||||
type RegisteredClaims struct {
|
|
||||||
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
|
|
||||||
Issuer string `json:"iss,omitempty"`
|
|
||||||
|
|
||||||
// the `sub` (Subject) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
|
|
||||||
Subject string `json:"sub,omitempty"`
|
|
||||||
|
|
||||||
// the `aud` (Audience) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
|
|
||||||
Audience ClaimStrings `json:"aud,omitempty"`
|
|
||||||
|
|
||||||
// the `exp` (Expiration Time) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
|
|
||||||
ExpiresAt *NumericDate `json:"exp,omitempty"`
|
|
||||||
|
|
||||||
// the `nbf` (Not Before) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
|
|
||||||
NotBefore *NumericDate `json:"nbf,omitempty"`
|
|
||||||
|
|
||||||
// the `iat` (Issued At) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
|
|
||||||
IssuedAt *NumericDate `json:"iat,omitempty"`
|
|
||||||
|
|
||||||
// the `jti` (JWT ID) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
|
|
||||||
ID string `json:"jti,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExpirationTime implements the Claims interface.
|
|
||||||
func (c RegisteredClaims) GetExpirationTime() *NumericDate {
|
|
||||||
return c.ExpiresAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNotBefore implements the Claims interface.
|
|
||||||
func (c RegisteredClaims) GetNotBefore() *NumericDate {
|
|
||||||
return c.NotBefore
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIssuedAt implements the Claims interface.
|
|
||||||
func (c RegisteredClaims) GetIssuedAt() *NumericDate {
|
|
||||||
return c.IssuedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAudience implements the Claims interface.
|
|
||||||
func (c RegisteredClaims) GetAudience() ClaimStrings {
|
|
||||||
return c.Audience
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIssuer implements the Claims interface.
|
|
||||||
func (c RegisteredClaims) GetIssuer() string {
|
|
||||||
return c.Issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c RegisteredClaims) Valid() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@ package request
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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/v5"
|
"git.internal/re/jwt/v4"
|
||||||
"github.com/golang-jwt/jwt/v5/test"
|
"git.internal/re/jwt/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
var requestTestData = []struct {
|
var requestTestData = []struct {
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
"github.com/golang-jwt/jwt/v5/test"
|
"git.internal/re/jwt/v4/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rsaPSSTestData = []struct {
|
var rsaPSSTestData = []struct {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rsaTestData = []struct {
|
var rsaTestData = []struct {
|
||||||
|
@ -147,7 +147,6 @@ func TestRSAKeyParsing(t *testing.T) {
|
||||||
if k, e := jwt.ParseRSAPublicKeyFromPEM(badKey); e == nil {
|
if k, e := jwt.ParseRSAPublicKeyFromPEM(badKey); e == nil {
|
||||||
t.Errorf("Parsed invalid key as valid private key: %v", k)
|
t.Errorf("Parsed invalid key as valid private key: %v", k)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkRSAParsing(b *testing.B) {
|
func BenchmarkRSAParsing(b *testing.B) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey {
|
func LoadRSAPrivateKeyFromDisk(location string) *rsa.PrivateKey {
|
||||||
|
|
26
token.go
26
token.go
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515
|
// DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515
|
||||||
|
@ -13,6 +14,17 @@ import (
|
||||||
// To use the non-recommended decoding, set this boolean to `true` prior to using this package.
|
// To use the non-recommended decoding, set this boolean to `true` prior to using this package.
|
||||||
var DecodePaddingAllowed bool
|
var DecodePaddingAllowed bool
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// 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 strict decoding, set this boolean to `true` prior to using this package.
|
||||||
|
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 Token. This allows you to use properties in the
|
// but unverified Token. This allows you to use properties in the
|
||||||
|
@ -93,6 +105,11 @@ func Parse(tokenString string, keyFunc Keyfunc, options ...ParserOption) (*Token
|
||||||
return NewParser(options...).Parse(tokenString, keyFunc)
|
return NewParser(options...).Parse(tokenString, keyFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
// make sure that a) you either 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)
|
||||||
}
|
}
|
||||||
|
@ -110,12 +127,17 @@ func EncodeSegment(seg []byte) string {
|
||||||
// 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 non-exported function, since it
|
||||||
// should only be used internally
|
// should only be used internally
|
||||||
func DecodeSegment(seg string) ([]byte, error) {
|
func DecodeSegment(seg string) ([]byte, error) {
|
||||||
|
encoding := base64.RawURLEncoding
|
||||||
|
|
||||||
if DecodePaddingAllowed {
|
if DecodePaddingAllowed {
|
||||||
if l := len(seg) % 4; l > 0 {
|
if l := len(seg) % 4; l > 0 {
|
||||||
seg += strings.Repeat("=", 4-l)
|
seg += strings.Repeat("=", 4-l)
|
||||||
}
|
}
|
||||||
return base64.URLEncoding.DecodeString(seg)
|
encoding = base64.URLEncoding
|
||||||
}
|
}
|
||||||
|
|
||||||
return base64.RawURLEncoding.DecodeString(seg)
|
if DecodeStrict {
|
||||||
|
encoding = encoding.Strict()
|
||||||
|
}
|
||||||
|
return encoding.DecodeString(seg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package jwt_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
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.RegisteredClaims{},
|
Claims: jwt.StandardClaims{},
|
||||||
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.RegisteredClaims{},
|
Claims: jwt.StandardClaims{},
|
||||||
}
|
}
|
||||||
b.Run("BenchmarkToken_SigningString", func(b *testing.B) {
|
b.Run("BenchmarkToken_SigningString", func(b *testing.B) {
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"git.internal/re/jwt/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNumericDate(t *testing.T) {
|
func TestNumericDate(t *testing.T) {
|
||||||
|
@ -41,7 +41,6 @@ func TestSingleArrayMarshal(t *testing.T) {
|
||||||
expected := `"test"`
|
expected := `"test"`
|
||||||
|
|
||||||
b, err := json.Marshal(s)
|
b, err := json.Marshal(s)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %s", err)
|
t.Errorf("Unexpected error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
201
validator.go
201
validator.go
|
@ -1,201 +0,0 @@
|
||||||
package jwt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/subtle"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validator is the core of the new Validation API. It is
|
|
||||||
type Validator struct {
|
|
||||||
// leeway is an optional leeway that can be provided to account for clock skew.
|
|
||||||
leeway time.Duration
|
|
||||||
|
|
||||||
// timeFunc is used to supply the current time that is needed for
|
|
||||||
// validation. If unspecified, this defaults to time.Now.
|
|
||||||
timeFunc func() time.Time
|
|
||||||
|
|
||||||
// verifyIat specifies whether the iat (Issued At) claim will be verified.
|
|
||||||
// According to https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6 this
|
|
||||||
// only specifies the age of the token, but no validation check is
|
|
||||||
// necessary. However, if wanted, it can be checked if the iat is
|
|
||||||
// unrealistic, i.e., in the future.
|
|
||||||
verifyIat bool
|
|
||||||
|
|
||||||
// expectedAud contains the audiences this token expects. Supplying an empty
|
|
||||||
// string will disable aud checking.
|
|
||||||
expectedAud string
|
|
||||||
}
|
|
||||||
|
|
||||||
type customValidationType interface {
|
|
||||||
CustomValidation() error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewValidator(opts ...ValidatorOption) *Validator {
|
|
||||||
v := &Validator{}
|
|
||||||
|
|
||||||
// Apply the validator options
|
|
||||||
for _, o := range opts {
|
|
||||||
o(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Validator) Validate(claims Claims) error {
|
|
||||||
var now time.Time
|
|
||||||
vErr := new(ValidationError)
|
|
||||||
|
|
||||||
// Check, if we have a time func
|
|
||||||
if v.timeFunc != nil {
|
|
||||||
now = v.timeFunc()
|
|
||||||
} else {
|
|
||||||
now = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !v.VerifyExpiresAt(claims, now, false) {
|
|
||||||
exp := claims.GetExpirationTime()
|
|
||||||
delta := now.Sub(exp.Time)
|
|
||||||
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
|
|
||||||
vErr.Errors |= ValidationErrorExpired
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check iat if the option is enabled
|
|
||||||
if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) {
|
|
||||||
vErr.Inner = ErrTokenUsedBeforeIssued
|
|
||||||
vErr.Errors |= ValidationErrorIssuedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
if !v.VerifyNotBefore(claims, now, false) {
|
|
||||||
vErr.Inner = ErrTokenNotValidYet
|
|
||||||
vErr.Errors |= ValidationErrorNotValidYet
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.expectedAud != "" && !v.VerifyAudience(claims, v.expectedAud, false) {
|
|
||||||
vErr.Inner = ErrTokenNotValidYet
|
|
||||||
vErr.Errors |= ValidationErrorNotValidYet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we want to give the claim itself some possibility to do some
|
|
||||||
// additional custom validation based on their custom claims
|
|
||||||
cvt, ok := claims.(customValidationType)
|
|
||||||
if ok {
|
|
||||||
if err := cvt.CustomValidation(); err != nil {
|
|
||||||
vErr.Inner = err
|
|
||||||
vErr.Errors |= ValidationErrorClaimsInvalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if vErr.valid() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return vErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyAudience compares the aud claim against cmp.
|
|
||||||
// If required is false, this method will return true if the value matches or is unset
|
|
||||||
func (v *Validator) VerifyAudience(claims Claims, cmp string, req bool) bool {
|
|
||||||
return verifyAud(claims.GetAudience(), cmp, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
|
|
||||||
// If req is false, it will return true, if exp is unset.
|
|
||||||
func (v *Validator) VerifyExpiresAt(claims Claims, cmp time.Time, req bool) bool {
|
|
||||||
exp := claims.GetExpirationTime()
|
|
||||||
if exp == nil {
|
|
||||||
return verifyExp(nil, cmp, req, v.leeway)
|
|
||||||
}
|
|
||||||
|
|
||||||
return verifyExp(&exp.Time, cmp, req, v.leeway)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
|
|
||||||
// If req is false, it will return true, if iat is unset.
|
|
||||||
func (v *Validator) VerifyIssuedAt(claims Claims, cmp time.Time, req bool) bool {
|
|
||||||
iat := claims.GetIssuedAt()
|
|
||||||
if iat == nil {
|
|
||||||
return verifyIat(nil, cmp, req, v.leeway)
|
|
||||||
}
|
|
||||||
|
|
||||||
return verifyIat(&iat.Time, cmp, req, v.leeway)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
|
|
||||||
// If req is false, it will return true, if nbf is unset.
|
|
||||||
func (v *Validator) VerifyNotBefore(claims Claims, cmp time.Time, req bool) bool {
|
|
||||||
nbf := claims.GetNotBefore()
|
|
||||||
if nbf == nil {
|
|
||||||
return verifyNbf(nil, cmp, req, v.leeway)
|
|
||||||
}
|
|
||||||
|
|
||||||
return verifyNbf(&nbf.Time, cmp, req, v.leeway)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyIssuer compares the iss claim against cmp.
|
|
||||||
// If required is false, this method will return true if the value matches or is unset
|
|
||||||
func (v *Validator) VerifyIssuer(claims Claims, cmp string, req bool) bool {
|
|
||||||
return verifyIss(claims.GetIssuer(), cmp, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- helpers
|
|
||||||
|
|
||||||
func verifyAud(aud []string, cmp string, required bool) bool {
|
|
||||||
if len(aud) == 0 {
|
|
||||||
return !required
|
|
||||||
}
|
|
||||||
// use a var here to keep constant time compare when looping over a number of claims
|
|
||||||
result := false
|
|
||||||
|
|
||||||
var stringClaims string
|
|
||||||
for _, a := range aud {
|
|
||||||
if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 {
|
|
||||||
result = true
|
|
||||||
}
|
|
||||||
stringClaims = stringClaims + a
|
|
||||||
}
|
|
||||||
|
|
||||||
// case where "" is sent in one or many aud claims
|
|
||||||
if len(stringClaims) == 0 {
|
|
||||||
return !required
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyExp(exp *time.Time, now time.Time, required bool, skew time.Duration) bool {
|
|
||||||
if exp == nil {
|
|
||||||
return !required
|
|
||||||
}
|
|
||||||
|
|
||||||
return now.Before((*exp).Add(+skew))
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyIat(iat *time.Time, now time.Time, required bool, skew time.Duration) bool {
|
|
||||||
if iat == nil {
|
|
||||||
return !required
|
|
||||||
}
|
|
||||||
|
|
||||||
t := (*iat).Add(-skew)
|
|
||||||
return now.After(t) || now.Equal(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyNbf(nbf *time.Time, now time.Time, required bool, skew time.Duration) bool {
|
|
||||||
if nbf == nil {
|
|
||||||
return !required
|
|
||||||
}
|
|
||||||
|
|
||||||
t := (*nbf).Add(-skew)
|
|
||||||
return now.After(t) || now.Equal(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyIss(iss string, cmp string, required bool) bool {
|
|
||||||
if iss == "" {
|
|
||||||
return !required
|
|
||||||
}
|
|
||||||
if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package jwt
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// ValidatorOption is used to implement functional-style options that modify the
|
|
||||||
// behavior of the validator. To add new options, just create a function
|
|
||||||
// (ideally beginning with With or Without) that returns an anonymous function
|
|
||||||
// that takes a *Parser type as input and manipulates its configuration
|
|
||||||
// accordingly.
|
|
||||||
type ValidatorOption func(*Validator)
|
|
||||||
|
|
||||||
// WithLeeway returns the ValidatorOption for specifying the leeway window.
|
|
||||||
func WithLeeway(leeway time.Duration) ValidatorOption {
|
|
||||||
return func(v *Validator) {
|
|
||||||
v.leeway = leeway
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTimeFunc returns the ValidatorOption for specifying the time func. The
|
|
||||||
// primary use-case for this is testing. If you are looking for a way to account
|
|
||||||
// for clock-skew, WithLeeway should be used instead.
|
|
||||||
func WithTimeFunc(f func() time.Time) ValidatorOption {
|
|
||||||
return func(v *Validator) {
|
|
||||||
v.timeFunc = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithIssuedAt returns the ValidatorOption to enable verification
|
|
||||||
// of issued-at.
|
|
||||||
func WithIssuedAt() ValidatorOption {
|
|
||||||
return func(v *Validator) {
|
|
||||||
v.verifyIat = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithAudience returns the ValidatorOption to set the expected audience.
|
|
||||||
func WithAudience(aud string) ValidatorOption {
|
|
||||||
return func(v *Validator) {
|
|
||||||
v.expectedAud = aud
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue