feat(09-01): reject cross-audience tokens on both guards
- Frontend verification accepts a missing audience for PHP tokens - An explicit audience must match the guard, even when the secret is shared
This commit is contained in:
@@ -153,23 +153,11 @@ func (g *jwtGuard) WriteUnauthorized(w http.ResponseWriter, err error) {
|
||||
}
|
||||
|
||||
// Verify parses a token with HS256 pinned and a required exp and sub.
|
||||
// A missing audience is accepted for PHP-issued frontend tokens. An explicit
|
||||
// audience must be AudienceUser, so a backend token cannot pass this guard.
|
||||
func Verify(tokenString, secret string) (string, error) {
|
||||
if strings.TrimSpace(secret) == "" {
|
||||
return "", fmt.Errorf("bouncer: jwt secret is empty")
|
||||
}
|
||||
parser := jwt.NewParser(jwt.WithValidMethods([]string{"HS256"}), jwt.WithExpirationRequired())
|
||||
claims := jwt.MapClaims{}
|
||||
_, err := parser.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (any, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", mapJWTError(err)
|
||||
}
|
||||
sub := subject(claims)
|
||||
if sub == "" {
|
||||
return "", errors.New(msgRequiredClaims)
|
||||
}
|
||||
return sub, nil
|
||||
sub, _, _, _, err := verifyClaims(tokenString, secret, "")
|
||||
return sub, err
|
||||
}
|
||||
|
||||
// VerifyClaims parses a token the same way Verify does and also returns iat, exp, and jti.
|
||||
@@ -198,7 +186,7 @@ func verifyClaims(tokenString, secret, audience string) (sub string, iat, exp ti
|
||||
if err != nil {
|
||||
return "", time.Time{}, time.Time{}, "", mapJWTError(err)
|
||||
}
|
||||
if audience != "" && !audienceMatches(claims, audience) {
|
||||
if !frontendAudienceOK(claims, audience) {
|
||||
return "", time.Time{}, time.Time{}, "", errors.New(msgBadSignature)
|
||||
}
|
||||
sub = subject(claims)
|
||||
@@ -211,6 +199,16 @@ func verifyClaims(tokenString, secret, audience string) (sub string, iat, exp ti
|
||||
return sub, iat, exp, jti, nil
|
||||
}
|
||||
|
||||
// frontendAudienceOK accepts a missing audience when expected is empty
|
||||
// (legacy frontend tokens) and otherwise requires expected.
|
||||
func frontendAudienceOK(claims jwt.MapClaims, expected string) bool {
|
||||
auds := claimAudiences(claims)
|
||||
if expected == "" {
|
||||
return len(auds) == 0 || audienceMatches(claims, AudienceUser)
|
||||
}
|
||||
return audienceMatches(claims, expected)
|
||||
}
|
||||
|
||||
func audienceMatches(claims jwt.MapClaims, expected string) bool {
|
||||
for _, aud := range claimAudiences(claims) {
|
||||
if aud == expected {
|
||||
|
||||
Reference in New Issue
Block a user