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
/* 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 crate::prompt::prompt_string;
use fxa_client::{error, AccessTokenInfo, Config, FirefoxAccount};
use std::collections::HashMap;
use std::{
    fs,
    io::{Read, Write},
};
use sync15::{KeyBundle, Sync15StorageClientInit};
use url::Url;

use anyhow::Result;

// Defaults - not clear they are the best option, but they are a currently
// working option.
const CLIENT_ID: &str = "e7ce535d93522896";
const REDIRECT_URI: &str = "https://lockbox.firefox.com/fxa/android-redirect.html";
const SYNC_SCOPE: &str = "https://identity.mozilla.com/apps/oldsync";

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: Config) -> 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)
    })
}

fn create_fxa_creds(path: &str, cfg: Config) -> Result<FirefoxAccount> {
    let mut acct = FirefoxAccount::with_config(cfg);
    let oauth_uri = acct.begin_oauth_flow(&[SYNC_SCOPE], "fxa_creds", None)?;

    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", fxa_client::device::Type::Desktop, &[])?;
    let mut file = fs::File::create(path)?;
    write!(file, "{}", acct.to_json()?)?;
    file.flush()?;
    Ok(acct)
}

// 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() -> Config {
    Config::release(CLIENT_ID, REDIRECT_URI)
}

fn get_account_and_token(
    config: Config,
    cred_file: &str,
) -> Result<(FirefoxAccount, AccessTokenInfo)> {
    // TODO: we should probably set a persist callback on acct?
    let mut acct = load_or_create_fxa_creds(cred_file, config.clone())?;
    // `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.kind() {
                // We can retry an auth error.
                error::ErrorKind::RemoteError { code: 401, .. } => {
                    println!("Saw an auth error using stored credentials - recreating them...");
                    acct = create_fxa_creds(cred_file, config)?;
                    let token = acct.get_access_token(SYNC_SCOPE, None)?;
                    Ok((acct, token))
                }
                _ => Err(e.into()),
            }
        }
    }
}

pub fn get_cli_fxa(config: Config, cred_file: &str) -> Result<CliFxa> {
    let tokenserver_url = config.token_server_endpoint_url()?;
    let (acct, token_info) = match get_account_and_token(config, cred_file) {
        Ok(v) => v,
        Err(e) => anyhow::bail!("Failed to use saved credentials. {}", e),
    };
    let key = token_info.key.unwrap();

    let client_init = Sync15StorageClientInit {
        key_id: key.kid.clone(),
        access_token: token_info.token,
        tokenserver_url: tokenserver_url.clone(),
    };
    let root_sync_key = KeyBundle::from_ksync_bytes(&key.key_bytes()?)?;

    Ok(CliFxa {
        account: acct,
        client_init,
        tokenserver_url,
        root_sync_key,
    })
}

pub struct CliFxa {
    pub account: FirefoxAccount,
    pub client_init: Sync15StorageClientInit,
    pub tokenserver_url: Url,
    pub root_sync_key: KeyBundle,
}