a06673b
use serde::Deserialize;
use crate::config::OAuthProviderConfig;
use crate::error::AuthError;
use crate::features::oauth::types::{OAuthProvider, ProviderIdentity};
#[derive(Debug, Deserialize)]
struct TokenResp {
access_token: String,
refresh_token: Option<String>,
expires_in: Option<i64>,
#[serde(default)]
scope: Option<String>,
}
#[derive(Debug, Deserialize)]
struct UserInfo {
id: i64,
login: String,
name: Option<String>,
email: Option<String>,
}
#[derive(Debug, Deserialize)]
struct UserEmail {
email: String,
primary: bool,
verified: bool,
}
async fn get_emails(
client: &reqwest::Client,
token: &str,
url: &str,
) -> Result<Vec<UserEmail>, AuthError> {
let v: Vec<UserEmail> = client
.get(url)
.bearer_auth(token)
.header("User-Agent", "untitled-auth")
.send()
.await
.map_err(|e| AuthError::Internal(format!("github emails: {e}")))?
.error_for_status()
.map_err(|e| AuthError::Internal(format!("github emails status: {e}")))?
.json()
.await
.map_err(|e| AuthError::Internal(format!("github emails parse: {e}")))?;
Ok(v)
}
/// Exchange a GitHub authorization code for an access token, fetch the user
/// profile, and resolve a primary verified email when needed.
pub async fn fetch_github_identity(
code: &str,
code_verifier: &str,
cfg: &OAuthProviderConfig,
client: &reqwest::Client,
) -> Result<ProviderIdentity, AuthError> {
let token: TokenResp = client
.post(&cfg.token_url)
.header("Accept", "application/json")
.form(&[
("code", code),
("client_id", &cfg.client_id),
("client_secret", &cfg.client_secret),
("redirect_uri", &cfg.redirect_uri),
("code_verifier", code_verifier),
])
.send()
.await
.map_err(|e| AuthError::Internal(format!("github token: {e}")))?
.error_for_status()
.map_err(|e| AuthError::Internal(format!("github token status: {e}")))?
.json()
.await
.map_err(|e| AuthError::Internal(format!("github token parse: {e}")))?;
let user: UserInfo = client
.get(&cfg.userinfo_url)
.bearer_auth(&token.access_token)
.header("User-Agent", "untitled-auth")
.send()
.await
.map_err(|e| AuthError::Internal(format!("github user: {e}")))?
.error_for_status()
.map_err(|e| AuthError::Internal(format!("github user status: {e}")))?
.json()
.await
.map_err(|e| AuthError::Internal(format!("github user parse: {e}")))?;
let mut email = user.email.clone();
let mut email_verified = false;
if (email.is_none() || email.as_deref().map(|e| e.is_empty()).unwrap_or(true))
&& let Some(url) = cfg.emails_url.as_deref()
{
let emails = get_emails(client, &token.access_token, url).await?;
if let Some(primary) = emails.iter().find(|e| e.primary && e.verified) {
email = Some(primary.email.clone());
email_verified = true;
} else if let Some(any_verified) = emails.iter().find(|e| e.verified) {
email = Some(any_verified.email.clone());
email_verified = true;
} else if let Some(first) = emails.first() {
email = Some(first.email.clone());
}
}
let scopes = token
.scope
.as_deref()
.map(|s| s.split(',').map(|x| x.trim().to_string()).collect())
.unwrap_or_default();
let expires_at = token
.expires_in
.map(|secs| time::OffsetDateTime::now_utc() + time::Duration::seconds(secs));
Ok(ProviderIdentity {
provider: OAuthProvider::Github,
provider_user_id: user.id.to_string(),
provider_username: user.name.unwrap_or(user.login),
email,
email_verified,
access_token: token.access_token,
refresh_token: token.refresh_token,
expires_at,
scopes,
})
}