fxa_client/internal/
config.rs

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
5use super::http_client;
6use crate::{FxaConfig, Result};
7use serde_derive::{Deserialize, Serialize};
8use std::{cell::RefCell, sync::Arc};
9use url::Url;
10
11#[derive(Clone, Debug, Serialize, Deserialize)]
12pub struct Config {
13    content_url: String,
14    token_server_url_override: Option<String>,
15    pub client_id: String,
16    pub redirect_uri: String,
17    // RemoteConfig is lazily fetched from the server.
18    #[serde(skip)]
19    remote_config: RefCell<Option<Arc<RemoteConfig>>>,
20}
21
22/// `RemoteConfig` struct stores configuration values from the FxA
23/// `/.well-known/fxa-client-configuration` and the
24/// `/.well-known/openid-configuration` endpoints.
25#[derive(Debug)]
26// allow(dead_code) since we want the struct to match the API data, even if some fields aren't
27// currently used.
28#[allow(dead_code)]
29pub struct RemoteConfig {
30    auth_url: String,
31    oauth_url: String,
32    profile_url: String,
33    token_server_endpoint_url: String,
34    authorization_endpoint: String,
35    issuer: String,
36    jwks_uri: String,
37    token_endpoint: String,
38    userinfo_endpoint: String,
39    introspection_endpoint: String,
40}
41
42pub(crate) const CONTENT_URL_RELEASE: &str = "https://accounts.firefox.com";
43pub(crate) const CONTENT_URL_CHINA: &str = "https://accounts.firefox.com.cn";
44
45impl Config {
46    fn remote_config(&self) -> Result<Arc<RemoteConfig>> {
47        if let Some(remote_config) = self.remote_config.borrow().clone() {
48            return Ok(remote_config);
49        }
50
51        let client_config = http_client::fxa_client_configuration(self.client_config_url()?)?;
52        let openid_config = http_client::openid_configuration(self.openid_config_url()?)?;
53
54        let remote_config = self.set_remote_config(RemoteConfig {
55            auth_url: format!("{}/", client_config.auth_server_base_url),
56            oauth_url: format!("{}/", client_config.oauth_server_base_url),
57            profile_url: format!("{}/", client_config.profile_server_base_url),
58            token_server_endpoint_url: format!("{}/", client_config.sync_tokenserver_base_url),
59            authorization_endpoint: openid_config.authorization_endpoint,
60            issuer: openid_config.issuer,
61            jwks_uri: openid_config.jwks_uri,
62            // TODO: bring back openid token endpoint once https://github.com/mozilla/fxa/issues/453 has been resolved
63            // and the openid response has been switched to the new endpoint.
64            // token_endpoint: openid_config.token_endpoint,
65            token_endpoint: format!("{}/v1/oauth/token", client_config.auth_server_base_url),
66            userinfo_endpoint: openid_config.userinfo_endpoint,
67            introspection_endpoint: openid_config.introspection_endpoint,
68        });
69        Ok(remote_config)
70    }
71
72    fn set_remote_config(&self, remote_config: RemoteConfig) -> Arc<RemoteConfig> {
73        let rc = Arc::new(remote_config);
74        let result = rc.clone();
75        self.remote_config.replace(Some(rc));
76        result
77    }
78
79    pub fn content_url(&self) -> Result<Url> {
80        Url::parse(&self.content_url).map_err(Into::into)
81    }
82
83    pub fn content_url_path(&self, path: &str) -> Result<Url> {
84        self.content_url()?.join(path).map_err(Into::into)
85    }
86
87    pub fn client_config_url(&self) -> Result<Url> {
88        self.content_url_path(".well-known/fxa-client-configuration")
89    }
90
91    pub fn openid_config_url(&self) -> Result<Url> {
92        self.content_url_path(".well-known/openid-configuration")
93    }
94
95    pub fn connect_another_device_url(&self) -> Result<Url> {
96        self.content_url_path("connect_another_device")
97    }
98
99    pub fn pair_url(&self) -> Result<Url> {
100        self.content_url_path("pair")
101    }
102
103    pub fn pair_supp_url(&self) -> Result<Url> {
104        self.content_url_path("pair/supp")
105    }
106
107    pub fn oauth_force_auth_url(&self) -> Result<Url> {
108        self.content_url_path("oauth/force_auth")
109    }
110
111    pub fn settings_url(&self) -> Result<Url> {
112        self.content_url_path("settings")
113    }
114
115    pub fn settings_clients_url(&self) -> Result<Url> {
116        self.content_url_path("settings/clients")
117    }
118
119    pub fn auth_url(&self) -> Result<Url> {
120        Url::parse(&self.remote_config()?.auth_url).map_err(Into::into)
121    }
122
123    pub fn auth_url_path(&self, path: &str) -> Result<Url> {
124        self.auth_url()?.join(path).map_err(Into::into)
125    }
126
127    pub fn oauth_url(&self) -> Result<Url> {
128        Url::parse(&self.remote_config()?.oauth_url).map_err(Into::into)
129    }
130
131    pub fn oauth_url_path(&self, path: &str) -> Result<Url> {
132        self.oauth_url()?.join(path).map_err(Into::into)
133    }
134
135    pub fn token_server_endpoint_url(&self) -> Result<Url> {
136        if let Some(token_server_url_override) = &self.token_server_url_override {
137            return Ok(Url::parse(token_server_url_override)?);
138        }
139        Ok(Url::parse(
140            &self.remote_config()?.token_server_endpoint_url,
141        )?)
142    }
143
144    pub fn authorization_endpoint(&self) -> Result<Url> {
145        Url::parse(&self.remote_config()?.authorization_endpoint).map_err(Into::into)
146    }
147
148    pub fn token_endpoint(&self) -> Result<Url> {
149        Url::parse(&self.remote_config()?.token_endpoint).map_err(Into::into)
150    }
151
152    pub fn introspection_endpoint(&self) -> Result<Url> {
153        Url::parse(&self.remote_config()?.introspection_endpoint).map_err(Into::into)
154    }
155
156    pub fn userinfo_endpoint(&self) -> Result<Url> {
157        Url::parse(&self.remote_config()?.userinfo_endpoint).map_err(Into::into)
158    }
159
160    fn normalize_token_server_url(token_server_url_override: &str) -> String {
161        // In self-hosting setups it is common to specify the `/1.0/sync/1.5` suffix on the
162        // tokenserver URL. Accept and strip this form as a convenience for users.
163        // (ideally we'd use `strip_suffix`, but we currently target a rust version
164        // where this doesn't exist - `trim_end_matches` will repeatedly remove
165        // the suffix, but that seems fine for this use-case)
166        token_server_url_override
167            .trim_end_matches("/1.0/sync/1.5")
168            .to_owned()
169    }
170}
171
172impl From<FxaConfig> for Config {
173    fn from(fxa_config: FxaConfig) -> Self {
174        let content_url = fxa_config.server.content_url().to_string();
175        let token_server_url_override = fxa_config
176            .token_server_url_override
177            .as_deref()
178            .map(Self::normalize_token_server_url);
179
180        Self {
181            content_url,
182            client_id: fxa_config.client_id,
183            redirect_uri: fxa_config.redirect_uri,
184            token_server_url_override,
185            remote_config: RefCell::new(None),
186        }
187    }
188}
189
190#[cfg(test)]
191/// Testing functionality
192impl Config {
193    pub fn release(client_id: &str, redirect_uri: &str) -> Self {
194        Self::new(CONTENT_URL_RELEASE, client_id, redirect_uri)
195    }
196
197    pub fn stable_dev(client_id: &str, redirect_uri: &str) -> Self {
198        Self::new("https://stable.dev.lcip.org", client_id, redirect_uri)
199    }
200
201    pub fn china(client_id: &str, redirect_uri: &str) -> Self {
202        Self::new(CONTENT_URL_CHINA, client_id, redirect_uri)
203    }
204
205    pub fn new(content_url: &str, client_id: &str, redirect_uri: &str) -> Self {
206        Self {
207            content_url: content_url.to_string(),
208            client_id: client_id.to_string(),
209            redirect_uri: redirect_uri.to_string(),
210            remote_config: RefCell::new(None),
211            token_server_url_override: None,
212        }
213    }
214
215    /// Override the token server URL that would otherwise be provided by the
216    /// FxA .well-known/fxa-client-configuration endpoint.
217    /// This is used by self-hosters that still use the product FxA servers
218    /// for authentication purposes but use their own Sync storage backend.
219    pub fn override_token_server_url<'a>(
220        &'a mut self,
221        token_server_url_override: &str,
222    ) -> &'a mut Self {
223        self.token_server_url_override =
224            Some(Self::normalize_token_server_url(token_server_url_override));
225        self
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_paths() {
235        let remote_config = RemoteConfig {
236            auth_url: "https://stable.dev.lcip.org/auth/".to_string(),
237            oauth_url: "https://oauth-stable.dev.lcip.org/".to_string(),
238            profile_url: "https://stable.dev.lcip.org/profile/".to_string(),
239            token_server_endpoint_url: "https://stable.dev.lcip.org/syncserver/token/1.0/sync/1.5"
240                .to_string(),
241            authorization_endpoint: "https://oauth-stable.dev.lcip.org/v1/authorization"
242                .to_string(),
243            issuer: "https://dev.lcip.org/".to_string(),
244            jwks_uri: "https://oauth-stable.dev.lcip.org/v1/jwks".to_string(),
245            token_endpoint: "https://stable.dev.lcip.org/auth/v1/oauth/token".to_string(),
246            introspection_endpoint: "https://oauth-stable.dev.lcip.org/v1/introspect".to_string(),
247            userinfo_endpoint: "https://stable.dev.lcip.org/profile/v1/profile".to_string(),
248        };
249
250        let config = Config {
251            content_url: "https://stable.dev.lcip.org/".to_string(),
252            remote_config: RefCell::new(Some(Arc::new(remote_config))),
253            client_id: "263ceaa5546dce83".to_string(),
254            redirect_uri: "https://127.0.0.1:8080".to_string(),
255            token_server_url_override: None,
256        };
257        assert_eq!(
258            config.auth_url_path("v1/account/keys").unwrap().to_string(),
259            "https://stable.dev.lcip.org/auth/v1/account/keys"
260        );
261        assert_eq!(
262            config.oauth_url_path("v1/token").unwrap().to_string(),
263            "https://oauth-stable.dev.lcip.org/v1/token"
264        );
265        assert_eq!(
266            config.content_url_path("oauth/signin").unwrap().to_string(),
267            "https://stable.dev.lcip.org/oauth/signin"
268        );
269        assert_eq!(
270            config.token_server_endpoint_url().unwrap().to_string(),
271            "https://stable.dev.lcip.org/syncserver/token/1.0/sync/1.5"
272        );
273
274        assert_eq!(
275            config.token_endpoint().unwrap().to_string(),
276            "https://stable.dev.lcip.org/auth/v1/oauth/token"
277        );
278
279        assert_eq!(
280            config.introspection_endpoint().unwrap().to_string(),
281            "https://oauth-stable.dev.lcip.org/v1/introspect"
282        );
283    }
284
285    #[test]
286    fn test_tokenserver_url_override() {
287        let remote_config = RemoteConfig {
288            auth_url: "https://stable.dev.lcip.org/auth/".to_string(),
289            oauth_url: "https://oauth-stable.dev.lcip.org/".to_string(),
290            profile_url: "https://stable.dev.lcip.org/profile/".to_string(),
291            token_server_endpoint_url: "https://stable.dev.lcip.org/syncserver/token/1.0/sync/1.5"
292                .to_string(),
293            authorization_endpoint: "https://oauth-stable.dev.lcip.org/v1/authorization"
294                .to_string(),
295            issuer: "https://dev.lcip.org/".to_string(),
296            jwks_uri: "https://oauth-stable.dev.lcip.org/v1/jwks".to_string(),
297            token_endpoint: "https://stable.dev.lcip.org/auth/v1/oauth/token".to_string(),
298            introspection_endpoint: "https://oauth-stable.dev.lcip.org/v1/introspect".to_string(),
299            userinfo_endpoint: "https://stable.dev.lcip.org/profile/v1/profile".to_string(),
300        };
301
302        let mut config = Config {
303            content_url: "https://stable.dev.lcip.org/".to_string(),
304            remote_config: RefCell::new(Some(Arc::new(remote_config))),
305            client_id: "263ceaa5546dce83".to_string(),
306            redirect_uri: "https://127.0.0.1:8080".to_string(),
307            token_server_url_override: None,
308        };
309
310        config.override_token_server_url("https://foo.bar");
311
312        assert_eq!(
313            config.token_server_endpoint_url().unwrap().to_string(),
314            "https://foo.bar/"
315        );
316    }
317
318    #[test]
319    fn test_tokenserver_url_override_strips_sync_service_prefix() {
320        let remote_config = RemoteConfig {
321            auth_url: "https://stable.dev.lcip.org/auth/".to_string(),
322            oauth_url: "https://oauth-stable.dev.lcip.org/".to_string(),
323            profile_url: "https://stable.dev.lcip.org/profile/".to_string(),
324            token_server_endpoint_url: "https://stable.dev.lcip.org/syncserver/token/".to_string(),
325            authorization_endpoint: "https://oauth-stable.dev.lcip.org/v1/authorization"
326                .to_string(),
327            issuer: "https://dev.lcip.org/".to_string(),
328            jwks_uri: "https://oauth-stable.dev.lcip.org/v1/jwks".to_string(),
329            token_endpoint: "https://stable.dev.lcip.org/auth/v1/oauth/token".to_string(),
330            introspection_endpoint: "https://oauth-stable.dev.lcip.org/v1/introspect".to_string(),
331            userinfo_endpoint: "https://stable.dev.lcip.org/profile/v1/profile".to_string(),
332        };
333
334        let mut config = Config {
335            content_url: "https://stable.dev.lcip.org/".to_string(),
336            remote_config: RefCell::new(Some(Arc::new(remote_config))),
337            client_id: "263ceaa5546dce83".to_string(),
338            redirect_uri: "https://127.0.0.1:8080".to_string(),
339            token_server_url_override: None,
340        };
341
342        config.override_token_server_url("https://foo.bar/prefix/1.0/sync/1.5");
343        assert_eq!(
344            config.token_server_endpoint_url().unwrap().to_string(),
345            "https://foo.bar/prefix"
346        );
347
348        config.override_token_server_url("https://foo.bar/prefix-1.0/sync/1.5");
349        assert_eq!(
350            config.token_server_endpoint_url().unwrap().to_string(),
351            "https://foo.bar/prefix-1.0/sync/1.5"
352        );
353
354        config.override_token_server_url("https://foo.bar/1.0/sync/1.5/foobar");
355        assert_eq!(
356            config.token_server_endpoint_url().unwrap().to_string(),
357            "https://foo.bar/1.0/sync/1.5/foobar"
358        );
359    }
360}