better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::super::{
    GenericOAuthConfig, OAuthAccountSubject, OAuthCodeExchange, OAuthIdTokenVerifier,
    OAuthProfileMapper, OAuthProvider, OAuthTokenHandler, OAuthTokenSet, OAuthUserInfoHandler,
    OAuthUserInfoRequest,
    generic::{GenericOAuthProfileContext, GenericOAuthUserInfoHandler},
};
use super::*;
use crate::plugins::{
    endpoint_context::EndpointContext,
    user_admission::{UserValidationData, UserValidationRejection, ValidateUserInfo},
};
use async_trait::async_trait;
use better_auth_core::{AuthSchema, NativeRequest};

pub(super) struct Callbacks(pub Arc<State>);

impl Callbacks {
    fn get(&self, tokens: &OAuthUserInfoRequest) -> AuthResult<()> {
        let fields = tokens.fields();
        if self.0.scenario.provider == "social" && self.0.scenario.operation == "callback" {
            self.0
                .generated_date(fields.get("accessTokenExpiresAt").unwrap(), 3_600_000)?;
        }
        self.0
            .log
            .event(json!({"kind": "get-user-info", "tokens": observe::native(&fields.into())?}));
        Ok(())
    }
}

#[async_trait]
impl GenericOAuthUserInfoHandler for Callbacks {
    async fn get_user_info_native(
        &self,
        tokens: &OAuthUserInfoRequest,
        _: GenericOAuthProfileContext<'_>,
    ) -> AuthResult<FieldValue> {
        self.get(tokens)?;
        Ok(self.0.raw.clone())
    }
}

#[async_trait]
impl OAuthUserInfoHandler for Callbacks {
    async fn get_user_info_native(&self, tokens: OAuthUserInfoRequest) -> AuthResult<FieldValue> {
        self.get(&tokens)?;
        self.0.tracking.lock().unwrap().info = Some(self.0.social_response.clone());
        Ok(self.0.social_response.clone())
    }
}

#[async_trait]
impl OAuthProfileMapper for Callbacks {
    async fn map_profile_native(&self, raw: &FieldValue) -> AuthResult<FieldValue> {
        if self.0.scenario.provider == "social" {
            self.0
                .log
                .event(json!({"kind": "unexpected-social-mapper"}));
            return Err(AuthError::from_thrown(self.0.failure.clone()));
        }
        self.0.tracking.lock().unwrap().mapper_input = Some(raw.clone());
        self.0
            .log
            .event(json!({"kind": "mapper", "sameProfile": raw.strict_equals(&self.0.raw)}));
        if self.0.scenario.shape == "mapper-error" {
            return Err(AuthError::from_thrown(self.0.failure.clone()));
        }
        if self.0.scenario.shape == "identity" {
            let _ = raw
                .as_object()
                .unwrap()
                .set("mapperMarker", self.0.marker.clone())?;
        }
        Ok(self.0.mapped.clone())
    }
}

#[async_trait]
impl OAuthAccountSubject for Callbacks {
    async fn resolve_subject_native(
        &self,
        tokens: &FieldValue,
        profile: &FieldValue,
    ) -> AuthResult<FieldValue> {
        self.0.tracking.lock().unwrap().subject_input = Some(profile.clone());
        let same = profile.strict_equals(&self.0.raw);
        let tokens = observe::native(tokens)?;
        self.0
            .log
            .event(json!({"kind": "subject", "sameProfile": same, "tokens": tokens}));
        if self.0.scenario.provider == "social" {
            return Ok(OAuthProvider::google("client", "secret")
                .builtin_account_subject(profile)?
                .unwrap());
        }
        self.0
            .log
            .event(json!({"kind": "generic-subject", "sameProfile": same, "tokens": tokens}));
        if self.0.scenario.operation == "account-info" {
            return Err(AuthError::from_thrown(self.0.failure.clone()));
        }
        if self.0.scenario.shape == "identity" {
            assert!(
                profile
                    .model_property("mapperMarker")?
                    .strict_equals(&self.0.marker)
            );
            let _ = profile
                .as_object()
                .unwrap()
                .set("subjectMarker", self.0.marker.clone())?;
            return Ok(23.0.into());
        }
        profile.model_property("id")
    }
}

