istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/security/jwt_test.go (about)

     1  //go:build integ
     2  // +build integ
     3  
     4  // Copyright Istio Authors
     5  //
     6  // Licensed under the Apache License, Version 2.0 (the "License");
     7  // you may not use this file except in compliance with the License.
     8  // You may obtain a copy of the License at
     9  //
    10  //     http://www.apache.org/licenses/LICENSE-2.0
    11  //
    12  // Unless required by applicable law or agreed to in writing, software
    13  // distributed under the License is distributed on an "AS IS" BASIS,
    14  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  // See the License for the specific language governing permissions and
    16  // limitations under the License.
    17  
    18  package security
    19  
    20  import (
    21  	"fmt"
    22  	"net/http"
    23  	"strings"
    24  	"testing"
    25  
    26  	"istio.io/istio/pkg/config/protocol"
    27  	"istio.io/istio/pkg/http/headers"
    28  	"istio.io/istio/pkg/test/framework"
    29  	"istio.io/istio/pkg/test/framework/components/crd"
    30  	"istio.io/istio/pkg/test/framework/components/echo"
    31  	"istio.io/istio/pkg/test/framework/components/echo/check"
    32  	"istio.io/istio/pkg/test/framework/components/echo/config"
    33  	"istio.io/istio/pkg/test/framework/components/echo/config/param"
    34  	"istio.io/istio/pkg/test/framework/components/istio"
    35  	"istio.io/istio/pkg/test/framework/components/istio/ingress"
    36  	"istio.io/istio/pkg/test/framework/label"
    37  	"istio.io/istio/tests/common/jwt"
    38  )
    39  
    40  // TestRequestAuthentication tests beta authn policy for jwt.
    41  func TestRequestAuthentication(t *testing.T) {
    42  	payload1 := strings.Split(jwt.TokenIssuer1, ".")[1]
    43  	payload2 := strings.Split(jwt.TokenIssuer2, ".")[1]
    44  	payload3 := strings.Split(jwt.TokenIssuer1WithNestedClaims2, ".")[1]
    45  	framework.NewTest(t).
    46  		Label(label.IPv4). // https://github.com/istio/istio/issues/35835
    47  		Run(func(t framework.TestContext) {
    48  			type testCase struct {
    49  				name          string
    50  				customizeCall func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions)
    51  			}
    52  
    53  			newTest := func(policy string, cases []testCase) func(framework.TestContext) {
    54  				return func(t framework.TestContext) {
    55  					if len(policy) > 0 {
    56  						// Apply the policy for all targets.
    57  						config.New(t).
    58  							Source(config.File(policy).WithParams(param.Params{
    59  								"JWTServer": jwtServer,
    60  							})).
    61  							BuildAll(nil, apps.Ns1.All).
    62  							Apply()
    63  					}
    64  
    65  					newTrafficTest(t, apps.Ns1.All.Instances()).Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
    66  						for _, c := range cases {
    67  							t.NewSubTest(c.name).Run(func(t framework.TestContext) {
    68  								opts := echo.CallOptions{
    69  									To: to,
    70  									Port: echo.Port{
    71  										Name: "http",
    72  									},
    73  								}
    74  
    75  								// Apply any custom options for the test.
    76  								c.customizeCall(t, from, &opts)
    77  
    78  								from.CallOrFail(t, opts)
    79  							})
    80  						}
    81  					})
    82  				}
    83  			}
    84  
    85  			t.NewSubTest("authn-only").Run(newTest("testdata/requestauthn/authn-only.yaml.tmpl", []testCase{
    86  				{
    87  					name: "valid-token-noauthz",
    88  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
    89  						opts.HTTP.Path = "/valid-token-noauthz"
    90  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
    91  						opts.Check = check.And(
    92  							check.OK(),
    93  							check.ReachedTargetClusters(t),
    94  							check.RequestHeaders(map[string]string{
    95  								headers.Authorization: "",
    96  								"X-Test-Payload":      payload1,
    97  								"X-Jwt-Iss":           "test-issuer-1@istio.io",
    98  								"X-Jwt-Iat":           "1562182722",
    99  							}))
   100  					},
   101  				},
   102  				{
   103  					name: "valid-token-2-noauthz",
   104  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   105  						opts.HTTP.Path = "/valid-token-2-noauthz"
   106  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer2).Build()
   107  						opts.Check = check.And(
   108  							check.OK(),
   109  							check.ReachedTargetClusters(t),
   110  							check.RequestHeaders(map[string]string{
   111  								headers.Authorization: "",
   112  								"X-Test-Payload":      payload2,
   113  							}))
   114  					},
   115  				},
   116  				{
   117  					name: "expired-token-noauthz",
   118  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   119  						opts.HTTP.Path = "/expired-token-noauthz"
   120  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenExpired).Build()
   121  						opts.Check = check.Status(http.StatusUnauthorized)
   122  					},
   123  				},
   124  				{
   125  					name: "expired-token-cors-preflight-request-allowed",
   126  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   127  						opts.HTTP.Path = "/expired-token-cors-preflight-request-allowed"
   128  						opts.HTTP.Method = "OPTIONS"
   129  						opts.HTTP.Headers = headers.New().
   130  							WithAuthz(jwt.TokenExpired).
   131  							With(headers.AccessControlRequestMethod, "POST").
   132  							With(headers.Origin, "https://istio.io").
   133  							Build()
   134  						opts.Check = check.And(
   135  							check.OK(),
   136  							check.ReachedTargetClusters(t))
   137  					},
   138  				},
   139  				{
   140  					name: "expired-token-bad-cors-preflight-request-rejected",
   141  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   142  						opts.HTTP.Path = "/expired-token-cors-preflight-request-allowed"
   143  						opts.HTTP.Method = "OPTIONS"
   144  						opts.HTTP.Headers = headers.New().
   145  							WithAuthz(jwt.TokenExpired).
   146  							With(headers.AccessControlRequestMethod, "POST").
   147  							// the required Origin header is missing.
   148  							Build()
   149  						opts.Check = check.Status(http.StatusUnauthorized)
   150  					},
   151  				},
   152  				{
   153  					name: "no-token-noauthz",
   154  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   155  						opts.HTTP.Path = "/no-token-noauthz"
   156  						opts.Check = check.And(
   157  							check.OK(),
   158  							check.ReachedTargetClusters(t))
   159  					},
   160  				},
   161  				{
   162  					name: "valid-nested-claim-token",
   163  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   164  						opts.HTTP.Path = "/valid-token-noauthz"
   165  						opts.HTTP.Headers = headers.New().
   166  							With("Authorization", "Bearer "+jwt.TokenIssuer1WithNestedClaims2).
   167  							With("X-Jwt-Nested-Claim", "value_to_be_replaced").
   168  							Build()
   169  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1WithNestedClaims2).Build()
   170  						opts.Check = check.And(
   171  							check.OK(),
   172  							check.ReachedTargetClusters(t),
   173  							check.RequestHeaders(map[string]string{
   174  								headers.Authorization: "",
   175  								"X-Test-Payload":      payload3,
   176  								"X-Jwt-Iss":           "test-issuer-1@istio.io",
   177  								"X-Jwt-Iat":           "1604008018",
   178  								"X-Jwt-Nested-Claim":  "valueC",
   179  							}))
   180  					},
   181  				},
   182  			}))
   183  
   184  			t.NewSubTest("authn-authz").Run(newTest("testdata/requestauthn/authn-authz.yaml.tmpl", []testCase{
   185  				{
   186  					name: "valid-token",
   187  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   188  						opts.HTTP.Path = "/valid-token"
   189  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
   190  						opts.Check = check.And(
   191  							check.OK(),
   192  							check.ReachedTargetClusters(t),
   193  							check.RequestHeader(headers.Authorization, ""))
   194  					},
   195  				},
   196  				{
   197  					name: "expired-token",
   198  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   199  						opts.HTTP.Path = "/expired-token"
   200  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenExpired).Build()
   201  						opts.Check = check.Status(http.StatusUnauthorized)
   202  					},
   203  				},
   204  				{
   205  					name: "no-token",
   206  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   207  						opts.HTTP.Path = "/no-token"
   208  						opts.Check = check.Status(http.StatusForbidden)
   209  					},
   210  				},
   211  			}))
   212  
   213  			t.NewSubTest("no-authn-authz").Run(newTest("", []testCase{
   214  				{
   215  					name: "no-authn-authz",
   216  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   217  						opts.HTTP.Path = "/no-authn-authz"
   218  						opts.Check = check.And(
   219  							check.OK(),
   220  							check.ReachedTargetClusters(t))
   221  					},
   222  				},
   223  			}))
   224  
   225  			t.NewSubTest("forward").Run(newTest("testdata/requestauthn/forward.yaml.tmpl", []testCase{
   226  				{
   227  					name: "valid-token-forward",
   228  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   229  						opts.HTTP.Path = "/valid-token-forward"
   230  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
   231  						opts.Check = check.And(
   232  							check.OK(),
   233  							check.ReachedTargetClusters(t),
   234  							check.RequestHeaders(map[string]string{
   235  								headers.Authorization: "Bearer " + jwt.TokenIssuer1,
   236  								"X-Test-Payload":      payload1,
   237  							}))
   238  					},
   239  				},
   240  			}))
   241  
   242  			t.NewSubTest("remote").Run(newTest("testdata/requestauthn/remote.yaml.tmpl", []testCase{
   243  				{
   244  					name: "valid-token-forward-remote-jwks",
   245  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   246  						opts.HTTP.Path = "/valid-token-forward-remote-jwks"
   247  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
   248  						opts.Check = check.And(
   249  							check.OK(),
   250  							check.ReachedTargetClusters(t),
   251  							check.RequestHeaders(map[string]string{
   252  								headers.Authorization: "Bearer " + jwt.TokenIssuer1,
   253  								"X-Test-Payload":      payload1,
   254  							}))
   255  					},
   256  				},
   257  			}))
   258  
   259  			t.NewSubTest("timeout").Run(newTest("testdata/requestauthn/timeout.yaml.tmpl", []testCase{
   260  				{
   261  					name: "valid-token-forward-remote-jwks",
   262  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   263  						opts.HTTP.Path = "/valid-token-forward-remote-jwks"
   264  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
   265  						opts.Check = check.And(
   266  							check.NotOK(),
   267  							check.Status(http.StatusUnauthorized))
   268  					},
   269  				},
   270  			}))
   271  
   272  			t.NewSubTest("aud").Run(newTest("testdata/requestauthn/aud.yaml.tmpl", []testCase{
   273  				{
   274  					name: "invalid-aud",
   275  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   276  						opts.HTTP.Path = "/valid-aud"
   277  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
   278  						opts.Check = check.Status(http.StatusForbidden)
   279  					},
   280  				},
   281  				{
   282  					name: "valid-aud",
   283  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   284  						opts.HTTP.Path = "/valid-aud"
   285  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1WithAud).Build()
   286  						opts.Check = check.And(
   287  							check.OK(),
   288  							check.ReachedTargetClusters(t))
   289  					},
   290  				},
   291  				{
   292  					name: "verify-policies-are-combined",
   293  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   294  						opts.HTTP.Path = "/verify-policies-are-combined"
   295  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer2).Build()
   296  						opts.Check = check.And(
   297  							check.OK(),
   298  							check.ReachedTargetClusters(t))
   299  					},
   300  				},
   301  			}))
   302  
   303  			t.NewSubTest("invalid-jwks").Run(newTest("testdata/requestauthn/invalid-jwks.yaml.tmpl", []testCase{
   304  				{
   305  					name: "invalid-jwks-valid-token-noauthz",
   306  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   307  						opts.HTTP.Path = ""
   308  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenIssuer1).Build()
   309  						opts.Check = check.Status(http.StatusUnauthorized)
   310  					},
   311  				},
   312  				{
   313  					name: "invalid-jwks-expired-token-noauthz",
   314  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   315  						opts.HTTP.Path = "/invalid-jwks-valid-token-noauthz"
   316  						opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenExpired).Build()
   317  						opts.Check = check.Status(http.StatusUnauthorized)
   318  					},
   319  				},
   320  				{
   321  					name: "invalid-jwks-no-token-noauthz",
   322  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   323  						opts.HTTP.Path = "/invalid-jwks-no-token-noauthz"
   324  						opts.Check = check.And(
   325  							check.OK(),
   326  							check.ReachedTargetClusters(t))
   327  					},
   328  				},
   329  			}))
   330  
   331  			t.NewSubTest("headers-params").Run(newTest("testdata/requestauthn/headers-params.yaml.tmpl", []testCase{
   332  				{
   333  					name: "valid-params",
   334  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   335  						opts.HTTP.Path = "/valid-token?token=" + jwt.TokenIssuer1
   336  						opts.Check = check.And(
   337  							check.OK(),
   338  							check.ReachedTargetClusters(t))
   339  					},
   340  				},
   341  				{
   342  					name: "valid-params-secondary",
   343  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   344  						opts.HTTP.Path = "/valid-token?secondary_token=" + jwt.TokenIssuer1
   345  						opts.Check = check.And(
   346  							check.OK(),
   347  							check.ReachedTargetClusters(t))
   348  					},
   349  				},
   350  				{
   351  					name: "invalid-params",
   352  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   353  						opts.HTTP.Path = "/valid-token?token_value=" + jwt.TokenIssuer1
   354  						opts.Check = check.Status(http.StatusForbidden)
   355  					},
   356  				},
   357  				{
   358  					name: "valid-token-set",
   359  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   360  						opts.HTTP.Path = "/valid-token?token=" + jwt.TokenIssuer1 + "&secondary_token=" + jwt.TokenIssuer1
   361  						opts.Check = check.And(
   362  							check.OK(),
   363  							check.ReachedTargetClusters(t))
   364  					},
   365  				},
   366  				{
   367  					name: "invalid-token-set",
   368  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   369  						opts.HTTP.Path = "/valid-token?token=" + jwt.TokenIssuer1 + "&secondary_token=" + jwt.TokenExpired
   370  						opts.Check = check.Status(http.StatusUnauthorized)
   371  					},
   372  				},
   373  				{
   374  					name: "valid-header",
   375  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   376  						opts.HTTP.Path = ""
   377  						opts.HTTP.Headers = headers.New().
   378  							With("X-Jwt-Token", "Value "+jwt.TokenIssuer1).
   379  							Build()
   380  						opts.Check = check.And(
   381  							check.OK(),
   382  							check.ReachedTargetClusters(t))
   383  					},
   384  				},
   385  				{
   386  					name: "valid-header-secondary",
   387  					customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   388  						opts.HTTP.Path = ""
   389  						opts.HTTP.Headers = headers.New().
   390  							With("Auth-Token", "Token "+jwt.TokenIssuer1).
   391  							Build()
   392  						opts.Check = check.And(
   393  							check.OK(),
   394  							check.ReachedTargetClusters(t))
   395  					},
   396  				},
   397  				{
   398  					name: "invalid-header",
   399  					customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   400  						opts.HTTP.Path = ""
   401  						opts.HTTP.Headers = headers.New().
   402  							With("Auth-Header-Param", "Bearer "+jwt.TokenIssuer1).
   403  							Build()
   404  						opts.Check = check.Status(http.StatusForbidden)
   405  					},
   406  				},
   407  			}))
   408  		})
   409  }
   410  
   411  // TestIngressRequestAuthentication tests beta authn policy for jwt on ingress.
   412  // The policy is also set at global namespace, with authorization on ingressgateway.
   413  func TestIngressRequestAuthentication(t *testing.T) {
   414  	framework.NewTest(t).
   415  		Label(label.IPv4). // https://github.com/istio/istio/issues/35835
   416  		Run(func(t framework.TestContext) {
   417  			config.New(t).
   418  				Source(config.File("testdata/requestauthn/global-jwt.yaml.tmpl").WithParams(param.Params{
   419  					param.Namespace.String(): istio.ClaimSystemNamespaceOrFail(t, t),
   420  					"Services":               apps.Ns1.All,
   421  					"GatewayIstioLabel":      i.Settings().IngressGatewayIstioLabel,
   422  				})).
   423  				Source(config.File("testdata/requestauthn/ingress.yaml.tmpl").WithParams(param.Params{
   424  					param.Namespace.String(): apps.Ns1.Namespace,
   425  					"GatewayIstioLabel":      i.Settings().IngressGatewayIstioLabel,
   426  				})).
   427  				BuildAll(nil, apps.Ns1.All).
   428  				Apply()
   429  
   430  			t.NewSubTest("in-mesh-authn").Run(func(t framework.TestContext) {
   431  				cases := []struct {
   432  					name          string
   433  					customizeCall func(framework.TestContext, echo.Instance, *echo.CallOptions)
   434  				}{
   435  					{
   436  						name: "in-mesh-with-expired-token",
   437  						customizeCall: func(_ framework.TestContext, _ echo.Instance, opts *echo.CallOptions) {
   438  							opts.HTTP.Headers = headers.New().WithAuthz(jwt.TokenExpired).Build()
   439  							opts.Check = check.Status(http.StatusUnauthorized)
   440  						},
   441  					},
   442  					{
   443  						name: "in-mesh-without-token",
   444  						customizeCall: func(t framework.TestContext, from echo.Instance, opts *echo.CallOptions) {
   445  							opts.Check = check.And(
   446  								check.OK(),
   447  								check.ReachedTargetClusters(t))
   448  						},
   449  					},
   450  				}
   451  				newTrafficTest(t, apps.Ns1.All.Instances()).
   452  					Run(func(t framework.TestContext, from echo.Instance, to echo.Target) {
   453  						for _, c := range cases {
   454  							t.NewSubTest(c.name).Run(func(t framework.TestContext) {
   455  								opts := echo.CallOptions{
   456  									To: to,
   457  									Port: echo.Port{
   458  										Name: "http",
   459  									},
   460  								}
   461  
   462  								// Apply any custom options for the test.
   463  								c.customizeCall(t, from, &opts)
   464  
   465  								from.CallOrFail(t, opts)
   466  							})
   467  						}
   468  					})
   469  			})
   470  
   471  			t.NewSubTest("ingress-authn").Run(func(t framework.TestContext) {
   472  				cases := []struct {
   473  					name          string
   474  					customizeCall func(opts *echo.CallOptions, to echo.Target)
   475  				}{
   476  					{
   477  						name: "deny without token",
   478  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   479  							opts.HTTP.Path = "/"
   480  							opts.HTTP.Headers = headers.New().
   481  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   482  								Build()
   483  							opts.Check = check.Status(http.StatusForbidden)
   484  						},
   485  					},
   486  					{
   487  						name: "allow with sub-1 token",
   488  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   489  							opts.HTTP.Path = "/"
   490  							opts.HTTP.Headers = headers.New().
   491  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   492  								WithAuthz(jwt.TokenIssuer1).
   493  								Build()
   494  							opts.Check = check.OK()
   495  						},
   496  					},
   497  					{
   498  						name: "deny with sub-2 token",
   499  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   500  							opts.HTTP.Path = "/"
   501  							opts.HTTP.Headers = headers.New().
   502  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   503  								WithAuthz(jwt.TokenIssuer2).
   504  								Build()
   505  							opts.Check = check.Status(http.StatusForbidden)
   506  						},
   507  					},
   508  					{
   509  						name: "deny with expired token",
   510  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   511  							opts.HTTP.Path = "/"
   512  							opts.HTTP.Headers = headers.New().
   513  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   514  								WithAuthz(jwt.TokenExpired).
   515  								Build()
   516  							opts.Check = check.Status(http.StatusUnauthorized)
   517  						},
   518  					},
   519  					{
   520  						name: "allow with sub-1 token on any.com",
   521  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   522  							opts.HTTP.Path = "/"
   523  							opts.HTTP.Headers = headers.New().
   524  								WithHost(fmt.Sprintf("any-request-principal-ok.%s.com", to.ServiceName())).
   525  								WithAuthz(jwt.TokenIssuer1).
   526  								Build()
   527  							opts.Check = check.OK()
   528  						},
   529  					},
   530  					{
   531  						name: "allow with sub-2 token on any.com",
   532  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   533  							opts.HTTP.Path = "/"
   534  							opts.HTTP.Headers = headers.New().
   535  								WithHost(fmt.Sprintf("any-request-principal-ok.%s.com", to.ServiceName())).
   536  								WithAuthz(jwt.TokenIssuer2).
   537  								Build()
   538  							opts.Check = check.OK()
   539  						},
   540  					},
   541  					{
   542  						name: "deny without token on any.com",
   543  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   544  							opts.HTTP.Path = "/"
   545  							opts.HTTP.Headers = headers.New().
   546  								WithHost(fmt.Sprintf("any-request-principal-ok.%s.com", to.ServiceName())).
   547  								Build()
   548  							opts.Check = check.Status(http.StatusForbidden)
   549  						},
   550  					},
   551  					{
   552  						name: "deny with token on other host",
   553  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   554  							opts.HTTP.Path = "/"
   555  							opts.HTTP.Headers = headers.New().
   556  								WithHost(fmt.Sprintf("other-host.%s.com", to.ServiceName())).
   557  								WithAuthz(jwt.TokenIssuer1).
   558  								Build()
   559  							opts.Check = check.Status(http.StatusForbidden)
   560  						},
   561  					},
   562  					{
   563  						name: "allow healthz",
   564  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   565  							opts.HTTP.Path = "/healthz"
   566  							opts.HTTP.Headers = headers.New().
   567  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   568  								Build()
   569  							opts.Check = check.OK()
   570  						},
   571  					},
   572  				}
   573  
   574  				newTrafficTest(t, apps.Ns1.All.Instances()).
   575  					RunViaIngress(func(t framework.TestContext, from ingress.Instance, to echo.Target) {
   576  						for _, c := range cases {
   577  							t.NewSubTest(c.name).Run(func(t framework.TestContext) {
   578  								opts := echo.CallOptions{
   579  									Port: echo.Port{
   580  										Protocol: protocol.HTTP,
   581  									},
   582  								}
   583  
   584  								c.customizeCall(&opts, to)
   585  
   586  								from.CallOrFail(t, opts)
   587  							})
   588  						}
   589  					})
   590  			})
   591  		})
   592  }
   593  
   594  // TestGatewayAPIRequestAuthentication tests beta authn policy for jwt on gateway API.
   595  func TestGatewayAPIRequestAuthentication(t *testing.T) {
   596  	framework.NewTest(t).
   597  		Label(label.IPv4). // https://github.com/istio/istio/issues/35835
   598  		Run(func(t framework.TestContext) {
   599  			crd.DeployGatewayAPIOrSkip(t)
   600  			config.New(t).
   601  				Source(config.File("testdata/requestauthn/gateway-api.yaml.tmpl").WithParams(param.Params{
   602  					param.Namespace.String(): apps.Ns1.Namespace,
   603  				})).
   604  				Source(config.File("testdata/requestauthn/gateway-jwt.yaml.tmpl").WithParams(param.Params{
   605  					param.Namespace.String(): apps.Ns1.Namespace,
   606  					"Services":               apps.Ns1.A.Append(apps.Ns1.B).Services(),
   607  				})).
   608  				BuildAll(nil, apps.Ns1.A.Append(apps.Ns1.B).Services()).
   609  				Apply()
   610  
   611  			t.NewSubTest("gateway-authn").Run(func(t framework.TestContext) {
   612  				cases := []struct {
   613  					name          string
   614  					customizeCall func(opts *echo.CallOptions, to echo.Target)
   615  				}{
   616  					{
   617  						name: "deny without token",
   618  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   619  							opts.HTTP.Path = "/"
   620  							opts.HTTP.Headers = headers.New().
   621  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   622  								Build()
   623  							opts.Check = check.Status(http.StatusForbidden)
   624  						},
   625  					},
   626  					{
   627  						name: "allow with sub-1 token",
   628  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   629  							opts.HTTP.Path = "/"
   630  							opts.HTTP.Headers = headers.New().
   631  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   632  								WithAuthz(jwt.TokenIssuer1).
   633  								Build()
   634  							opts.Check = check.OK()
   635  						},
   636  					},
   637  					{
   638  						name: "allow with sub-1 token despite \"ignored\" RequestAuthentication (enableGatewayAPISelectorPolicies flag = true)",
   639  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   640  							opts.HTTP.Path = "/"
   641  							opts.HTTP.Headers = headers.New().
   642  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   643  								WithAuthz(jwt.TokenIssuer3).
   644  								Build()
   645  							opts.Check = check.OK()
   646  						},
   647  					},
   648  					{
   649  						name: "deny with sub-2 token",
   650  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   651  							opts.HTTP.Path = "/"
   652  							opts.HTTP.Headers = headers.New().
   653  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   654  								WithAuthz(jwt.TokenIssuer2).
   655  								Build()
   656  							opts.Check = check.Status(http.StatusForbidden)
   657  						},
   658  					},
   659  					{
   660  						name: "deny with expired token",
   661  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   662  							opts.HTTP.Path = "/"
   663  							opts.HTTP.Headers = headers.New().
   664  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   665  								WithAuthz(jwt.TokenExpired).
   666  								Build()
   667  							opts.Check = check.Status(http.StatusUnauthorized)
   668  						},
   669  					},
   670  					{
   671  						name: "allow with sub-1 token on any.com",
   672  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   673  							opts.HTTP.Path = "/"
   674  							opts.HTTP.Headers = headers.New().
   675  								WithHost(fmt.Sprintf("any-request-principal-ok.%s.com", to.ServiceName())).
   676  								WithAuthz(jwt.TokenIssuer1).
   677  								Build()
   678  							opts.Check = check.OK()
   679  						},
   680  					},
   681  					{
   682  						name: "allow with sub-2 token on any.com",
   683  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   684  							opts.HTTP.Path = "/"
   685  							opts.HTTP.Headers = headers.New().
   686  								WithHost(fmt.Sprintf("any-request-principal-ok.%s.com", to.ServiceName())).
   687  								WithAuthz(jwt.TokenIssuer2).
   688  								Build()
   689  							opts.Check = check.OK()
   690  						},
   691  					},
   692  					{
   693  						name: "deny without token on any.com",
   694  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   695  							opts.HTTP.Path = "/"
   696  							opts.HTTP.Headers = headers.New().
   697  								WithHost(fmt.Sprintf("any-request-principal-ok.%s.com", to.ServiceName())).
   698  								Build()
   699  							opts.Check = check.Status(http.StatusForbidden)
   700  						},
   701  					},
   702  					{
   703  						name: "deny with token on other host",
   704  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   705  							opts.HTTP.Path = "/"
   706  							opts.HTTP.Headers = headers.New().
   707  								WithHost(fmt.Sprintf("other-host.%s.com", to.ServiceName())).
   708  								WithAuthz(jwt.TokenIssuer1).
   709  								Build()
   710  							opts.Check = check.Status(http.StatusForbidden)
   711  						},
   712  					},
   713  					{
   714  						name: "allow healthz",
   715  						customizeCall: func(opts *echo.CallOptions, to echo.Target) {
   716  							opts.HTTP.Path = "/healthz"
   717  							opts.HTTP.Headers = headers.New().
   718  								WithHost(fmt.Sprintf("example.%s.com", to.ServiceName())).
   719  								Build()
   720  							opts.Check = check.OK()
   721  						},
   722  					},
   723  				}
   724  
   725  				newTrafficTest(t, apps.Ns1.A.Append(apps.Ns1.B)).
   726  					RunViaGatewayIngress("istio", func(t framework.TestContext, from ingress.Instance, to echo.Target) {
   727  						for _, c := range cases {
   728  							t.NewSubTest(c.name).Run(func(t framework.TestContext) {
   729  								opts := echo.CallOptions{
   730  									Port: echo.Port{
   731  										Protocol: protocol.HTTP,
   732  									},
   733  								}
   734  
   735  								c.customizeCall(&opts, to)
   736  
   737  								from.CallOrFail(t, opts)
   738  							})
   739  						}
   740  					})
   741  			})
   742  		})
   743  }