authorization_test.go•10.8 kB
package http
import (
"strings"
"testing"
"github.com/go-jose/go-jose/v4/jwt"
)
const (
// https://jwt.io/#token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJtY3Atc2VydmVyIl0sImV4cCI6MjUzNDAyMjk3MTk5LCJpYXQiOjAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiOTkyMjJkNTYtMzQwZS00ZWI2LTg1ODgtMjYxNDExZjM1ZDI2Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJlYWNiNmFkMi04MGI3LTQxNzktODQzZC05MmViMWU2YmJiYTYifX0sIm5iZiI6MCwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6ZGVmYXVsdCJ9.ld9aJaQX5k44KOV1bv8MCY2RceAZ9jAjN2vKswKmINNiOpRMl0f8Y0trrq7gdRlKwGLsCUjz8hbHsGcM43QtNrcwfvH5imRnlAKANPUgswwEadCTjASihlo6ADsn9fjAWB4viplFwq8VdzcwpcyActYJi2TBFoRq204STZJIcAW_B40HOuCB2XxQ81V4_XWLzL03Bt-YmYUhliiiE5YSKS1WEEWIbdel--b7Gvp-VS1I2eeiOqV3SelMBHbF9EwKGAkyObg0JhGqr5XHLd6WOmhvLus4eCkyakQMgr2tZIdvbt2yEUDiId6r27tlgAPLmqlyYMEhyiM212_Sth3T3Q // notsecret
tokenBasicNotExpired = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJtY3Atc2VydmVyIl0sImV4cCI6MjUzNDAyMjk3MTk5LCJpYXQiOjAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiOTkyMjJkNTYtMzQwZS00ZWI2LTg1ODgtMjYxNDExZjM1ZDI2Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJlYWNiNmFkMi04MGI3LTQxNzktODQzZC05MmViMWU2YmJiYTYifX0sIm5iZiI6MCwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6ZGVmYXVsdCJ9.ld9aJaQX5k44KOV1bv8MCY2RceAZ9jAjN2vKswKmINNiOpRMl0f8Y0trrq7gdRlKwGLsCUjz8hbHsGcM43QtNrcwfvH5imRnlAKANPUgswwEadCTjASihlo6ADsn9fjAWB4viplFwq8VdzcwpcyActYJi2TBFoRq204STZJIcAW_B40HOuCB2XxQ81V4_XWLzL03Bt-YmYUhliiiE5YSKS1WEEWIbdel--b7Gvp-VS1I2eeiOqV3SelMBHbF9EwKGAkyObg0JhGqr5XHLd6WOmhvLus4eCkyakQMgr2tZIdvbt2yEUDiId6r27tlgAPLmqlyYMEhyiM212_Sth3T3Q" // notsecret
// https://jwt.io/#token=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6Ijk4ZDU3YmUwNWI3ZjUzNWIwMzYyYjg2MDJhNTJlNGYxIn0.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJtY3Atc2VydmVyIl0sImV4cCI6MSwiaWF0IjowLCJpc3MiOiJodHRwczovL2t1YmVybmV0ZXMuZGVmYXVsdC5zdmMuY2x1c3Rlci5sb2NhbCIsImp0aSI6Ijk5MjIyZDU2LTM0MGUtNGViNi04NTg4LTI2MTQxMWYzNWQyNiIsImt1YmVybmV0ZXMuaW8iOnsibmFtZXNwYWNlIjoiZGVmYXVsdCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiZWFjYjZhZDItODBiNy00MTc5LTg0M2QtOTJlYjFlNmJiYmE2In19LCJuYmYiOjAsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.iVrxt6glbY3Qe_mEtK-lYpx4Z3VC1a7zgGRSmfu29pMmnKhlTk56y0Wx45DQ4PSYCTwC6CJnGGZNbJyr4JS8PQ // notsecret
tokenBasicExpired = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6Ijk4ZDU3YmUwNWI3ZjUzNWIwMzYyYjg2MDJhNTJlNGYxIn0.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJtY3Atc2VydmVyIl0sImV4cCI6MSwiaWF0IjowLCJpc3MiOiJodHRwczovL2t1YmVybmV0ZXMuZGVmYXVsdC5zdmMuY2x1c3Rlci5sb2NhbCIsImp0aSI6Ijk5MjIyZDU2LTM0MGUtNGViNi04NTg4LTI2MTQxMWYzNWQyNiIsImt1YmVybmV0ZXMuaW8iOnsibmFtZXNwYWNlIjoiZGVmYXVsdCIsInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiZWFjYjZhZDItODBiNy00MTc5LTg0M2QtOTJlYjFlNmJiYmE2In19LCJuYmYiOjAsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.iVrxt6glbY3Qe_mEtK-lYpx4Z3VC1a7zgGRSmfu29pMmnKhlTk56y0Wx45DQ4PSYCTwC6CJnGGZNbJyr4JS8PQ" // notsecret
// https://jwt.io/#token=eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6Ijk4ZDU3YmUwNWI3ZjUzNWIwMzYyYjg2MDJhNTJlNGYxIn0.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJtY3Atc2VydmVyIl0sImV4cCI6MjUzNDAyMjk3MTk5LCJpYXQiOjAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiOTkyMjJkNTYtMzQwZS00ZWI2LTg1ODgtMjYxNDExZjM1ZDI2Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJlYWNiNmFkMi04MGI3LTQxNzktODQzZC05MmViMWU2YmJiYTYifX0sIm5iZiI6MCwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6ZGVmYXVsdCIsInNjb3BlIjoicmVhZCB3cml0ZSJ9.m5mFXp0TDSvgLevQ76nX65N14w1RxTClMaannLLOuBIUEsmXhMYZjGtf5mWMcxVOkSh65rLFiKugaMXgv877Mg // notsecret
tokenMultipleAudienceNotExpired = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6Ijk4ZDU3YmUwNWI3ZjUzNWIwMzYyYjg2MDJhNTJlNGYxIn0.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJtY3Atc2VydmVyIl0sImV4cCI6MjUzNDAyMjk3MTk5LCJpYXQiOjAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiOTkyMjJkNTYtMzQwZS00ZWI2LTg1ODgtMjYxNDExZjM1ZDI2Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJlYWNiNmFkMi04MGI3LTQxNzktODQzZC05MmViMWU2YmJiYTYifX0sIm5iZiI6MCwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6ZGVmYXVsdCIsInNjb3BlIjoicmVhZCB3cml0ZSJ9.m5mFXp0TDSvgLevQ76nX65N14w1RxTClMaannLLOuBIUEsmXhMYZjGtf5mWMcxVOkSh65rLFiKugaMXgv877Mg" // notsecret
)
func TestParseJWTClaimsPayloadValid(t *testing.T) {
basicClaims, err := ParseJWTClaims(tokenBasicNotExpired)
t.Run("Is parseable", func(t *testing.T) {
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if basicClaims == nil {
t.Fatal("expected claims, got nil")
}
})
t.Run("Parses issuer", func(t *testing.T) {
if basicClaims.Issuer != "https://kubernetes.default.svc.cluster.local" {
t.Errorf("expected issuer 'https://kubernetes.default.svc.cluster.local', got %s", basicClaims.Issuer)
}
})
t.Run("Parses audience", func(t *testing.T) {
expectedAudiences := []string{"https://kubernetes.default.svc.cluster.local", "mcp-server"}
for _, expected := range expectedAudiences {
if !basicClaims.Audience.Contains(expected) {
t.Errorf("expected audience to contain %s", expected)
}
}
})
t.Run("Parses expiration", func(t *testing.T) {
if *basicClaims.Expiry != jwt.NumericDate(253402297199) {
t.Errorf("expected expiration 253402297199, got %d", basicClaims.Expiry)
}
})
t.Run("Parses scope", func(t *testing.T) {
scopeClaims, err := ParseJWTClaims(tokenMultipleAudienceNotExpired)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if scopeClaims == nil {
t.Fatal("expected claims, got nil")
}
scopes := scopeClaims.GetScopes()
expectedScopes := []string{"read", "write"}
if len(scopes) != len(expectedScopes) {
t.Errorf("expected %d scopes, got %d", len(expectedScopes), len(scopes))
}
for i, expectedScope := range expectedScopes {
if scopes[i] != expectedScope {
t.Errorf("expected scope[%d] to be '%s', got '%s'", i, expectedScope, scopes[i])
}
}
})
t.Run("Parses expired token", func(t *testing.T) {
expiredClaims, err := ParseJWTClaims(tokenBasicExpired)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if *expiredClaims.Expiry != jwt.NumericDate(1) {
t.Errorf("expected expiration 1, got %d", basicClaims.Expiry)
}
})
}
func TestParseJWTClaimsPayloadInvalid(t *testing.T) {
t.Run("invalid token segments", func(t *testing.T) {
invalidToken := "header.payload.signature.extra"
_, err := ParseJWTClaims(invalidToken)
if err == nil {
t.Fatal("expected error for invalid token segments, got nil")
}
if !strings.Contains(err.Error(), "compact JWS format must have three parts") {
t.Errorf("expected invalid token segments error message, got %v", err)
}
})
t.Run("invalid base64 payload", func(t *testing.T) {
invalidPayload := strings.ReplaceAll(tokenBasicNotExpired, ".", ".invalid")
_, err := ParseJWTClaims(invalidPayload)
if err == nil {
t.Fatal("expected error for invalid base64, got nil")
}
if !strings.Contains(err.Error(), "illegal base64 data") {
t.Errorf("expected decode error message, got %v", err)
}
})
}
func TestJWTTokenValidateOffline(t *testing.T) {
t.Run("expired token returns error", func(t *testing.T) {
claims, err := ParseJWTClaims(tokenBasicExpired)
if err != nil {
t.Fatalf("expected no error for expired token parsing, got %v", err)
}
err = claims.ValidateOffline("mcp-server")
if err == nil {
t.Fatalf("expected error for expired token, got nil")
}
if !strings.Contains(err.Error(), "token is expired (exp)") {
t.Errorf("expected expiration error message, got %v", err)
}
})
t.Run("multiple audiences with correct one", func(t *testing.T) {
claims, err := ParseJWTClaims(tokenMultipleAudienceNotExpired)
if err != nil {
t.Fatalf("expected no error for multiple audience token parsing, got %v", err)
}
if claims == nil {
t.Fatalf("expected claims to be returned, got nil")
}
err = claims.ValidateOffline("mcp-server")
if err != nil {
t.Fatalf("expected no error for valid audience, got %v", err)
}
})
t.Run("multiple audiences with mismatch returns error", func(t *testing.T) {
claims, err := ParseJWTClaims(tokenMultipleAudienceNotExpired)
if err != nil {
t.Fatalf("expected no error for multiple audience token parsing, got %v", err)
}
if claims == nil {
t.Fatalf("expected claims to be returned, got nil")
}
err = claims.ValidateOffline("missing-audience")
if err == nil {
t.Fatalf("expected error for token with wrong audience, got nil")
}
if !strings.Contains(err.Error(), "invalid audience claim (aud)") {
t.Errorf("expected audience mismatch error, got %v", err)
}
})
}
func TestJWTClaimsGetScopes(t *testing.T) {
t.Run("no scopes", func(t *testing.T) {
claims, err := ParseJWTClaims(tokenBasicExpired)
if err != nil {
t.Fatalf("expected no error for parsing token, got %v", err)
}
if scopes := claims.GetScopes(); len(scopes) != 0 {
t.Errorf("expected no scopes, got %d", len(scopes))
}
})
t.Run("single scope", func(t *testing.T) {
claims := &JWTClaims{
Scope: "read",
}
scopes := claims.GetScopes()
expected := []string{"read"}
if len(scopes) != 1 {
t.Errorf("expected 1 scope, got %d", len(scopes))
}
if scopes[0] != expected[0] {
t.Errorf("expected scope 'read', got '%s'", scopes[0])
}
})
t.Run("multiple scopes", func(t *testing.T) {
claims := &JWTClaims{
Scope: "read write admin",
}
scopes := claims.GetScopes()
expected := []string{"read", "write", "admin"}
if len(scopes) != 3 {
t.Errorf("expected 3 scopes, got %d", len(scopes))
}
for i, expectedScope := range expected {
if i >= len(scopes) || scopes[i] != expectedScope {
t.Errorf("expected scope[%d] to be '%s', got '%s'", i, expectedScope, scopes[i])
}
}
})
t.Run("scopes with extra whitespace", func(t *testing.T) {
claims := &JWTClaims{
Scope: " read write admin ",
}
scopes := claims.GetScopes()
expected := []string{"read", "write", "admin"}
if len(scopes) != 3 {
t.Errorf("expected 3 scopes, got %d", len(scopes))
}
for i, expectedScope := range expected {
if i >= len(scopes) || scopes[i] != expectedScope {
t.Errorf("expected scope[%d] to be '%s', got '%s'", i, expectedScope, scopes[i])
}
}
})
}