github.com/mook-as/cf-cli@v7.0.0-beta.28.0.20200120190804-b91c115fae48+incompatible/cf/api/authentication/authentication_test.go (about) 1 package authentication_test 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "net/http" 7 "net/http/httptest" 8 "time" 9 10 "code.cloudfoundry.org/cli/cf/configuration/coreconfig" 11 "code.cloudfoundry.org/cli/cf/net" 12 "code.cloudfoundry.org/cli/cf/terminal/terminalfakes" 13 testconfig "code.cloudfoundry.org/cli/cf/util/testhelpers/configuration" 14 testnet "code.cloudfoundry.org/cli/cf/util/testhelpers/net" 15 16 . "code.cloudfoundry.org/cli/cf/api/authentication" 17 "code.cloudfoundry.org/cli/cf/trace/tracefakes" 18 . "code.cloudfoundry.org/cli/cf/util/testhelpers/matchers" 19 . "github.com/onsi/ginkgo" 20 . "github.com/onsi/gomega" 21 "github.com/onsi/gomega/ghttp" 22 ) 23 24 var testAccessToken = testconfig.BuildTokenString(time.Now()) 25 26 var _ = Describe("AuthenticationRepository", func() { 27 Describe("legacy tests", func() { 28 var ( 29 gateway net.Gateway 30 testServer *httptest.Server 31 handler *testnet.TestHandler 32 config coreconfig.ReadWriter 33 auth Repository 34 dumper net.RequestDumper 35 fakePrinter *tracefakes.FakePrinter 36 ) 37 38 BeforeEach(func() { 39 config = testconfig.NewRepository() 40 fakePrinter = new(tracefakes.FakePrinter) 41 gateway = net.NewUAAGateway(config, new(terminalfakes.FakeUI), fakePrinter, "") 42 dumper = net.NewRequestDumper(fakePrinter) 43 auth = NewUAARepository(gateway, config, dumper) 44 }) 45 46 AfterEach(func() { 47 testServer.Close() 48 }) 49 50 var setupTestServer = func(request testnet.TestRequest) { 51 testServer, handler = testnet.NewServer([]testnet.TestRequest{request}) 52 config.SetAuthenticationEndpoint(testServer.URL) 53 config.SetUAAOAuthClient("cf") 54 } 55 56 Describe("authenticating", func() { 57 var err error 58 59 JustBeforeEach(func() { 60 err = auth.Authenticate(map[string]string{ 61 "username": "foo@example.com", 62 "password": "bar", 63 }) 64 }) 65 66 Describe("when login succeeds", func() { 67 BeforeEach(func() { 68 setupTestServer(successfulPasswordLoginRequest) 69 }) 70 71 It("stores the access and refresh tokens in the config", func() { 72 Expect(handler).To(HaveAllRequestsCalled()) 73 Expect(err).NotTo(HaveOccurred()) 74 Expect(config.AuthenticationEndpoint()).To(Equal(testServer.URL)) 75 Expect(config.AccessToken()).To(Equal(fmt.Sprintf("BEARER %s", testAccessToken))) 76 Expect(config.RefreshToken()).To(Equal("my_refresh_token")) 77 }) 78 }) 79 80 Describe("when login fails", func() { 81 BeforeEach(func() { 82 setupTestServer(unsuccessfulLoginRequest) 83 }) 84 85 It("returns an error", func() { 86 Expect(handler).To(HaveAllRequestsCalled()) 87 Expect(err).NotTo(BeNil()) 88 Expect(err.Error()).To(Equal("Credentials were rejected, please try again.")) 89 Expect(config.AccessToken()).To(BeEmpty()) 90 Expect(config.RefreshToken()).To(BeEmpty()) 91 }) 92 }) 93 94 Context("when the authentication server returns status code 500", func() { 95 BeforeEach(func() { 96 setupTestServer(errorLoginRequest) 97 }) 98 99 It("returns a failure response", func() { 100 Expect(handler).To(HaveAllRequestsCalled()) 101 Expect(err).To(HaveOccurred()) 102 Expect(err.Error()).To(Equal("The targeted API endpoint could not be reached.")) 103 Expect(config.AccessToken()).To(BeEmpty()) 104 }) 105 }) 106 107 Context("when the authentication server returns status code 502", func() { 108 var request testnet.TestRequest 109 110 BeforeEach(func() { 111 request = testnet.TestRequest{ 112 Method: "POST", 113 Path: "/oauth/token", 114 Response: testnet.TestResponse{ 115 Status: http.StatusBadGateway, 116 }, 117 } 118 setupTestServer(request) 119 }) 120 121 It("returns a failure response", func() { 122 Expect(handler).To(HaveAllRequestsCalled()) 123 Expect(err).To(HaveOccurred()) 124 Expect(err.Error()).To(Equal("The targeted API endpoint could not be reached.")) 125 Expect(config.AccessToken()).To(BeEmpty()) 126 }) 127 }) 128 129 Describe("when the UAA server has an error but still returns a 200", func() { 130 BeforeEach(func() { 131 setupTestServer(errorMaskedAsSuccessLoginRequest) 132 }) 133 134 It("returns an error", func() { 135 Expect(handler).To(HaveAllRequestsCalled()) 136 Expect(err).To(HaveOccurred()) 137 Expect(err.Error()).To(ContainSubstring("I/O error: uaa.10.244.0.22.xip.io; nested exception is java.net.UnknownHostException: uaa.10.244.0.22.xip.io")) 138 Expect(config.AccessToken()).To(BeEmpty()) 139 }) 140 }) 141 }) 142 143 Describe("getting login info", func() { 144 var ( 145 apiErr error 146 prompts map[string]coreconfig.AuthPrompt 147 ) 148 149 JustBeforeEach(func() { 150 prompts, apiErr = auth.GetLoginPromptsAndSaveUAAServerURL() 151 }) 152 153 Describe("when the login info API succeeds", func() { 154 BeforeEach(func() { 155 setupTestServer(loginServerLoginRequest) 156 }) 157 158 It("does not return an error", func() { 159 Expect(apiErr).NotTo(HaveOccurred()) 160 }) 161 162 It("gets the login prompts", func() { 163 Expect(prompts).To(Equal(map[string]coreconfig.AuthPrompt{ 164 "username": { 165 DisplayName: "Email", 166 Type: coreconfig.AuthPromptTypeText, 167 }, 168 "pin": { 169 DisplayName: "PIN Number", 170 Type: coreconfig.AuthPromptTypePassword, 171 }, 172 })) 173 }) 174 175 It("saves the UAA server to the config", func() { 176 Expect(config.UaaEndpoint()).To(Equal("https://uaa.run.pivotal.io")) 177 }) 178 }) 179 180 Describe("when the login info API fails", func() { 181 BeforeEach(func() { 182 setupTestServer(loginServerLoginFailureRequest) 183 }) 184 185 It("returns a failure response when the login info API fails", func() { 186 Expect(handler).To(HaveAllRequestsCalled()) 187 Expect(apiErr).To(HaveOccurred()) 188 Expect(prompts).To(BeEmpty()) 189 }) 190 }) 191 192 Context("when the response does not contain links", func() { 193 BeforeEach(func() { 194 setupTestServer(uaaServerLoginRequest) 195 }) 196 197 It("presumes that the authorization server is the UAA", func() { 198 Expect(config.UaaEndpoint()).To(Equal(config.AuthenticationEndpoint())) 199 }) 200 }) 201 }) 202 203 Describe("refreshing the auth token", func() { 204 var ( 205 apiErr error 206 accessToken string 207 ) 208 209 JustBeforeEach(func() { 210 accessToken, apiErr = auth.RefreshAuthToken() 211 }) 212 213 Context("when the user is authenticated with client credentials grant", func() { 214 BeforeEach(func() { 215 config.SetUAAGrantType("client_credentials") 216 config.SetAccessToken(testAccessToken) 217 setupTestServer(successfulClientCredentialsLoginRequest) 218 }) 219 220 It("uses client credentials to re-authenticate and obtain a new access token", func() { 221 Expect(apiErr).ToNot(HaveOccurred()) 222 Expect(accessToken).To(Equal(fmt.Sprintf("BEARER %s", testAccessToken))) 223 }) 224 225 When("the access token is still valid", func() { 226 var existingToken string 227 228 BeforeEach(func() { 229 expiringAnHourFromNow := time.Now().Add(time.Hour) 230 t := testconfig.BuildTokenString(expiringAnHourFromNow) 231 existingToken = fmt.Sprintf("bearer %s", t) 232 config.SetAccessToken(existingToken) 233 }) 234 235 It("returns the existing access token without re-authenticating", func() { 236 Expect(apiErr).ToNot(HaveOccurred()) 237 Expect(handler.CallCount).To(Equal(0)) 238 Expect(accessToken).To(Equal(existingToken)) 239 }) 240 }) 241 }) 242 243 Context("when the user is authenticated with password grant", func() { 244 BeforeEach(func() { 245 config.SetUAAGrantType("") 246 config.SetAccessToken(testAccessToken) 247 setupTestServer(successfulPasswordRefreshTokenRequest) 248 }) 249 250 It("uses the refresh token to refresh the access token", func() { 251 Expect(accessToken).To(Equal(fmt.Sprintf("BEARER %s", testAccessToken))) 252 }) 253 254 When("the access token is still valid", func() { 255 var existingToken string 256 257 BeforeEach(func() { 258 expiringAnHourFromNow := time.Now().Add(time.Hour) 259 t := testconfig.BuildTokenString(expiringAnHourFromNow) 260 existingToken = fmt.Sprintf("bearer %s", t) 261 config.SetAccessToken(existingToken) 262 }) 263 264 It("returns the existing access token without refreshing", func() { 265 Expect(apiErr).ToNot(HaveOccurred()) 266 Expect(handler.CallCount).To(Equal(0)) 267 Expect(accessToken).To(Equal(existingToken)) 268 }) 269 }) 270 }) 271 272 Context("when the refresh token has expired", func() { 273 BeforeEach(func() { 274 setupTestServer(refreshTokenExpiredRequestError) 275 config.SetAccessToken(testAccessToken) 276 }) 277 278 It("the returns the reauthentication error message", func() { 279 Expect(apiErr.Error()).To(Equal("Authentication has expired. Please log back in to re-authenticate.\n\nTIP: Use `cf login -a <endpoint> -u <user> -o <org> -s <space>` to log back in and re-authenticate.")) 280 }) 281 }) 282 283 Context("when there is a UAA error", func() { 284 BeforeEach(func() { 285 setupTestServer(errorLoginRequest) 286 }) 287 288 It("returns the API error", func() { 289 Expect(apiErr).NotTo(BeNil()) 290 }) 291 }) 292 }) 293 }) 294 295 Describe("Authorize", func() { 296 var ( 297 uaaServer *ghttp.Server 298 gateway net.Gateway 299 config coreconfig.ReadWriter 300 authRepo Repository 301 dumper net.RequestDumper 302 fakePrinter *tracefakes.FakePrinter 303 ) 304 305 BeforeEach(func() { 306 uaaServer = ghttp.NewServer() 307 config = testconfig.NewRepository() 308 config.SetUaaEndpoint(uaaServer.URL()) 309 config.SetSSHOAuthClient("ssh-oauth-client") 310 311 fakePrinter = new(tracefakes.FakePrinter) 312 gateway = net.NewUAAGateway(config, new(terminalfakes.FakeUI), fakePrinter, "") 313 dumper = net.NewRequestDumper(fakePrinter) 314 authRepo = NewUAARepository(gateway, config, dumper) 315 316 uaaServer.AppendHandlers( 317 ghttp.CombineHandlers( 318 ghttp.VerifyHeader(http.Header{"authorization": []string{"auth-token"}}), 319 ghttp.VerifyHeaderKV("Connection", "close"), 320 ghttp.VerifyRequest("GET", "/oauth/authorize", 321 "response_type=code&grant_type=authorization_code&client_id=ssh-oauth-client", 322 ), 323 ghttp.RespondWith(http.StatusFound, ``, http.Header{ 324 "Location": []string{"https://www.cloudfoundry.example.com?code=F45jH"}, 325 }), 326 ), 327 ) 328 }) 329 330 AfterEach(func() { 331 uaaServer.Close() 332 }) 333 334 It("requests the one time code", func() { 335 _, err := authRepo.Authorize("auth-token") 336 Expect(err).NotTo(HaveOccurred()) 337 Expect(uaaServer.ReceivedRequests()).To(HaveLen(1)) 338 }) 339 340 It("returns the one time code", func() { 341 code, err := authRepo.Authorize("auth-token") 342 Expect(err).NotTo(HaveOccurred()) 343 Expect(code).To(Equal("F45jH")) 344 }) 345 346 Context("when the authentication endpoint is malformed", func() { 347 BeforeEach(func() { 348 config.SetUaaEndpoint(":not-well-formed") 349 }) 350 351 It("returns an error", func() { 352 _, err := authRepo.Authorize("auth-token") 353 Expect(err).To(HaveOccurred()) 354 }) 355 }) 356 357 Context("when the authorization server does not return a redirect", func() { 358 BeforeEach(func() { 359 uaaServer.SetHandler(0, ghttp.RespondWith(http.StatusOK, ``)) 360 }) 361 362 It("returns an error", func() { 363 _, err := authRepo.Authorize("auth-token") 364 Expect(err).To(HaveOccurred()) 365 Expect(err.Error()).To(Equal("Authorization server did not redirect with one time code")) 366 }) 367 }) 368 369 Context("when the authorization server does not return a redirect", func() { 370 BeforeEach(func() { 371 config.SetUaaEndpoint("https://127.0.0.1:1") 372 }) 373 374 It("returns an error", func() { 375 _, err := authRepo.Authorize("auth-token") 376 Expect(err).To(HaveOccurred()) 377 Expect(err.Error()).To(ContainSubstring("Error requesting one time code from server")) 378 }) 379 }) 380 381 Context("when the authorization server returns multiple codes", func() { 382 BeforeEach(func() { 383 uaaServer.SetHandler(0, ghttp.RespondWith(http.StatusFound, ``, http.Header{ 384 "Location": []string{"https://www.cloudfoundry.example.com?code=F45jH&code=LLLLL"}, 385 })) 386 }) 387 388 It("returns an error", func() { 389 _, err := authRepo.Authorize("auth-token") 390 Expect(err).To(HaveOccurred()) 391 Expect(err.Error()).To(Equal("Unable to acquire one time code from authorization response")) 392 }) 393 }) 394 }) 395 }) 396 397 var passwordGrantTypeAuthHeaders = http.Header{ 398 "accept": {"application/json"}, 399 "content-type": {"application/x-www-form-urlencoded"}, 400 "authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("cf:"))}, 401 } 402 403 var clientGrantTypeAuthHeaders = http.Header{ 404 "accept": {"application/json"}, 405 "content-type": {"application/x-www-form-urlencoded"}, 406 } 407 408 var successfulPasswordLoginRequest = testnet.TestRequest{ 409 Method: "POST", 410 Path: "/oauth/token", 411 Header: passwordGrantTypeAuthHeaders, 412 Matcher: successfulPasswordLoginMatcher, 413 Response: testnet.TestResponse{ 414 Status: http.StatusOK, 415 Body: fmt.Sprintf(` 416 { 417 "access_token": "%s", 418 "token_type": "BEARER", 419 "refresh_token": "my_refresh_token", 420 "scope": "openid", 421 "expires_in": 98765 422 } `, testAccessToken)}, 423 } 424 425 var successfulClientCredentialsLoginRequest = testnet.TestRequest{ 426 Method: "POST", 427 Path: "/oauth/token", 428 Header: clientGrantTypeAuthHeaders, 429 Matcher: successfulClientCredentialsLoginMatcher, 430 Response: testnet.TestResponse{ 431 Status: http.StatusOK, 432 Body: fmt.Sprintf(` 433 { 434 "access_token": "%s", 435 "token_type": "BEARER", 436 "scope": "openid", 437 "expires_in": 98765 438 } `, testAccessToken)}, 439 } 440 441 var successfulPasswordRefreshTokenRequest = testnet.TestRequest{ 442 Method: "POST", 443 Path: "/oauth/token", 444 Header: passwordGrantTypeAuthHeaders, 445 Matcher: successfulPasswordRefreshTokenLoginMatcher, 446 Response: testnet.TestResponse{ 447 Status: http.StatusOK, 448 Body: fmt.Sprintf(` 449 { 450 "access_token": "%s", 451 "token_type": "BEARER", 452 "refresh_token": "my_refresh_token", 453 "scope": "openid", 454 "expires_in": 98765 455 } `, testAccessToken)}, 456 } 457 458 var successfulPasswordLoginMatcher = func(request *http.Request) { 459 err := request.ParseForm() 460 if err != nil { 461 Fail(fmt.Sprintf("Failed to parse form: %s", err)) 462 return 463 } 464 465 Expect(request.Form.Get("username")).To(Equal("foo@example.com")) 466 Expect(request.Form.Get("password")).To(Equal("bar")) 467 Expect(request.Form.Get("grant_type")).To(Equal("password")) 468 Expect(request.Form.Get("scope")).To(Equal("")) 469 } 470 471 var successfulClientCredentialsLoginMatcher = func(request *http.Request) { 472 err := request.ParseForm() 473 if err != nil { 474 Fail(fmt.Sprintf("Failed to parse form: %s", err)) 475 return 476 } 477 478 Expect(request.Form.Get("grant_type")).To(Equal("client_credentials")) 479 } 480 481 var successfulPasswordRefreshTokenLoginMatcher = func(request *http.Request) { 482 err := request.ParseForm() 483 if err != nil { 484 Fail(fmt.Sprintf("Failed to parse form: %s", err)) 485 return 486 } 487 488 Expect(request.Form.Get("grant_type")).To(Equal("refresh_token")) 489 } 490 491 var unsuccessfulLoginRequest = testnet.TestRequest{ 492 Method: "POST", 493 Path: "/oauth/token", 494 Response: testnet.TestResponse{ 495 Status: http.StatusUnauthorized, 496 }, 497 } 498 var refreshTokenExpiredRequestError = testnet.TestRequest{ 499 Method: "POST", 500 Path: "/oauth/token", 501 Response: testnet.TestResponse{ 502 Status: http.StatusUnauthorized, 503 Body: ` 504 { 505 "error": "invalid_token", 506 "error_description": "Invalid auth token: Invalid refresh token (expired): eyJhbGckjsdfdf" 507 } 508 `}, 509 } 510 511 var errorLoginRequest = testnet.TestRequest{ 512 Method: "POST", 513 Path: "/oauth/token", 514 Response: testnet.TestResponse{ 515 Status: http.StatusInternalServerError, 516 }, 517 } 518 519 var errorMaskedAsSuccessLoginRequest = testnet.TestRequest{ 520 Method: "POST", 521 Path: "/oauth/token", 522 Response: testnet.TestResponse{ 523 Status: http.StatusOK, 524 Body: ` 525 { 526 "error": { 527 "error": "rest_client_error", 528 "error_description": "I/O error: uaa.10.244.0.22.xip.io; nested exception is java.net.UnknownHostException: uaa.10.244.0.22.xip.io" 529 } 530 } 531 `}, 532 } 533 534 var loginServerLoginRequest = testnet.TestRequest{ 535 Method: "GET", 536 Path: "/login", 537 Response: testnet.TestResponse{ 538 Status: http.StatusOK, 539 Body: ` 540 { 541 "timestamp":"2013-12-18T11:26:53-0700", 542 "app":{ 543 "artifact":"cloudfoundry-identity-uaa", 544 "description":"User Account and Authentication Service", 545 "name":"UAA", 546 "version":"1.4.7" 547 }, 548 "commit_id":"2701cc8", 549 "links":{ 550 "register":"https://console.run.pivotal.io/register", 551 "passwd":"https://console.run.pivotal.io/password_resets/new", 552 "home":"https://console.run.pivotal.io", 553 "support":"https://support.cloudfoundry.com/home", 554 "login":"https://login.run.pivotal.io", 555 "uaa":"https://uaa.run.pivotal.io" 556 }, 557 "prompts":{ 558 "username": ["text","Email"], 559 "pin": ["password", "PIN Number"] 560 } 561 }`, 562 }, 563 } 564 565 var loginServerLoginFailureRequest = testnet.TestRequest{ 566 Method: "GET", 567 Path: "/login", 568 Response: testnet.TestResponse{ 569 Status: http.StatusInternalServerError, 570 }, 571 } 572 573 var uaaServerLoginRequest = testnet.TestRequest{ 574 Method: "GET", 575 Path: "/login", 576 Response: testnet.TestResponse{ 577 Status: http.StatusOK, 578 Body: ` 579 { 580 "timestamp":"2013-12-18T11:26:53-0700", 581 "app":{ 582 "artifact":"cloudfoundry-identity-uaa", 583 "description":"User Account and Authentication Service", 584 "name":"UAA", 585 "version":"1.4.7" 586 }, 587 "commit_id":"2701cc8", 588 "prompts":{ 589 "username": ["text","Email"], 590 "pin": ["password", "PIN Number"] 591 } 592 }`, 593 }, 594 }