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