istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/util/jwtutil_test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package util
    16  
    17  import (
    18  	"fmt"
    19  	"reflect"
    20  	"testing"
    21  	"time"
    22  )
    23  
    24  var (
    25  	// thirdPartyJwt is generated in a testing K8s cluster, using the "istio-token" projected volume.
    26  	// Token is issued at 2020-04-04 22:13:54 Pacific Daylight time.
    27  	// Expiration time is 2020-04-05 10:13:54 Pacific Daylight time.
    28  	thirdPartyJwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9." +
    29  		"eyJhdWQiOlsieW9uZ2dhbmdsLWlzdGlvLTQuc3ZjLmlkLmdvb2ciXSwiZXhwIjoxNTg2MTA2ODM0LCJpYXQiOjE1" +
    30  		"ODYwNjM2MzQsImlzcyI6Imh0dHBzOi8vY29udGFpbmVyLmdvb2dsZWFwaXMuY29tL3YxL3Byb2plY3RzL3lvbmdn" +
    31  		"YW5nbC1pc3Rpby00L2xvY2F0aW9ucy91cy1jZW50cmFsMS1hL2NsdXN0ZXJzL2NsdXN0ZXItMyIsImt1YmVybmV0" +
    32  		"ZXMuaW8iOnsibmFtZXNwYWNlIjoiZm9vIiwicG9kIjp7Im5hbWUiOiJodHRwYmluLTY0Nzc2YmY3OGQtanFsNWIi" +
    33  		"LCJ1aWQiOiI5YWQ3NTcxYi03NjBhLTExZWEtODllNy00MjAxMGE4MDAxYzEifSwic2VydmljZWFjY291bnQiOnsi" +
    34  		"bmFtZSI6Imh0dHBiaW4iLCJ1aWQiOiI5OWY2NWY1MC03NjBhLTExZWEtODllNy00MjAxMGE4MDAxYzEifX0sIm5i" +
    35  		"ZiI6MTU4NjA2MzYzNCwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmZvbzpodHRwYmluIn0.XWSCdarBR0cx" +
    36  		"MlVV5X9pvgI9m0lyO-17B45aBKJBIQjvjluKXqxnCuIeD3X2ItLkCUzmKUa3ftTjUUov1MJ89MdBngNfUP7IfwnD" +
    37  		"2dBl7Jtju0-Ks7aTFOkgtoMYqNnQ1VSDTAOfNpdZVUnsR_oY8obXSQR_H4uMcaNOGED2RX5HLBWFlvymtn4JXuyg" +
    38  		"_rpOrJ8dv-snrmO3LT9y-zaUnZqSceDC8skzStrJIRvsRkO8GEcoQd5VwDn-UVgOcqWb-S-vgSjdtwBsnGPXsh_I" +
    39  		"NZCq3ftr0Qu8-IxsIjpMjhLmAGTH1bR324aqLTYhAXp6fk06Pe3T9stCY5acSeadKA"
    40  	// firstPartyJwt is generated in a testing K8s cluster. It is the default service account JWT.
    41  	// No expiration time.
    42  	firstPartyJwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9." +
    43  		"eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9u" +
    44  		"YW1lc3BhY2UiOiJmb28iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiaHR0cGJp" +
    45  		"bi10b2tlbi14cWRncCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUi" +
    46  		"OiJodHRwYmluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiOTlm" +
    47  		"NjVmNTAtNzYwYS0xMWVhLTg5ZTctNDIwMTBhODAwMWMxIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmZv" +
    48  		"bzpodHRwYmluIn0.4kIl9TRjXEw6DfhtR-LdpxsAlYjJgC6Ly1DY_rqYY4h0haxcXB3kYZ3b2He-3fqOBryz524W" +
    49  		"KkZscZgvs5L-sApmvlqdUG61TMAl7josB0x4IMHm1NS995LNEaXiI4driffwfopvqc_z3lVKfbF9j-mBgnCepxz3" +
    50  		"UyWo5irFa3qcwbOUB9kuuUNGBdtbFBN5yIYLpfa9E-MtTX_zJ9fQ9j2pi8Z4ljii0tEmPmRxokHkmG_xNJjUkxKU" +
    51  		"WZf4bLDdCEjVFyshNae-FdxiUVyeyYorTYzwZZYQch9MJeedg4keKKUOvCCJUlKixd2qAe-H7r15RPmo4AU5O5YL" +
    52  		"65xiNg"
    53  
    54  	// oneAudString includes one `aud` claim "abc" of type string.
    55  	oneAudString = "header.eyJhdWQiOiJhYmMiLCJleHAiOjQ3MzI5OTQ4MDEsImlhdCI6MTU3OTM5NDgwMSwiaXNzIjoidGVzdC1pc3N1ZXItMUBpc3Rpby5pbyIsInN1YiI6InN1Yi0xIn0.signature" // nolint: lll
    56  
    57  	// twoAudList includes two `aud` claims ["abc", "xyz"] of type []string.
    58  	twoAudList = "header.eyJhdWQiOlsiYWJjIiwieHl6Il0sImV4cCI6NDczMjk5NDgwMSwiaWF0IjoxNTc5Mzk0ODAxLCJpc3MiOiJ0ZXN0LWlzc3Vlci0xQGlzdGlvLmlvIiwic3ViIjoic3ViLTEifQ.signature" // nolint: lll
    59  
    60  	// A JWT encoded payload that has the padding stripped and contains an underscore character from the base64url alphabet
    61  	base64UrlEncodedPaddingStrippedPart = "eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXVkIjoiSm9lIEFyZG_DsWV6In0"
    62  )
    63  
    64  func TestGetExp(t *testing.T) {
    65  	testCases := map[string]struct {
    66  		jwt         string
    67  		expectedExp time.Time
    68  		expectedErr error
    69  	}{
    70  		"jwt with expiration time": {
    71  			jwt:         thirdPartyJwt,
    72  			expectedExp: time.Date(2020, time.April, 5, 10, 13, 54, 0, time.FixedZone("PDT", -int((7*time.Hour).Seconds()))),
    73  			expectedErr: nil,
    74  		},
    75  		"jwt with no expiration time": {
    76  			jwt:         firstPartyJwt,
    77  			expectedExp: time.Time{},
    78  			expectedErr: nil,
    79  		},
    80  		"invalid jwt": {
    81  			jwt:         "invalid-section1.invalid-section2.invalid-section3",
    82  			expectedExp: time.Time{},
    83  			expectedErr: fmt.Errorf("failed to decode the JWT claims"),
    84  		},
    85  	}
    86  
    87  	for id, tc := range testCases {
    88  		t.Run(id, func(t *testing.T) {
    89  			exp, err := GetExp(tc.jwt)
    90  			if err != nil && tc.expectedErr == nil || err == nil && tc.expectedErr != nil {
    91  				t.Errorf("%s: Got error \"%v\", expected error \"%v\"", id, err, tc.expectedErr)
    92  			} else if err != nil && tc.expectedErr != nil && err.Error() != tc.expectedErr.Error() {
    93  				t.Errorf("%s: Got error \"%v\", expected error \"%v\"", id, err, tc.expectedErr)
    94  			} else if err == nil && exp.Sub(tc.expectedExp) != time.Duration(0) {
    95  				t.Errorf("%s: Got expiration time: %s, expected expiration time: %s",
    96  					id, exp.String(), tc.expectedExp.String())
    97  			}
    98  		})
    99  	}
   100  }
   101  
   102  func TestGetAud(t *testing.T) {
   103  	testCases := map[string]struct {
   104  		jwt string
   105  		aud []string
   106  	}{
   107  		"no audience": {
   108  			jwt: firstPartyJwt,
   109  		},
   110  		"one audience string": {
   111  			jwt: oneAudString,
   112  			aud: []string{"abc"},
   113  		},
   114  		"one audience list": {
   115  			jwt: thirdPartyJwt,
   116  			aud: []string{"yonggangl-istio-4.svc.id.goog"},
   117  		},
   118  		"two audiences list": {
   119  			jwt: twoAudList,
   120  			aud: []string{"abc", "xyz"},
   121  		},
   122  	}
   123  
   124  	for id, tc := range testCases {
   125  		t.Run(id, func(t *testing.T) {
   126  			if got, _ := GetAud(tc.jwt); !reflect.DeepEqual(tc.aud, got) {
   127  				t.Errorf("want audience %v but got %v", tc.aud, got)
   128  			}
   129  		})
   130  	}
   131  }
   132  
   133  func Test3p(t *testing.T) {
   134  	for _, s := range []string{thirdPartyJwt, "InvalidToken"} {
   135  		if IsK8SUnbound(s) {
   136  			t.Error("Expecting bound token, detected unbound ", s)
   137  		}
   138  	}
   139  	for _, s := range []string{firstPartyJwt, ".bnVsbM."} {
   140  		if !IsK8SUnbound(s) {
   141  			t.Error("Expecting unbound, detected bound ", s)
   142  		}
   143  	}
   144  }
   145  
   146  func TestBase64UrlPartDecoding(t *testing.T) {
   147  	payloadBytes, err := DecodeJwtPart(base64UrlEncodedPaddingStrippedPart)
   148  	if err != nil {
   149  		t.Error("Expected DecodeJwtPart success, got failure", err)
   150  	}
   151  	if payloadBytes == nil {
   152  		t.Error("Expected DecodeJwtPart to return non-nil, got nil")
   153  	}
   154  
   155  	expectedAud := "Joe ArdoƱez"
   156  	if got, _ := GetAud("header." + base64UrlEncodedPaddingStrippedPart + ".signature"); len(got) != 1 || expectedAud != got[0] {
   157  		t.Errorf("want audience %v but got %v", expectedAud, got)
   158  	}
   159  }