better-auth-api 1.0.0-alpha.4

Plugin implementations for better-auth
Documentation
use better_auth_core::{AuthError, AuthResult};
use serde::Deserialize;
use serde_json::Value;

use super::{
    OAuthProvider, OAuthTokenSet, OAuthUserInfo, OAuthUserInfoRequest, OAuthUserInfoResponse,
};
use crate::plugins::oauth::provider_tokens::expiry_at;
use crate::plugins::oauth::social_profile::fetch_http_profile;
use crate::plugins::oauth::token::{TokenGrantType, TokenRequest};

pub(in crate::plugins::oauth) async fn exchange_code(
    provider: &OAuthProvider,
    code: &str,
) -> AuthResult<OAuthTokenSet> {
    let value = TokenRequest::send_query(
        &provider.token_url,
        &[
            ("appid", &provider.client_id),
            ("secret", &provider.client_secret),
            ("code", code),
            ("grant_type", "authorization_code"),
        ],
    )
    .await?;
    tokens(value, TokenGrantType::AuthorizationCode)
}

pub(in crate::plugins::oauth) async fn refresh_tokens(
    provider: &OAuthProvider,
    endpoint: &str,
    refresh_token: &str,
) -> AuthResult<OAuthTokenSet> {
    let value = TokenRequest::send_query(
        endpoint,
        &[
            ("appid", &provider.client_id),
            ("grant_type", "refresh_token"),
            ("refresh_token", refresh_token),
        ],
    )
    .await?;
    tokens(value, TokenGrantType::RefreshToken)
}

fn tokens(value: Value, grant: TokenGrantType) -> AuthResult<OAuthTokenSet> {
    if value.is_null() || provider_error(&value) {
        let operation = match grant {
            TokenGrantType::AuthorizationCode => "validate authorization code",
            TokenGrantType::RefreshToken => "refresh access token",
        };
        let message = value
            .get("errmsg")
            .and_then(Value::as_str)
            .filter(|message| !message.is_empty())
            .unwrap_or("Unknown error");
        return Err(AuthError::internal(format!(
            "Failed to {operation}: {message}"
        )));
    }
    #[derive(Deserialize)]
    struct TokenResponse {
        access_token: String,
        refresh_token: String,
        expires_in: f64,
        scope: String,
    }
    let response: TokenResponse = serde_json::from_value(value.clone())
        .map_err(|error| AuthError::internal(format!("Invalid WeChat token response: {error}")))?;
    Ok(OAuthTokenSet {
        token_type: Some("Bearer".into()),
        access_token: Some(response.access_token),
        refresh_token: Some(response.refresh_token),
        access_token_expires_at: Some(expiry_at(response.expires_in)?),
        scopes: response.scope.split(',').map(str::to_owned).collect(),
        raw: (grant == TokenGrantType::AuthorizationCode).then_some(value),
        ..Default::default()
    })
}

fn provider_error(value: &Value) -> bool {
    value
        .get("errcode")
        .is_some_and(crate::plugins::json_body::is_truthy)
}

pub(in crate::plugins::oauth) async fn fetch_user_info(
    provider: &OAuthProvider,
    request: &OAuthUserInfoRequest,
) -> AuthResult<Option<OAuthUserInfoResponse>> {
    let Some(openid) = request
        .raw
        .as_ref()
        .and_then(|raw| raw.get("openid"))
        .and_then(Value::as_str)
        .filter(|openid| !openid.is_empty())
    else {
        return Ok(None);
    };
    let endpoint = provider
        .user_info_url
        .as_deref()
        .ok_or_else(|| AuthError::internal("Missing user_info_url for provider"))?;
    let request = reqwest::Client::new().get(endpoint).query(&[
        (
            "access_token",
            request.access_token.as_deref().unwrap_or_default(),
        ),
        ("openid", openid),
        ("lang", "zh_CN"),
    ]);
    let Some(data) = fetch_http_profile(request).await? else {
        return Ok(None);
    };
    if data.is_null() || provider_error(&data) {
        return Ok(None);
    }
    Ok(provider
        .decode_profile(data.clone())?
        .map(|user| OAuthUserInfoResponse { user, data }))
}

pub(super) fn decode_profile(profile: Value) -> Result<OAuthUserInfo, String> {
    Ok(OAuthUserInfo {
        id: profile
            .get("unionid")
            .and_then(Value::as_str)
            .filter(|id| !id.is_empty())
            .or_else(|| profile.get("openid").and_then(Value::as_str))
            .ok_or("Missing WeChat profile identifier")?
            .into(),
        name: profile
            .get("nickname")
            .cloned()
            .map(serde_json::from_value::<Option<String>>)
            .transpose()
            .map(|value| value.map(Into::into).unwrap_or_default())
            .map_err(|error| format!("Invalid WeChat nickname: {error}"))?,
        email: super::defaults::profile_email(&profile)?,
        image: profile
            .get("headimgurl")
            .cloned()
            .map(serde_json::from_value)
            .transpose()
            .map_err(|error| format!("Invalid WeChat image: {error}"))?,
        email_verified: Some(false).into(),
        additional_fields: Default::default(),
    })
}