login_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. /*
  2. Copyright 2020 Docker, Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package login
  14. import (
  15. "context"
  16. "io/ioutil"
  17. "net/http"
  18. "net/url"
  19. "os"
  20. "path/filepath"
  21. "reflect"
  22. "testing"
  23. "time"
  24. "github.com/stretchr/testify/mock"
  25. "github.com/stretchr/testify/suite"
  26. "golang.org/x/oauth2"
  27. . "github.com/onsi/gomega"
  28. )
  29. type LoginSuite struct {
  30. suite.Suite
  31. dir string
  32. mockHelper *MockAzureHelper
  33. azureLogin AzureLoginService
  34. }
  35. func (suite *LoginSuite) BeforeTest(suiteName, testName string) {
  36. dir, err := ioutil.TempDir("", "test_store")
  37. Expect(err).To(BeNil())
  38. suite.dir = dir
  39. suite.mockHelper = &MockAzureHelper{}
  40. suite.azureLogin, err = newAzureLoginServiceFromPath(filepath.Join(dir, tokenStoreFilename), suite.mockHelper)
  41. Expect(err).To(BeNil())
  42. }
  43. func (suite *LoginSuite) AfterTest(suiteName, testName string) {
  44. err := os.RemoveAll(suite.dir)
  45. Expect(err).To(BeNil())
  46. }
  47. func (suite *LoginSuite) TestRefreshInValidToken() {
  48. data := refreshTokenData("refreshToken")
  49. suite.mockHelper.On("queryToken", data, "123456").Return(azureToken{
  50. RefreshToken: "newRefreshToken",
  51. AccessToken: "newAccessToken",
  52. ExpiresIn: 3600,
  53. Foci: "1",
  54. }, nil)
  55. azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
  56. Expect(err).To(BeNil())
  57. suite.azureLogin = azureLogin
  58. err = suite.azureLogin.tokenStore.writeLoginInfo(TokenInfo{
  59. TenantID: "123456",
  60. Token: oauth2.Token{
  61. AccessToken: "accessToken",
  62. RefreshToken: "refreshToken",
  63. Expiry: time.Now().Add(-1 * time.Hour),
  64. TokenType: "Bearer",
  65. },
  66. })
  67. Expect(err).To(BeNil())
  68. token, _ := suite.azureLogin.GetValidToken()
  69. Expect(token.AccessToken).To(Equal("newAccessToken"))
  70. Expect(token.Expiry).To(BeTemporally(">", time.Now().Add(3500*time.Second)))
  71. storedToken, _ := suite.azureLogin.tokenStore.readToken()
  72. Expect(storedToken.Token.AccessToken).To(Equal("newAccessToken"))
  73. Expect(storedToken.Token.RefreshToken).To(Equal("newRefreshToken"))
  74. Expect(storedToken.Token.Expiry).To(BeTemporally(">", time.Now().Add(3500*time.Second)))
  75. }
  76. func (suite *LoginSuite) TestClearErrorMessageIfNotAlreadyLoggedIn() {
  77. _, err := newAuthorizerFromLoginStorePath(filepath.Join(suite.dir, tokenStoreFilename))
  78. Expect(err.Error()).To(ContainSubstring("not logged in to azure, you need to run \"docker login azure\" first"))
  79. }
  80. func (suite *LoginSuite) TestDoesNotRefreshValidToken() {
  81. expiryDate := time.Now().Add(1 * time.Hour)
  82. err := suite.azureLogin.tokenStore.writeLoginInfo(TokenInfo{
  83. TenantID: "123456",
  84. Token: oauth2.Token{
  85. AccessToken: "accessToken",
  86. RefreshToken: "refreshToken",
  87. Expiry: expiryDate,
  88. TokenType: "Bearer",
  89. },
  90. })
  91. Expect(err).To(BeNil())
  92. token, _ := suite.azureLogin.GetValidToken()
  93. Expect(suite.mockHelper.Calls).To(BeEmpty())
  94. Expect(token.AccessToken).To(Equal("accessToken"))
  95. }
  96. func (suite *LoginSuite) TestInvalidLogin() {
  97. suite.mockHelper.On("openAzureLoginPage", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
  98. redirectURL := args.Get(0).(string)
  99. err := queryKeyValue(redirectURL, "error", "access denied: login failed")
  100. Expect(err).To(BeNil())
  101. })
  102. azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
  103. Expect(err).To(BeNil())
  104. err = azureLogin.Login(context.TODO(), "")
  105. Expect(err.Error()).To(BeEquivalentTo("no login code: login failed"))
  106. }
  107. func (suite *LoginSuite) TestValidLogin() {
  108. var redirectURL string
  109. suite.mockHelper.On("openAzureLoginPage", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
  110. redirectURL = args.Get(0).(string)
  111. err := queryKeyValue(redirectURL, "code", "123456879")
  112. Expect(err).To(BeNil())
  113. })
  114. suite.mockHelper.On("queryToken", mock.MatchedBy(func(data url.Values) bool {
  115. //Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
  116. return reflect.DeepEqual(data, url.Values{
  117. "grant_type": []string{"authorization_code"},
  118. "client_id": []string{clientID},
  119. "code": []string{"123456879"},
  120. "scope": []string{scopes},
  121. "redirect_uri": []string{redirectURL},
  122. })
  123. }), "organizations").Return(azureToken{
  124. RefreshToken: "firstRefreshToken",
  125. AccessToken: "firstAccessToken",
  126. ExpiresIn: 3600,
  127. Foci: "1",
  128. }, nil)
  129. authBody := `{"value":[{"id":"/tenants/12345a7c-c56d-43e8-9549-dd230ce8a038","tenantId":"12345a7c-c56d-43e8-9549-dd230ce8a038"}]}`
  130. suite.mockHelper.On("queryAuthorizationAPI", authorizationURL, "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
  131. data := refreshTokenData("firstRefreshToken")
  132. suite.mockHelper.On("queryToken", data, "12345a7c-c56d-43e8-9549-dd230ce8a038").Return(azureToken{
  133. RefreshToken: "newRefreshToken",
  134. AccessToken: "newAccessToken",
  135. ExpiresIn: 3600,
  136. Foci: "1",
  137. }, nil)
  138. azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
  139. Expect(err).To(BeNil())
  140. err = azureLogin.Login(context.TODO(), "")
  141. Expect(err).To(BeNil())
  142. loginToken, err := suite.azureLogin.tokenStore.readToken()
  143. Expect(err).To(BeNil())
  144. Expect(loginToken.Token.AccessToken).To(Equal("newAccessToken"))
  145. Expect(loginToken.Token.RefreshToken).To(Equal("newRefreshToken"))
  146. Expect(loginToken.Token.Expiry).To(BeTemporally(">", time.Now().Add(3500*time.Second)))
  147. Expect(loginToken.TenantID).To(Equal("12345a7c-c56d-43e8-9549-dd230ce8a038"))
  148. Expect(loginToken.Token.Type()).To(Equal("Bearer"))
  149. }
  150. func (suite *LoginSuite) TestValidLoginRequestedTenant() {
  151. var redirectURL string
  152. suite.mockHelper.On("openAzureLoginPage", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
  153. redirectURL = args.Get(0).(string)
  154. err := queryKeyValue(redirectURL, "code", "123456879")
  155. Expect(err).To(BeNil())
  156. })
  157. suite.mockHelper.On("queryToken", mock.MatchedBy(func(data url.Values) bool {
  158. //Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
  159. return reflect.DeepEqual(data, url.Values{
  160. "grant_type": []string{"authorization_code"},
  161. "client_id": []string{clientID},
  162. "code": []string{"123456879"},
  163. "scope": []string{scopes},
  164. "redirect_uri": []string{redirectURL},
  165. })
  166. }), "organizations").Return(azureToken{
  167. RefreshToken: "firstRefreshToken",
  168. AccessToken: "firstAccessToken",
  169. ExpiresIn: 3600,
  170. Foci: "1",
  171. }, nil)
  172. authBody := `{"value":[{"id":"/tenants/00000000-c56d-43e8-9549-dd230ce8a038","tenantId":"00000000-c56d-43e8-9549-dd230ce8a038"},
  173. {"id":"/tenants/12345a7c-c56d-43e8-9549-dd230ce8a038","tenantId":"12345a7c-c56d-43e8-9549-dd230ce8a038"}]}`
  174. suite.mockHelper.On("queryAuthorizationAPI", authorizationURL, "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
  175. data := refreshTokenData("firstRefreshToken")
  176. suite.mockHelper.On("queryToken", data, "12345a7c-c56d-43e8-9549-dd230ce8a038").Return(azureToken{
  177. RefreshToken: "newRefreshToken",
  178. AccessToken: "newAccessToken",
  179. ExpiresIn: 3600,
  180. Foci: "1",
  181. }, nil)
  182. azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
  183. Expect(err).To(BeNil())
  184. err = azureLogin.Login(context.TODO(), "12345a7c-c56d-43e8-9549-dd230ce8a038")
  185. Expect(err).To(BeNil())
  186. loginToken, err := suite.azureLogin.tokenStore.readToken()
  187. Expect(err).To(BeNil())
  188. Expect(loginToken.Token.AccessToken).To(Equal("newAccessToken"))
  189. Expect(loginToken.Token.RefreshToken).To(Equal("newRefreshToken"))
  190. Expect(loginToken.Token.Expiry).To(BeTemporally(">", time.Now().Add(3500*time.Second)))
  191. Expect(loginToken.TenantID).To(Equal("12345a7c-c56d-43e8-9549-dd230ce8a038"))
  192. Expect(loginToken.Token.Type()).To(Equal("Bearer"))
  193. }
  194. func (suite *LoginSuite) TestLoginNoTenant() {
  195. var redirectURL string
  196. suite.mockHelper.On("openAzureLoginPage", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
  197. redirectURL = args.Get(0).(string)
  198. err := queryKeyValue(redirectURL, "code", "123456879")
  199. Expect(err).To(BeNil())
  200. })
  201. suite.mockHelper.On("queryToken", mock.MatchedBy(func(data url.Values) bool {
  202. //Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
  203. return reflect.DeepEqual(data, url.Values{
  204. "grant_type": []string{"authorization_code"},
  205. "client_id": []string{clientID},
  206. "code": []string{"123456879"},
  207. "scope": []string{scopes},
  208. "redirect_uri": []string{redirectURL},
  209. })
  210. }), "organizations").Return(azureToken{
  211. RefreshToken: "firstRefreshToken",
  212. AccessToken: "firstAccessToken",
  213. ExpiresIn: 3600,
  214. Foci: "1",
  215. }, nil)
  216. authBody := `{"value":[{"id":"/tenants/12345a7c-c56d-43e8-9549-dd230ce8a038","tenantId":"12345a7c-c56d-43e8-9549-dd230ce8a038"}]}`
  217. suite.mockHelper.On("queryAuthorizationAPI", authorizationURL, "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
  218. azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
  219. Expect(err).To(BeNil())
  220. err = azureLogin.Login(context.TODO(), "00000000-c56d-43e8-9549-dd230ce8a038")
  221. Expect(err.Error()).To(BeEquivalentTo("could not find requested azure tenant 00000000-c56d-43e8-9549-dd230ce8a038: login failed"))
  222. }
  223. func (suite *LoginSuite) TestLoginRequestedTenantNotFound() {
  224. var redirectURL string
  225. suite.mockHelper.On("openAzureLoginPage", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
  226. redirectURL = args.Get(0).(string)
  227. err := queryKeyValue(redirectURL, "code", "123456879")
  228. Expect(err).To(BeNil())
  229. })
  230. suite.mockHelper.On("queryToken", mock.MatchedBy(func(data url.Values) bool {
  231. //Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
  232. return reflect.DeepEqual(data, url.Values{
  233. "grant_type": []string{"authorization_code"},
  234. "client_id": []string{clientID},
  235. "code": []string{"123456879"},
  236. "scope": []string{scopes},
  237. "redirect_uri": []string{redirectURL},
  238. })
  239. }), "organizations").Return(azureToken{
  240. RefreshToken: "firstRefreshToken",
  241. AccessToken: "firstAccessToken",
  242. ExpiresIn: 3600,
  243. Foci: "1",
  244. }, nil)
  245. authBody := `{"value":[]}`
  246. suite.mockHelper.On("queryAuthorizationAPI", authorizationURL, "Bearer firstAccessToken").Return([]byte(authBody), 200, nil)
  247. azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
  248. Expect(err).To(BeNil())
  249. err = azureLogin.Login(context.TODO(), "")
  250. Expect(err.Error()).To(BeEquivalentTo("could not find azure tenant: login failed"))
  251. }
  252. func (suite *LoginSuite) TestLoginAuthorizationFailed() {
  253. var redirectURL string
  254. suite.mockHelper.On("openAzureLoginPage", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
  255. redirectURL = args.Get(0).(string)
  256. err := queryKeyValue(redirectURL, "code", "123456879")
  257. Expect(err).To(BeNil())
  258. })
  259. suite.mockHelper.On("queryToken", mock.MatchedBy(func(data url.Values) bool {
  260. //Need a matcher here because the value of redirectUrl is not known until executing openAzureLoginPage
  261. return reflect.DeepEqual(data, url.Values{
  262. "grant_type": []string{"authorization_code"},
  263. "client_id": []string{clientID},
  264. "code": []string{"123456879"},
  265. "scope": []string{scopes},
  266. "redirect_uri": []string{redirectURL},
  267. })
  268. }), "organizations").Return(azureToken{
  269. RefreshToken: "firstRefreshToken",
  270. AccessToken: "firstAccessToken",
  271. ExpiresIn: 3600,
  272. Foci: "1",
  273. }, nil)
  274. authBody := `[access denied]`
  275. suite.mockHelper.On("queryAuthorizationAPI", authorizationURL, "Bearer firstAccessToken").Return([]byte(authBody), 400, nil)
  276. azureLogin, err := newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenStoreFilename), suite.mockHelper)
  277. Expect(err).To(BeNil())
  278. err = azureLogin.Login(context.TODO(), "")
  279. Expect(err.Error()).To(BeEquivalentTo("unable to login status code 400: [access denied]: login failed"))
  280. }
  281. func refreshTokenData(refreshToken string) url.Values {
  282. return url.Values{
  283. "grant_type": []string{"refresh_token"},
  284. "client_id": []string{clientID},
  285. "scope": []string{scopes},
  286. "refresh_token": []string{refreshToken},
  287. }
  288. }
  289. func queryKeyValue(redirectURL string, key string, value string) error {
  290. req, err := http.NewRequest("GET", redirectURL, nil)
  291. Expect(err).To(BeNil())
  292. q := req.URL.Query()
  293. q.Add(key, value)
  294. req.URL.RawQuery = q.Encode()
  295. client := &http.Client{}
  296. _, err = client.Do(req)
  297. return err
  298. }
  299. func TestLoginSuite(t *testing.T) {
  300. RegisterTestingT(t)
  301. suite.Run(t, new(LoginSuite))
  302. }
  303. type MockAzureHelper struct {
  304. mock.Mock
  305. }
  306. func (s *MockAzureHelper) queryToken(data url.Values, tenantID string) (token azureToken, err error) {
  307. args := s.Called(data, tenantID)
  308. return args.Get(0).(azureToken), args.Error(1)
  309. }
  310. func (s *MockAzureHelper) queryAuthorizationAPI(authorizationURL string, authorizationHeader string) ([]byte, int, error) {
  311. args := s.Called(authorizationURL, authorizationHeader)
  312. return args.Get(0).([]byte), args.Int(1), args.Error(2)
  313. }
  314. func (s *MockAzureHelper) openAzureLoginPage(redirectURL string) {
  315. s.Called(redirectURL)
  316. }