openauth-core 0.0.4

Core types and primitives for OpenAuth.
Documentation
use http::{header, HeaderValue, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::api::{
    json_response, serialize_cookie, ApiRequest, ApiResponse, BodyField, BodySchema,
    JsonSchemaType, PathParams,
};
use crate::auth::oauth::OAuthBaseUrlOverride;
use crate::auth::oauth::OAuthUserInfoError;
use crate::error::OpenAuthError;

#[derive(Debug, Serialize)]
struct SocialRedirectBody {
    url: String,
    redirect: bool,
}

#[derive(Debug, Deserialize)]
pub(super) struct SocialSignInBody {
    pub provider: String,
    #[serde(default, alias = "callbackURL")]
    pub callback_url: Option<String>,
    #[serde(default, alias = "errorCallbackURL")]
    pub error_callback_url: Option<String>,
    #[serde(default, alias = "newUserCallbackURL")]
    pub new_user_callback_url: Option<String>,
    #[serde(default, alias = "disableRedirect")]
    pub disable_redirect: bool,
    #[serde(default)]
    pub scopes: Vec<String>,
    #[serde(default, alias = "loginHint")]
    pub login_hint: Option<String>,
    #[serde(default, alias = "requestSignUp")]
    pub request_sign_up: bool,
    #[serde(default, alias = "additionalData")]
    pub additional_data: Option<Value>,
    #[serde(default, alias = "idToken")]
    pub id_token: Option<IdTokenBody>,
}

#[derive(Debug, Deserialize)]
pub(super) struct LinkSocialBody {
    pub provider: String,
    #[serde(default, alias = "callbackURL")]
    pub callback_url: Option<String>,
    #[serde(default, alias = "errorCallbackURL")]
    pub error_callback_url: Option<String>,
    #[serde(default, alias = "disableRedirect")]
    pub disable_redirect: bool,
    #[serde(default)]
    pub scopes: Vec<String>,
    #[serde(default, alias = "requestSignUp")]
    pub request_sign_up: bool,
    #[serde(default, alias = "additionalData")]
    pub additional_data: Option<Value>,
    #[serde(default, alias = "idToken")]
    pub id_token: Option<IdTokenBody>,
}

#[derive(Debug, Clone, Deserialize)]
pub(super) struct IdTokenBody {
    pub token: String,
    #[serde(default)]
    pub nonce: Option<String>,
    #[serde(default, alias = "accessToken")]
    pub access_token: Option<String>,
    #[serde(default, alias = "refreshToken")]
    pub refresh_token: Option<String>,
    #[serde(default)]
    pub scopes: Vec<String>,
    #[serde(default)]
    pub user: Option<Value>,
}

#[derive(Debug, Serialize)]
pub(super) struct SocialSessionBody {
    pub redirect: bool,
    pub token: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub url: Option<String>,
    pub user: crate::db::User,
}

#[derive(Debug, Serialize)]
pub(super) struct LinkStatusBody {
    pub url: String,
    pub redirect: bool,
    pub status: bool,
}

pub(super) fn path_param<'a>(
    request: &'a ApiRequest,
    name: &str,
) -> Result<&'a str, OpenAuthError> {
    request
        .extensions()
        .get::<PathParams>()
        .and_then(|params| params.get(name))
        .ok_or_else(|| OpenAuthError::Api(format!("missing path param `{name}`")))
}

pub(super) fn redirect_uri(
    context: &crate::context::AuthContext,
    request: &ApiRequest,
    provider_id: &str,
) -> String {
    let base_url = request
        .extensions()
        .get::<OAuthBaseUrlOverride>()
        .map(|value| value.0.as_str())
        .unwrap_or(&context.base_url);
    format!("{}/callback/{provider_id}", base_url.trim_end_matches('/'))
}

pub(super) fn redirect_json_response(
    url: String,
    redirect: bool,
) -> Result<ApiResponse, OpenAuthError> {
    let mut response = json_response(
        StatusCode::OK,
        &SocialRedirectBody {
            url: url.clone(),
            redirect,
        },
        Vec::new(),
    )?;
    if redirect {
        response.headers_mut().insert(
            header::LOCATION,
            HeaderValue::from_str(&url).map_err(|error| OpenAuthError::Api(error.to_string()))?,
        );
    }
    Ok(response)
}

pub(super) fn redirect(
    location: &str,
    cookies: Vec<crate::cookies::Cookie>,
) -> Result<ApiResponse, OpenAuthError> {
    let mut response = http::Response::builder()
        .status(StatusCode::FOUND)
        .header(header::LOCATION, location)
        .body(Vec::new())
        .map_err(|error| OpenAuthError::Api(error.to_string()))?;
    for cookie in cookies {
        response.headers_mut().append(
            header::SET_COOKIE,
            HeaderValue::from_str(&serialize_cookie(&cookie))
                .map_err(|error| OpenAuthError::Cookie(error.to_string()))?,
        );
    }
    Ok(response)
}

pub(super) fn redirect_with_error(
    location: &str,
    error: &str,
) -> Result<ApiResponse, OpenAuthError> {
    let separator = if location.contains('?') { '&' } else { '?' };
    redirect(
        &format!("{location}{separator}error={}", percent_encode(error)),
        Vec::new(),
    )
}

pub(super) fn body_string(body: &Value, key: &str) -> Option<String> {
    body.get(key).and_then(|value| match value {
        Value::String(value) => Some(value.clone()),
        Value::Number(value) => Some(value.to_string()),
        Value::Bool(value) => Some(value.to_string()),
        _ => None,
    })
}

pub(super) fn percent_encode(value: &str) -> String {
    url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
}

pub(super) fn social_sign_in_body_schema() -> BodySchema {
    BodySchema::object([
        BodyField::new("provider", JsonSchemaType::String),
        BodyField::optional("callbackURL", JsonSchemaType::String),
        BodyField::optional("errorCallbackURL", JsonSchemaType::String),
        BodyField::optional("newUserCallbackURL", JsonSchemaType::String),
        BodyField::optional("disableRedirect", JsonSchemaType::Boolean),
        BodyField::optional("scopes", JsonSchemaType::Array),
        BodyField::optional("loginHint", JsonSchemaType::String),
        BodyField::optional("requestSignUp", JsonSchemaType::Boolean),
        BodyField::optional("additionalData", JsonSchemaType::Object),
        BodyField::optional("idToken", JsonSchemaType::Object),
    ])
}

pub(super) fn link_social_body_schema() -> BodySchema {
    BodySchema::object([
        BodyField::new("provider", JsonSchemaType::String),
        BodyField::optional("callbackURL", JsonSchemaType::String),
        BodyField::optional("errorCallbackURL", JsonSchemaType::String),
        BodyField::optional("disableRedirect", JsonSchemaType::Boolean),
        BodyField::optional("scopes", JsonSchemaType::Array),
        BodyField::optional("requestSignUp", JsonSchemaType::Boolean),
        BodyField::optional("additionalData", JsonSchemaType::Object),
        BodyField::optional("idToken", JsonSchemaType::Object),
    ])
}

pub(super) fn oauth_user_info_error(error: OAuthUserInfoError) -> String {
    match error {
        OAuthUserInfoError::AccountNotLinked => "account_not_linked",
        OAuthUserInfoError::SignupDisabled => "signup_disabled",
        OAuthUserInfoError::UnableToCreateUser => "unable_to_create_user",
        OAuthUserInfoError::UnableToCreateSession => "unable_to_create_session",
        OAuthUserInfoError::UnableToLinkAccount => "unable_to_link_account",
    }
    .to_owned()
}