mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
221 lines
11 KiB
Go
221 lines
11 KiB
Go
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])
|
|
}
|
|
}
|
|
})
|
|
}
|