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, expires_in: Option, #[serde(default)] scope: Option, } #[derive(Debug, Deserialize)] struct UserInfo { sub: String, email: Option, email_verified: Option, name: Option, #[allow(dead_code)] picture: Option, } /// 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 { 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, }) }