1use 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 #[serde(skip)]
19 remote_config: RefCell<Option<Arc<RemoteConfig>>>,
20}
21
22#[derive(Debug)]
26#[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 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 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)]
191impl 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 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}