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  }