forked from mirror/jwt
Added more options to check iss and sub
This commit is contained in:
parent
5d57c292ea
commit
5a65c47732
|
@ -11,5 +11,6 @@ type Claims interface {
|
||||||
GetIssuedAt() (*NumericDate, error)
|
GetIssuedAt() (*NumericDate, error)
|
||||||
GetNotBefore() (*NumericDate, error)
|
GetNotBefore() (*NumericDate, error)
|
||||||
GetIssuer() (string, error)
|
GetIssuer() (string, error)
|
||||||
|
GetSubject() (string, error)
|
||||||
GetAudience() (ClaimStrings, error)
|
GetAudience() (ClaimStrings, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ var (
|
||||||
ErrTokenExpired = errors.New("token is expired")
|
ErrTokenExpired = errors.New("token is expired")
|
||||||
ErrTokenUsedBeforeIssued = errors.New("token used before issued")
|
ErrTokenUsedBeforeIssued = errors.New("token used before issued")
|
||||||
ErrTokenInvalidIssuer = errors.New("token has invalid issuer")
|
ErrTokenInvalidIssuer = errors.New("token has invalid issuer")
|
||||||
|
ErrTokenInvalidSubject = errors.New("token has invalid subject")
|
||||||
ErrTokenNotValidYet = errors.New("token is not valid yet")
|
ErrTokenNotValidYet = errors.New("token is not valid yet")
|
||||||
ErrTokenInvalidId = errors.New("token has invalid id")
|
ErrTokenInvalidId = errors.New("token has invalid id")
|
||||||
ErrTokenInvalidClaims = errors.New("token has invalid claims")
|
ErrTokenInvalidClaims = errors.New("token has invalid claims")
|
||||||
|
@ -29,11 +30,12 @@ const (
|
||||||
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
|
||||||
|
|
||||||
// Standard Claim validation errors
|
// Registered Claim validation errors
|
||||||
ValidationErrorAudience // AUD validation failed
|
ValidationErrorAudience // AUD validation failed
|
||||||
ValidationErrorExpired // EXP validation failed
|
ValidationErrorExpired // EXP validation failed
|
||||||
ValidationErrorIssuedAt // IAT validation failed
|
ValidationErrorIssuedAt // IAT validation failed
|
||||||
ValidationErrorIssuer // ISS validation failed
|
ValidationErrorIssuer // ISS validation failed
|
||||||
|
ValidationErrorSubject // SUB validation failed
|
||||||
ValidationErrorNotValidYet // NBF validation failed
|
ValidationErrorNotValidYet // NBF validation failed
|
||||||
ValidationErrorId // JTI validation failed
|
ValidationErrorId // JTI validation failed
|
||||||
ValidationErrorClaimsInvalid // Generic claims validation error
|
ValidationErrorClaimsInvalid // Generic claims validation error
|
||||||
|
|
|
@ -36,6 +36,11 @@ func (m MapClaims) GetIssuer() (string, error) {
|
||||||
return m.ParseString("iss")
|
return m.ParseString("iss")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubject implements the Claims interface.
|
||||||
|
func (m MapClaims) GetSubject() (string, error) {
|
||||||
|
return m.ParseString("sub")
|
||||||
|
}
|
||||||
|
|
||||||
// ParseNumericDate tries to parse a key in the map claims type as a number
|
// 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
|
// date. This will succeed, if the underlying type is either a [float64] or a
|
||||||
// [json.Number]. Otherwise, nil will be returned.
|
// [json.Number]. Otherwise, nil will be returned.
|
||||||
|
|
|
@ -56,3 +56,8 @@ func (c RegisteredClaims) GetAudience() (ClaimStrings, error) {
|
||||||
func (c RegisteredClaims) GetIssuer() (string, error) {
|
func (c RegisteredClaims) GetIssuer() (string, error) {
|
||||||
return c.Issuer, nil
|
return c.Issuer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSubject implements the Claims interface.
|
||||||
|
func (c RegisteredClaims) GetSubject() (string, error) {
|
||||||
|
return c.Subject, nil
|
||||||
|
}
|
||||||
|
|
62
validator.go
62
validator.go
|
@ -24,9 +24,17 @@ type Validator struct {
|
||||||
// unrealistic, i.e., in the future.
|
// unrealistic, i.e., in the future.
|
||||||
verifyIat bool
|
verifyIat bool
|
||||||
|
|
||||||
// expectedAud contains the audiences this token expects. Supplying an empty
|
// expectedAud contains the audience this token expects. Supplying an empty
|
||||||
// string will disable aud checking.
|
// string will disable aud checking.
|
||||||
expectedAud string
|
expectedAud string
|
||||||
|
|
||||||
|
// expectedIss contains the issuer this token expects. Supplying an empty
|
||||||
|
// string will disable iss checking.
|
||||||
|
expectedIss string
|
||||||
|
|
||||||
|
// expectedSub contains the subject this token expects. Supplying an empty
|
||||||
|
// string will disable sub checking.
|
||||||
|
expectedSub string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomClaims represents a custom claims interface, which can be built upon the integrated
|
// CustomClaims represents a custom claims interface, which can be built upon the integrated
|
||||||
|
@ -60,28 +68,42 @@ func (v *Validator) Validate(claims Claims) error {
|
||||||
now = time.Now()
|
now = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We always need to check the expiration time, but the claim itself is OPTIONAL
|
||||||
if !v.VerifyExpiresAt(claims, now, false) {
|
if !v.VerifyExpiresAt(claims, now, false) {
|
||||||
vErr.Inner = ErrTokenExpired
|
vErr.Inner = ErrTokenExpired
|
||||||
vErr.Errors |= ValidationErrorExpired
|
vErr.Errors |= ValidationErrorExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check iat if the option is enabled
|
// We always need to check not-before, but the claim itself is OPTIONAL
|
||||||
if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) {
|
|
||||||
vErr.Inner = ErrTokenUsedBeforeIssued
|
|
||||||
vErr.Errors |= ValidationErrorIssuedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
if !v.VerifyNotBefore(claims, now, false) {
|
if !v.VerifyNotBefore(claims, now, false) {
|
||||||
vErr.Inner = ErrTokenNotValidYet
|
vErr.Inner = ErrTokenNotValidYet
|
||||||
vErr.Errors |= ValidationErrorNotValidYet
|
vErr.Errors |= ValidationErrorNotValidYet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check issued-at if the option is enabled
|
||||||
|
if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) {
|
||||||
|
vErr.Inner = ErrTokenUsedBeforeIssued
|
||||||
|
vErr.Errors |= ValidationErrorIssuedAt
|
||||||
|
}
|
||||||
|
|
||||||
// If we have an expected audience, we also require the audience claim
|
// If we have an expected audience, we also require the audience claim
|
||||||
if v.expectedAud != "" && !v.VerifyAudience(claims, v.expectedAud, true) {
|
if v.expectedAud != "" && !v.VerifyAudience(claims, v.expectedAud, true) {
|
||||||
vErr.Inner = ErrTokenInvalidAudience
|
vErr.Inner = ErrTokenInvalidAudience
|
||||||
vErr.Errors |= ValidationErrorAudience
|
vErr.Errors |= ValidationErrorAudience
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have an expected issuer, we also require the issuer claim
|
||||||
|
if v.expectedIss != "" && !v.VerifyIssuer(claims, v.expectedIss, true) {
|
||||||
|
vErr.Inner = ErrTokenInvalidIssuer
|
||||||
|
vErr.Errors |= ValidationErrorIssuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have an expected subject, we also require the subject claim
|
||||||
|
if v.expectedSub != "" && !v.VerifySubject(claims, v.expectedSub, true) {
|
||||||
|
vErr.Inner = ErrTokenInvalidSubject
|
||||||
|
vErr.Errors |= ValidationErrorSubject
|
||||||
|
}
|
||||||
|
|
||||||
// Finally, we want to give the claim itself some possibility to do some
|
// Finally, we want to give the claim itself some possibility to do some
|
||||||
// additional custom validation based on their custom claims
|
// additional custom validation based on their custom claims
|
||||||
cvt, ok := claims.(CustomClaims)
|
cvt, ok := claims.(CustomClaims)
|
||||||
|
@ -166,6 +188,17 @@ func (v *Validator) VerifyIssuer(claims Claims, cmp string, req bool) bool {
|
||||||
return verifyIss(iss, cmp, req)
|
return verifyIss(iss, cmp, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifySubject compares the sub claim against cmp.
|
||||||
|
// If required is false, this method will return true if the value matches or is unset
|
||||||
|
func (v *Validator) VerifySubject(claims Claims, cmp string, req bool) bool {
|
||||||
|
iss, err := claims.GetSubject()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifySub(iss, cmp, req)
|
||||||
|
}
|
||||||
|
|
||||||
// ----- helpers
|
// ----- helpers
|
||||||
|
|
||||||
func verifyAud(aud []string, cmp string, required bool) bool {
|
func verifyAud(aud []string, cmp string, required bool) bool {
|
||||||
|
@ -221,9 +254,14 @@ func verifyIss(iss string, cmp string, required bool) bool {
|
||||||
if iss == "" {
|
if iss == "" {
|
||||||
return !required
|
return !required
|
||||||
}
|
}
|
||||||
if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 {
|
|
||||||
return true
|
return iss == cmp
|
||||||
} else {
|
}
|
||||||
return false
|
|
||||||
}
|
func verifySub(sub string, cmp string, required bool) bool {
|
||||||
|
if sub == "" {
|
||||||
|
return !required
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub == cmp
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,9 +33,41 @@ func WithIssuedAt() ValidatorOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAudience returns the ValidatorOption to set the expected audience.
|
// WithAudience configures the validator to require the specified audience in
|
||||||
|
// the `aud` claim. Validation will fail if the audience is not listed in the
|
||||||
|
// token or the `aud` claim is missing.
|
||||||
|
//
|
||||||
|
// NOTE: While the `aud` claim is OPTIONAL is a JWT, the handling of it is
|
||||||
|
// application-specific. Since this validation API is helping developers in
|
||||||
|
// writing secure application, we decided to REQUIRE the existence of the claim.
|
||||||
func WithAudience(aud string) ValidatorOption {
|
func WithAudience(aud string) ValidatorOption {
|
||||||
return func(v *Validator) {
|
return func(v *Validator) {
|
||||||
v.expectedAud = aud
|
v.expectedAud = aud
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithIssuer configures the validator to require the specified issuer in the
|
||||||
|
// `iss` claim. Validation will fail if a different issuer is specified in the
|
||||||
|
// token or the `iss` claim is missing.
|
||||||
|
//
|
||||||
|
// NOTE: While the `iss` claim is OPTIONAL is a JWT, the handling of it is
|
||||||
|
// application-specific. Since this validation API is helping developers in
|
||||||
|
// writing secure application, we decided to REQUIRE the existence of the claim.
|
||||||
|
func WithIssuer(iss string) ValidatorOption {
|
||||||
|
return func(v *Validator) {
|
||||||
|
v.expectedIss = iss
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSubject configures the validator to require the specified subject in the
|
||||||
|
// `sub` claim. Validation will fail if a different subject is specified in the
|
||||||
|
// token or the `sub` claim is missing.
|
||||||
|
//
|
||||||
|
// NOTE: While the `sub` claim is OPTIONAL is a JWT, the handling of it is
|
||||||
|
// application-specific. Since this validation API is helping developers in
|
||||||
|
// writing secure application, we decided to REQUIRE the existence of the claim.
|
||||||
|
func WithSubject(sub string) ValidatorOption {
|
||||||
|
return func(v *Validator) {
|
||||||
|
v.expectedSub = sub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue