FxAccountsConfig.sys.mjs (10988B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { RESTRequest } from "resource://services-common/rest.sys.mjs"; 6 7 import { 8 log, 9 SCOPE_APP_SYNC, 10 SCOPE_PROFILE, 11 } from "resource://gre/modules/FxAccountsCommon.sys.mjs"; 12 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 13 14 const lazy = {}; 15 16 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { 17 return ChromeUtils.importESModule( 18 "resource://gre/modules/FxAccounts.sys.mjs" 19 ).getFxAccountsSingleton(); 20 }); 21 22 ChromeUtils.defineESModuleGetters(lazy, { 23 EnsureFxAccountsWebChannel: 24 "resource://gre/modules/FxAccountsWebChannel.sys.mjs", 25 }); 26 27 XPCOMUtils.defineLazyPreferenceGetter( 28 lazy, 29 "ROOT_URL", 30 "identity.fxaccounts.remote.root" 31 ); 32 XPCOMUtils.defineLazyPreferenceGetter( 33 lazy, 34 "CONTEXT_PARAM", 35 "identity.fxaccounts.contextParam" 36 ); 37 XPCOMUtils.defineLazyPreferenceGetter( 38 lazy, 39 "REQUIRES_HTTPS", 40 "identity.fxaccounts.allowHttp", 41 false, 42 null, 43 val => !val 44 ); 45 46 const CONFIG_PREFS = [ 47 "identity.fxaccounts.remote.root", 48 "identity.fxaccounts.auth.uri", 49 "identity.fxaccounts.remote.oauth.uri", 50 "identity.fxaccounts.remote.profile.uri", 51 "identity.fxaccounts.remote.pairing.uri", 52 "identity.sync.tokenserver.uri", 53 ]; 54 const SYNC_PARAM = "sync"; 55 56 export var FxAccountsConfig = { 57 async promiseEmailURI(email, entrypoint, extraParams = {}) { 58 const authParams = await this._getAuthParams(); 59 return this._buildURL("", { 60 extraParams: { 61 entrypoint, 62 email, 63 ...authParams, 64 ...extraParams, 65 }, 66 }); 67 }, 68 69 async promiseConnectAccountURI(entrypoint, extraParams = {}) { 70 const authParams = await this._getAuthParams(); 71 return this._buildURL("", { 72 extraParams: { 73 entrypoint, 74 action: "email", 75 ...authParams, 76 ...extraParams, 77 }, 78 }); 79 }, 80 81 async promiseManageURI(entrypoint, extraParams = {}) { 82 return this._buildURL("settings", { 83 extraParams: { entrypoint, ...extraParams }, 84 addAccountIdentifiers: true, 85 }); 86 }, 87 88 async promiseChangeAvatarURI(entrypoint, extraParams = {}) { 89 return this._buildURL("settings/avatar/change", { 90 extraParams: { entrypoint, ...extraParams }, 91 addAccountIdentifiers: true, 92 }); 93 }, 94 95 async promiseManageDevicesURI(entrypoint, extraParams = {}) { 96 return this._buildURL("settings/clients", { 97 extraParams: { entrypoint, ...extraParams }, 98 addAccountIdentifiers: true, 99 }); 100 }, 101 102 async promiseConnectDeviceURI(entrypoint, extraParams = {}) { 103 return this._buildURL("connect_another_device", { 104 extraParams: { entrypoint, service: SYNC_PARAM, ...extraParams }, 105 addAccountIdentifiers: true, 106 }); 107 }, 108 109 async promiseSetPasswordURI(entrypoint, extraParams = {}) { 110 const authParams = await this._getAuthParams(); 111 return this._buildURL("post_verify/third_party_auth/set_password", { 112 extraParams: { 113 entrypoint, 114 ...authParams, 115 ...extraParams, 116 }, 117 addAccountIdentifiers: true, 118 }); 119 }, 120 121 async promisePairingURI(extraParams = {}) { 122 return this._buildURL("pair", { 123 extraParams, 124 includeDefaultParams: false, 125 }); 126 }, 127 128 async promiseOAuthURI(extraParams = {}) { 129 return this._buildURL("oauth", { 130 extraParams, 131 includeDefaultParams: false, 132 }); 133 }, 134 135 async promiseMetricsFlowURI(entrypoint, extraParams = {}) { 136 return this._buildURL("metrics-flow", { 137 extraParams: { entrypoint, ...extraParams }, 138 includeDefaultParams: false, 139 }); 140 }, 141 142 get defaultParams() { 143 return { context: lazy.CONTEXT_PARAM }; 144 }, 145 146 /** 147 * @param path should be parsable by the URL constructor first parameter. 148 * @param {bool} [options.includeDefaultParams] If true include the default search params. 149 * @param {{[key: string]: string}} [options.extraParams] Additionnal search params. 150 * @param {bool} [options.addAccountIdentifiers] if true we add the current logged-in user uid and email to the search params. 151 */ 152 async _buildURL( 153 path, 154 { 155 includeDefaultParams = true, 156 extraParams = {}, 157 addAccountIdentifiers = false, 158 } 159 ) { 160 await this.ensureConfigured(); 161 const url = new URL(path, lazy.ROOT_URL); 162 if (lazy.REQUIRES_HTTPS && url.protocol != "https:") { 163 throw new Error("Firefox Accounts server must use HTTPS"); 164 } 165 const params = { 166 ...(includeDefaultParams ? this.defaultParams : null), 167 ...extraParams, 168 }; 169 for (let [k, v] of Object.entries(params)) { 170 url.searchParams.append(k, v); 171 } 172 if (addAccountIdentifiers) { 173 const accountData = await this.getSignedInUser(); 174 if (!accountData) { 175 return null; 176 } 177 url.searchParams.append("uid", accountData.uid); 178 url.searchParams.append("email", accountData.email); 179 } 180 return url.href; 181 }, 182 183 async _buildURLFromString(href, extraParams = {}) { 184 const url = new URL(href); 185 for (let [k, v] of Object.entries(extraParams)) { 186 url.searchParams.append(k, v); 187 } 188 return url.href; 189 }, 190 191 resetConfigURLs() { 192 let autoconfigURL = this.getAutoConfigURL(); 193 if (autoconfigURL) { 194 return; 195 } 196 // They have the autoconfig uri pref set, so we clear all the prefs that we 197 // will have initialized, which will leave them pointing at production. 198 for (let pref of CONFIG_PREFS) { 199 Services.prefs.clearUserPref(pref); 200 } 201 // Reset the webchannel. 202 lazy.EnsureFxAccountsWebChannel(); 203 }, 204 205 getAutoConfigURL() { 206 let pref = Services.prefs.getStringPref( 207 "identity.fxaccounts.autoconfig.uri", 208 "" 209 ); 210 if (!pref) { 211 // no pref / empty pref means we don't bother here. 212 return ""; 213 } 214 let rootURL = Services.urlFormatter.formatURL(pref); 215 if (rootURL.endsWith("/")) { 216 rootURL = rootURL.slice(0, -1); 217 } 218 return rootURL; 219 }, 220 221 async ensureConfigured() { 222 let isSignedIn = !!(await this.getSignedInUser()); 223 if (!isSignedIn) { 224 await this.updateConfigURLs(); 225 } 226 }, 227 228 // Returns true if this user is using the FxA "production" systems, false 229 // if using any other configuration, including self-hosting or the FxA 230 // non-production systems such as "dev" or "staging". 231 // It's typically used as a proxy for "is this likely to be a self-hosted 232 // user?", but it's named this way to make the implementation less 233 // surprising. As a result, it's fairly conservative and would prefer to have 234 // a false-negative than a false-position as it determines things which users 235 // might consider sensitive (notably, telemetry). 236 // Note also that while it's possible to self-host just sync and not FxA, we 237 // don't make that distinction - that's a self-hoster from the POV of this 238 // function. 239 isProductionConfig() { 240 // Specifically, if the autoconfig URLs, or *any* of the URLs that 241 // we consider configurable are modified, we assume self-hosted. 242 if (this.getAutoConfigURL()) { 243 return false; 244 } 245 for (let pref of CONFIG_PREFS) { 246 if (Services.prefs.prefHasUserValue(pref)) { 247 return false; 248 } 249 } 250 return true; 251 }, 252 253 // Read expected client configuration from the fxa auth server 254 // (from `identity.fxaccounts.autoconfig.uri`/.well-known/fxa-client-configuration) 255 // and replace all the relevant our prefs with the information found there. 256 // This is only done before sign-in and sign-up, and even then only if the 257 // `identity.fxaccounts.autoconfig.uri` preference is set. 258 async updateConfigURLs() { 259 let rootURL = this.getAutoConfigURL(); 260 if (!rootURL) { 261 return; 262 } 263 const config = await this.fetchConfigDocument(rootURL); 264 try { 265 // Update the prefs directly specified by the config. 266 let authServerBase = config.auth_server_base_url; 267 if (!authServerBase.endsWith("/v1")) { 268 authServerBase += "/v1"; 269 } 270 Services.prefs.setStringPref( 271 "identity.fxaccounts.auth.uri", 272 authServerBase 273 ); 274 Services.prefs.setStringPref( 275 "identity.fxaccounts.remote.oauth.uri", 276 config.oauth_server_base_url + "/v1" 277 ); 278 // At the time of landing this, our servers didn't yet answer with pairing_server_base_uri. 279 // Remove this condition check once Firefox 68 is stable. 280 if (config.pairing_server_base_uri) { 281 Services.prefs.setStringPref( 282 "identity.fxaccounts.remote.pairing.uri", 283 config.pairing_server_base_uri 284 ); 285 } 286 Services.prefs.setStringPref( 287 "identity.fxaccounts.remote.profile.uri", 288 config.profile_server_base_url + "/v1" 289 ); 290 Services.prefs.setStringPref( 291 "identity.sync.tokenserver.uri", 292 config.sync_tokenserver_base_url + "/1.0/sync/1.5" 293 ); 294 Services.prefs.setStringPref("identity.fxaccounts.remote.root", rootURL); 295 296 // Ensure the webchannel is pointed at the correct uri 297 lazy.EnsureFxAccountsWebChannel(); 298 } catch (e) { 299 log.error( 300 "Failed to initialize configuration preferences from autoconfig object", 301 e 302 ); 303 throw e; 304 } 305 }, 306 307 // Read expected client configuration from the fxa auth server 308 // (or from the provided rootURL, if present) and return it as an object. 309 async fetchConfigDocument(rootURL = null) { 310 if (!rootURL) { 311 rootURL = lazy.ROOT_URL; 312 } 313 let configURL = rootURL + "/.well-known/fxa-client-configuration"; 314 let request = new RESTRequest(configURL); 315 request.setHeader("Accept", "application/json"); 316 317 // Catch and rethrow the error inline. 318 let resp = await request.get().catch(e => { 319 log.error(`Failed to get configuration object from "${configURL}"`, e); 320 throw e; 321 }); 322 if (!resp.success) { 323 // Note: 'resp.body' is included with the error log below as we are not concerned 324 // that the body will contain PII, but if that changes it should be excluded. 325 log.error( 326 `Received HTTP response code ${resp.status} from configuration object request: 327 ${resp.body}` 328 ); 329 throw new Error( 330 `HTTP status ${resp.status} from configuration object request` 331 ); 332 } 333 log.debug("Got successful configuration response", resp.body); 334 try { 335 return JSON.parse(resp.body); 336 } catch (e) { 337 log.error( 338 `Failed to parse configuration preferences from ${configURL}`, 339 e 340 ); 341 throw e; 342 } 343 }, 344 345 // For test purposes, returns a Promise. 346 getSignedInUser() { 347 return lazy.fxAccounts.getSignedInUser(); 348 }, 349 350 async _getAuthParams() { 351 let params = { service: SYNC_PARAM }; 352 const scopes = [SCOPE_APP_SYNC, SCOPE_PROFILE]; 353 Object.assign( 354 params, 355 await lazy.fxAccounts._internal.beginOAuthFlow(scopes) 356 ); 357 return params; 358 }, 359 };