[NET-4792] Add integrations tests for jwt-auth (#18169)

This commit is contained in:
Ronald 2023-07-18 14:59:01 -04:00 committed by GitHub
parent 62005369b5
commit 921445712e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 167 additions and 82 deletions

View File

@ -8,7 +8,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/hashicorp/consul/test/integration/consul-container/libs/utils"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -18,6 +17,7 @@ import (
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/test/integration/consul-container/libs/utils"
"github.com/hashicorp/serf/serf" "github.com/hashicorp/serf/serf"
goretry "github.com/avast/retry-go" goretry "github.com/avast/retry-go"

View File

@ -167,14 +167,13 @@ func NewConnectService(ctx context.Context, sidecarCfg SidecarConfig, serviceBin
namePrefix := fmt.Sprintf("%s-service-connect-%s", node.GetDatacenter(), sidecarCfg.Name) namePrefix := fmt.Sprintf("%s-service-connect-%s", node.GetDatacenter(), sidecarCfg.Name)
containerName := utils.RandName(namePrefix) containerName := utils.RandName(namePrefix)
agentConfig := node.GetConfig()
internalAdminPort, err := node.ClaimAdminPort() internalAdminPort, err := node.ClaimAdminPort()
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmt.Println("agent image name", agentConfig.DockerImage()) fmt.Println("agent image name", nodeConfig.DockerImage())
imageVersion := utils.SideCarVersion(agentConfig.DockerImage()) imageVersion := utils.SideCarVersion(nodeConfig.DockerImage())
req := testcontainers.ContainerRequest{ req := testcontainers.ContainerRequest{
Image: fmt.Sprintf("consul-envoy:%s", imageVersion), Image: fmt.Sprintf("consul-envoy:%s", imageVersion),
WaitingFor: wait.ForLog("").WithStartupTimeout(100 * time.Second), 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) 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 ( var (
appPortStrs []string appPortStrs []string
adminPortStr = strconv.Itoa(internalAdminPort) adminPortStr = strconv.Itoa(internalAdminPort)

View File

@ -24,20 +24,22 @@ import (
"time" "time"
) )
// TestJWTAuthConnectService summary // TestJWTAuthConnectService summary:
// This test ensures that when we have an intention referencing a JWT, requests // 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 // without JWT authorization headers are denied and requests with the correct JWT
// Authorization header are successful // Authorization header are successful.
// //
// Steps: // Steps:
// - Creates a single agent cluster // - 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 // - Creates a static-server and sidecar containers
// - Registers the created static-server and sidecar with consul // - Registers the created static-server and sidecar with consul
// - Create a static-client and sidecar containers // - Create a static-client and sidecar containers
// - Registers the static-client and sidecar with consul // - Registers the static-client and sidecar with consul
// - Ensure client sidecar is running as expected // - Ensure client sidecar is running as expected
// - Make a request without the JWT Authorization header and expects 401 StatusUnauthorized // - Runs a couple of scenarios to ensure jwt validation works as expected
// - Make a request with the JWT Authorization header and expects a 200
func TestJWTAuthConnectService(t *testing.T) { func TestJWTAuthConnectService(t *testing.T) {
t.Parallel() t.Parallel()
@ -47,39 +49,65 @@ func TestJWTAuthConnectService(t *testing.T) {
ApplyDefaultProxySettings: true, ApplyDefaultProxySettings: true,
BuildOpts: &libcluster.BuildOptions{ BuildOpts: &libcluster.BuildOptions{
Datacenter: "dc1", Datacenter: "dc1",
InjectAutoEncryption: true, InjectCerts: true,
InjectGossipEncryption: 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) clientService := createServices(t, cluster)
_, clientPort := clientService.GetAddr() _, clientPort := clientService.GetAddr()
_, clientAdminPort := clientService.GetAdminAddr() _, adminPort := clientService.GetAdminAddr()
libassert.AssertUpstreamEndpointStatus(t, clientAdminPort, "static-server.default", "HEALTHY", 1)
libassert.AssertContainerState(t, clientService, "running") 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{ // request to restricted endpoint with no jwt should be denied
Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", doRequest(t, fmt.Sprintf("http://localhost:%d/restricted/foo", clientPort), http.StatusForbidden, "")
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)),
}
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 // request with jwt 1 /other/foo should be allowed
configureProxyDefaults(t, cluster) libassert.HTTPServiceEchoesWithHeaders(t, "localhost", clientPort, "other/foo", makeAuthHeaders(jwtOne))
configureJWTProvider(t, cluster, jwks, claims)
configureIntentions(t, cluster)
baseURL := fmt.Sprintf("http://localhost:%d", clientPort) // request with jwt 1 /other/foo with mismatched claims should be disallowed
// TODO(roncodingenthusiast): update test to reflect jwt-auth filter in metadata mode doRequest(t, fmt.Sprintf("http://localhost:%d/other/foo", clientPort), http.StatusForbidden, jwtOneAdmin)
doRequest(t, baseURL, http.StatusOK, "")
// succeeds with jwt // request with provider 1 /foo should be allowed
doRequest(t, baseURL, http.StatusOK, jwt) 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 { 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, HTTPPort: 8080,
GRPCPort: 8079, GRPCPort: 8079,
} }
apiOpts := &api.QueryOptions{Token: cluster.TokenBootstrap}
// Create a service and proxy instance // Create a service and proxy instance
_, _, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOpts) _, _, err := libservice.CreateAndRegisterStaticServerAndSidecar(node, serviceOpts)
require.NoError(t, err) require.NoError(t, err)
libassert.CatalogServiceExists(t, client, "static-server-sidecar-proxy", nil) libassert.CatalogServiceExists(t, client, "static-server-sidecar-proxy", apiOpts)
libassert.CatalogServiceExists(t, client, libservice.StaticServerServiceName, nil) libassert.CatalogServiceExists(t, client, libservice.StaticServerServiceName, apiOpts)
// Create a client proxy instance with the server as an upstream // Create a client proxy instance with the server as an upstream
clientConnectProxy, err := libservice.CreateAndRegisterStaticClientSidecar(node, "", false, false) clientConnectProxy, err := libservice.CreateAndRegisterStaticClientSidecar(node, "", false, false)
require.NoError(t, err) require.NoError(t, err)
libassert.CatalogServiceExists(t, client, "static-client-sidecar-proxy", nil) libassert.CatalogServiceExists(t, client, "static-client-sidecar-proxy", apiOpts)
return clientConnectProxy return clientConnectProxy
} }
// creates a JWKS and JWT that will be used for validation func makeJWKS(t *testing.T) (string, string) {
func makeJWKSAndJWT(t *testing.T, claims jwt.Claims) (string, string) {
pub, priv, err := libutils.GenerateKey() pub, priv, err := libutils.GenerateKey()
require.NoError(t, err) require.NoError(t, err)
@ -120,46 +148,36 @@ func makeJWKSAndJWT(t *testing.T, claims jwt.Claims) (string, string) {
jwksJson, err := json.Marshal(jwks) jwksJson, err := json.Marshal(jwks)
require.NoError(t, err) require.NoError(t, err)
type orgs struct { return string(jwksJson), priv
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"},
}
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) require.NoError(t, err)
return string(jwksJson), jwt
return jwt
} }
// configures the protocol to http as this is needed for jwt-auth // configures the protocol to http as this is needed for jwt-auth
func configureProxyDefaults(t *testing.T, cluster *libcluster.Cluster) { func configureProxyDefaults(t *testing.T, cluster *libcluster.Cluster) {
client := cluster.Agents[0].GetClient() require.NoError(t, cluster.ConfigEntryWrite(&api.ProxyConfigEntry{
ok, _, err := client.ConfigEntries().Set(&api.ProxyConfigEntry{
Kind: api.ProxyDefaults, Kind: api.ProxyDefaults,
Name: api.ProxyConfigGlobal, Name: api.ProxyConfigGlobal,
Config: map[string]interface{}{ Config: map[string]interface{}{
"protocol": "http", "protocol": "http",
}, },
}, nil) }))
require.NoError(t, err)
require.True(t, ok)
} }
// creates a JWT local provider func makeTestJWTProvider(name string, jwks string, claims jwt.Claims) *api.JWTProviderConfigEntry {
func configureJWTProvider(t *testing.T, cluster *libcluster.Cluster, jwks string, claims jwt.Claims) { return &api.JWTProviderConfigEntry{
client := cluster.Agents[0].GetClient()
ok, _, err := client.ConfigEntries().Set(&api.JWTProviderConfigEntry{
Kind: api.JWTProvider, Kind: api.JWTProvider,
Name: "test-jwt", Name: name,
JSONWebKeySet: &api.JSONWebKeySet{ JSONWebKeySet: &api.JSONWebKeySet{
Local: &api.LocalJWKS{ Local: &api.LocalJWKS{
JWKS: base64.StdEncoding.EncodeToString([]byte(jwks)), JWKS: base64.StdEncoding.EncodeToString([]byte(jwks)),
@ -167,42 +185,85 @@ func configureJWTProvider(t *testing.T, cluster *libcluster.Cluster, jwks string
}, },
Issuer: claims.Issuer, Issuer: claims.Issuer,
Audiences: claims.Audience, 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 // creates an intention referencing the jwt provider
func configureIntentions(t *testing.T, cluster *libcluster.Cluster) { func configureIntentions(t *testing.T, cluster *libcluster.Cluster, provider1, provider2 *api.JWTProviderConfigEntry) {
client := cluster.Agents[0].GetClient() intention := api.ServiceIntentionsConfigEntry{
ok, _, err := client.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{
Kind: "service-intentions", Kind: "service-intentions",
Name: libservice.StaticServerServiceName, Name: libservice.StaticServerServiceName,
Sources: []*api.SourceIntention{ Sources: []*api.SourceIntention{
{ {
Name: libservice.StaticClientServiceName, Name: libservice.StaticClientServiceName,
Action: api.IntentionActionAllow, 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{ Name: "other-client",
Providers: []*api.IntentionJWTProvider{ Permissions: []*api.IntentionPermission{
{ {
Name: "test-jwt", Action: api.IntentionActionAllow,
VerifyClaims: []*api.IntentionJWTClaimVerification{}, HTTP: &api.IntentionHTTPPermission{
PathPrefix: "/other/",
},
JWT: &api.IntentionJWTRequirement{
Providers: []*api.IntentionJWTProvider{
{
Name: provider2.Name,
},
},
},
},
}, },
}, },
}, },
}, nil) }
require.NoError(t, err) require.NoError(t, cluster.ConfigEntryWrite(&intention))
require.True(t, ok)
} }
func doRequest(t *testing.T, url string, expStatus int, jwt string) { 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) { retry.RunWith(&retry.Timer{Timeout: 5 * time.Second, Wait: time.Second}, t, func(r *retry.R) {
client := cleanhttp.DefaultClient() client := cleanhttp.DefaultClient()
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
require.NoError(r, err) require.NoError(r, err)
if jwt != "" { if jwt != "" {
@ -213,3 +274,13 @@ func doRequest(t *testing.T, url string, expStatus int, jwt string) {
require.Equal(r, expStatus, resp.StatusCode) 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)),
}
}