cli_support/
fxa_creds.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
5/// Utilities for command-line utilities which want to use fxa credentials.
6use std::{
7    collections::HashMap,
8    fs,
9    io::{Read, Write},
10};
11
12use anyhow::Result;
13use url::Url;
14
15// This crate awkardly uses some internal implementation details of the fxa-client crate,
16// because we haven't worked on exposing those test-only features via UniFFI.
17use fxa_client::{AccessTokenInfo, FirefoxAccount, FxaConfig, FxaError};
18use sync15::client::Sync15StorageClientInit;
19use sync15::KeyBundle;
20
21use crate::prompt::prompt_string;
22
23// Defaults - not clear they are the best option, but they are a currently
24// working option.
25const CLIENT_ID: &str = "3c49430b43dfba77";
26const REDIRECT_URI: &str = "https://accounts.firefox.com/oauth/success/3c49430b43dfba77";
27pub const SYNC_SCOPE: &str = "https://identity.mozilla.com/apps/oldsync";
28pub const SESSION_SCOPE: &str = "https://identity.mozilla.com/tokens/session";
29
30fn load_fxa_creds(path: &str) -> Result<FirefoxAccount> {
31    let mut file = fs::File::open(path)?;
32    let mut s = String::new();
33    file.read_to_string(&mut s)?;
34    Ok(FirefoxAccount::from_json(&s)?)
35}
36
37fn load_or_create_fxa_creds(path: &str, cfg: FxaConfig, scopes: &[&str]) -> Result<FirefoxAccount> {
38    load_fxa_creds(path).or_else(|e| {
39        log::info!(
40            "Failed to load existing FxA credentials from {:?} (error: {}), launching OAuth flow",
41            path,
42            e
43        );
44        create_fxa_creds(path, cfg, scopes)
45    })
46}
47
48fn create_fxa_creds(path: &str, cfg: FxaConfig, scopes: &[&str]) -> Result<FirefoxAccount> {
49    let acct = FirefoxAccount::new(cfg);
50    handle_oauth_flow(path, &acct, scopes)?;
51    Ok(acct)
52}
53
54fn handle_oauth_flow(path: &str, acct: &FirefoxAccount, scopes: &[&str]) -> Result<()> {
55    let oauth_uri = acct.begin_oauth_flow(scopes, "fxa_creds")?;
56
57    if webbrowser::open(oauth_uri.as_ref()).is_err() {
58        log::warn!("Failed to open a web browser D:");
59        println!("Please visit this URL, sign in, and then copy-paste the final URL below.");
60        println!("\n    {}\n", oauth_uri);
61    } else {
62        println!("Please paste the final URL below:\n");
63    }
64
65    let final_url = url::Url::parse(&prompt_string("Final URL").unwrap_or_default())?;
66    let query_params = final_url
67        .query_pairs()
68        .into_owned()
69        .collect::<HashMap<String, String>>();
70
71    acct.complete_oauth_flow(&query_params["code"], &query_params["state"])?;
72    // Device registration.
73    acct.initialize_device("CLI Device", sync15::DeviceType::Desktop, vec![])?;
74    let mut file = fs::File::create(path)?;
75    write!(file, "{}", acct.to_json()?)?;
76    file.flush()?;
77    Ok(())
78}
79
80// Our public functions. It would be awesome if we could somehow integrate
81// better with clap, so we could automagically support various args (such as
82// the config to use or filenames to read), but this will do for now.
83pub fn get_default_fxa_config() -> FxaConfig {
84    FxaConfig::release(CLIENT_ID, REDIRECT_URI)
85}
86
87pub fn get_account_and_token(
88    config: FxaConfig,
89    cred_file: &str,
90    scopes: &[&str],
91) -> Result<(FirefoxAccount, AccessTokenInfo)> {
92    // TODO: we should probably set a persist callback on acct?
93    let acct = load_or_create_fxa_creds(cred_file, config.clone(), scopes)?;
94    // `scope` could be a param, but I can't see it changing.
95    match acct.get_access_token(SYNC_SCOPE, None) {
96        Ok(t) => Ok((acct, t)),
97        Err(e) => {
98            match e {
99                // We can retry an auth error.
100                FxaError::Authentication => {
101                    println!("Saw an auth error using stored credentials - attempting to re-authenticate");
102                    println!("If fails, consider deleting {cred_file} to start from scratch");
103                    handle_oauth_flow(cred_file, &acct, scopes)?;
104                    let token = acct.get_access_token(SYNC_SCOPE, None)?;
105                    Ok((acct, token))
106                }
107                _ => Err(e.into()),
108            }
109        }
110    }
111}
112
113pub fn get_cli_fxa(config: FxaConfig, cred_file: &str, scopes: &[&str]) -> Result<CliFxa> {
114    let (account, token_info) = match get_account_and_token(config, cred_file, scopes) {
115        Ok(v) => v,
116        Err(e) => anyhow::bail!("Failed to use saved credentials. {}", e),
117    };
118    let tokenserver_url = Url::parse(&account.get_token_server_endpoint_url()?)?;
119
120    let client_init = Sync15StorageClientInit {
121        key_id: token_info.key.as_ref().unwrap().kid.clone(),
122        access_token: token_info.token.clone(),
123        tokenserver_url: tokenserver_url.clone(),
124    };
125
126    Ok(CliFxa {
127        account,
128        client_init,
129        tokenserver_url,
130        token_info,
131    })
132}
133
134pub struct CliFxa {
135    pub account: FirefoxAccount,
136    pub client_init: Sync15StorageClientInit,
137    pub tokenserver_url: Url,
138    pub token_info: AccessTokenInfo,
139}
140
141impl CliFxa {
142    // A helper for consumers who use this with the sync manager.
143    pub fn as_auth_info(&self) -> sync_manager::SyncAuthInfo {
144        let scoped_key = self.token_info.key.as_ref().unwrap();
145        sync_manager::SyncAuthInfo {
146            kid: scoped_key.kid.clone(),
147            sync_key: scoped_key.k.clone(),
148            fxa_access_token: self.token_info.token.clone(),
149            tokenserver_url: self.tokenserver_url.to_string(),
150        }
151    }
152
153    // A helper for consumers who use this directly with sync15
154    pub fn as_key_bundle(&self) -> Result<KeyBundle> {
155        let scoped_key = self.token_info.key.as_ref().unwrap();
156        Ok(KeyBundle::from_ksync_bytes(&scoped_key.key_bytes()?)?)
157    }
158}