Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport of [NET-4792] Add integrations tests for jwt-auth into release/1.16.x #18173

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/integration/consul-container/libs/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/hashicorp/consul/test/integration/consul-container/libs/utils"
"os"
"path/filepath"
"strconv"
Expand All @@ -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"
Expand Down
20 changes: 17 additions & 3 deletions test/integration/consul-container/libs/service/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down
229 changes: 150 additions & 79 deletions test/integration/consul-container/test/jwtauth/jwt_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)

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 should be allowed
libassert.HTTPServiceEchoesWithHeaders(t, "localhost", clientPort, "other/foo", makeAuthHeaders(jwtOne))

// 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 {
Expand All @@ -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)

Expand All @@ -120,89 +148,122 @@ 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
}

type testClaimPayload struct {
UserType string
FirstName string
}

jwt, err := libutils.SignJWT(priv, claims, privateCl)
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)),
},
},
Issuer: claims.Issuer,
Audiences: claims.Audience,
}, nil)
require.NoError(t, err)
require.True(t, ok)
}
}

// creates an intention referencing the jwt provider
func configureIntentions(t *testing.T, cluster *libcluster.Cluster) {
client := cluster.Agents[0].GetClient()
// 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))
}
}

ok, _, err := client.ConfigEntries().Set(&api.ServiceIntentionsConfigEntry{
// creates an intention referencing the jwt provider
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 != "" {
Expand All @@ -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)),
}
}