From 921445712eea2e949afb1888d2c81a245fdcec93 Mon Sep 17 00:00:00 2001 From: Ronald Date: Tue, 18 Jul 2023 14:59:01 -0400 Subject: [PATCH] [NET-4792] Add integrations tests for jwt-auth (#18169) --- .../consul-container/libs/cluster/cluster.go | 2 +- .../consul-container/libs/service/connect.go | 20 +- .../test/jwtauth/jwt_auth_test.go | 227 ++++++++++++------ 3 files changed, 167 insertions(+), 82 deletions(-) diff --git a/test/integration/consul-container/libs/cluster/cluster.go b/test/integration/consul-container/libs/cluster/cluster.go index e5f6537c16..aedf0ac926 100644 --- a/test/integration/consul-container/libs/cluster/cluster.go +++ b/test/integration/consul-container/libs/cluster/cluster.go @@ -8,7 +8,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/hashicorp/consul/test/integration/consul-container/libs/utils" "os" "path/filepath" "strconv" @@ -18,6 +17,7 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/hashicorp/consul/test/integration/consul-container/libs/utils" "github.com/hashicorp/serf/serf" goretry "github.com/avast/retry-go" diff --git a/test/integration/consul-container/libs/service/connect.go b/test/integration/consul-container/libs/service/connect.go index 006ca804a9..4251a6d3c8 100644 --- a/test/integration/consul-container/libs/service/connect.go +++ b/test/integration/consul-container/libs/service/connect.go @@ -167,14 +167,13 @@ func NewConnectService(ctx context.Context, sidecarCfg SidecarConfig, serviceBin namePrefix := fmt.Sprintf("%s-service-connect-%s", node.GetDatacenter(), sidecarCfg.Name) containerName := utils.RandName(namePrefix) - agentConfig := node.GetConfig() internalAdminPort, err := node.ClaimAdminPort() if err != nil { return nil, err } - fmt.Println("agent image name", agentConfig.DockerImage()) - imageVersion := utils.SideCarVersion(agentConfig.DockerImage()) + fmt.Println("agent image name", nodeConfig.DockerImage()) + imageVersion := utils.SideCarVersion(nodeConfig.DockerImage()) req := testcontainers.ContainerRequest{ Image: fmt.Sprintf("consul-envoy:%s", imageVersion), WaitingFor: wait.ForLog("").WithStartupTimeout(100 * time.Second), @@ -238,6 +237,21 @@ func NewConnectService(ctx context.Context, sidecarCfg SidecarConfig, serviceBin req.Env["CONSUL_GRPC_ADDR"] = fmt.Sprintf("http://127.0.0.1:%d", 8502) } + if nodeConfig.ACLEnabled { + client := node.GetClient() + token, _, err := client.ACL().TokenCreate(&api.ACLToken{ + ServiceIdentities: []*api.ACLServiceIdentity{ + {ServiceName: sidecarCfg.ServiceID}, + }, + }, nil) + + if err != nil { + return nil, err + } + + req.Env["CONSUL_HTTP_TOKEN"] = token.SecretID + } + var ( appPortStrs []string adminPortStr = strconv.Itoa(internalAdminPort) diff --git a/test/integration/consul-container/test/jwtauth/jwt_auth_test.go b/test/integration/consul-container/test/jwtauth/jwt_auth_test.go index 498bdcedf1..2ff3938f92 100644 --- a/test/integration/consul-container/test/jwtauth/jwt_auth_test.go +++ b/test/integration/consul-container/test/jwtauth/jwt_auth_test.go @@ -24,20 +24,22 @@ import ( "time" ) -// TestJWTAuthConnectService summary +// TestJWTAuthConnectService summary: // This test ensures that when we have an intention referencing a JWT, requests -// without JWT authorization headers are denied. And requests with the correct JWT -// Authorization header are successful +// without JWT authorization headers are denied and requests with the correct JWT +// Authorization header are successful. // // Steps: // - Creates a single agent cluster +// - Generates a JWKS and 2 JWTs with different claims +// - Generates another JWKS with a single JWT +// - Configures proxy defaults, providers and intentions // - Creates a static-server and sidecar containers // - Registers the created static-server and sidecar with consul // - Create a static-client and sidecar containers // - Registers the static-client and sidecar with consul // - Ensure client sidecar is running as expected -// - Make a request without the JWT Authorization header and expects 401 StatusUnauthorized -// - Make a request with the JWT Authorization header and expects a 200 +// - Runs a couple of scenarios to ensure jwt validation works as expected func TestJWTAuthConnectService(t *testing.T) { t.Parallel() @@ -47,39 +49,65 @@ func TestJWTAuthConnectService(t *testing.T) { ApplyDefaultProxySettings: true, BuildOpts: &libcluster.BuildOptions{ Datacenter: "dc1", - InjectAutoEncryption: true, - InjectGossipEncryption: true, + InjectCerts: true, + InjectGossipEncryption: false, + AllowHTTPAnyway: true, + ACLEnabled: true, }, }) + // generate jwks and 2 jwts with different claims for provider 1 + jwksOne, privOne := makeJWKS(t) + claimsOne := makeTestClaims("https://legit.issuer.internal/", "https://consul.test") + jwtOne := makeJWT(t, privOne, claimsOne, testClaimPayload{UserType: "admin", FirstName: "admin"}) + jwtOneAdmin := makeJWT(t, privOne, claimsOne, testClaimPayload{UserType: "client", FirstName: "non-admin"}) + provider1 := makeTestJWTProvider("okta", jwksOne, claimsOne) + + // generate another jwks and jwt for provider 2 + jwksTwo, privTwo := makeJWKS(t) + claimsTwo := makeTestClaims("https://another.issuer.internal/", "https://consul.test") + jwtTwo := makeJWT(t, privTwo, claimsTwo, testClaimPayload{}) + provider2 := makeTestJWTProvider("auth0", jwksTwo, claimsTwo) + + // configure proxy-defaults, jwt providers and intentions + configureProxyDefaults(t, cluster) + configureJWTProviders(t, cluster, provider1, provider2) + configureIntentions(t, cluster, provider1, provider2) + clientService := createServices(t, cluster) _, clientPort := clientService.GetAddr() - _, clientAdminPort := clientService.GetAdminAddr() + _, adminPort := clientService.GetAdminAddr() - libassert.AssertUpstreamEndpointStatus(t, clientAdminPort, "static-server.default", "HEALTHY", 1) libassert.AssertContainerState(t, clientService, "running") - libassert.AssertFortioName(t, fmt.Sprintf("http://localhost:%d", clientPort), "static-server", "") + libassert.AssertUpstreamEndpointStatus(t, adminPort, "static-server.default", "HEALTHY", 1) - claims := jwt.Claims{ - Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", - Audience: jwt.Audience{"https://consul.test"}, - Issuer: "https://legit.issuer.internal/", - NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), - Expiry: jwt.NewNumericDate(time.Now().Add(60 * time.Minute)), - } + // request to restricted endpoint with no jwt should be denied + doRequest(t, fmt.Sprintf("http://localhost:%d/restricted/foo", clientPort), http.StatusForbidden, "") - jwks, jwt := makeJWKSAndJWT(t, claims) + // request with jwt 1 /restricted/foo should be disallowed + doRequest(t, fmt.Sprintf("http://localhost:%d/restricted/foo", clientPort), http.StatusForbidden, jwtOne) - // configure proxy-defaults, jwt-provider and intention - configureProxyDefaults(t, cluster) - configureJWTProvider(t, cluster, jwks, claims) - configureIntentions(t, cluster) + // request with jwt 1 /other/foo should be allowed + libassert.HTTPServiceEchoesWithHeaders(t, "localhost", clientPort, "other/foo", makeAuthHeaders(jwtOne)) - baseURL := fmt.Sprintf("http://localhost:%d", clientPort) - // TODO(roncodingenthusiast): update test to reflect jwt-auth filter in metadata mode - doRequest(t, baseURL, http.StatusOK, "") - // succeeds with jwt - doRequest(t, baseURL, http.StatusOK, jwt) + // request with jwt 1 /other/foo with mismatched claims should be disallowed + doRequest(t, fmt.Sprintf("http://localhost:%d/other/foo", clientPort), http.StatusForbidden, jwtOneAdmin) + + // request with provider 1 /foo should be allowed + libassert.HTTPServiceEchoesWithHeaders(t, "localhost", clientPort, "foo", makeAuthHeaders(jwtOne)) + + // request with jwt 2 to /foo should be denied + doRequest(t, fmt.Sprintf("http://localhost:%d/foo", clientPort), http.StatusForbidden, jwtTwo) + + // request with jwt 2 to /restricted/foo should be allowed + libassert.HTTPServiceEchoesWithHeaders(t, "localhost", clientPort, "restricted/foo", makeAuthHeaders(jwtTwo)) + + // request with jwt 2 to /other/foo should be denied + doRequest(t, fmt.Sprintf("http://localhost:%d/other/foo", clientPort), http.StatusForbidden, jwtTwo) +} + +func makeAuthHeaders(jwt string) map[string]string { + return map[string]string{"Authorization": fmt.Sprintf("Bearer %s", jwt)} } func createServices(t *testing.T, cluster *libcluster.Cluster) libservice.Service { @@ -92,25 +120,25 @@ func createServices(t *testing.T, cluster *libcluster.Cluster) libservice.Servic HTTPPort: 8080, GRPCPort: 8079, } + apiOpts := &api.QueryOptions{Token: cluster.TokenBootstrap} // Create a service and proxy instance _, _, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOpts) require.NoError(t, err) - libassert.CatalogServiceExists(t, client, "static-server-sidecar-proxy", nil) - libassert.CatalogServiceExists(t, client, libservice.StaticServerServiceName, nil) + libassert.CatalogServiceExists(t, client, "static-server-sidecar-proxy", apiOpts) + libassert.CatalogServiceExists(t, client, libservice.StaticServerServiceName, apiOpts) // Create a client proxy instance with the server as an upstream clientConnectProxy, err := libservice.CreateAndRegisterStaticClientSidecar(node, "", false, false) require.NoError(t, err) - libassert.CatalogServiceExists(t, client, "static-client-sidecar-proxy", nil) + libassert.CatalogServiceExists(t, client, "static-client-sidecar-proxy", apiOpts) return clientConnectProxy } -// creates a JWKS and JWT that will be used for validation -func makeJWKSAndJWT(t *testing.T, claims jwt.Claims) (string, string) { +func makeJWKS(t *testing.T) (string, string) { pub, priv, err := libutils.GenerateKey() require.NoError(t, err) @@ -120,46 +148,36 @@ func makeJWKSAndJWT(t *testing.T, claims jwt.Claims) (string, string) { jwksJson, err := json.Marshal(jwks) require.NoError(t, err) - type orgs struct { - Primary string `json:"primary"` - } - privateCl := struct { - FirstName string `json:"first_name"` - Org orgs `json:"org"` - Groups []string `json:"groups"` - }{ - FirstName: "jeff2", - Org: orgs{"engineering"}, - Groups: []string{"foo", "bar"}, - } + return string(jwksJson), priv +} - jwt, err := libutils.SignJWT(priv, claims, privateCl) +type testClaimPayload struct { + UserType string + FirstName string +} + +func makeJWT(t *testing.T, priv string, claims jwt.Claims, payload testClaimPayload) string { + jwt, err := libutils.SignJWT(priv, claims, payload) require.NoError(t, err) - return string(jwksJson), jwt + + return jwt } // configures the protocol to http as this is needed for jwt-auth func configureProxyDefaults(t *testing.T, cluster *libcluster.Cluster) { - client := cluster.Agents[0].GetClient() - - ok, _, err := client.ConfigEntries().Set(&api.ProxyConfigEntry{ + require.NoError(t, cluster.ConfigEntryWrite(&api.ProxyConfigEntry{ Kind: api.ProxyDefaults, Name: api.ProxyConfigGlobal, Config: map[string]interface{}{ "protocol": "http", }, - }, nil) - require.NoError(t, err) - require.True(t, ok) + })) } -// creates a JWT local provider -func configureJWTProvider(t *testing.T, cluster *libcluster.Cluster, jwks string, claims jwt.Claims) { - client := cluster.Agents[0].GetClient() - - ok, _, err := client.ConfigEntries().Set(&api.JWTProviderConfigEntry{ +func makeTestJWTProvider(name string, jwks string, claims jwt.Claims) *api.JWTProviderConfigEntry { + return &api.JWTProviderConfigEntry{ Kind: api.JWTProvider, - Name: "test-jwt", + Name: name, JSONWebKeySet: &api.JSONWebKeySet{ Local: &api.LocalJWKS{ JWKS: base64.StdEncoding.EncodeToString([]byte(jwks)), @@ -167,42 +185,85 @@ func configureJWTProvider(t *testing.T, cluster *libcluster.Cluster, jwks string }, Issuer: claims.Issuer, Audiences: claims.Audience, - }, nil) - require.NoError(t, err) - require.True(t, ok) + } +} + +// creates a JWT local provider +func configureJWTProviders(t *testing.T, cluster *libcluster.Cluster, providers ...*api.JWTProviderConfigEntry) { + for _, prov := range providers { + require.NoError(t, cluster.ConfigEntryWrite(prov)) + } } // creates an intention referencing the jwt provider -func configureIntentions(t *testing.T, cluster *libcluster.Cluster) { - client := cluster.Agents[0].GetClient() - - ok, _, err := client.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{ +func configureIntentions(t *testing.T, cluster *libcluster.Cluster, provider1, provider2 *api.JWTProviderConfigEntry) { + intention := api.ServiceIntentionsConfigEntry{ Kind: "service-intentions", Name: libservice.StaticServerServiceName, Sources: []*api.SourceIntention{ { - Name: libservice.StaticClientServiceName, - Action: api.IntentionActionAllow, + Name: libservice.StaticClientServiceName, + Permissions: []*api.IntentionPermission{ + { + Action: api.IntentionActionAllow, + HTTP: &api.IntentionHTTPPermission{ + PathPrefix: "/restricted/", + }, + JWT: &api.IntentionJWTRequirement{ + Providers: []*api.IntentionJWTProvider{ + { + Name: provider2.Name, + }, + }, + }, + }, + { + Action: api.IntentionActionAllow, + HTTP: &api.IntentionHTTPPermission{ + PathPrefix: "/", + }, + JWT: &api.IntentionJWTRequirement{ + Providers: []*api.IntentionJWTProvider{ + { + Name: provider1.Name, + VerifyClaims: []*api.IntentionJWTClaimVerification{ + { + Path: []string{"UserType"}, + Value: "admin", + }, + }, + }, + }, + }, + }, + }, }, - }, - JWT: &api.IntentionJWTRequirement{ - Providers: []*api.IntentionJWTProvider{ - { - Name: "test-jwt", - VerifyClaims: []*api.IntentionJWTClaimVerification{}, + { + Name: "other-client", + Permissions: []*api.IntentionPermission{ + { + Action: api.IntentionActionAllow, + HTTP: &api.IntentionHTTPPermission{ + PathPrefix: "/other/", + }, + JWT: &api.IntentionJWTRequirement{ + Providers: []*api.IntentionJWTProvider{ + { + Name: provider2.Name, + }, + }, + }, + }, }, }, }, - }, nil) - require.NoError(t, err) - require.True(t, ok) + } + require.NoError(t, cluster.ConfigEntryWrite(&intention)) } func doRequest(t *testing.T, url string, expStatus int, jwt string) { retry.RunWith(&retry.Timer{Timeout: 5 * time.Second, Wait: time.Second}, t, func(r *retry.R) { - client := cleanhttp.DefaultClient() - req, err := http.NewRequest("GET", url, nil) require.NoError(r, err) if jwt != "" { @@ -213,3 +274,13 @@ func doRequest(t *testing.T, url string, expStatus int, jwt string) { require.Equal(r, expStatus, resp.StatusCode) }) } + +func makeTestClaims(issuer, audience string) jwt.Claims { + return jwt.Claims{ + Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", + Audience: jwt.Audience{audience}, + Issuer: issuer, + NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), + Expiry: jwt.NewNumericDate(time.Now().Add(60 * time.Minute)), + } +}