mirror of https://github.com/golang-jwt/jwt.git
Merge branch 'custom-parsing' of https://github.com/itsjamie/jwt-go into release_3_0_0
This commit is contained in:
commit
17be525a73
65
README.md
65
README.md
|
@ -4,6 +4,63 @@ A [go](http://www.golang.org) (or 'golang' for search engine friendliness) imple
|
||||||
|
|
||||||
**NOTICE:** A vulnerability in JWT was [recently published](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). As this library doesn't force users to validate the `alg` is what they expected, it's possible your usage is effected. There will be an update soon to remedy this, and it will likey require backwards-incompatible changes to the API. In the short term, please make sure your implementation verifies the `alg` is what you expect.
|
**NOTICE:** A vulnerability in JWT was [recently published](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). As this library doesn't force users to validate the `alg` is what they expected, it's possible your usage is effected. There will be an update soon to remedy this, and it will likey require backwards-incompatible changes to the API. In the short term, please make sure your implementation verifies the `alg` is what you expect.
|
||||||
|
|
||||||
|
## Migration Guide from v2 -> v3
|
||||||
|
|
||||||
|
Added the ability to supply a typed object for the claims section of the token.
|
||||||
|
|
||||||
|
Unfortunately this requires a breaking change. A few new methods were added to support this,
|
||||||
|
and the old default of `map[string]interface{}` was changed to `jwt.MapClaim`.
|
||||||
|
|
||||||
|
The old example for creating a token looked like this..
|
||||||
|
|
||||||
|
```go
|
||||||
|
token := jwt.New(jwt.SigningMethodHS256)
|
||||||
|
token.Claims["foo"] = "bar"
|
||||||
|
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
||||||
|
```
|
||||||
|
|
||||||
|
is now directly mapped to...
|
||||||
|
|
||||||
|
```go
|
||||||
|
token := jwt.New(jwt.SigningMethodHS256)
|
||||||
|
claims := token.Claims.(jwt.MapClaim)
|
||||||
|
claims["foo"] = "bar"
|
||||||
|
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
||||||
|
```
|
||||||
|
|
||||||
|
However, we added a helper `jwt.NewWithClaims` which accepts a claims object.
|
||||||
|
|
||||||
|
Any type can now be used as the claim object for inside a token so long as it implements the interface `jwt.Claims`.
|
||||||
|
|
||||||
|
So, we added an additional claim type `jwt.StandardClaims` was added.
|
||||||
|
This is intended to be used as a base for creating your own types from,
|
||||||
|
and includes a few helper functions for verifying the claims defined [here](https://tools.ietf.org/html/rfc7519#section-4.1).
|
||||||
|
|
||||||
|
```go
|
||||||
|
claims := jwt.StandardClaims{
|
||||||
|
Audience: "myapi"
|
||||||
|
ExpiresAt: time.Now().Add(time.Hour * 72).Unix(),
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
```
|
||||||
|
|
||||||
|
On the other end of usage all of the `jwt.Parse` and friends got a `WithClaims` suffix added to them.
|
||||||
|
|
||||||
|
```go
|
||||||
|
token, err := jwt.Parse(token, keyFunc)
|
||||||
|
claims := token.Claims.(jwt.MapClaim)
|
||||||
|
//like you used to..
|
||||||
|
claims["foo"]
|
||||||
|
claims["bar"]
|
||||||
|
```
|
||||||
|
|
||||||
|
New method usage:
|
||||||
|
```go
|
||||||
|
token, err := jwt.ParseWithClaims(token, keyFunc, &jwt.StandardClaims{})
|
||||||
|
claims := token.Claims.(jwt.StandardClaims)
|
||||||
|
fmt.Println(claims.IssuedAt)
|
||||||
|
```
|
||||||
|
|
||||||
## What the heck is a JWT?
|
## What the heck is a JWT?
|
||||||
|
|
||||||
In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way.
|
In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way.
|
||||||
|
@ -40,10 +97,10 @@ Parsing and verifying tokens is pretty straight forward. You pass in the token
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Create the token
|
// Create the token
|
||||||
token := jwt.New(jwt.SigningMethodHS256)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaim{
|
||||||
// Set some claims
|
"foo": "bar",
|
||||||
token.Claims["foo"] = "bar"
|
"exp": time.Now().Add(time.Hour * 72).Unix(),
|
||||||
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
})
|
||||||
// Sign and get the complete encoded token as a string
|
// Sign and get the complete encoded token as a string
|
||||||
tokenString, err := token.SignedString(mySigningKey)
|
tokenString, err := token.SignedString(mySigningKey)
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import "crypto/subtle"
|
||||||
|
|
||||||
|
// For a type to be a Claims object, it must just have a Valid method that determines
|
||||||
|
// if the token is invalid for any supported reason
|
||||||
|
type Claims interface {
|
||||||
|
Valid() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structured version of Claims Section, as referenced at
|
||||||
|
// https://tools.ietf.org/html/rfc7519#section-4.1
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) == false {
|
||||||
|
vErr.err = "Token is expired"
|
||||||
|
vErr.Errors |= ValidationErrorExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.VerifyIssuedAt(now, false) == false {
|
||||||
|
vErr.err = "Token used before issued, clock skew issue?"
|
||||||
|
vErr.Errors |= ValidationErrorIssuedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.VerifyNotBefore(now, false) == false {
|
||||||
|
vErr.err = "Token is not valid yet"
|
||||||
|
vErr.Errors |= ValidationErrorNotValidYet
|
||||||
|
}
|
||||||
|
|
||||||
|
if vErr.valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return vErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(c.Audience, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares the exp claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool {
|
||||||
|
return verifyExp(c.ExpiresAt, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares the iat claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool {
|
||||||
|
return verifyIat(c.IssuedAt, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares the nbf claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
|
||||||
|
return verifyNbf(c.NotBefore, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MapClaim map[string]interface{}
|
||||||
|
|
||||||
|
// Compares the aud claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (m MapClaim) VerifyAudience(cmp string, req bool) bool {
|
||||||
|
aud, _ := m["aud"].(string)
|
||||||
|
return verifyAud(aud, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares the exp claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (m MapClaim) VerifyExpiresAt(cmp int64, req bool) bool {
|
||||||
|
exp, _ := m["exp"].(float64)
|
||||||
|
return verifyExp(int64(exp), cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares the iat claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (m MapClaim) VerifyIssuedAt(cmp int64, req bool) bool {
|
||||||
|
iat, _ := m["iat"].(float64)
|
||||||
|
return verifyIat(int64(iat), cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares the iss claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (m MapClaim) VerifyIssuer(cmp string, req bool) bool {
|
||||||
|
iss, _ := m["iss"].(string)
|
||||||
|
return verifyIss(iss, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares the nbf claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (m MapClaim) VerifyNotBefore(cmp int64, req bool) bool {
|
||||||
|
nbf, _ := m["nbf"].(float64)
|
||||||
|
return verifyNbf(int64(nbf), cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 MapClaim) Valid() error {
|
||||||
|
vErr := new(ValidationError)
|
||||||
|
now := TimeFunc().Unix()
|
||||||
|
|
||||||
|
if m.VerifyExpiresAt(now, false) == false {
|
||||||
|
vErr.err = "Token is expired"
|
||||||
|
vErr.Errors |= ValidationErrorExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.VerifyIssuedAt(now, false) == false {
|
||||||
|
vErr.err = "Token used before issued, clock skew issue?"
|
||||||
|
vErr.Errors |= ValidationErrorIssuedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.VerifyNotBefore(now, false) == false {
|
||||||
|
vErr.err = "Token is not valid yet"
|
||||||
|
vErr.Errors |= ValidationErrorNotValidYet
|
||||||
|
}
|
||||||
|
|
||||||
|
if vErr.valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return vErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyAud(aud string, cmp string, required bool) bool {
|
||||||
|
if aud == "" {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyExp(exp int64, now int64, required bool) bool {
|
||||||
|
if exp == 0 {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
return now <= exp
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyIat(iat int64, now int64, required bool) bool {
|
||||||
|
if iat == 0 {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
return now >= iat
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyNbf(nbf int64, now int64, required bool) bool {
|
||||||
|
if nbf == 0 {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
return now >= nbf
|
||||||
|
}
|
|
@ -155,7 +155,7 @@ func signToken() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the JSON of the claims
|
// parse the JSON of the claims
|
||||||
var claims map[string]interface{}
|
var claims jwt.MapClaim
|
||||||
if err := json.Unmarshal(tokData, &claims); err != nil {
|
if err := json.Unmarshal(tokData, &claims); err != nil {
|
||||||
return fmt.Errorf("Couldn't parse claims JSON: %v", err)
|
return fmt.Errorf("Couldn't parse claims JSON: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -173,8 +173,7 @@ func signToken() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new token
|
// create a new token
|
||||||
token := jwt.New(alg)
|
token := jwt.NewWithClaims(alg, claims)
|
||||||
token.Claims = claims
|
|
||||||
|
|
||||||
if out, err := token.SignedString(keyData); err == nil {
|
if out, err := token.SignedString(keyData); err == nil {
|
||||||
fmt.Println(out)
|
fmt.Println(out)
|
||||||
|
|
|
@ -16,8 +16,15 @@ const (
|
||||||
ValidationErrorMalformed uint32 = 1 << iota // Token is malformed
|
ValidationErrorMalformed uint32 = 1 << iota // Token is malformed
|
||||||
ValidationErrorUnverifiable // Token could not be verified because of signing problems
|
ValidationErrorUnverifiable // Token could not be verified because of signing problems
|
||||||
ValidationErrorSignatureInvalid // Signature validation failed
|
ValidationErrorSignatureInvalid // Signature validation failed
|
||||||
ValidationErrorExpired // Exp validation failed
|
|
||||||
|
// Standard Claim validation errors
|
||||||
|
ValidationErrorAudience // AUD validation failed
|
||||||
|
ValidationErrorExpired // EXP validation failed
|
||||||
|
ValidationErrorIssuedAt // IAT validation failed
|
||||||
|
ValidationErrorIssuer // ISS validation failed
|
||||||
ValidationErrorNotValidYet // NBF validation failed
|
ValidationErrorNotValidYet // NBF validation failed
|
||||||
|
ValidationErrorId // JTI validation failed
|
||||||
|
ValidationErrorClaimsInvalid // Generic claims validation error
|
||||||
)
|
)
|
||||||
|
|
||||||
// The error from Parse if token is not valid
|
// The error from Parse if token is not valid
|
||||||
|
|
|
@ -2,8 +2,9 @@ package jwt_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, error)) {
|
func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, error)) {
|
||||||
|
@ -18,15 +19,28 @@ func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, er
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleNew(mySigningKey []byte) (string, error) {
|
func ExampleNew() {
|
||||||
// Create the token
|
// Create the token
|
||||||
token := jwt.New(jwt.SigningMethodHS256)
|
token := jwt.New(jwt.SigningMethodRS256)
|
||||||
|
|
||||||
// Set some claims
|
// Set some claims
|
||||||
token.Claims["foo"] = "bar"
|
claims := token.Claims.(jwt.MapClaim)
|
||||||
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
claims["foo"] = "bar"
|
||||||
// Sign and get the complete encoded token as a string
|
claims["exp"] = time.Unix(0, 0).Add(time.Hour * 1).Unix()
|
||||||
tokenString, err := token.SignedString(mySigningKey)
|
|
||||||
return tokenString, err
|
fmt.Printf("%v\n", token.Claims)
|
||||||
|
//Output: map[foo:bar exp:3600]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNewWithClaims(mySigningKey []byte) (string, error) {
|
||||||
|
// Create the Claims
|
||||||
|
claims := jwt.StandardClaims{
|
||||||
|
ExpiresAt: 15000,
|
||||||
|
Issuer: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
|
return token.SignedString(mySigningKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleParse_errorChecking(myToken string, myLookupKey func(interface{}) (interface{}, error)) {
|
func ExampleParse_errorChecking(myToken string, myLookupKey func(interface{}) (interface{}, error)) {
|
||||||
|
|
68
jwt.go
68
jwt.go
|
@ -25,19 +25,23 @@ type Token struct {
|
||||||
Raw string // The raw token. Populated when you Parse a token
|
Raw string // The raw token. Populated when you Parse a token
|
||||||
Method SigningMethod // The signing method used or to be used
|
Method SigningMethod // The signing method used or to be used
|
||||||
Header map[string]interface{} // The first segment of the token
|
Header map[string]interface{} // The first segment of the token
|
||||||
Claims map[string]interface{} // The second segment of the token
|
Claims Claims // The second segment of the token
|
||||||
Signature string // The third segment of the token. Populated when you Parse a token
|
Signature string // 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 // Is the token valid? Populated when you Parse/Verify a token
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new Token. Takes a signing method
|
// Create a new Token. Takes a signing method
|
||||||
func New(method SigningMethod) *Token {
|
func New(method SigningMethod) *Token {
|
||||||
|
return NewWithClaims(method, MapClaim{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWithClaims(method SigningMethod, claims Claims) *Token {
|
||||||
return &Token{
|
return &Token{
|
||||||
Header: map[string]interface{}{
|
Header: map[string]interface{}{
|
||||||
"typ": "JWT",
|
"typ": "JWT",
|
||||||
"alg": method.Alg(),
|
"alg": method.Alg(),
|
||||||
},
|
},
|
||||||
Claims: make(map[string]interface{}),
|
Claims: claims,
|
||||||
Method: method,
|
Method: method,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,17 +67,16 @@ func (t *Token) SigningString() (string, error) {
|
||||||
var err error
|
var err error
|
||||||
parts := make([]string, 2)
|
parts := make([]string, 2)
|
||||||
for i, _ := range parts {
|
for i, _ := range parts {
|
||||||
var source map[string]interface{}
|
|
||||||
if i == 0 {
|
|
||||||
source = t.Header
|
|
||||||
} else {
|
|
||||||
source = t.Claims
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonValue []byte
|
var jsonValue []byte
|
||||||
if jsonValue, err = json.Marshal(source); err != nil {
|
if i == 0 {
|
||||||
|
if jsonValue, err = json.Marshal(t.Header); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if jsonValue, err = json.Marshal(t.Claims); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parts[i] = EncodeSegment(jsonValue)
|
parts[i] = EncodeSegment(jsonValue)
|
||||||
}
|
}
|
||||||
|
@ -84,13 +87,20 @@ func (t *Token) SigningString() (string, error) {
|
||||||
// keyFunc will receive the parsed token and should return the key for validating.
|
// keyFunc will receive the parsed token and should return the key for validating.
|
||||||
// If everything is kosher, err will be nil
|
// If everything is kosher, err will be nil
|
||||||
func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
||||||
|
return ParseWithClaims(tokenString, keyFunc, &MapClaim{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseWithClaims(tokenString string, keyFunc Keyfunc, claims Claims) (*Token, error) {
|
||||||
parts := strings.Split(tokenString, ".")
|
parts := strings.Split(tokenString, ".")
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
return nil, &ValidationError{err: "token contains an invalid number of segments", Errors: ValidationErrorMalformed}
|
return nil, &ValidationError{err: "token contains an invalid number of segments", Errors: ValidationErrorMalformed}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
token := &Token{Raw: tokenString}
|
token := &Token{
|
||||||
|
Raw: tokenString,
|
||||||
|
}
|
||||||
|
|
||||||
// parse Header
|
// parse Header
|
||||||
var headerBytes []byte
|
var headerBytes []byte
|
||||||
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
|
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
|
||||||
|
@ -102,13 +112,17 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
||||||
|
|
||||||
// parse Claims
|
// parse Claims
|
||||||
var claimBytes []byte
|
var claimBytes []byte
|
||||||
|
|
||||||
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
|
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
|
||||||
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
|
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal(claimBytes, &token.Claims); err != nil {
|
|
||||||
|
if err = json.Unmarshal(claimBytes, &claims); err != nil {
|
||||||
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
|
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token.Claims = claims
|
||||||
|
|
||||||
// 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 {
|
||||||
|
@ -129,19 +143,17 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
||||||
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorUnverifiable}
|
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorUnverifiable}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check expiration times
|
|
||||||
vErr := &ValidationError{}
|
vErr := &ValidationError{}
|
||||||
now := TimeFunc().Unix()
|
|
||||||
if exp, ok := token.Claims["exp"].(float64); ok {
|
// Validate Claims
|
||||||
if now > int64(exp) {
|
if err := token.Claims.Valid(); err != nil {
|
||||||
vErr.err = "token is expired"
|
|
||||||
vErr.Errors |= ValidationErrorExpired
|
// 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 {
|
||||||
if nbf, ok := token.Claims["nbf"].(float64); ok {
|
vErr = &ValidationError{err: err.Error(), Errors: ValidationErrorClaimsInvalid}
|
||||||
if now < int64(nbf) {
|
} else {
|
||||||
vErr.err = "token is not valid yet"
|
vErr = e
|
||||||
vErr.Errors |= ValidationErrorNotValidYet
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,23 +176,25 @@ func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
|
||||||
// Currently, it looks in the Authorization header as well as
|
// Currently, it looks in the Authorization header as well as
|
||||||
// looking for an 'access_token' request parameter in req.Form.
|
// looking for an 'access_token' request parameter in req.Form.
|
||||||
func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) {
|
func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) {
|
||||||
|
return ParseFromRequestWithClaims(req, keyFunc, &MapClaim{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFromRequestWithClaims(req *http.Request, keyFunc Keyfunc, claims Claims) (token *Token, err error) {
|
||||||
// Look for an Authorization header
|
// Look for an Authorization header
|
||||||
if ah := req.Header.Get("Authorization"); ah != "" {
|
if ah := req.Header.Get("Authorization"); ah != "" {
|
||||||
// Should be a bearer token
|
// Should be a bearer token
|
||||||
if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" {
|
if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" {
|
||||||
return Parse(ah[7:], keyFunc)
|
return ParseWithClaims(ah[7:], keyFunc, claims)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for "access_token" parameter
|
// Look for "access_token" parameter
|
||||||
req.ParseMultipartForm(10e6)
|
req.ParseMultipartForm(10e6)
|
||||||
if tokStr := req.Form.Get("access_token"); tokStr != "" {
|
if tokStr := req.Form.Get("access_token"); tokStr != "" {
|
||||||
return Parse(tokStr, keyFunc)
|
return ParseWithClaims(tokStr, keyFunc, claims)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrNoTokenInRequest
|
return nil, ErrNoTokenInRequest
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode JWT specific base64url encoding with padding stripped
|
// Encode JWT specific base64url encoding with padding stripped
|
||||||
|
|
38
jwt_test.go
38
jwt_test.go
|
@ -3,12 +3,13 @@ package jwt_test
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -23,7 +24,7 @@ var jwtTestData = []struct {
|
||||||
name string
|
name string
|
||||||
tokenString string
|
tokenString string
|
||||||
keyfunc jwt.Keyfunc
|
keyfunc jwt.Keyfunc
|
||||||
claims map[string]interface{}
|
claims jwt.MapClaim
|
||||||
valid bool
|
valid bool
|
||||||
errors uint32
|
errors uint32
|
||||||
}{
|
}{
|
||||||
|
@ -31,7 +32,7 @@ var jwtTestData = []struct {
|
||||||
"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,
|
||||||
map[string]interface{}{"foo": "bar"},
|
jwt.MapClaim{"foo": "bar"},
|
||||||
true,
|
true,
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
|
@ -39,7 +40,7 @@ var jwtTestData = []struct {
|
||||||
"basic expired",
|
"basic expired",
|
||||||
"", // autogen
|
"", // autogen
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
map[string]interface{}{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
|
jwt.MapClaim{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorExpired,
|
jwt.ValidationErrorExpired,
|
||||||
},
|
},
|
||||||
|
@ -47,7 +48,7 @@ var jwtTestData = []struct {
|
||||||
"basic nbf",
|
"basic nbf",
|
||||||
"", // autogen
|
"", // autogen
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
map[string]interface{}{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
|
jwt.MapClaim{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorNotValidYet,
|
jwt.ValidationErrorNotValidYet,
|
||||||
},
|
},
|
||||||
|
@ -55,7 +56,7 @@ var jwtTestData = []struct {
|
||||||
"expired and nbf",
|
"expired and nbf",
|
||||||
"", // autogen
|
"", // autogen
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
map[string]interface{}{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)},
|
jwt.MapClaim{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
|
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
|
||||||
},
|
},
|
||||||
|
@ -63,7 +64,7 @@ var jwtTestData = []struct {
|
||||||
"basic invalid",
|
"basic invalid",
|
||||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
|
||||||
defaultKeyFunc,
|
defaultKeyFunc,
|
||||||
map[string]interface{}{"foo": "bar"},
|
jwt.MapClaim{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorSignatureInvalid,
|
jwt.ValidationErrorSignatureInvalid,
|
||||||
},
|
},
|
||||||
|
@ -71,7 +72,7 @@ var jwtTestData = []struct {
|
||||||
"basic nokeyfunc",
|
"basic nokeyfunc",
|
||||||
"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",
|
||||||
nilKeyFunc,
|
nilKeyFunc,
|
||||||
map[string]interface{}{"foo": "bar"},
|
jwt.MapClaim{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorUnverifiable,
|
jwt.ValidationErrorUnverifiable,
|
||||||
},
|
},
|
||||||
|
@ -79,7 +80,7 @@ var jwtTestData = []struct {
|
||||||
"basic nokey",
|
"basic nokey",
|
||||||
"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",
|
||||||
emptyKeyFunc,
|
emptyKeyFunc,
|
||||||
map[string]interface{}{"foo": "bar"},
|
jwt.MapClaim{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorSignatureInvalid,
|
jwt.ValidationErrorSignatureInvalid,
|
||||||
},
|
},
|
||||||
|
@ -87,7 +88,7 @@ var jwtTestData = []struct {
|
||||||
"basic errorkey",
|
"basic errorkey",
|
||||||
"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",
|
||||||
errorKeyFunc,
|
errorKeyFunc,
|
||||||
map[string]interface{}{"foo": "bar"},
|
jwt.MapClaim{"foo": "bar"},
|
||||||
false,
|
false,
|
||||||
jwt.ValidationErrorUnverifiable,
|
jwt.ValidationErrorUnverifiable,
|
||||||
},
|
},
|
||||||
|
@ -103,7 +104,7 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeSample(c map[string]interface{}) string {
|
func makeSample(c jwt.MapClaim) string {
|
||||||
keyData, e := ioutil.ReadFile("test/sample_key")
|
keyData, e := ioutil.ReadFile("test/sample_key")
|
||||||
if e != nil {
|
if e != nil {
|
||||||
panic(e.Error())
|
panic(e.Error())
|
||||||
|
@ -113,8 +114,7 @@ func makeSample(c map[string]interface{}) string {
|
||||||
panic(e.Error())
|
panic(e.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
token := jwt.New(jwt.SigningMethodRS256)
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, c)
|
||||||
token.Claims = c
|
|
||||||
s, e := token.SignedString(key)
|
s, e := token.SignedString(key)
|
||||||
|
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
@ -129,17 +129,21 @@ func TestJWT(t *testing.T) {
|
||||||
if data.tokenString == "" {
|
if data.tokenString == "" {
|
||||||
data.tokenString = makeSample(data.claims)
|
data.tokenString = makeSample(data.claims)
|
||||||
}
|
}
|
||||||
token, err := jwt.Parse(data.tokenString, data.keyfunc)
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(data.claims, token.Claims) {
|
token, err := jwt.ParseWithClaims(data.tokenString, data.keyfunc, &jwt.MapClaim{})
|
||||||
|
|
||||||
|
if !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)
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.valid && err != nil {
|
if data.valid && err != nil {
|
||||||
t.Errorf("[%v] Error while verifying token: %T:%v", data.name, err, err)
|
t.Errorf("[%v] Error while verifying token: %T:%v", data.name, err, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.valid && err == nil {
|
if !data.valid && err == nil {
|
||||||
t.Errorf("[%v] Invalid token passed validation", data.name)
|
t.Errorf("[%v] Invalid token passed validation", data.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.errors != 0 {
|
if data.errors != 0 {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("[%v] Expecting error. Didn't get one.", data.name)
|
t.Errorf("[%v] Expecting error. Didn't get one.", data.name)
|
||||||
|
@ -163,13 +167,13 @@ func TestParseRequest(t *testing.T) {
|
||||||
|
|
||||||
r, _ := http.NewRequest("GET", "/", nil)
|
r, _ := http.NewRequest("GET", "/", nil)
|
||||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", data.tokenString))
|
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", data.tokenString))
|
||||||
token, err := jwt.ParseFromRequest(r, data.keyfunc)
|
token, err := jwt.ParseFromRequestWithClaims(r, data.keyfunc, &jwt.MapClaim{})
|
||||||
|
|
||||||
if token == nil {
|
if token == nil {
|
||||||
t.Errorf("[%v] Token was not found: %v", data.name, err)
|
t.Errorf("[%v] Token was not found: %v", data.name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(data.claims, token.Claims) {
|
if !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)
|
||||||
}
|
}
|
||||||
if data.valid && err != nil {
|
if data.valid && err != nil {
|
||||||
|
|
Loading…
Reference in New Issue