go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth/internal/common_test.go (about) 1 // Copyright 2017 The LUCI 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 internal 16 17 import ( 18 "context" 19 "math/rand" 20 "testing" 21 "time" 22 23 "golang.org/x/oauth2" 24 "golang.org/x/oauth2/google" 25 26 . "github.com/smartystreets/goconvey/convey" 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/data/rand/mathrand" 30 ) 31 32 func TestCacheKey(t *testing.T) { 33 t.Parallel() 34 35 Convey("ToMapKey empty", t, func() { 36 k := CacheKey{} 37 So(k.ToMapKey(), ShouldEqual, "\x00") 38 }) 39 40 Convey("ToMapKey works", t, func() { 41 k := CacheKey{ 42 Key: "a", 43 Scopes: []string{"x", "y", "z"}, 44 } 45 So(k.ToMapKey(), ShouldEqual, "a\x00x\x00y\x00z\x00") 46 }) 47 48 Convey("EqualCacheKeys works", t, func() { 49 So(EqualCacheKeys(nil, nil), ShouldBeTrue) 50 So(EqualCacheKeys(&CacheKey{}, nil), ShouldBeFalse) 51 So(EqualCacheKeys(nil, &CacheKey{}), ShouldBeFalse) 52 53 So(EqualCacheKeys( 54 &CacheKey{ 55 Key: "k", 56 Scopes: []string{"a", "b"}, 57 }, 58 &CacheKey{ 59 Key: "k", 60 Scopes: []string{"a", "b"}, 61 }), 62 ShouldBeTrue) 63 64 So(EqualCacheKeys( 65 &CacheKey{ 66 Key: "k1", 67 Scopes: []string{"a", "b"}, 68 }, 69 &CacheKey{ 70 Key: "k2", 71 Scopes: []string{"a", "b"}, 72 }), 73 ShouldBeFalse) 74 75 So(EqualCacheKeys( 76 &CacheKey{ 77 Key: "k", 78 Scopes: []string{"a1", "b"}, 79 }, 80 &CacheKey{ 81 Key: "k", 82 Scopes: []string{"a2", "b"}, 83 }), 84 ShouldBeFalse) 85 86 So(EqualCacheKeys( 87 &CacheKey{ 88 Key: "k", 89 Scopes: []string{"a"}, 90 }, 91 &CacheKey{ 92 Key: "k", 93 Scopes: []string{"a", "b"}, 94 }), 95 ShouldBeFalse) 96 }) 97 } 98 99 func TestTokenHelpers(t *testing.T) { 100 t.Parallel() 101 102 ctx := mathrand.Set(context.Background(), rand.New(rand.NewSource(123))) 103 ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeLocal) 104 exp := testclock.TestRecentTimeLocal.Add(time.Hour) 105 106 Convey("TokenExpiresIn works", t, func() { 107 // Invalid tokens. 108 So(TokenExpiresIn(ctx, nil, time.Minute), ShouldBeTrue) 109 So(TokenExpiresIn(ctx, &Token{ 110 Token: oauth2.Token{ 111 AccessToken: "", 112 Expiry: exp, 113 }, 114 }, time.Minute), ShouldBeTrue) 115 116 // If expiry is not set, the token is non-expirable. 117 So(TokenExpiresIn(ctx, &Token{ 118 Token: oauth2.Token{AccessToken: "abc"}, 119 }, 10*time.Hour), ShouldBeFalse) 120 121 So(TokenExpiresIn(ctx, &Token{ 122 Token: oauth2.Token{ 123 AccessToken: "abc", 124 Expiry: exp, 125 }, 126 }, time.Minute), ShouldBeFalse) 127 128 tc.Add(59*time.Minute + 1*time.Second) 129 130 So(TokenExpiresIn(ctx, &Token{ 131 Token: oauth2.Token{ 132 AccessToken: "abc", 133 Expiry: exp, 134 }, 135 }, time.Minute), ShouldBeTrue) 136 }) 137 138 Convey("TokenExpiresInRnd works", t, func() { 139 // Invalid tokens. 140 So(TokenExpiresInRnd(ctx, nil, time.Minute), ShouldBeTrue) 141 So(TokenExpiresInRnd(ctx, &Token{ 142 Token: oauth2.Token{ 143 AccessToken: "", 144 Expiry: exp, 145 }, 146 }, time.Minute), ShouldBeTrue) 147 148 // If expiry is not set, the token is non-expirable. 149 So(TokenExpiresInRnd(ctx, &Token{ 150 Token: oauth2.Token{AccessToken: "abc"}, 151 }, 10*time.Hour), ShouldBeFalse) 152 153 // Generate a histogram of positive TokenExpiresInRnd responses per second, 154 // for the duration of 10 min, assuming each TokenExpiresInRnd is called 155 // 100 times per second. 156 tokenLifetime := 5 * time.Minute 157 requestedLifetime := time.Minute 158 tok := &Token{ 159 Token: oauth2.Token{ 160 AccessToken: "abc", 161 Expiry: clock.Now(ctx).Add(tokenLifetime), 162 }, 163 } 164 hist := make([]int, 600) 165 for s := 0; s < 600; s++ { 166 for i := 0; i < 100; i++ { 167 if TokenExpiresInRnd(ctx, tok, requestedLifetime) { 168 hist[s]++ 169 } 170 tc.Add(10 * time.Millisecond) 171 } 172 } 173 174 // The histogram should have a shape: 175 // * 0 samples until somewhere around 3 min 30 sec 176 // (which is tokenLifetime - requestedLifetime - expiryRandInterval). 177 // * increasingly more non zero samples in the following 178 // expiryRandInterval seconds (3 min 30 sec - 4 min 00 sec). 179 // * 100% samples after tokenLifetime - requestedLifetime (> 4 min). 180 firstNonZero := -1 181 firstFull := -1 182 for i, val := range hist { 183 switch { 184 case val != 0 && firstNonZero == -1: 185 firstNonZero = i 186 case val == 100 && firstFull == -1: 187 firstFull = i 188 } 189 } 190 191 // The first non-zero sample is at 3 min 30 sec (+1, by chance). 192 So(firstNonZero, ShouldEqual, 3*60+30+1) 193 // The first 100% sample is at 4 min (-1, by chance). 194 So(firstFull, ShouldEqual, 4*60-1) 195 // The in-between contains linearly increasing chance of early expiry, as 196 // can totally be seen from this assertion. 197 So(hist[firstNonZero:firstFull], ShouldResemble, []int{ 198 8, 6, 8, 23, 20, 27, 28, 29, 25, 36, 43, 41, 53, 49, 199 49, 57, 59, 62, 69, 69, 76, 72, 82, 73, 85, 83, 93, 93, 200 }) 201 }) 202 } 203 204 func TestIsBadKeyError(t *testing.T) { 205 Convey("Correctly sniffs out bad key error", t, func() { 206 brokenKeyJSON := `{ 207 "type": "service_account", 208 "project_id": "blah", 209 "private_key_id": "zzzzzzzzzzzzzzzzzz", 210 "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhBROKENq\nEpldRN3tNnLoLNy6\n-----END PRIVATE KEY-----\n", 211 "client_email": "blah@blah.iam.gserviceaccount.com", 212 "auth_uri": "https://accounts.google.com/o/oauth2/auth", 213 "token_uri": "https://accounts.google.com/o/oauth2/token", 214 "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs" 215 }` 216 cfg, err := google.JWTConfigFromJSON([]byte(brokenKeyJSON), "scope") 217 So(err, ShouldBeNil) 218 _, err = cfg.TokenSource(context.Background()).Token() 219 So(err, ShouldNotBeNil) 220 So(isBadKeyError(err), ShouldBeTrue) 221 }) 222 }