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  }