go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/openid/id_token_method_test.go (about) 1 // Copyright 2020 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 openid 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "net/http" 22 "net/http/httptest" 23 "testing" 24 "time" 25 26 "go.chromium.org/luci/common/clock" 27 "go.chromium.org/luci/common/clock/testclock" 28 29 "go.chromium.org/luci/server/auth" 30 "go.chromium.org/luci/server/auth/authtest" 31 "go.chromium.org/luci/server/auth/signing/signingtest" 32 "go.chromium.org/luci/server/caching" 33 34 . "github.com/smartystreets/goconvey/convey" 35 . "go.chromium.org/luci/common/testing/assertions" 36 ) 37 38 func TestGoogleIDTokenAuthMethod(t *testing.T) { 39 t.Parallel() 40 41 ctx := context.Background() 42 ctx = caching.WithEmptyProcessCache(ctx) 43 ctx = authtest.MockAuthConfig(ctx) 44 ctx, _ = testclock.UseTime(ctx, time.Unix(1442540000, 0)) 45 46 provider := &fakeIdentityProvider{ 47 Signer: signingtest.NewSigner(nil), 48 SigningKeyID: "signing-key", 49 Issuer: "https://issuer.example.com", 50 } 51 provider.start() 52 defer provider.stop() 53 54 const fakeHost = "fake-host.example.com" 55 56 method := GoogleIDTokenAuthMethod{ 57 Audience: []string{"aud1", "aud2"}, 58 AudienceCheck: AudienceMatchesHost, 59 discoveryURL: provider.discoveryURL, 60 } 61 call := func(authHeader string) (*auth.User, error) { 62 req := authtest.NewFakeRequestMetadata() 63 req.FakeHost = fakeHost 64 req.FakeHeader.Set("Authorization", authHeader) 65 u, _, err := method.Authenticate(ctx, req) 66 return u, err 67 } 68 69 Convey("Skipped if no header", t, func() { 70 user, err := call("") 71 So(err, ShouldBeNil) 72 So(user, ShouldBeNil) 73 }) 74 75 Convey("Skipped if not Bearer", t, func() { 76 user, err := call("OAuth zzz") 77 So(err, ShouldBeNil) 78 So(user, ShouldBeNil) 79 }) 80 81 Convey("Not JWT token and SkipNonJWT == false", t, func() { 82 user, err := call("Bearer " + "im-not-a-jwt") 83 So(err, ShouldErrLike, "bad ID token: bad JWT") 84 So(user, ShouldBeNil) 85 }) 86 87 Convey("Not JWT token and SkipNonJWT == true", t, func() { 88 method.SkipNonJWT = true 89 90 user, err := call("Bearer " + "im-not-a-jwt") 91 So(err, ShouldBeNil) 92 So(user, ShouldBeNil) 93 }) 94 95 Convey("Regular user", t, func() { 96 Convey("Happy path", func() { 97 user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{ 98 Iss: provider.Issuer, 99 EmailVerified: true, 100 Sub: "some-sub", 101 Email: "user@example.com", 102 Name: "Some Dude", 103 Picture: "https://picture/url/s64/photo.jpg", 104 Aud: "some-client-id", 105 Iat: clock.Now(ctx).Unix(), 106 Exp: clock.Now(ctx).Add(time.Hour).Unix(), 107 })) 108 So(err, ShouldBeNil) 109 So(user, ShouldResemble, &auth.User{ 110 Identity: "user:user@example.com", 111 Email: "user@example.com", 112 Name: "Some Dude", 113 Picture: "https://picture/url/s64/photo.jpg", 114 ClientID: "some-client-id", 115 }) 116 }) 117 118 Convey("Expired token", func() { 119 _, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{ 120 Iss: provider.Issuer, 121 EmailVerified: true, 122 Sub: "some-sub", 123 Email: "user@example.com", 124 Name: "Some Dude", 125 Picture: "https://picture/url/s64/photo.jpg", 126 Aud: "some-client-id", 127 Iat: clock.Now(ctx).Add(-2 * time.Hour).Unix(), 128 Exp: clock.Now(ctx).Add(-1 * time.Hour).Unix(), 129 })) 130 So(err, ShouldErrLike, "bad ID token: expired") 131 }) 132 }) 133 134 Convey("Service account", t, func() { 135 Convey("Happy path using Audience field", func() { 136 user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{ 137 Iss: provider.Issuer, 138 EmailVerified: true, 139 Sub: "some-sub", 140 Email: "example@example.gserviceaccount.com", 141 Aud: "aud2", 142 Iat: clock.Now(ctx).Unix(), 143 Exp: clock.Now(ctx).Add(time.Hour).Unix(), 144 })) 145 So(err, ShouldBeNil) 146 So(user, ShouldResemble, &auth.User{ 147 Identity: "user:example@example.gserviceaccount.com", 148 Email: "example@example.gserviceaccount.com", 149 }) 150 }) 151 152 Convey("Happy path using AudienceCheck field (direct host hit)", func() { 153 user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{ 154 Iss: provider.Issuer, 155 EmailVerified: true, 156 Sub: "some-sub", 157 Email: "example@example.gserviceaccount.com", 158 Aud: "https://" + fakeHost, 159 Iat: clock.Now(ctx).Unix(), 160 Exp: clock.Now(ctx).Add(time.Hour).Unix(), 161 })) 162 So(err, ShouldBeNil) 163 So(user, ShouldResemble, &auth.User{ 164 Identity: "user:example@example.gserviceaccount.com", 165 Email: "example@example.gserviceaccount.com", 166 }) 167 }) 168 169 Convey("Happy path using AudienceCheck field (host prefix hit)", func() { 170 user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{ 171 Iss: provider.Issuer, 172 EmailVerified: true, 173 Sub: "some-sub", 174 Email: "example@example.gserviceaccount.com", 175 Aud: "https://" + fakeHost + "/some/path", 176 Iat: clock.Now(ctx).Unix(), 177 Exp: clock.Now(ctx).Add(time.Hour).Unix(), 178 })) 179 So(err, ShouldBeNil) 180 So(user, ShouldResemble, &auth.User{ 181 Identity: "user:example@example.gserviceaccount.com", 182 Email: "example@example.gserviceaccount.com", 183 }) 184 }) 185 186 Convey("Happy path using OAuth2 client ID", func() { 187 user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{ 188 Iss: provider.Issuer, 189 EmailVerified: true, 190 Sub: "some-sub", 191 Email: "example@example.gserviceaccount.com", 192 Aud: "blah.apps.googleusercontent.com", 193 Iat: clock.Now(ctx).Unix(), 194 Exp: clock.Now(ctx).Add(time.Hour).Unix(), 195 })) 196 So(err, ShouldBeNil) 197 So(user, ShouldResemble, &auth.User{ 198 Identity: "user:example@example.gserviceaccount.com", 199 Email: "example@example.gserviceaccount.com", 200 ClientID: "blah.apps.googleusercontent.com", 201 }) 202 }) 203 204 Convey("Unknown audience", func() { 205 _, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{ 206 Iss: provider.Issuer, 207 EmailVerified: true, 208 Sub: "some-sub", 209 Email: "example@example.gserviceaccount.com", 210 Aud: "what is this", 211 Iat: clock.Now(ctx).Unix(), 212 Exp: clock.Now(ctx).Add(time.Hour).Unix(), 213 })) 214 So(err, ShouldEqual, auth.ErrBadAudience) 215 }) 216 }) 217 } 218 219 type fakeIdentityProvider struct { 220 Signer *signingtest.Signer 221 SigningKeyID string 222 Issuer string 223 224 ts *httptest.Server 225 discoveryURL string 226 } 227 228 func (f *fakeIdentityProvider) start() { 229 jwks := jwksForTest(f.SigningKeyID, &f.Signer.KeyForTest().PublicKey) 230 231 // Serve the fake discovery document and singing keys. 232 f.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 233 switch r.URL.Path { 234 case "/discovery": 235 w.Write([]byte(fmt.Sprintf(`{ 236 "issuer": "%s", 237 "jwks_uri": "%s/jwks" 238 }`, f.Issuer, f.ts.URL))) 239 case "/jwks": 240 json.NewEncoder(w).Encode(jwks) 241 default: 242 http.Error(w, "Not found", http.StatusNotFound) 243 } 244 })) 245 246 f.discoveryURL = f.ts.URL + "/discovery" 247 } 248 249 func (f *fakeIdentityProvider) stop() { 250 f.ts.Close() 251 } 252 253 func (f *fakeIdentityProvider) mintIDToken(ctx context.Context, tok IDToken) string { 254 return idTokenForTest(ctx, &tok, f.SigningKeyID, f.Signer) 255 }