github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/client/auth/session_test.go (about) 1 package auth 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "net/http" 7 "net/http/httptest" 8 "net/url" 9 "testing" 10 "time" 11 12 "github.com/docker/distribution/registry/client/transport" 13 "github.com/docker/distribution/testutil" 14 ) 15 16 // An implementation of clock for providing fake time data. 17 type fakeClock struct { 18 current time.Time 19 } 20 21 // Now implements clock 22 func (fc *fakeClock) Now() time.Time { return fc.current } 23 24 func testServer(rrm testutil.RequestResponseMap) (string, func()) { 25 h := testutil.NewHandler(rrm) 26 s := httptest.NewServer(h) 27 return s.URL, s.Close 28 } 29 30 type testAuthenticationWrapper struct { 31 headers http.Header 32 authCheck func(string) bool 33 next http.Handler 34 } 35 36 func (w *testAuthenticationWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 37 auth := r.Header.Get("Authorization") 38 if auth == "" || !w.authCheck(auth) { 39 h := rw.Header() 40 for k, values := range w.headers { 41 h[k] = values 42 } 43 rw.WriteHeader(http.StatusUnauthorized) 44 return 45 } 46 w.next.ServeHTTP(rw, r) 47 } 48 49 func testServerWithAuth(rrm testutil.RequestResponseMap, authenticate string, authCheck func(string) bool) (string, func()) { 50 h := testutil.NewHandler(rrm) 51 wrapper := &testAuthenticationWrapper{ 52 53 headers: http.Header(map[string][]string{ 54 "X-API-Version": {"registry/2.0"}, 55 "X-Multi-API-Version": {"registry/2.0", "registry/2.1", "trust/1.0"}, 56 "WWW-Authenticate": {authenticate}, 57 }), 58 authCheck: authCheck, 59 next: h, 60 } 61 62 s := httptest.NewServer(wrapper) 63 return s.URL, s.Close 64 } 65 66 // ping pings the provided endpoint to determine its required authorization challenges. 67 // If a version header is provided, the versions will be returned. 68 func ping(manager ChallengeManager, endpoint, versionHeader string) ([]APIVersion, error) { 69 resp, err := http.Get(endpoint) 70 if err != nil { 71 return nil, err 72 } 73 defer resp.Body.Close() 74 75 if err := manager.AddResponse(resp); err != nil { 76 return nil, err 77 } 78 79 return APIVersions(resp, versionHeader), err 80 } 81 82 type testCredentialStore struct { 83 username string 84 password string 85 } 86 87 func (tcs *testCredentialStore) Basic(*url.URL) (string, string) { 88 return tcs.username, tcs.password 89 } 90 91 func TestEndpointAuthorizeToken(t *testing.T) { 92 service := "localhost.localdomain" 93 repo1 := "some/registry" 94 repo2 := "other/registry" 95 scope1 := fmt.Sprintf("repository:%s:pull,push", repo1) 96 scope2 := fmt.Sprintf("repository:%s:pull,push", repo2) 97 tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ 98 { 99 Request: testutil.Request{ 100 Method: "GET", 101 Route: fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope1), service), 102 }, 103 Response: testutil.Response{ 104 StatusCode: http.StatusOK, 105 Body: []byte(`{"token":"statictoken"}`), 106 }, 107 }, 108 { 109 Request: testutil.Request{ 110 Method: "GET", 111 Route: fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope2), service), 112 }, 113 Response: testutil.Response{ 114 StatusCode: http.StatusOK, 115 Body: []byte(`{"token":"badtoken"}`), 116 }, 117 }, 118 }) 119 te, tc := testServer(tokenMap) 120 defer tc() 121 122 m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ 123 { 124 Request: testutil.Request{ 125 Method: "GET", 126 Route: "/v2/hello", 127 }, 128 Response: testutil.Response{ 129 StatusCode: http.StatusAccepted, 130 }, 131 }, 132 }) 133 134 authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) 135 validCheck := func(a string) bool { 136 return a == "Bearer statictoken" 137 } 138 e, c := testServerWithAuth(m, authenicate, validCheck) 139 defer c() 140 141 challengeManager1 := NewSimpleChallengeManager() 142 versions, err := ping(challengeManager1, e+"/v2/", "x-api-version") 143 if err != nil { 144 t.Fatal(err) 145 } 146 if len(versions) != 1 { 147 t.Fatalf("Unexpected version count: %d, expected 1", len(versions)) 148 } 149 if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { 150 t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) 151 } 152 transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandler(nil, nil, repo1, "pull", "push"))) 153 client := &http.Client{Transport: transport1} 154 155 req, _ := http.NewRequest("GET", e+"/v2/hello", nil) 156 resp, err := client.Do(req) 157 if err != nil { 158 t.Fatalf("Error sending get request: %s", err) 159 } 160 161 if resp.StatusCode != http.StatusAccepted { 162 t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) 163 } 164 165 badCheck := func(a string) bool { 166 return a == "Bearer statictoken" 167 } 168 e2, c2 := testServerWithAuth(m, authenicate, badCheck) 169 defer c2() 170 171 challengeManager2 := NewSimpleChallengeManager() 172 versions, err = ping(challengeManager2, e+"/v2/", "x-multi-api-version") 173 if err != nil { 174 t.Fatal(err) 175 } 176 if len(versions) != 3 { 177 t.Fatalf("Unexpected version count: %d, expected 3", len(versions)) 178 } 179 if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { 180 t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) 181 } 182 if check := (APIVersion{Type: "registry", Version: "2.1"}); versions[1] != check { 183 t.Fatalf("Unexpected api version: %#v, expected %#v", versions[1], check) 184 } 185 if check := (APIVersion{Type: "trust", Version: "1.0"}); versions[2] != check { 186 t.Fatalf("Unexpected api version: %#v, expected %#v", versions[2], check) 187 } 188 transport2 := transport.NewTransport(nil, NewAuthorizer(challengeManager2, NewTokenHandler(nil, nil, repo2, "pull", "push"))) 189 client2 := &http.Client{Transport: transport2} 190 191 req, _ = http.NewRequest("GET", e2+"/v2/hello", nil) 192 resp, err = client2.Do(req) 193 if err != nil { 194 t.Fatalf("Error sending get request: %s", err) 195 } 196 197 if resp.StatusCode != http.StatusUnauthorized { 198 t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized) 199 } 200 } 201 202 func basicAuth(username, password string) string { 203 auth := username + ":" + password 204 return base64.StdEncoding.EncodeToString([]byte(auth)) 205 } 206 207 func TestEndpointAuthorizeTokenBasic(t *testing.T) { 208 service := "localhost.localdomain" 209 repo := "some/fun/registry" 210 scope := fmt.Sprintf("repository:%s:pull,push", repo) 211 username := "tokenuser" 212 password := "superSecretPa$$word" 213 214 tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ 215 { 216 Request: testutil.Request{ 217 Method: "GET", 218 Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), 219 }, 220 Response: testutil.Response{ 221 StatusCode: http.StatusOK, 222 Body: []byte(`{"access_token":"statictoken"}`), 223 }, 224 }, 225 }) 226 227 authenicate1 := fmt.Sprintf("Basic realm=localhost") 228 basicCheck := func(a string) bool { 229 return a == fmt.Sprintf("Basic %s", basicAuth(username, password)) 230 } 231 te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck) 232 defer tc() 233 234 m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ 235 { 236 Request: testutil.Request{ 237 Method: "GET", 238 Route: "/v2/hello", 239 }, 240 Response: testutil.Response{ 241 StatusCode: http.StatusAccepted, 242 }, 243 }, 244 }) 245 246 authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) 247 bearerCheck := func(a string) bool { 248 return a == "Bearer statictoken" 249 } 250 e, c := testServerWithAuth(m, authenicate2, bearerCheck) 251 defer c() 252 253 creds := &testCredentialStore{ 254 username: username, 255 password: password, 256 } 257 258 challengeManager := NewSimpleChallengeManager() 259 _, err := ping(challengeManager, e+"/v2/", "") 260 if err != nil { 261 t.Fatal(err) 262 } 263 transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewTokenHandler(nil, creds, repo, "pull", "push"), NewBasicHandler(creds))) 264 client := &http.Client{Transport: transport1} 265 266 req, _ := http.NewRequest("GET", e+"/v2/hello", nil) 267 resp, err := client.Do(req) 268 if err != nil { 269 t.Fatalf("Error sending get request: %s", err) 270 } 271 272 if resp.StatusCode != http.StatusAccepted { 273 t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) 274 } 275 } 276 277 func TestEndpointAuthorizeTokenBasicWithExpiresIn(t *testing.T) { 278 service := "localhost.localdomain" 279 repo := "some/fun/registry" 280 scope := fmt.Sprintf("repository:%s:pull,push", repo) 281 username := "tokenuser" 282 password := "superSecretPa$$word" 283 284 tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ 285 { 286 Request: testutil.Request{ 287 Method: "GET", 288 Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), 289 }, 290 Response: testutil.Response{ 291 StatusCode: http.StatusOK, 292 Body: []byte(`{"token":"statictoken", "expires_in": 3001}`), 293 }, 294 }, 295 { 296 Request: testutil.Request{ 297 Method: "GET", 298 Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), 299 }, 300 Response: testutil.Response{ 301 StatusCode: http.StatusOK, 302 Body: []byte(`{"access_token":"statictoken", "expires_in": 3001}`), 303 }, 304 }, 305 }) 306 307 authenicate1 := fmt.Sprintf("Basic realm=localhost") 308 tokenExchanges := 0 309 basicCheck := func(a string) bool { 310 tokenExchanges = tokenExchanges + 1 311 return a == fmt.Sprintf("Basic %s", basicAuth(username, password)) 312 } 313 te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck) 314 defer tc() 315 316 m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ 317 { 318 Request: testutil.Request{ 319 Method: "GET", 320 Route: "/v2/hello", 321 }, 322 Response: testutil.Response{ 323 StatusCode: http.StatusAccepted, 324 }, 325 }, 326 { 327 Request: testutil.Request{ 328 Method: "GET", 329 Route: "/v2/hello", 330 }, 331 Response: testutil.Response{ 332 StatusCode: http.StatusAccepted, 333 }, 334 }, 335 { 336 Request: testutil.Request{ 337 Method: "GET", 338 Route: "/v2/hello", 339 }, 340 Response: testutil.Response{ 341 StatusCode: http.StatusAccepted, 342 }, 343 }, 344 { 345 Request: testutil.Request{ 346 Method: "GET", 347 Route: "/v2/hello", 348 }, 349 Response: testutil.Response{ 350 StatusCode: http.StatusAccepted, 351 }, 352 }, 353 { 354 Request: testutil.Request{ 355 Method: "GET", 356 Route: "/v2/hello", 357 }, 358 Response: testutil.Response{ 359 StatusCode: http.StatusAccepted, 360 }, 361 }, 362 }) 363 364 authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) 365 bearerCheck := func(a string) bool { 366 return a == "Bearer statictoken" 367 } 368 e, c := testServerWithAuth(m, authenicate2, bearerCheck) 369 defer c() 370 371 creds := &testCredentialStore{ 372 username: username, 373 password: password, 374 } 375 376 challengeManager := NewSimpleChallengeManager() 377 _, err := ping(challengeManager, e+"/v2/", "") 378 if err != nil { 379 t.Fatal(err) 380 } 381 clock := &fakeClock{current: time.Now()} 382 transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, newTokenHandler(nil, creds, clock, repo, "pull", "push"), NewBasicHandler(creds))) 383 client := &http.Client{Transport: transport1} 384 385 // First call should result in a token exchange 386 // Subsequent calls should recycle the token from the first request, until the expiration has lapsed. 387 timeIncrement := 1000 * time.Second 388 for i := 0; i < 4; i++ { 389 req, _ := http.NewRequest("GET", e+"/v2/hello", nil) 390 resp, err := client.Do(req) 391 if err != nil { 392 t.Fatalf("Error sending get request: %s", err) 393 } 394 if resp.StatusCode != http.StatusAccepted { 395 t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) 396 } 397 if tokenExchanges != 1 { 398 t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i) 399 } 400 clock.current = clock.current.Add(timeIncrement) 401 } 402 403 // After we've exceeded the expiration, we should see a second token exchange. 404 req, _ := http.NewRequest("GET", e+"/v2/hello", nil) 405 resp, err := client.Do(req) 406 if err != nil { 407 t.Fatalf("Error sending get request: %s", err) 408 } 409 if resp.StatusCode != http.StatusAccepted { 410 t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) 411 } 412 if tokenExchanges != 2 { 413 t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges) 414 } 415 } 416 417 func TestEndpointAuthorizeTokenBasicWithExpiresInAndIssuedAt(t *testing.T) { 418 service := "localhost.localdomain" 419 repo := "some/fun/registry" 420 scope := fmt.Sprintf("repository:%s:pull,push", repo) 421 username := "tokenuser" 422 password := "superSecretPa$$word" 423 424 // This test sets things up such that the token was issued one increment 425 // earlier than its sibling in TestEndpointAuthorizeTokenBasicWithExpiresIn. 426 // This will mean that the token expires after 3 increments instead of 4. 427 clock := &fakeClock{current: time.Now()} 428 timeIncrement := 1000 * time.Second 429 firstIssuedAt := clock.Now() 430 clock.current = clock.current.Add(timeIncrement) 431 secondIssuedAt := clock.current.Add(2 * timeIncrement) 432 tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ 433 { 434 Request: testutil.Request{ 435 Method: "GET", 436 Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), 437 }, 438 Response: testutil.Response{ 439 StatusCode: http.StatusOK, 440 Body: []byte(`{"token":"statictoken", "issued_at": "` + firstIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`), 441 }, 442 }, 443 { 444 Request: testutil.Request{ 445 Method: "GET", 446 Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), 447 }, 448 Response: testutil.Response{ 449 StatusCode: http.StatusOK, 450 Body: []byte(`{"access_token":"statictoken", "issued_at": "` + secondIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`), 451 }, 452 }, 453 }) 454 455 authenicate1 := fmt.Sprintf("Basic realm=localhost") 456 tokenExchanges := 0 457 basicCheck := func(a string) bool { 458 tokenExchanges = tokenExchanges + 1 459 return a == fmt.Sprintf("Basic %s", basicAuth(username, password)) 460 } 461 te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck) 462 defer tc() 463 464 m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ 465 { 466 Request: testutil.Request{ 467 Method: "GET", 468 Route: "/v2/hello", 469 }, 470 Response: testutil.Response{ 471 StatusCode: http.StatusAccepted, 472 }, 473 }, 474 { 475 Request: testutil.Request{ 476 Method: "GET", 477 Route: "/v2/hello", 478 }, 479 Response: testutil.Response{ 480 StatusCode: http.StatusAccepted, 481 }, 482 }, 483 { 484 Request: testutil.Request{ 485 Method: "GET", 486 Route: "/v2/hello", 487 }, 488 Response: testutil.Response{ 489 StatusCode: http.StatusAccepted, 490 }, 491 }, 492 { 493 Request: testutil.Request{ 494 Method: "GET", 495 Route: "/v2/hello", 496 }, 497 Response: testutil.Response{ 498 StatusCode: http.StatusAccepted, 499 }, 500 }, 501 }) 502 503 authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) 504 bearerCheck := func(a string) bool { 505 return a == "Bearer statictoken" 506 } 507 e, c := testServerWithAuth(m, authenicate2, bearerCheck) 508 defer c() 509 510 creds := &testCredentialStore{ 511 username: username, 512 password: password, 513 } 514 515 challengeManager := NewSimpleChallengeManager() 516 _, err := ping(challengeManager, e+"/v2/", "") 517 if err != nil { 518 t.Fatal(err) 519 } 520 transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, newTokenHandler(nil, creds, clock, repo, "pull", "push"), NewBasicHandler(creds))) 521 client := &http.Client{Transport: transport1} 522 523 // First call should result in a token exchange 524 // Subsequent calls should recycle the token from the first request, until the expiration has lapsed. 525 // We shaved one increment off of the equivalent logic in TestEndpointAuthorizeTokenBasicWithExpiresIn 526 // so this loop should have one fewer iteration. 527 for i := 0; i < 3; i++ { 528 req, _ := http.NewRequest("GET", e+"/v2/hello", nil) 529 resp, err := client.Do(req) 530 if err != nil { 531 t.Fatalf("Error sending get request: %s", err) 532 } 533 if resp.StatusCode != http.StatusAccepted { 534 t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) 535 } 536 if tokenExchanges != 1 { 537 t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i) 538 } 539 clock.current = clock.current.Add(timeIncrement) 540 } 541 542 // After we've exceeded the expiration, we should see a second token exchange. 543 req, _ := http.NewRequest("GET", e+"/v2/hello", nil) 544 resp, err := client.Do(req) 545 if err != nil { 546 t.Fatalf("Error sending get request: %s", err) 547 } 548 if resp.StatusCode != http.StatusAccepted { 549 t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) 550 } 551 if tokenExchanges != 2 { 552 t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges) 553 } 554 } 555 556 func TestEndpointAuthorizeBasic(t *testing.T) { 557 m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ 558 { 559 Request: testutil.Request{ 560 Method: "GET", 561 Route: "/v2/hello", 562 }, 563 Response: testutil.Response{ 564 StatusCode: http.StatusAccepted, 565 }, 566 }, 567 }) 568 569 username := "user1" 570 password := "funSecretPa$$word" 571 authenicate := fmt.Sprintf("Basic realm=localhost") 572 validCheck := func(a string) bool { 573 return a == fmt.Sprintf("Basic %s", basicAuth(username, password)) 574 } 575 e, c := testServerWithAuth(m, authenicate, validCheck) 576 defer c() 577 creds := &testCredentialStore{ 578 username: username, 579 password: password, 580 } 581 582 challengeManager := NewSimpleChallengeManager() 583 _, err := ping(challengeManager, e+"/v2/", "") 584 if err != nil { 585 t.Fatal(err) 586 } 587 transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewBasicHandler(creds))) 588 client := &http.Client{Transport: transport1} 589 590 req, _ := http.NewRequest("GET", e+"/v2/hello", nil) 591 resp, err := client.Do(req) 592 if err != nil { 593 t.Fatalf("Error sending get request: %s", err) 594 } 595 596 if resp.StatusCode != http.StatusAccepted { 597 t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) 598 } 599 }