cli_support/
fxa_creds.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/// Utilities for command-line utilities which want to use fxa credentials.
use std::{
    collections::HashMap,
    fs,
    io::{Read, Write},
};

use anyhow::Result;
use url::Url;

// This crate awkardly uses some internal implementation details of the fxa-client crate,
// because we haven't worked on exposing those test-only features via UniFFI.
use fxa_client::{AccessTokenInfo, FirefoxAccount, FxaConfig, FxaError};
use sync15::client::Sync15StorageClientInit;
use sync15::KeyBundle;

use crate::prompt::prompt_string;

// Defaults - not clear they are the best option, but they are a currently
// working option.
const CLIENT_ID: &str = "3c49430b43dfba77";
const REDIRECT_URI: &str = "https://accounts.firefox.com/oauth/success/3c49430b43dfba77";
pub const SYNC_SCOPE: &str = "https://identity.mozilla.com/apps/oldsync";
pub const SESSION_SCOPE: &str = "https://identity.mozilla.com/tokens/session";

fn load_fxa_creds(path: &str) -> Result<FirefoxAccount> {
    let mut file = fs::File::open(path)?;
    let mut s = String::new();
    file.read_to_string(&mut s)?;
    Ok(FirefoxAccount::from_json(&s)?)
}

fn load_or_create_fxa_creds(path: &str, cfg: FxaConfig, scopes: &[&str]) -> Result<FirefoxAccount> {
    load_fxa_creds(path).or_else(|e| {
        log::info!(
            "Failed to load existing FxA credentials from {:?} (error: {}), launching OAuth flow",
            path,
            e
        );
        create_fxa_creds(path, cfg, scopes)
    })
}

fn create_fxa_creds(path: &str, cfg: FxaConfig, scopes: &[&str]) -> Result<FirefoxAccount> {
    let acct = FirefoxAccount::new(cfg);
    handle_oauth_flow(path, &acct, scopes)?;
    Ok(acct)
}

fn handle_oauth_flow(path: &str, acct: &FirefoxAccount, scopes: &[&str]) -> Result<()> {
    let oauth_uri = acct.begin_oauth_flow(scopes, "fxa_creds")?;

    if webbrowser::open(oauth_uri.as_ref()).is_err() {
        log::warn!("Failed to open a web browser D:");
        println!("Please visit this URL, sign in, and then copy-paste the final URL below.");
        println!("\n    {}\n", oauth_uri);
    } else {
        println!("Please paste the final URL below:\n");
    }

    let final_url = url::Url::parse(&prompt_string("Final URL").unwrap_or_default())?;
    let query_params = final_url
        .query_pairs()
        .into_owned()
        .collect::<HashMap<String, String>>();

    acct.complete_oauth_flow(&query_params["code"], &query_params["state"])?;
    // Device registration.
    acct.initialize_device("CLI Device", sync15::DeviceType::Desktop, vec![])?;
    let mut file = fs::File::create(path)?;
    write!(file, "{}", acct.to_json()?)?;
    file.flush()?;
    Ok(())
}

// Our public functions. It would be awesome if we could somehow integrate
// better with clap, so we could automagically support various args (such as
// the config to use or filenames to read), but this will do for now.
pub fn get_default_fxa_config() -> FxaConfig {
    FxaConfig::release(CLIENT_ID, REDIRECT_URI)
}

pub fn get_account_and_token(
    config: FxaConfig,
    cred_file: &str,
    scopes: &[&str],
) -> Result<(FirefoxAccount, AccessTokenInfo)> {
    // TODO: we should probably set a persist callback on acct?
    let acct = load_or_create_fxa_creds(cred_file, config.clone(), scopes)?;
    // `scope` could be a param, but I can't see it changing.
    match acct.get_access_token(SYNC_SCOPE, None) {
        Ok(t) => Ok((acct, t)),
        Err(e) => {
            match e {
                // We can retry an auth error.
                FxaError::Authentication => {
                    println!("Saw an auth error using stored credentials - attempting to re-authenticate");
                    println!("If fails, consider deleting {cred_file} to start from scratch");
                    handle_oauth_flow(cred_file, &acct, scopes)?;
                    let token = acct.get_access_token(SYNC_SCOPE, None)?;
                    Ok((acct, token))
                }
                _ => Err(e.into()),
            }
        }
    }
}

pub fn get_cli_fxa(config: FxaConfig, cred_file: &str, scopes: &[&str]) -> Result<CliFxa> {
    let (account, token_info) = match get_account_and_token(config, cred_file, scopes) {
        Ok(v) => v,
        Err(e) => anyhow::bail!("Failed to use saved credentials. {}", e),
    };
    let tokenserver_url = Url::parse(&account.get_token_server_endpoint_url()?)?;

    let client_init = Sync15StorageClientInit {
        key_id: token_info.key.as_ref().unwrap().kid.clone(),
        access_token: token_info.token.clone(),
        tokenserver_url: tokenserver_url.clone(),
    };

    Ok(CliFxa {
        account,
        client_init,
        tokenserver_url,
        token_info,
    })
}

pub struct CliFxa {
    pub account: FirefoxAccount,
    pub client_init: Sync15StorageClientInit,
    pub tokenserver_url: Url,
    pub token_info: AccessTokenInfo,
}

impl CliFxa {
    // A helper for consumers who use this with the sync manager.
    pub fn as_auth_info(&self) -> sync_manager::SyncAuthInfo {
        let scoped_key = self.token_info.key.as_ref().unwrap();
        sync_manager::SyncAuthInfo {
            kid: scoped_key.kid.clone(),
            sync_key: scoped_key.k.clone(),
            fxa_access_token: self.token_info.token.clone(),
            tokenserver_url: self.tokenserver_url.to_string(),
        }
    }

    // A helper for consumers who use this directly with sync15
    pub fn as_key_bundle(&self) -> Result<KeyBundle> {
        let scoped_key = self.token_info.key.as_ref().unwrap();
        Ok(KeyBundle::from_ksync_bytes(&scoped_key.key_bytes()?)?)
    }
}