Skip to main content

oxide_cli/auth/
login.rs

1use std::path::Path;
2
3use anyhow::Result;
4use inquire::Confirm;
5
6use crate::{
7  BACKEND_URL,
8  auth::{server::run_local_auth_server, token::get_auth_user},
9};
10
11pub async fn login(auth_path: &Path) -> Result<()> {
12  if let Ok(existing) = get_auth_user(auth_path) {
13    let proceed = Confirm::new(&format!(
14      "Already logged in as @{}. Log in with a different account?",
15      existing.name
16    ))
17    .with_default(false)
18    .prompt()?;
19
20    if !proceed {
21      return Ok(());
22    }
23  }
24
25  let state = generate_state_token();
26  // NOTE: oxide-server must forward the `?state=` query param it receives
27  // at /auth/cli-login through to the localhost callback redirect so that
28  // the CSRF check below can validate it.
29  open::that(format!("{}/auth/cli-login?state={}", BACKEND_URL, state))?;
30  println!("Go to your browser for further authorization");
31  let user = run_local_auth_server(state).await?;
32
33  let auth_json = serde_json::to_string(&user)?;
34  write_auth_file(auth_path, &auth_json)?;
35
36  println!("✅ Authorization successful as @{}", user.name);
37
38  Ok(())
39}
40
41/// Generates a single-use state token from process ID + nanosecond timestamp.
42/// Not cryptographically random, but sufficient to prevent CSRF on localhost
43/// since an attacker cannot predict both values within the login window.
44fn generate_state_token() -> String {
45  use std::collections::hash_map::DefaultHasher;
46  use std::hash::{Hash, Hasher};
47
48  let mut hasher = DefaultHasher::new();
49  std::time::SystemTime::now().hash(&mut hasher);
50  std::process::id().hash(&mut hasher);
51  format!("{:016x}", hasher.finish())
52}
53
54/// Writes `content` to `path` with owner-only read/write permissions (0600)
55/// on Unix, preventing other local users from reading the auth token.
56fn write_auth_file(path: &Path, content: &str) -> Result<()> {
57  #[cfg(unix)]
58  {
59    use std::io::Write;
60    use std::os::unix::fs::OpenOptionsExt;
61    let mut file = std::fs::OpenOptions::new()
62      .write(true)
63      .create(true)
64      .truncate(true)
65      .mode(0o600)
66      .open(path)?;
67    file.write_all(content.as_bytes())?;
68  }
69  #[cfg(not(unix))]
70  {
71    std::fs::write(path, content)?;
72  }
73  Ok(())
74}