a06673b
use serde::Deserialize;
use time::OffsetDateTime;
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 {
sub: String,
email: Option<String>,
email_verified: Option<bool>,
name: Option<String>,
#[allow(dead_code)]
picture: Option<String>,
}
/// Exchange a Google authorization code for an access token, then fetch the
/// user's profile. Returns a normalised [`ProviderIdentity`].
pub async fn fetch_google_identity(
code: &str,
code_verifier: &str,
cfg: &OAuthProviderConfig,
client: &reqwest::Client,
) -> Result<ProviderIdentity, AuthError> {
let token: TokenResp = client
.post(&cfg.token_url)
.form(&[
("code", code),
("client_id", &cfg.client_id),
("client_secret", &cfg.client_secret),
("redirect_uri", &cfg.redirect_uri),
("grant_type", "authorization_code"),
("code_verifier", code_verifier),
])
.send()
.await
.map_err(|e| AuthError::Internal(format!("google token: {e}")))?
.error_for_status()
.map_err(|e| AuthError::Internal(format!("google token status: {e}")))?
.json()
.await
.map_err(|e| AuthError::Internal(format!("google token parse: {e}")))?;
let info: UserInfo = client
.get(&cfg.userinfo_url)
.bearer_auth(&token.access_token)
.send()
.await
.map_err(|e| AuthError::Internal(format!("google userinfo: {e}")))?
.error_for_status()
.map_err(|e| AuthError::Internal(format!("google userinfo status: {e}")))?
.json()
.await
.map_err(|e| AuthError::Internal(format!("google userinfo parse: {e}")))?;
let scopes = token
.scope
.as_deref()
.map(|s| s.split_whitespace().map(|x| x.to_string()).collect())
.unwrap_or_default();
let expires_at = token
.expires_in
.map(|secs| OffsetDateTime::now_utc() + time::Duration::seconds(secs));
Ok(ProviderIdentity {
provider: OAuthProvider::Google,
provider_user_id: info.sub,
provider_username: info.name.unwrap_or_default(),
email: info.email,
email_verified: info.email_verified.unwrap_or(false),
access_token: token.access_token,
refresh_token: token.refresh_token,
expires_at,
scopes,
})
}