Added more options to check iss and sub

This commit is contained in:
Christian Banse 2022-10-26 22:05:32 +02:00
parent 5d57c292ea
commit 5a65c47732
6 changed files with 97 additions and 14 deletions

View File

@ -11,5 +11,6 @@ type Claims interface {
GetIssuedAt() (*NumericDate, error)
GetNotBefore() (*NumericDate, error)
GetIssuer() (string, error)
GetSubject() (string, error)
GetAudience() (ClaimStrings, error)
}

View File

@ -18,6 +18,7 @@ var (
ErrTokenExpired = errors.New("token is expired")
ErrTokenUsedBeforeIssued = errors.New("token used before issued")
ErrTokenInvalidIssuer = errors.New("token has invalid issuer")
ErrTokenInvalidSubject = errors.New("token has invalid subject")
ErrTokenNotValidYet = errors.New("token is not valid yet")
ErrTokenInvalidId = errors.New("token has invalid id")
ErrTokenInvalidClaims = errors.New("token has invalid claims")
@ -29,11 +30,12 @@ const (
ValidationErrorUnverifiable // Token could not be verified because of signing problems
ValidationErrorSignatureInvalid // Signature validation failed
// Standard Claim validation errors
// Registered Claim validation errors
ValidationErrorAudience // AUD validation failed
ValidationErrorExpired // EXP validation failed
ValidationErrorIssuedAt // IAT validation failed
ValidationErrorIssuer // ISS validation failed
ValidationErrorSubject // SUB validation failed
ValidationErrorNotValidYet // NBF validation failed
ValidationErrorId // JTI validation failed
ValidationErrorClaimsInvalid // Generic claims validation error

View File

@ -36,6 +36,11 @@ func (m MapClaims) GetIssuer() (string, error) {
return m.ParseString("iss")
}
// GetSubject implements the Claims interface.
func (m MapClaims) GetSubject() (string, error) {
return m.ParseString("sub")
}
// ParseNumericDate tries to parse a key in the map claims type as a number
// date. This will succeed, if the underlying type is either a [float64] or a
// [json.Number]. Otherwise, nil will be returned.

View File

@ -56,3 +56,8 @@ func (c RegisteredClaims) GetAudience() (ClaimStrings, error) {
func (c RegisteredClaims) GetIssuer() (string, error) {
return c.Issuer, nil
}
// GetSubject implements the Claims interface.
func (c RegisteredClaims) GetSubject() (string, error) {
return c.Subject, nil
}

View File

@ -24,9 +24,17 @@ type Validator struct {
// unrealistic, i.e., in the future.
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.
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
@ -60,28 +68,42 @@ func (v *Validator) Validate(claims Claims) error {
now = time.Now()
}
// We always need to check the expiration time, but the claim itself is OPTIONAL
if !v.VerifyExpiresAt(claims, now, false) {
vErr.Inner = ErrTokenExpired
vErr.Errors |= ValidationErrorExpired
}
// Check iat if the option is enabled
if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) {
vErr.Inner = ErrTokenUsedBeforeIssued
vErr.Errors |= ValidationErrorIssuedAt
}
// We always need to check not-before, but the claim itself is OPTIONAL
if !v.VerifyNotBefore(claims, now, false) {
vErr.Inner = ErrTokenNotValidYet
vErr.Errors |= ValidationErrorNotValidYet
}
// Check issued-at if the option is enabled
if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) {
vErr.Inner = ErrTokenUsedBeforeIssued
vErr.Errors |= ValidationErrorIssuedAt
}
// If we have an expected audience, we also require the audience claim
if v.expectedAud != "" && !v.VerifyAudience(claims, v.expectedAud, true) {
vErr.Inner = ErrTokenInvalidAudience
vErr.Errors |= ValidationErrorAudience
}
// If we have an expected issuer, we also require the issuer claim
if v.expectedIss != "" && !v.VerifyIssuer(claims, v.expectedIss, true) {
vErr.Inner = ErrTokenInvalidIssuer
vErr.Errors |= ValidationErrorIssuer
}
// If we have an expected subject, we also require the subject claim
if v.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
// additional custom validation based on their custom claims
cvt, ok := claims.(CustomClaims)
@ -166,6 +188,17 @@ func (v *Validator) VerifyIssuer(claims Claims, cmp string, req bool) bool {
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
func verifyAud(aud []string, cmp string, required bool) bool {
@ -221,9 +254,14 @@ 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
}
return iss == cmp
}
func verifySub(sub string, cmp string, required bool) bool {
if sub == "" {
return !required
}
return sub == cmp
}

View File

@ -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 {
return func(v *Validator) {
v.expectedAud = aud
}
}
// WithIssuer configures the validator to require the specified issuer in the
// `iss` claim. Validation will fail if a different issuer is specified in the
// token or the `iss` claim is missing.
//
// NOTE: While the `iss` claim is OPTIONAL is a JWT, the handling of it is
// application-specific. Since this validation API is helping developers in
// writing secure application, we decided to REQUIRE the existence of the claim.
func WithIssuer(iss string) ValidatorOption {
return func(v *Validator) {
v.expectedIss = iss
}
}
// WithSubject configures the validator to require the specified subject in the
// `sub` claim. Validation will fail if a different subject is specified in the
// token or the `sub` claim is missing.
//
// NOTE: While the `sub` claim is OPTIONAL is a JWT, the handling of it is
// application-specific. Since this validation API is helping developers in
// writing secure application, we decided to REQUIRE the existence of the claim.
func WithSubject(sub string) ValidatorOption {
return func(v *Validator) {
v.expectedSub = sub
}
}