Skip to main content

libwally/commands/
login.rs

1use std::path::{Path, PathBuf};
2use std::thread::sleep;
3use std::time::Duration;
4
5use anyhow::format_err;
6use opener;
7use reqwest::blocking::Client;
8use reqwest::Url;
9use serde::Deserialize;
10use structopt::StructOpt;
11
12use crate::{
13    auth::AuthStore,
14    manifest::Manifest,
15    package_index::{PackageIndex, PackageIndexConfig},
16};
17
18/// Log into a registry.
19#[derive(Debug, StructOpt)]
20pub struct LoginSubcommand {
21    /// Path to a project to decide how to login
22    #[structopt(long = "project-path", default_value = ".")]
23    pub project_path: PathBuf,
24    /// GitHub auth token to set directly
25    #[structopt(long = "token")]
26    pub token: Option<String>,
27    /// URL of the remote index to add an auth token for
28    #[structopt(long = "api")]
29    pub api: Option<String>,
30}
31
32#[derive(Debug, Deserialize)]
33struct DeviceCodeResponse {
34    device_code: String,
35    user_code: String,
36    verification_uri: String,
37    expires_in: u64,
38    interval: u64,
39}
40
41#[derive(Deserialize)]
42struct DeviceCodeAuth {
43    access_token: String,
44    token_type: String,
45    scope: String,
46}
47
48#[derive(Deserialize)]
49#[serde(untagged)]
50enum AuthResponse {
51    Ok(DeviceCodeAuth),
52    Err { error: String },
53}
54
55fn wait_for_github_auth(
56    device_code_response: DeviceCodeResponse,
57    github_oauth_id: &str,
58) -> anyhow::Result<DeviceCodeAuth> {
59    sleep(Duration::from_secs(device_code_response.interval));
60
61    let client = Client::new();
62    let response = client
63        .post("https://github.com/login/oauth/access_token")
64        .header("accept", "application/json")
65        .json(&serde_json::json!({
66            "client_id": github_oauth_id,
67            "device_code": device_code_response.device_code,
68            "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
69        }))
70        .send()?
71        .json::<AuthResponse>()?;
72
73    match response {
74        AuthResponse::Ok(auth) => Ok(auth),
75        AuthResponse::Err { error } => match error.as_ref() {
76            "authorization_pending" => wait_for_github_auth(device_code_response, github_oauth_id),
77            _ => Err(format_err!("Oauth request error: {}", error)),
78        },
79    }
80}
81
82fn prompt_api_key(api: url::Url) -> anyhow::Result<()> {
83    println!("Enter an API token for {}", api);
84    println!();
85    let token = rpassword::prompt_password_stdout("Enter token: ")?;
86
87    AuthStore::set_token(api.as_str(), Some(&token))
88}
89
90fn prompt_github_auth(api: url::Url, github_oauth_id: &str) -> anyhow::Result<()> {
91    let client = Client::new();
92    let device_code_response = client
93        .post("https://github.com/login/device/code")
94        .header("accept", "application/json")
95        .json(&serde_json::json!({
96            "client_id": github_oauth_id,
97            "scope": "read:user",
98        }))
99        .send()?
100        .json::<DeviceCodeResponse>()?;
101
102    println!();
103    println!("Go to {}", device_code_response.verification_uri);
104    println!("And enter the code: {}", device_code_response.user_code);
105    println!();
106
107    opener::open(&device_code_response.verification_uri).ok();
108
109    println!("Awaiting authorization...");
110    let auth = wait_for_github_auth(device_code_response, github_oauth_id)?;
111
112    println!("Authorization successful!");
113    AuthStore::set_token(api.as_str(), Some(&auth.access_token))
114}
115
116fn fetch_package_index_config(project_path: &Path) -> anyhow::Result<PackageIndexConfig> {
117    let manifest = Manifest::load(project_path)?;
118    let registry = Url::parse(&manifest.package.registry)?;
119    let package_index = PackageIndex::new(&registry, None)?;
120    package_index.config()
121}
122
123impl LoginSubcommand {
124    pub fn run(self) -> anyhow::Result<()> {
125        match (self.token, self.api) {
126            (Some(token), Some(api)) => AuthStore::set_token(&api, Some(&token)),
127            (Some(token), None) => {
128                let config = fetch_package_index_config(&self.project_path)?;
129
130                AuthStore::set_token(config.api.as_str(), Some(&token))
131            }
132            (None, _) => {
133                let config = fetch_package_index_config(&self.project_path)?;
134
135                match config.github_oauth_id {
136                    None => prompt_api_key(config.api),
137                    Some(github_oauth_id) => prompt_github_auth(config.api, &github_oauth_id),
138                }
139            }
140        }
141    }
142}