#[async_trait]
impl OAuthTokenHandler for Callbacks {
    async fn get_token(&self, request: OAuthCodeExchange<'_>) -> AuthResult<OAuthTokenSet> {
        self.0
            .remember(request.code_verifier.unwrap(), "code-verifier");
        let input = object([
            ("code", request.code.into()),
            (
                "codeVerifier",
                request
                    .code_verifier
                    .map_or(FieldValue::Undefined, Into::into),
            ),
            (
                "deviceId",
                request.device_id.map_or(FieldValue::Undefined, Into::into),
            ),
            ("redirectURI", request.redirect_uri.into()),
        ]);
        self.0
            .log
            .event(json!({"kind": "token", "input": observe::native(&input)?}));
        Ok(OAuthTokenSet::from_fields(fields([
            ("accessToken", ACCESS_TOKEN.into()),
            ("scopes", vec!["profile".into()].into()),
        ])))
    }
}

#[async_trait]
impl OAuthIdTokenVerifier for Callbacks {
    async fn verify_id_token(
        &self,
        token: &str,
        nonce: Option<&str>,
        _: Option<NativeRequest<'_>>,
    ) -> Result<bool, String> {
        let path = better_auth_core::hooks::current_request_hook_context()
            .unwrap()
            .path;
        self.0
            .log
            .event(json!({"kind": "verify", "token": token, "nonce": nonce, "path": path}));
        Ok(true)
    }
}

#[async_trait]
impl<S: AuthSchema> ValidateUserInfo<S> for Callbacks {
    async fn validate(
        &self,
        data: &UserValidationData,
        context: &EndpointContext<'_, S>,
    ) -> AuthResult<Option<UserValidationRejection>> {
        let oauth = data.source.oauth.as_ref().unwrap();
        let profile = oauth.profile.as_ref().unwrap();
        self.0.generated_record_dates(&data.user, false)?;
        let (mapper, subject) = {
            let tracking = self.0.tracking.lock().unwrap();
            (
                tracking.mapper_input.clone(),
                tracking.subject_input.clone(),
            )
        };
        let source = object([
            ("method", data.source.method.clone().into()),
            (
                "oauth",
                object([
                    ("providerId", oauth.provider_id.field_value()),
                    ("profile", profile.clone()),
                ]),
            ),
            (
                "action",
                FieldValue::from(serde_json::to_value(data.source.action)?),
            ),
        ]);
        self.0.tracking.lock().unwrap().admissions.push(object([
            ("user", data.user.clone().into()),
            ("source", source),
        ]));
        self.0.log.event(json!({
            "kind": "admission", "path": context.path, "action": data.source.action,
            "method": data.source.method, "providerId": oauth.provider_id,
            "sameProfile": profile.strict_equals(&self.0.raw),
            "sameMapperProfile": mapper.as_ref().is_some_and(|value| profile.strict_equals(value)),
            "sameSubjectProfile": subject.as_ref().is_some_and(|value| profile.strict_equals(value)),
            "user": observe::native(&data.user.clone().into())?
        }));
        if self.0.scenario.shape == "identity" {
            assert!(profile.strict_equals(mapper.as_ref().unwrap()));
            assert!(profile.strict_equals(subject.as_ref().unwrap()));
            assert!(
                profile
                    .model_property("subjectMarker")?
                    .strict_equals(&self.0.marker)
            );
            let _ = profile
                .as_object()
                .unwrap()
                .set("admissionMarker", self.0.marker.clone())?;
        }
        Ok(None)
    }
}

pub(super) fn generic(callbacks: &Arc<Callbacks>) -> GenericOAuthConfig {
    GenericOAuthConfig {
        client_id: "client".into(),
        authorization_url: Some("https://provider.example.test/authorize".into()),
        get_token: Some(callbacks.clone()),
        get_user_info: Some(callbacks.clone()),
        map_profile_to_user: Some(callbacks.clone()),
        account_subject: Some(callbacks.clone()),
        ..Default::default()
    }
}