github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/store/auth.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2016 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 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "io" 27 "io/ioutil" 28 "net/http" 29 30 "gopkg.in/macaroon.v1" 31 32 "github.com/snapcore/snapd/httputil" 33 "github.com/snapcore/snapd/snapdenv" 34 ) 35 36 var ( 37 developerAPIBase = storeDeveloperURL() 38 // macaroonACLAPI points to Developer API endpoint to get an ACL macaroon 39 MacaroonACLAPI = developerAPIBase + "dev/api/acl/" 40 ubuntuoneAPIBase = authURL() 41 // UbuntuoneLocation is the Ubuntuone location as defined in the store macaroon 42 UbuntuoneLocation = authLocation() 43 // UbuntuoneDischargeAPI points to SSO endpoint to discharge a macaroon 44 UbuntuoneDischargeAPI = ubuntuoneAPIBase + "/tokens/discharge" 45 // UbuntuoneRefreshDischargeAPI points to SSO endpoint to refresh a discharge macaroon 46 UbuntuoneRefreshDischargeAPI = ubuntuoneAPIBase + "/tokens/refresh" 47 ) 48 49 // a stringList is something that can be deserialized from a JSON 50 // []string or a string, like the values of the "extra" documents in 51 // error responses 52 type stringList []string 53 54 func (sish *stringList) UnmarshalJSON(bs []byte) error { 55 var ss []string 56 e1 := json.Unmarshal(bs, &ss) 57 if e1 == nil { 58 *sish = stringList(ss) 59 return nil 60 } 61 62 var s string 63 e2 := json.Unmarshal(bs, &s) 64 if e2 == nil { 65 *sish = stringList([]string{s}) 66 return nil 67 } 68 69 return e1 70 } 71 72 type ssoMsg struct { 73 Code string `json:"code"` 74 Message string `json:"message"` 75 Extra map[string]stringList `json:"extra"` 76 } 77 78 // returns true if the http status code is in the "success" range (2xx) 79 func httpStatusCodeSuccess(httpStatusCode int) bool { 80 return httpStatusCode/100 == 2 81 } 82 83 // returns true if the http status code is in the "client-error" range (4xx) 84 func httpStatusCodeClientError(httpStatusCode int) bool { 85 return httpStatusCode/100 == 4 86 } 87 88 // loginCaveatID returns the 3rd party caveat from the macaroon to be discharged by Ubuntuone 89 func loginCaveatID(m *macaroon.Macaroon) (string, error) { 90 caveatID := "" 91 for _, caveat := range m.Caveats() { 92 if caveat.Location == UbuntuoneLocation { 93 caveatID = caveat.Id 94 break 95 } 96 } 97 if caveatID == "" { 98 return "", fmt.Errorf("missing login caveat") 99 } 100 return caveatID, nil 101 } 102 103 // retryPostRequestDecodeJSON calls retryPostRequest and decodes the response into either success or failure. 104 func retryPostRequestDecodeJSON(httpClient *http.Client, endpoint string, headers map[string]string, data []byte, success interface{}, failure interface{}) (resp *http.Response, err error) { 105 return retryPostRequest(httpClient, endpoint, headers, data, func(resp *http.Response) error { 106 return decodeJSONBody(resp, success, failure) 107 }) 108 } 109 110 // retryPostRequest calls doRequest and decodes the response in a retry loop. 111 func retryPostRequest(httpClient *http.Client, endpoint string, headers map[string]string, data []byte, readResponseBody func(resp *http.Response) error) (*http.Response, error) { 112 return httputil.RetryRequest(endpoint, func() (*http.Response, error) { 113 req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(data)) 114 if err != nil { 115 return nil, err 116 } 117 for k, v := range headers { 118 req.Header.Set(k, v) 119 } 120 121 return httpClient.Do(req) 122 }, readResponseBody, defaultRetryStrategy) 123 } 124 125 // requestStoreMacaroon requests a macaroon for accessing package data from the ubuntu store. 126 func requestStoreMacaroon(httpClient *http.Client) (string, error) { 127 const errorPrefix = "cannot get snap access permission from store: " 128 129 data := map[string]interface{}{ 130 "permissions": []string{"package_access", "package_purchase"}, 131 } 132 133 var err error 134 macaroonJSONData, err := json.Marshal(data) 135 if err != nil { 136 return "", fmt.Errorf(errorPrefix+"%v", err) 137 } 138 139 var responseData struct { 140 Macaroon string `json:"macaroon"` 141 } 142 143 headers := map[string]string{ 144 "User-Agent": snapdenv.UserAgent(), 145 "Accept": "application/json", 146 "Content-Type": "application/json", 147 } 148 resp, err := retryPostRequestDecodeJSON(httpClient, MacaroonACLAPI, headers, macaroonJSONData, &responseData, nil) 149 if err != nil { 150 return "", fmt.Errorf(errorPrefix+"%v", err) 151 } 152 153 // check return code, error on anything !200 154 if resp.StatusCode != 200 { 155 return "", fmt.Errorf(errorPrefix+"store server returned status %d", resp.StatusCode) 156 } 157 158 if responseData.Macaroon == "" { 159 return "", fmt.Errorf(errorPrefix + "empty macaroon returned") 160 } 161 return responseData.Macaroon, nil 162 } 163 164 func requestDischargeMacaroon(httpClient *http.Client, endpoint string, data map[string]string) (string, error) { 165 const errorPrefix = "cannot authenticate to snap store: " 166 167 var err error 168 dischargeJSONData, err := json.Marshal(data) 169 if err != nil { 170 return "", fmt.Errorf(errorPrefix+"%v", err) 171 } 172 173 var responseData struct { 174 Macaroon string `json:"discharge_macaroon"` 175 } 176 var msg ssoMsg 177 178 headers := map[string]string{ 179 "User-Agent": snapdenv.UserAgent(), 180 "Accept": "application/json", 181 "Content-Type": "application/json", 182 } 183 resp, err := retryPostRequestDecodeJSON(httpClient, endpoint, headers, dischargeJSONData, &responseData, &msg) 184 if err != nil { 185 return "", fmt.Errorf(errorPrefix+"%v", err) 186 } 187 188 // check return code, error on 4xx and anything !200 189 switch { 190 case httpStatusCodeClientError(resp.StatusCode): 191 switch msg.Code { 192 case "TWOFACTOR_REQUIRED": 193 return "", ErrAuthenticationNeeds2fa 194 case "TWOFACTOR_FAILURE": 195 return "", Err2faFailed 196 case "INVALID_CREDENTIALS": 197 return "", ErrInvalidCredentials 198 case "INVALID_DATA": 199 return "", InvalidAuthDataError(msg.Extra) 200 case "PASSWORD_POLICY_ERROR": 201 return "", PasswordPolicyError(msg.Extra) 202 } 203 204 if msg.Message != "" { 205 return "", fmt.Errorf(errorPrefix+"%v", msg.Message) 206 } 207 fallthrough 208 209 case !httpStatusCodeSuccess(resp.StatusCode): 210 return "", fmt.Errorf(errorPrefix+"server returned status %d", resp.StatusCode) 211 } 212 213 if responseData.Macaroon == "" { 214 return "", fmt.Errorf(errorPrefix + "empty macaroon returned") 215 } 216 return responseData.Macaroon, nil 217 } 218 219 // dischargeAuthCaveat returns a macaroon with the store auth caveat discharged. 220 func dischargeAuthCaveat(httpClient *http.Client, caveat, username, password, otp string) (string, error) { 221 data := map[string]string{ 222 "email": username, 223 "password": password, 224 "caveat_id": caveat, 225 } 226 if otp != "" { 227 data["otp"] = otp 228 } 229 230 return requestDischargeMacaroon(httpClient, UbuntuoneDischargeAPI, data) 231 } 232 233 // refreshDischargeMacaroon returns a soft-refreshed discharge macaroon. 234 func refreshDischargeMacaroon(httpClient *http.Client, discharge string) (string, error) { 235 data := map[string]string{ 236 "discharge_macaroon": discharge, 237 } 238 239 return requestDischargeMacaroon(httpClient, UbuntuoneRefreshDischargeAPI, data) 240 } 241 242 // requestStoreDeviceNonce requests a nonce for device authentication against the store. 243 func requestStoreDeviceNonce(httpClient *http.Client, deviceNonceEndpoint string) (string, error) { 244 const errorPrefix = "cannot get nonce from store: " 245 246 var responseData struct { 247 Nonce string `json:"nonce"` 248 } 249 250 headers := map[string]string{ 251 "User-Agent": snapdenv.UserAgent(), 252 "Accept": "application/json", 253 } 254 resp, err := retryPostRequestDecodeJSON(httpClient, deviceNonceEndpoint, headers, nil, &responseData, nil) 255 if err != nil { 256 return "", fmt.Errorf(errorPrefix+"%v", err) 257 } 258 259 // check return code, error on anything !200 260 if resp.StatusCode != 200 { 261 return "", fmt.Errorf(errorPrefix+"store server returned status %d", resp.StatusCode) 262 } 263 264 if responseData.Nonce == "" { 265 return "", fmt.Errorf(errorPrefix + "empty nonce returned") 266 } 267 return responseData.Nonce, nil 268 } 269 270 type deviceSessionRequestParamsEncoder interface { 271 EncodedRequest() string 272 EncodedSerial() string 273 EncodedModel() string 274 } 275 276 // requestDeviceSession requests a device session macaroon from the store. 277 func requestDeviceSession(httpClient *http.Client, deviceSessionEndpoint string, paramsEncoder deviceSessionRequestParamsEncoder, previousSession string) (string, error) { 278 const errorPrefix = "cannot get device session from store: " 279 280 data := map[string]string{ 281 "device-session-request": paramsEncoder.EncodedRequest(), 282 "serial-assertion": paramsEncoder.EncodedSerial(), 283 "model-assertion": paramsEncoder.EncodedModel(), 284 } 285 var err error 286 deviceJSONData, err := json.Marshal(data) 287 if err != nil { 288 return "", fmt.Errorf(errorPrefix+"%v", err) 289 } 290 291 var responseData struct { 292 Macaroon string `json:"macaroon"` 293 } 294 295 headers := map[string]string{ 296 "User-Agent": snapdenv.UserAgent(), 297 "Accept": "application/json", 298 "Content-Type": "application/json", 299 } 300 if previousSession != "" { 301 headers["X-Device-Authorization"] = fmt.Sprintf(`Macaroon root="%s"`, previousSession) 302 } 303 304 _, err = retryPostRequest(httpClient, deviceSessionEndpoint, headers, deviceJSONData, func(resp *http.Response) error { 305 if resp.StatusCode == 200 || resp.StatusCode == 202 { 306 return json.NewDecoder(resp.Body).Decode(&responseData) 307 } 308 body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1e6)) // do our best to read the body 309 return fmt.Errorf("store server returned status %d and body %q", resp.StatusCode, body) 310 }) 311 if err != nil { 312 return "", fmt.Errorf(errorPrefix+"%v", err) 313 } 314 // TODO: retry at least once on 400 315 316 if responseData.Macaroon == "" { 317 return "", fmt.Errorf(errorPrefix + "empty session returned") 318 } 319 320 return responseData.Macaroon, nil 321 }