github.com/cloudfoundry-community/cloudfoundry-cli@v6.44.1-0.20240130060226-cda5ed8e89a5+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 refresh the access token", func() { 221 Expect(apiErr).ToNot(HaveOccurred()) 222 Expect(accessToken).To(Equal(fmt.Sprintf("BEARER %s", testAccessToken))) 223 }) 224 }) 225 226 Context("when the user is authenticated with password grant", func() { 227 BeforeEach(func() { 228 config.SetUAAGrantType("") 229 config.SetAccessToken(testAccessToken) 230 setupTestServer(successfulPasswordRefreshTokenRequest) 231 }) 232 233 It("uses the refresh token to refresh the access token", func() { 234 Expect(accessToken).To(Equal(fmt.Sprintf("BEARER %s", testAccessToken))) 235 }) 236 }) 237 238 Context("when the refresh token has expired", func() { 239 BeforeEach(func() { 240 setupTestServer(refreshTokenExpiredRequestError) 241 config.SetAccessToken(testAccessToken) 242 }) 243 244 It("the returns the reauthentication error message", func() { 245 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.")) 246 }) 247 }) 248 249 Context("when there is a UAA error", func() { 250 BeforeEach(func() { 251 setupTestServer(errorLoginRequest) 252 }) 253 254 It("returns the API error", func() { 255 Expect(apiErr).NotTo(BeNil()) 256 }) 257 }) 258 }) 259 }) 260 261 Describe("Authorize", func() { 262 var ( 263 uaaServer *ghttp.Server 264 gateway net.Gateway 265 config coreconfig.ReadWriter 266 authRepo Repository 267 dumper net.RequestDumper 268 fakePrinter *tracefakes.FakePrinter 269 ) 270 271 BeforeEach(func() { 272 uaaServer = ghttp.NewServer() 273 config = testconfig.NewRepository() 274 config.SetUaaEndpoint(uaaServer.URL()) 275 config.SetSSHOAuthClient("ssh-oauth-client") 276 277 fakePrinter = new(tracefakes.FakePrinter) 278 gateway = net.NewUAAGateway(config, new(terminalfakes.FakeUI), fakePrinter, "") 279 dumper = net.NewRequestDumper(fakePrinter) 280 authRepo = NewUAARepository(gateway, config, dumper) 281 282 uaaServer.AppendHandlers( 283 ghttp.CombineHandlers( 284 ghttp.VerifyHeader(http.Header{"authorization": []string{"auth-token"}}), 285 ghttp.VerifyHeaderKV("Connection", "close"), 286 ghttp.VerifyRequest("GET", "/oauth/authorize", 287 "response_type=code&grant_type=authorization_code&client_id=ssh-oauth-client", 288 ), 289 ghttp.RespondWith(http.StatusFound, ``, http.Header{ 290 "Location": []string{"https://www.cloudfoundry.example.com?code=F45jH"}, 291 }), 292 ), 293 ) 294 }) 295 296 AfterEach(func() { 297 uaaServer.Close() 298 }) 299 300 It("requests the one time code", func() { 301 _, err := authRepo.Authorize("auth-token") 302 Expect(err).NotTo(HaveOccurred()) 303 Expect(uaaServer.ReceivedRequests()).To(HaveLen(1)) 304 }) 305 306 It("returns the one time code", func() { 307 code, err := authRepo.Authorize("auth-token") 308 Expect(err).NotTo(HaveOccurred()) 309 Expect(code).To(Equal("F45jH")) 310 }) 311 312 Context("when the authentication endpoint is malformed", func() { 313 BeforeEach(func() { 314 config.SetUaaEndpoint(":not-well-formed") 315 }) 316 317 It("returns an error", func() { 318 _, err := authRepo.Authorize("auth-token") 319 Expect(err).To(HaveOccurred()) 320 }) 321 }) 322 323 Context("when the authorization server does not return a redirect", func() { 324 BeforeEach(func() { 325 uaaServer.SetHandler(0, ghttp.RespondWith(http.StatusOK, ``)) 326 }) 327 328 It("returns an error", func() { 329 _, err := authRepo.Authorize("auth-token") 330 Expect(err).To(HaveOccurred()) 331 Expect(err.Error()).To(Equal("Authorization server did not redirect with one time code")) 332 }) 333 }) 334 335 Context("when the authorization server does not return a redirect", func() { 336 BeforeEach(func() { 337 config.SetUaaEndpoint("https://127.0.0.1:1") 338 }) 339 340 It("returns an error", func() { 341 _, err := authRepo.Authorize("auth-token") 342 Expect(err).To(HaveOccurred()) 343 Expect(err.Error()).To(ContainSubstring("Error requesting one time code from server")) 344 }) 345 }) 346 347 Context("when the authorization server returns multiple codes", func() { 348 BeforeEach(func() { 349 uaaServer.SetHandler(0, ghttp.RespondWith(http.StatusFound, ``, http.Header{ 350 "Location": []string{"https://www.cloudfoundry.example.com?code=F45jH&code=LLLLL"}, 351 })) 352 }) 353 354 It("returns an error", func() { 355 _, err := authRepo.Authorize("auth-token") 356 Expect(err).To(HaveOccurred()) 357 Expect(err.Error()).To(Equal("Unable to acquire one time code from authorization response")) 358 }) 359 }) 360 }) 361 }) 362 363 var passwordGrantTypeAuthHeaders = http.Header{ 364 "accept": {"application/json"}, 365 "content-type": {"application/x-www-form-urlencoded"}, 366 "authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("cf:"))}, 367 } 368 369 var clientGrantTypeAuthHeaders = http.Header{ 370 "accept": {"application/json"}, 371 "content-type": {"application/x-www-form-urlencoded"}, 372 } 373 374 var successfulPasswordLoginRequest = testnet.TestRequest{ 375 Method: "POST", 376 Path: "/oauth/token", 377 Header: passwordGrantTypeAuthHeaders, 378 Matcher: successfulPasswordLoginMatcher, 379 Response: testnet.TestResponse{ 380 Status: http.StatusOK, 381 Body: fmt.Sprintf(` 382 { 383 "access_token": "%s", 384 "token_type": "BEARER", 385 "refresh_token": "my_refresh_token", 386 "scope": "openid", 387 "expires_in": 98765 388 } `, testAccessToken)}, 389 } 390 391 var successfulClientCredentialsLoginRequest = testnet.TestRequest{ 392 Method: "POST", 393 Path: "/oauth/token", 394 Header: clientGrantTypeAuthHeaders, 395 Matcher: successfulClientCredentialsLoginMatcher, 396 Response: testnet.TestResponse{ 397 Status: http.StatusOK, 398 Body: fmt.Sprintf(` 399 { 400 "access_token": "%s", 401 "token_type": "BEARER", 402 "scope": "openid", 403 "expires_in": 98765 404 } `, testAccessToken)}, 405 } 406 407 var successfulPasswordRefreshTokenRequest = testnet.TestRequest{ 408 Method: "POST", 409 Path: "/oauth/token", 410 Header: passwordGrantTypeAuthHeaders, 411 Matcher: successfulPasswordRefreshTokenLoginMatcher, 412 Response: testnet.TestResponse{ 413 Status: http.StatusOK, 414 Body: fmt.Sprintf(` 415 { 416 "access_token": "%s", 417 "token_type": "BEARER", 418 "refresh_token": "my_refresh_token", 419 "scope": "openid", 420 "expires_in": 98765 421 } `, testAccessToken)}, 422 } 423 424 var successfulPasswordLoginMatcher = func(request *http.Request) { 425 err := request.ParseForm() 426 if err != nil { 427 Fail(fmt.Sprintf("Failed to parse form: %s", err)) 428 return 429 } 430 431 Expect(request.Form.Get("username")).To(Equal("foo@example.com")) 432 Expect(request.Form.Get("password")).To(Equal("bar")) 433 Expect(request.Form.Get("grant_type")).To(Equal("password")) 434 Expect(request.Form.Get("scope")).To(Equal("")) 435 } 436 437 var successfulClientCredentialsLoginMatcher = func(request *http.Request) { 438 err := request.ParseForm() 439 if err != nil { 440 Fail(fmt.Sprintf("Failed to parse form: %s", err)) 441 return 442 } 443 444 Expect(request.Form.Get("grant_type")).To(Equal("client_credentials")) 445 } 446 447 var successfulPasswordRefreshTokenLoginMatcher = func(request *http.Request) { 448 err := request.ParseForm() 449 if err != nil { 450 Fail(fmt.Sprintf("Failed to parse form: %s", err)) 451 return 452 } 453 454 Expect(request.Form.Get("grant_type")).To(Equal("refresh_token")) 455 } 456 457 var unsuccessfulLoginRequest = testnet.TestRequest{ 458 Method: "POST", 459 Path: "/oauth/token", 460 Response: testnet.TestResponse{ 461 Status: http.StatusUnauthorized, 462 }, 463 } 464 var refreshTokenExpiredRequestError = testnet.TestRequest{ 465 Method: "POST", 466 Path: "/oauth/token", 467 Response: testnet.TestResponse{ 468 Status: http.StatusUnauthorized, 469 Body: ` 470 { 471 "error": "invalid_token", 472 "error_description": "Invalid auth token: Invalid refresh token (expired): eyJhbGckjsdfdf" 473 } 474 `}, 475 } 476 477 var errorLoginRequest = testnet.TestRequest{ 478 Method: "POST", 479 Path: "/oauth/token", 480 Response: testnet.TestResponse{ 481 Status: http.StatusInternalServerError, 482 }, 483 } 484 485 var errorMaskedAsSuccessLoginRequest = testnet.TestRequest{ 486 Method: "POST", 487 Path: "/oauth/token", 488 Response: testnet.TestResponse{ 489 Status: http.StatusOK, 490 Body: ` 491 { 492 "error": { 493 "error": "rest_client_error", 494 "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" 495 } 496 } 497 `}, 498 } 499 500 var loginServerLoginRequest = testnet.TestRequest{ 501 Method: "GET", 502 Path: "/login", 503 Response: testnet.TestResponse{ 504 Status: http.StatusOK, 505 Body: ` 506 { 507 "timestamp":"2013-12-18T11:26:53-0700", 508 "app":{ 509 "artifact":"cloudfoundry-identity-uaa", 510 "description":"User Account and Authentication Service", 511 "name":"UAA", 512 "version":"1.4.7" 513 }, 514 "commit_id":"2701cc8", 515 "links":{ 516 "register":"https://console.run.pivotal.io/register", 517 "passwd":"https://console.run.pivotal.io/password_resets/new", 518 "home":"https://console.run.pivotal.io", 519 "support":"https://support.cloudfoundry.com/home", 520 "login":"https://login.run.pivotal.io", 521 "uaa":"https://uaa.run.pivotal.io" 522 }, 523 "prompts":{ 524 "username": ["text","Email"], 525 "pin": ["password", "PIN Number"] 526 } 527 }`, 528 }, 529 } 530 531 var loginServerLoginFailureRequest = testnet.TestRequest{ 532 Method: "GET", 533 Path: "/login", 534 Response: testnet.TestResponse{ 535 Status: http.StatusInternalServerError, 536 }, 537 } 538 539 var uaaServerLoginRequest = testnet.TestRequest{ 540 Method: "GET", 541 Path: "/login", 542 Response: testnet.TestResponse{ 543 Status: http.StatusOK, 544 Body: ` 545 { 546 "timestamp":"2013-12-18T11:26:53-0700", 547 "app":{ 548 "artifact":"cloudfoundry-identity-uaa", 549 "description":"User Account and Authentication Service", 550 "name":"UAA", 551 "version":"1.4.7" 552 }, 553 "commit_id":"2701cc8", 554 "prompts":{ 555 "username": ["text","Email"], 556 "pin": ["password", "PIN Number"] 557 } 558 }`, 559 }, 560 }