github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/store/auth_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package store_test 21 22 import ( 23 "io" 24 "io/ioutil" 25 "net/http" 26 "net/http/httptest" 27 "time" 28 29 . "gopkg.in/check.v1" 30 "gopkg.in/macaroon.v1" 31 "gopkg.in/retry.v1" 32 33 "github.com/snapcore/snapd/store" 34 "github.com/snapcore/snapd/testutil" 35 ) 36 37 type authTestSuite struct { 38 testutil.BaseTest 39 } 40 41 var _ = Suite(&authTestSuite{}) 42 43 const mockStoreInvalidLoginCode = 401 44 const mockStoreInvalidLogin = ` 45 { 46 "message": "Provided email/password is not correct.", 47 "code": "INVALID_CREDENTIALS", 48 "extra": {} 49 } 50 ` 51 52 const mockStoreNeeds2faHTTPCode = 401 53 const mockStoreNeeds2fa = ` 54 { 55 "message": "2-factor authentication required.", 56 "code": "TWOFACTOR_REQUIRED", 57 "extra": {} 58 } 59 ` 60 61 const mockStore2faFailedHTTPCode = 403 62 const mockStore2faFailedResponse = ` 63 { 64 "message": "The provided 2-factor key is not recognised.", 65 "code": "TWOFACTOR_FAILURE", 66 "extra": {} 67 } 68 ` 69 70 const mockStoreReturnMacaroon = `{"macaroon": "the-root-macaroon-serialized-data"}` 71 72 const mockStoreReturnDischarge = `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}` 73 74 const mockStoreReturnNoMacaroon = `{}` 75 76 const mockStoreReturnNonce = `{"nonce": "the-nonce"}` 77 78 const mockStoreReturnNoNonce = `{}` 79 80 func (s *authTestSuite) SetUpTest(c *C) { 81 store.MockDefaultRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.LimitTime(1*time.Second, 82 retry.Exponential{ 83 Initial: 1 * time.Millisecond, 84 Factor: 1.1, 85 }, 86 ))) 87 } 88 89 func (s *authTestSuite) TestRequestStoreMacaroon(c *C) { 90 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 91 io.WriteString(w, mockStoreReturnMacaroon) 92 })) 93 defer mockServer.Close() 94 store.MacaroonACLAPI = mockServer.URL + "/acl/" 95 96 macaroon, err := store.RequestStoreMacaroon(&http.Client{}) 97 c.Assert(err, IsNil) 98 c.Assert(macaroon, Equals, "the-root-macaroon-serialized-data") 99 } 100 101 func (s *authTestSuite) TestRequestStoreMacaroonMissingData(c *C) { 102 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 103 io.WriteString(w, mockStoreReturnNoMacaroon) 104 })) 105 defer mockServer.Close() 106 store.MacaroonACLAPI = mockServer.URL + "/acl/" 107 108 macaroon, err := store.RequestStoreMacaroon(&http.Client{}) 109 c.Assert(err, ErrorMatches, "cannot get snap access permission from store: empty macaroon returned") 110 c.Assert(macaroon, Equals, "") 111 } 112 113 func (s *authTestSuite) TestRequestStoreMacaroonError(c *C) { 114 n := 0 115 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 116 w.WriteHeader(500) 117 n++ 118 })) 119 defer mockServer.Close() 120 store.MacaroonACLAPI = mockServer.URL + "/acl/" 121 122 macaroon, err := store.RequestStoreMacaroon(&http.Client{}) 123 c.Assert(err, ErrorMatches, "cannot get snap access permission from store: store server returned status 500") 124 c.Assert(n, Equals, 5) 125 c.Assert(macaroon, Equals, "") 126 } 127 128 func (s *authTestSuite) TestDischargeAuthCaveat(c *C) { 129 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 130 io.WriteString(w, mockStoreReturnDischarge) 131 })) 132 defer mockServer.Close() 133 store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge" 134 135 discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "guy@example.com", "passwd", "") 136 c.Assert(err, IsNil) 137 c.Assert(discharge, Equals, "the-discharge-macaroon-serialized-data") 138 } 139 140 func (s *authTestSuite) TestDischargeAuthCaveatNeeds2fa(c *C) { 141 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 142 w.WriteHeader(mockStoreNeeds2faHTTPCode) 143 io.WriteString(w, mockStoreNeeds2fa) 144 })) 145 defer mockServer.Close() 146 store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge" 147 148 discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "") 149 c.Assert(err, Equals, store.ErrAuthenticationNeeds2fa) 150 c.Assert(discharge, Equals, "") 151 } 152 153 func (s *authTestSuite) TestDischargeAuthCaveatFails2fa(c *C) { 154 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 155 w.WriteHeader(mockStore2faFailedHTTPCode) 156 io.WriteString(w, mockStore2faFailedResponse) 157 })) 158 defer mockServer.Close() 159 store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge" 160 161 discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "") 162 c.Assert(err, Equals, store.Err2faFailed) 163 c.Assert(discharge, Equals, "") 164 } 165 166 func (s *authTestSuite) TestDischargeAuthCaveatInvalidLogin(c *C) { 167 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 168 w.WriteHeader(mockStoreInvalidLoginCode) 169 io.WriteString(w, mockStoreInvalidLogin) 170 })) 171 defer mockServer.Close() 172 store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge" 173 174 discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "") 175 c.Assert(err, Equals, store.ErrInvalidCredentials) 176 c.Assert(discharge, Equals, "") 177 } 178 179 func (s *authTestSuite) TestDischargeAuthCaveatMissingData(c *C) { 180 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 181 io.WriteString(w, mockStoreReturnNoMacaroon) 182 })) 183 defer mockServer.Close() 184 store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge" 185 186 discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "") 187 c.Assert(err, ErrorMatches, "cannot authenticate to snap store: empty macaroon returned") 188 c.Assert(discharge, Equals, "") 189 } 190 191 func (s *authTestSuite) TestDischargeAuthCaveatError(c *C) { 192 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 193 w.WriteHeader(500) 194 })) 195 defer mockServer.Close() 196 store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge" 197 198 discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "") 199 c.Assert(err, ErrorMatches, "cannot authenticate to snap store: server returned status 500") 200 c.Assert(discharge, Equals, "") 201 } 202 203 func (s *authTestSuite) TestRefreshDischargeMacaroon(c *C) { 204 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 205 io.WriteString(w, mockStoreReturnDischarge) 206 })) 207 defer mockServer.Close() 208 store.UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh" 209 210 discharge, err := store.RefreshDischargeMacaroon(&http.Client{}, "soft-expired-serialized-discharge-macaroon") 211 c.Assert(err, IsNil) 212 c.Assert(discharge, Equals, "the-discharge-macaroon-serialized-data") 213 } 214 215 func (s *authTestSuite) TestRefreshDischargeMacaroonInvalidLogin(c *C) { 216 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 217 w.WriteHeader(mockStoreInvalidLoginCode) 218 io.WriteString(w, mockStoreInvalidLogin) 219 })) 220 defer mockServer.Close() 221 store.UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh" 222 223 discharge, err := store.RefreshDischargeMacaroon(&http.Client{}, "soft-expired-serialized-discharge-macaroon") 224 c.Assert(err, Equals, store.ErrInvalidCredentials) 225 c.Assert(discharge, Equals, "") 226 } 227 228 func (s *authTestSuite) TestRefreshDischargeMacaroonMissingData(c *C) { 229 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 230 io.WriteString(w, mockStoreReturnNoMacaroon) 231 })) 232 defer mockServer.Close() 233 store.UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh" 234 235 discharge, err := store.RefreshDischargeMacaroon(&http.Client{}, "soft-expired-serialized-discharge-macaroon") 236 c.Assert(err, ErrorMatches, "cannot authenticate to snap store: empty macaroon returned") 237 c.Assert(discharge, Equals, "") 238 } 239 240 func (s *authTestSuite) TestRefreshDischargeMacaroonError(c *C) { 241 n := 0 242 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 243 data, err := ioutil.ReadAll(r.Body) 244 c.Assert(err, IsNil) 245 c.Assert(data, NotNil) 246 c.Assert(string(data), Equals, `{"discharge_macaroon":"soft-expired-serialized-discharge-macaroon"}`) 247 w.WriteHeader(500) 248 n++ 249 })) 250 defer mockServer.Close() 251 store.UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh" 252 253 discharge, err := store.RefreshDischargeMacaroon(&http.Client{}, "soft-expired-serialized-discharge-macaroon") 254 c.Assert(err, ErrorMatches, "cannot authenticate to snap store: server returned status 500") 255 c.Assert(n, Equals, 5) 256 c.Assert(discharge, Equals, "") 257 } 258 259 func (s *authTestSuite) TestLoginCaveatIDReturnCaveatID(c *C) { 260 m, err := macaroon.New([]byte("secret"), "some-id", "location") 261 c.Check(err, IsNil) 262 err = m.AddThirdPartyCaveat([]byte("shared-key"), "third-party-caveat", store.UbuntuoneLocation) 263 c.Check(err, IsNil) 264 265 caveat, err := store.LoginCaveatID(m) 266 c.Check(err, IsNil) 267 c.Check(caveat, Equals, "third-party-caveat") 268 } 269 270 func (s *authTestSuite) TestLoginCaveatIDMacaroonMissingCaveat(c *C) { 271 m, err := macaroon.New([]byte("secret"), "some-id", "location") 272 c.Check(err, IsNil) 273 err = m.AddThirdPartyCaveat([]byte("shared-key"), "third-party-caveat", "other-location") 274 c.Check(err, IsNil) 275 276 caveat, err := store.LoginCaveatID(m) 277 c.Check(err, NotNil) 278 c.Check(caveat, Equals, "") 279 } 280 281 func (s *authTestSuite) TestRequestStoreDeviceNonce(c *C) { 282 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 283 io.WriteString(w, mockStoreReturnNonce) 284 })) 285 defer mockServer.Close() 286 287 deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces" 288 nonce, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI) 289 c.Assert(err, IsNil) 290 c.Assert(nonce, Equals, "the-nonce") 291 } 292 293 func (s *authTestSuite) TestRequestStoreDeviceNonceRetry500(c *C) { 294 n := 0 295 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 296 n++ 297 if n < 4 { 298 w.WriteHeader(500) 299 } else { 300 io.WriteString(w, mockStoreReturnNonce) 301 } 302 })) 303 defer mockServer.Close() 304 305 deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces" 306 nonce, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI) 307 c.Assert(err, IsNil) 308 c.Assert(nonce, Equals, "the-nonce") 309 c.Assert(n, Equals, 4) 310 } 311 312 func (s *authTestSuite) TestRequestStoreDeviceNonce500(c *C) { 313 n := 0 314 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 315 n++ 316 w.WriteHeader(500) 317 })) 318 defer mockServer.Close() 319 320 deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces" 321 _, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI) 322 c.Assert(err, NotNil) 323 c.Assert(err, ErrorMatches, `cannot get nonce from store: store server returned status 500`) 324 c.Assert(n, Equals, 5) 325 } 326 327 func (s *authTestSuite) TestRequestStoreDeviceNonceFailureOnDNS(c *C) { 328 deviceNonceAPI := "http://nonexistingserver121321.com/api/v1/snaps/auth/nonces" 329 _, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI) 330 c.Assert(err, NotNil) 331 c.Assert(err, ErrorMatches, `cannot get nonce from store.*`) 332 } 333 334 func (s *authTestSuite) TestRequestStoreDeviceNonceEmptyResponse(c *C) { 335 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 336 io.WriteString(w, mockStoreReturnNoNonce) 337 })) 338 defer mockServer.Close() 339 340 deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces" 341 nonce, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI) 342 c.Assert(err, ErrorMatches, "cannot get nonce from store: empty nonce returned") 343 c.Assert(nonce, Equals, "") 344 } 345 346 func (s *authTestSuite) TestRequestStoreDeviceNonceError(c *C) { 347 n := 0 348 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 349 w.WriteHeader(500) 350 n++ 351 })) 352 defer mockServer.Close() 353 354 deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces" 355 nonce, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI) 356 c.Assert(err, ErrorMatches, "cannot get nonce from store: store server returned status 500") 357 c.Assert(n, Equals, 5) 358 c.Assert(nonce, Equals, "") 359 } 360 361 type testDeviceSessionRequestParamsEncoder struct{} 362 363 func (pe *testDeviceSessionRequestParamsEncoder) EncodedRequest() string { 364 return "session-request" 365 } 366 367 func (pe *testDeviceSessionRequestParamsEncoder) EncodedSerial() string { 368 return "serial-assertion" 369 } 370 371 func (pe *testDeviceSessionRequestParamsEncoder) EncodedModel() string { 372 return "model-assertion" 373 } 374 375 func (s *authTestSuite) TestRequestDeviceSession(c *C) { 376 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 377 jsonReq, err := ioutil.ReadAll(r.Body) 378 c.Assert(err, IsNil) 379 c.Check(string(jsonReq), Equals, `{"device-session-request":"session-request","model-assertion":"model-assertion","serial-assertion":"serial-assertion"}`) 380 c.Check(r.Header.Get("X-Device-Authorization"), Equals, "") 381 382 io.WriteString(w, mockStoreReturnMacaroon) 383 })) 384 defer mockServer.Close() 385 386 deviceSessionAPI := mockServer.URL + "/api/v1/snaps/auth/sessions" 387 macaroon, err := store.RequestDeviceSession(&http.Client{}, deviceSessionAPI, &testDeviceSessionRequestParamsEncoder{}, "") 388 c.Assert(err, IsNil) 389 c.Assert(macaroon, Equals, "the-root-macaroon-serialized-data") 390 } 391 392 func (s *authTestSuite) TestRequestDeviceSessionWithPreviousSession(c *C) { 393 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 394 jsonReq, err := ioutil.ReadAll(r.Body) 395 c.Assert(err, IsNil) 396 c.Check(string(jsonReq), Equals, `{"device-session-request":"session-request","model-assertion":"model-assertion","serial-assertion":"serial-assertion"}`) 397 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="previous-session"`) 398 399 io.WriteString(w, mockStoreReturnMacaroon) 400 })) 401 defer mockServer.Close() 402 403 deviceSessionAPI := mockServer.URL + "/api/v1/snaps/auth/sessions" 404 macaroon, err := store.RequestDeviceSession(&http.Client{}, deviceSessionAPI, &testDeviceSessionRequestParamsEncoder{}, "previous-session") 405 c.Assert(err, IsNil) 406 c.Assert(macaroon, Equals, "the-root-macaroon-serialized-data") 407 } 408 409 func (s *authTestSuite) TestRequestDeviceSessionMissingData(c *C) { 410 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 411 io.WriteString(w, mockStoreReturnNoMacaroon) 412 })) 413 defer mockServer.Close() 414 415 deviceSessionAPI := mockServer.URL + "/api/v1/snaps/auth/sessions" 416 macaroon, err := store.RequestDeviceSession(&http.Client{}, deviceSessionAPI, &testDeviceSessionRequestParamsEncoder{}, "") 417 c.Assert(err, ErrorMatches, "cannot get device session from store: empty session returned") 418 c.Assert(macaroon, Equals, "") 419 } 420 421 func (s *authTestSuite) TestRequestDeviceSessionError(c *C) { 422 n := 0 423 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 424 w.WriteHeader(500) 425 w.Write([]byte("error body")) 426 n++ 427 })) 428 defer mockServer.Close() 429 430 deviceSessionAPI := mockServer.URL + "/api/v1/snaps/auth/sessions" 431 macaroon, err := store.RequestDeviceSession(&http.Client{}, deviceSessionAPI, &testDeviceSessionRequestParamsEncoder{}, "") 432 c.Assert(err, ErrorMatches, `cannot get device session from store: store server returned status 500 and body "error body"`) 433 c.Assert(n, Equals, 5) 434 c.Assert(macaroon, Equals, "") 435 }