mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-11-04 07:53:59 +00:00
fix(security): classify audience failures by sentinel error
The lenient-audience path chose itself when the verify error text contained the word audience. The key lookup error embeds the kid from the unverified token header. A kid of audience made a failed signature look like an audience-only failure. verifyAudience now returns a sentinel and the caller uses errors.Is.
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/big"
|
"math/big"
|
||||||
@@ -195,11 +196,16 @@ func (j *JWT) Verify(issuerURL, expectedAudience string) error {
|
|||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An error if the claim type is invalid or the expected audience is not present
|
// - 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 {
|
func verifyAudience(tokenAudience interface{}, expectedAudience string) error {
|
||||||
switch aud := tokenAudience.(type) {
|
switch aud := tokenAudience.(type) {
|
||||||
case string:
|
case string:
|
||||||
if aud != expectedAudience {
|
if aud != expectedAudience {
|
||||||
return fmt.Errorf("invalid audience")
|
return errInvalidAudience
|
||||||
}
|
}
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
found := false
|
found := false
|
||||||
@@ -210,7 +216,7 @@ func verifyAudience(tokenAudience interface{}, expectedAudience string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
return fmt.Errorf("invalid audience")
|
return errInvalidAudience
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid 'aud' claim type")
|
return fmt.Errorf("invalid 'aud' claim type")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ package traefikoidc
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -266,8 +267,7 @@ func (t *TraefikOidc) validateStandardTokensRS(rs *requestState) (bool, bool, bo
|
|||||||
var accessVerifyErr error
|
var accessVerifyErr error
|
||||||
if err := t.verifyToken(rs.accessToken); err != nil {
|
if err := t.verifyToken(rs.accessToken); err != nil {
|
||||||
accessVerifyErr = err
|
accessVerifyErr = err
|
||||||
errMsg := err.Error()
|
if errors.Is(err, errInvalidAudience) {
|
||||||
if strings.Contains(errMsg, "invalid audience") || strings.Contains(errMsg, "audience") {
|
|
||||||
if t.strictAudienceValidation {
|
if t.strictAudienceValidation {
|
||||||
if rs.refreshToken != "" {
|
if rs.refreshToken != "" {
|
||||||
return false, true, false
|
return false, true, false
|
||||||
|
|||||||
Reference in New Issue
Block a user