Skip to main content

gor/auth/
token.rs

1//! Token verification and stdin reading utilities.
2//!
3//! Provides functions to verify that an OAuth token is valid by calling
4//! `GET /user`, and to read tokens from stdin (for piping).
5
6use crate::error::GorError;
7use crate::host::Host;
8use serde::Deserialize;
9
10/// Minimal user response from `GET /user` for token verification.
11#[derive(Debug, Deserialize)]
12pub struct UserResponse {
13    /// The authenticated user's login name.
14    pub login: String,
15}
16
17/// Verify that an OAuth token is valid by calling `GET /user`.
18///
19/// Returns the authenticated user's login name on success.
20///
21/// # Errors
22///
23/// Returns [`GorError::Auth`] if the token is invalid or the request fails.
24///
25/// # Examples
26///
27/// ```no_run
28/// use gor::auth::token::verify_token;
29/// use gor::host::Host;
30///
31/// let host = Host::new("github.com");
32/// let login = verify_token(&host, "gho_abc123").unwrap();
33/// println!("Authenticated as {login}");
34/// ```
35pub fn verify_token(host: &Host, token: &str) -> Result<String, GorError> {
36    let client = reqwest::blocking::Client::new();
37    let url = host.api_url("/user");
38
39    tracing::info!("Verifying token with GET /user");
40    let response = client
41        .get(&url)
42        .header("Accept", "application/vnd.github+json")
43        .header("Authorization", format!("Bearer {token}"))
44        .header("User-Agent", concat!("gor/", env!("CARGO_PKG_VERSION")))
45        .send()
46        .map_err(GorError::Http)?;
47
48    let status = response.status();
49    if status == reqwest::StatusCode::UNAUTHORIZED {
50        return Err(GorError::Auth(
51            "token is invalid or expired (HTTP 401)".to_string(),
52        ));
53    }
54    if status == reqwest::StatusCode::FORBIDDEN {
55        return Err(GorError::Auth(
56            "token lacks required permissions (HTTP 403)".to_string(),
57        ));
58    }
59    if !status.is_success() {
60        let body = response.text().unwrap_or_default();
61        return Err(GorError::Auth(format!(
62            "token verification failed ({status}): {body}"
63        )));
64    }
65
66    let user: UserResponse = response
67        .json()
68        .map_err(|e| GorError::Auth(format!("failed to parse user response: {e}")))?;
69
70    Ok(user.login)
71}
72
73/// Read a token from stdin, trimming whitespace.
74///
75/// Used for piping tokens: `echo gho_abc | gor auth login --with-token`
76///
77/// # Errors
78///
79/// Returns an I/O error if stdin cannot be read.
80///
81/// # Examples
82///
83/// ```no_run
84/// use gor::auth::token::read_token_from_stdin;
85///
86/// let token = read_token_from_stdin().unwrap();
87/// ```
88pub fn read_token_from_stdin() -> Result<String, GorError> {
89    use std::io::Read;
90    let mut buffer = String::new();
91    std::io::stdin()
92        .read_to_string(&mut buffer)
93        .map_err(GorError::Io)?;
94    Ok(buffer.trim().to_string())
95}