openauth-plugins 0.0.3

Official OpenAuth plugin modules.
Documentation
use openauth_core::auth::oauth::{
    set_token_util, OAuthAccountInput, OAuthStateLink, OAuthUserInfo,
};
use openauth_core::context::AuthContext;
use openauth_core::db::DbAdapter;
use openauth_core::error::OpenAuthError;
use openauth_core::user::{CreateOAuthAccountInput, DbUserStore, UpdateAccountInput};
use openauth_oauth::oauth2::{OAuth2Tokens, OAuth2UserInfo};

use super::config::GenericOAuthConfig;

pub(super) async fn link_account(
    context: &AuthContext,
    adapter: &dyn DbAdapter,
    config: &GenericOAuthConfig,
    link: &OAuthStateLink,
    info: &OAuth2UserInfo,
    tokens: &OAuth2Tokens,
) -> Result<(), OpenAuthError> {
    let normalized = normalize_user_info(info)?;
    if normalized.email.to_lowercase() != link.email.to_lowercase()
        && !context
            .options
            .account
            .account_linking
            .allow_different_emails
    {
        return Err(OpenAuthError::Api("email_doesn't_match".to_owned()));
    }
    let store = DbUserStore::new(adapter);
    if let Some(existing) = store
        .find_account_by_provider_account(&normalized.id, &config.provider_id)
        .await?
    {
        if existing.user_id != link.user_id {
            return Err(OpenAuthError::Api(
                "account_already_linked_to_different_user".to_owned(),
            ));
        }
        store
            .update_account(
                &existing.id,
                UpdateAccountInput {
                    access_token: Some(set_token_util(tokens.access_token.as_deref(), context)?),
                    refresh_token: Some(set_token_util(tokens.refresh_token.as_deref(), context)?),
                    id_token: Some(tokens.id_token.clone()),
                    access_token_expires_at: Some(tokens.access_token_expires_at),
                    refresh_token_expires_at: Some(tokens.refresh_token_expires_at),
                    scope: Some((!tokens.scopes.is_empty()).then(|| tokens.scopes.join(","))),
                },
            )
            .await?;
        return Ok(());
    }
    store
        .link_account(CreateOAuthAccountInput {
            id: None,
            provider_id: config.provider_id.clone(),
            account_id: normalized.id,
            user_id: link.user_id.clone(),
            access_token: set_token_util(tokens.access_token.as_deref(), context)?,
            refresh_token: set_token_util(tokens.refresh_token.as_deref(), context)?,
            id_token: tokens.id_token.clone(),
            access_token_expires_at: tokens.access_token_expires_at,
            refresh_token_expires_at: tokens.refresh_token_expires_at,
            scope: (!tokens.scopes.is_empty()).then(|| tokens.scopes.join(",")),
        })
        .await?;
    Ok(())
}

pub(super) fn oauth_account(
    context: &AuthContext,
    provider_id: &str,
    account_id: &str,
    tokens: &OAuth2Tokens,
) -> Result<OAuthAccountInput, OpenAuthError> {
    Ok(OAuthAccountInput {
        provider_id: provider_id.to_owned(),
        account_id: account_id.to_owned(),
        access_token: set_token_util(tokens.access_token.as_deref(), context)?,
        refresh_token: set_token_util(tokens.refresh_token.as_deref(), context)?,
        id_token: tokens.id_token.clone(),
        access_token_expires_at: tokens.access_token_expires_at,
        refresh_token_expires_at: tokens.refresh_token_expires_at,
        scope: (!tokens.scopes.is_empty()).then(|| tokens.scopes.join(",")),
    })
}

pub(super) fn normalize_user_info(info: &OAuth2UserInfo) -> Result<OAuthUserInfo, OpenAuthError> {
    let email = info
        .email
        .clone()
        .ok_or_else(|| OpenAuthError::Api("OAuth provider did not return an email".to_owned()))?;
    Ok(OAuthUserInfo {
        id: info.id.clone(),
        name: info.name.clone().unwrap_or_default(),
        email,
        image: info.image.clone(),
        email_verified: info.email_verified,
    })
}

pub(super) fn link_error_code(error: &OpenAuthError) -> &str {
    match error {
        OpenAuthError::Api(code) if code == "email_doesn't_match" => "email_doesn't_match",
        OpenAuthError::Api(code) if code == "account_already_linked_to_different_user" => {
            "account_already_linked_to_different_user"
        }
        _ => "unable_to_link_account",
    }
}