diff --git a/audience_sentinel_test.go b/audience_sentinel_test.go new file mode 100644 index 0000000..815212c --- /dev/null +++ b/audience_sentinel_test.go @@ -0,0 +1,24 @@ +package traefikoidc + +import ( + "errors" + "fmt" + "testing" +) + +// Only a real audience mismatch may select the lenient-audience path; an +// error text that merely embeds the attacker-chosen kid must not. +func TestAudienceErrorClassifiedBySentinel(t *testing.T) { + if !errors.Is(fmt.Errorf("standard claim verification failed: %w", verifyAudience("x", "y")), errInvalidAudience) { + t.Fatal("audience mismatch not recognized through wrapping") + } + if errors.Is(verifyAudience("y", "y"), errInvalidAudience) { + t.Fatal("matching audience classified as mismatch") + } + for _, kid := range []string{"audience", "invalid audience"} { + err := fmt.Errorf("failed to get public key: no matching public key found for kid: %s", kid) + if errors.Is(err, errInvalidAudience) { + t.Errorf("kid %q fooled the audience classifier", kid) + } + } +} diff --git a/jwt.go b/jwt.go index 4ad463d..a7b8ca4 100644 --- a/jwt.go +++ b/jwt.go @@ -6,6 +6,7 @@ import ( "crypto/ecdsa" "crypto/rsa" "encoding/base64" + "errors" "fmt" "math" "math/big" @@ -195,11 +196,16 @@ func (j *JWT) Verify(issuerURL, expectedAudience string) error { // // Returns: // - An error if the claim type is invalid or the expected audience is not present +// +// errInvalidAudience marks a token whose aud does not match; callers match it +// with errors.Is, never by message text. +var errInvalidAudience = errors.New("invalid audience") + func verifyAudience(tokenAudience interface{}, expectedAudience string) error { switch aud := tokenAudience.(type) { case string: if aud != expectedAudience { - return fmt.Errorf("invalid audience") + return errInvalidAudience } case []interface{}: found := false @@ -210,7 +216,7 @@ func verifyAudience(tokenAudience interface{}, expectedAudience string) error { } } if !found { - return fmt.Errorf("invalid audience") + return errInvalidAudience } default: return fmt.Errorf("invalid 'aud' claim type") diff --git a/token_validation_rs.go b/token_validation_rs.go index 69958ec..d05285b 100644 --- a/token_validation_rs.go +++ b/token_validation_rs.go @@ -15,6 +15,7 @@ package traefikoidc import ( "encoding/base64" "encoding/json" + "errors" "strings" "time" ) @@ -266,8 +267,7 @@ func (t *TraefikOidc) validateStandardTokensRS(rs *requestState) (bool, bool, bo var accessVerifyErr error if err := t.verifyToken(rs.accessToken); err != nil { accessVerifyErr = err - errMsg := err.Error() - if strings.Contains(errMsg, "invalid audience") || strings.Contains(errMsg, "audience") { + if errors.Is(err, errInvalidAudience) { if t.strictAudienceValidation { if rs.refreshToken != "" { return false, true, false