go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/deprecated/cookie_method_test.go (about) 1 // Copyright 2015 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 deprecated 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "net/http" 22 "net/http/httptest" 23 "net/url" 24 "testing" 25 "time" 26 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/clock/testclock" 29 30 "go.chromium.org/luci/server/auth" 31 "go.chromium.org/luci/server/auth/authtest" 32 "go.chromium.org/luci/server/auth/openid" 33 "go.chromium.org/luci/server/auth/signing/signingtest" 34 "go.chromium.org/luci/server/caching" 35 "go.chromium.org/luci/server/router" 36 "go.chromium.org/luci/server/secrets" 37 "go.chromium.org/luci/server/secrets/testsecrets" 38 "go.chromium.org/luci/server/settings" 39 40 . "github.com/smartystreets/goconvey/convey" 41 . "go.chromium.org/luci/common/testing/assertions" 42 ) 43 44 func TestFullFlow(t *testing.T) { 45 t.Parallel() 46 47 Convey("with test context", t, func(c C) { 48 ctx := context.Background() 49 ctx = caching.WithEmptyProcessCache(ctx) 50 ctx = authtest.MockAuthConfig(ctx) 51 ctx = settings.Use(ctx, settings.New(&settings.MemoryStorage{})) 52 ctx, _ = testclock.UseTime(ctx, time.Unix(1442540000, 0)) 53 ctx = secrets.Use(ctx, &testsecrets.Store{}) 54 55 // Prepare the signing keys and the ID token. 56 const signingKeyID = "signing-key" 57 const clientID = "client_id" 58 signer := signingtest.NewSigner(nil) 59 idToken := idTokenForTest(ctx, &openid.IDToken{ 60 Iss: "https://issuer.example.com", 61 EmailVerified: true, 62 Sub: "user_id_sub", 63 Email: "user@example.com", 64 Name: "Some Dude", 65 Picture: "https://picture/url/s64/photo.jpg", 66 Aud: clientID, 67 Iat: clock.Now(ctx).Unix(), 68 Exp: clock.Now(ctx).Add(time.Hour).Unix(), 69 }, signingKeyID, signer) 70 jwks := jwksForTest(signingKeyID, &signer.KeyForTest().PublicKey) 71 72 var ts *httptest.Server 73 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 74 switch r.URL.Path { 75 76 case "/discovery": 77 w.Write([]byte(fmt.Sprintf(`{ 78 "issuer": "https://issuer.example.com", 79 "authorization_endpoint": "%s/authorization", 80 "token_endpoint": "%s/token", 81 "jwks_uri": "%s/jwks" 82 }`, ts.URL, ts.URL, ts.URL))) 83 84 case "/jwks": 85 json.NewEncoder(w).Encode(jwks) 86 87 case "/token": 88 c.So(r.ParseForm(), ShouldBeNil) 89 c.So(r.Form, ShouldResemble, url.Values{ 90 "redirect_uri": {"http://fake/redirect"}, 91 "client_id": {"client_id"}, 92 "client_secret": {"client_secret"}, 93 "code": {"omg_auth_code"}, 94 "grant_type": {"authorization_code"}, 95 }) 96 w.Write([]byte(fmt.Sprintf(`{"id_token": "%s"}`, idToken))) 97 98 default: 99 http.Error(w, "Not found", http.StatusNotFound) 100 } 101 })) 102 defer ts.Close() 103 104 cfg := Settings{ 105 DiscoveryURL: ts.URL + "/discovery", 106 ClientID: clientID, 107 ClientSecret: "client_secret", 108 RedirectURI: "http://fake/redirect", 109 } 110 So(settings.Set(ctx, SettingsKey, &cfg), ShouldBeNil) 111 112 method := CookieAuthMethod{ 113 SessionStore: &MemorySessionStore{}, 114 Insecure: true, 115 IncompatibleCookies: []string{"wrong_cookie"}, 116 } 117 118 Convey("Full flow", func() { 119 So(method.Warmup(ctx), ShouldBeNil) 120 121 // Generate login URL. 122 loginURL, err := method.LoginURL(ctx, "/destination") 123 So(err, ShouldBeNil) 124 So(loginURL, ShouldEqual, "/auth/openid/login?r=%2Fdestination") 125 126 // "Visit" login URL. 127 req, err := http.NewRequestWithContext(ctx, "GET", "http://fake"+loginURL, nil) 128 So(err, ShouldBeNil) 129 rec := httptest.NewRecorder() 130 method.loginHandler(&router.Context{ 131 Writer: rec, 132 Request: req, 133 }) 134 135 // It asks us to visit authorizarion endpoint. 136 So(rec.Code, ShouldEqual, http.StatusFound) 137 parsed, err := url.Parse(rec.Header().Get("Location")) 138 So(err, ShouldBeNil) 139 So(parsed.Host, ShouldEqual, ts.URL[len("http://"):]) 140 So(parsed.Path, ShouldEqual, "/authorization") 141 So(parsed.Query(), ShouldResemble, url.Values{ 142 "client_id": {"client_id"}, 143 "redirect_uri": {"http://fake/redirect"}, 144 "response_type": {"code"}, 145 "scope": {"openid email profile"}, 146 "prompt": {"select_account"}, 147 "state": { 148 "AXsiX2kiOiIxNDQyNTQwMDAwMDAwIiwiZGVzdF91cmwiOiIvZGVzdGluYXRpb24iLC" + 149 "Job3N0X3VybCI6ImZha2UifUFtzG6wPbuvHG2mY_Wf6eQ_Eiu7n3_Tf6GmRcse1g" + 150 "YE", 151 }, 152 }) 153 154 // Pretend we've done it. OpenID redirects user's browser to callback URI. 155 // `callbackHandler` will call /token and /jwks fake endpoints exposed 156 // by testserver. 157 callbackParams := url.Values{} 158 callbackParams.Set("code", "omg_auth_code") 159 callbackParams.Set("state", parsed.Query().Get("state")) 160 req, err = http.NewRequestWithContext(ctx, "GET", "http://fake/redirect?"+callbackParams.Encode(), nil) 161 So(err, ShouldBeNil) 162 rec = httptest.NewRecorder() 163 method.callbackHandler(&router.Context{ 164 Writer: rec, 165 Request: req, 166 }) 167 168 // We should be redirected to the login page, with session cookie set. 169 expectedCookie := "oid_session=AXsiX2kiOiIxNDQyNTQwMDAwMDAwIiwic2lkIjoi" + 170 "dXNlcl9pZF9zdWIvMSJ9PmRzaOv-mS0PMHkve897iiELNmpiLi_j3ICG1VKuNCs" 171 So(rec.Code, ShouldEqual, http.StatusFound) 172 So(rec.Header().Get("Location"), ShouldEqual, "/destination") 173 So(rec.Header().Get("Set-Cookie"), ShouldEqual, 174 expectedCookie+"; Path=/; Expires=Sun, 18 Oct 2015 01:18:20 GMT; Max-Age=2591100; HttpOnly") 175 176 // Use the cookie to authenticate some call. 177 req, err = http.NewRequest("GET", "http://fake/something", nil) 178 So(err, ShouldBeNil) 179 req.Header.Add("Cookie", expectedCookie) 180 user, session, err := method.Authenticate(ctx, auth.RequestMetadataForHTTP(req)) 181 So(err, ShouldBeNil) 182 So(user, ShouldResemble, &auth.User{ 183 Identity: "user:user@example.com", 184 Email: "user@example.com", 185 Name: "Some Dude", 186 Picture: "https://picture/url/s64/photo.jpg", 187 }) 188 So(session, ShouldBeNil) 189 190 // Now generate URL to and visit logout page. 191 logoutURL, err := method.LogoutURL(ctx, "/another_destination") 192 So(err, ShouldBeNil) 193 So(logoutURL, ShouldEqual, "/auth/openid/logout?r=%2Fanother_destination") 194 req, err = http.NewRequestWithContext(ctx, "GET", "http://fake"+logoutURL, nil) 195 So(err, ShouldBeNil) 196 req.Header.Add("Cookie", expectedCookie) 197 rec = httptest.NewRecorder() 198 method.logoutHandler(&router.Context{ 199 Writer: rec, 200 Request: req, 201 }) 202 203 // Should be redirected to destination with the cookie killed. 204 So(rec.Code, ShouldEqual, http.StatusFound) 205 So(rec.Header().Get("Location"), ShouldEqual, "/another_destination") 206 So(rec.Header().Get("Set-Cookie"), ShouldEqual, 207 "oid_session=deleted; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0") 208 }) 209 }) 210 } 211 212 func TestCallbackHandleEdgeCases(t *testing.T) { 213 Convey("with test context", t, func(c C) { 214 ctx := context.Background() 215 ctx = settings.Use(ctx, settings.New(&settings.MemoryStorage{})) 216 ctx, _ = testclock.UseTime(ctx, time.Unix(1442540000, 0)) 217 ctx = secrets.Use(ctx, &testsecrets.Store{}) 218 219 method := CookieAuthMethod{SessionStore: &MemorySessionStore{}} 220 221 call := func(query map[string]string) *httptest.ResponseRecorder { 222 q := url.Values{} 223 for k, v := range query { 224 q.Add(k, v) 225 } 226 req, err := http.NewRequestWithContext(ctx, "GET", "/auth/openid/callback?"+q.Encode(), nil) 227 c.So(err, ShouldBeNil) 228 req.Host = "fake.com" 229 rec := httptest.NewRecorder() 230 method.callbackHandler(&router.Context{ 231 Writer: rec, 232 Request: req, 233 }) 234 return rec 235 } 236 237 Convey("handles 'error'", func() { 238 rec := call(map[string]string{"error": "Omg, error"}) 239 So(rec.Code, ShouldEqual, 400) 240 So(rec.Body.String(), ShouldEqual, "OpenID login error: Omg, error\n") 241 }) 242 243 Convey("handles no 'code'", func() { 244 rec := call(map[string]string{}) 245 So(rec.Code, ShouldEqual, 400) 246 So(rec.Body.String(), ShouldEqual, "Missing 'code' parameter\n") 247 }) 248 249 Convey("handles no 'state'", func() { 250 rec := call(map[string]string{"code": "123"}) 251 So(rec.Code, ShouldEqual, 400) 252 So(rec.Body.String(), ShouldEqual, "Missing 'state' parameter\n") 253 }) 254 255 Convey("handles bad 'state'", func() { 256 rec := call(map[string]string{"code": "123", "state": "garbage"}) 257 So(rec.Code, ShouldEqual, 400) 258 So(rec.Body.String(), ShouldEqual, "Failed to validate 'state' token\n") 259 }) 260 261 Convey("handles redirect to another host", func() { 262 state := map[string]string{ 263 "dest_url": "/", 264 "host_url": "non-default.fake.com", 265 } 266 stateTok, err := openIDStateToken.Generate(ctx, nil, state, 0) 267 So(err, ShouldBeNil) 268 269 rec := call(map[string]string{"code": "123", "state": stateTok}) 270 So(rec.Code, ShouldEqual, 302) 271 So(rec.Header().Get("Location"), ShouldEqual, 272 "https://non-default.fake.com/auth/openid/callback?"+ 273 "code=123&state=AXsiX2kiOiIxNDQyNTQwMDAwMDAwIiwiZGVzdF91cmwiOiIvIiw"+ 274 "iaG9zdF91cmwiOiJub24tZGVmYXVsdC5mYWtlLmNvbSJ92y0UJtCrN2qGYbcbCiZsV"+ 275 "9OdFEa3zAauzz4lmwPJLwI") 276 }) 277 }) 278 } 279 280 func TestNotConfigured(t *testing.T) { 281 Convey("Returns ErrNotConfigured is on SessionStore", t, func() { 282 ctx := context.Background() 283 method := CookieAuthMethod{} 284 285 _, err := method.LoginURL(ctx, "/") 286 So(err, ShouldEqual, ErrNotConfigured) 287 288 _, err = method.LogoutURL(ctx, "/") 289 So(err, ShouldEqual, ErrNotConfigured) 290 291 _, _, err = method.Authenticate(ctx, authtest.NewFakeRequestMetadata()) 292 So(err, ShouldEqual, ErrNotConfigured) 293 }) 294 } 295 296 func TestNormalizeURL(t *testing.T) { 297 Convey("Normalizes good URLs", t, func(ctx C) { 298 cases := []struct { 299 in string 300 out string 301 }{ 302 {"/", "/"}, 303 {"/?asd=def#blah", "/?asd=def#blah"}, 304 {"/abc/def", "/abc/def"}, 305 {"/blah//abc///def/", "/blah/abc/def/"}, 306 {"/blah/..//./abc/", "/abc/"}, 307 {"/abc/%2F/def", "/abc/def"}, 308 } 309 for _, c := range cases { 310 out, err := normalizeURL(c.in) 311 if err != nil { 312 ctx.Printf("Failed while checking %q\n", c.in) 313 So(err, ShouldBeNil) 314 } 315 So(out, ShouldEqual, c.out) 316 } 317 }) 318 319 Convey("Rejects bad URLs", t, func(ctx C) { 320 cases := []string{ 321 "", 322 "//", 323 "///", 324 "://", 325 ":", 326 "http://another/abc/def", 327 "abc/def", 328 "//host.example.com", 329 } 330 for _, c := range cases { 331 _, err := normalizeURL(c) 332 if err == nil { 333 ctx.Printf("Didn't fail while testing %q\n", c) 334 } 335 So(err, ShouldNotBeNil) 336 } 337 }) 338 } 339 340 func TestBadDestinationURLs(t *testing.T) { 341 Convey("Rejects bad destination URLs", t, func() { 342 ctx := context.Background() 343 method := CookieAuthMethod{SessionStore: &MemorySessionStore{}} 344 345 _, err := method.LoginURL(ctx, "http://somesite") 346 So(err, ShouldErrLike, "openid: dest URL in LoginURL or LogoutURL must be relative") 347 348 _, err = method.LogoutURL(ctx, "http://somesite") 349 So(err, ShouldErrLike, "openid: dest URL in LoginURL or LogoutURL must be relative") 350 }) 351 }