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  }