openauth-core 0.0.2

Core types and primitives for OpenAuth.
Documentation
use crate::env::is_production;
use crate::error::OpenAuthError;
use crate::options::{CookieAttributesOverride, OpenAuthOptions};

use super::types::{AuthCookie, AuthCookies, CookieOptions, SECURE_COOKIE_PREFIX};

pub fn get_cookies(options: &OpenAuthOptions) -> Result<AuthCookies, OpenAuthError> {
    let secure = resolve_secure(options);
    let secure_prefix = if secure { SECURE_COOKIE_PREFIX } else { "" };
    let prefix = options
        .advanced
        .cookie_prefix
        .as_deref()
        .unwrap_or("better-auth");
    let domain = resolve_domain(options)?;
    let session_max_age = options.session.expires_in.unwrap_or(60 * 60 * 24 * 7);
    let cache_max_age = options.session.cookie_cache.max_age.unwrap_or(60 * 5);

    let create = |name: &str, max_age: Option<u64>| AuthCookie {
        name: format!("{secure_prefix}{prefix}.{name}"),
        attributes: merge_cookie_attributes(
            CookieOptions {
                max_age,
                expires: None,
                domain: domain.clone(),
                path: Some("/".to_owned()),
                secure: Some(secure),
                http_only: Some(true),
                same_site: Some("lax".to_owned()),
                partitioned: None,
            },
            &options.advanced.default_cookie_attributes,
        ),
    };

    Ok(AuthCookies {
        session_token: create("session_token", Some(session_max_age)),
        session_data: create("session_data", Some(cache_max_age)),
        account_data: create("account_data", Some(cache_max_age)),
        dont_remember_token: create("dont_remember", None),
    })
}

fn resolve_secure(options: &OpenAuthOptions) -> bool {
    if let Some(secure) = options.advanced.use_secure_cookies {
        return secure;
    }
    if let Some(base_url) = &options.base_url {
        return base_url.starts_with("https://");
    }
    options.production || is_production()
}

fn resolve_domain(options: &OpenAuthOptions) -> Result<Option<String>, OpenAuthError> {
    let Some(config) = &options.advanced.cross_subdomain_cookies else {
        return Ok(None);
    };
    if !config.enabled {
        return Ok(None);
    }
    if let Some(domain) = &config.domain {
        return Ok(Some(domain.clone()));
    }
    let Some(base_url) = &options.base_url else {
        return Err(OpenAuthError::Cookie(
            "base_url is required when cross-subdomain cookies are enabled".to_owned(),
        ));
    };
    host_from_url(base_url)
        .map(Some)
        .ok_or_else(|| OpenAuthError::Cookie("could not resolve cookie domain".to_owned()))
}

fn host_from_url(url: &str) -> Option<String> {
    let (_, rest) = url.split_once("://")?;
    let host = rest.split('/').next().unwrap_or(rest);
    let host = host.split(':').next().unwrap_or(host);
    (!host.is_empty()).then(|| host.to_owned())
}

fn merge_cookie_attributes(
    mut base: CookieOptions,
    override_attrs: &CookieAttributesOverride,
) -> CookieOptions {
    if override_attrs.domain.is_some() {
        base.domain.clone_from(&override_attrs.domain);
    }
    if override_attrs.path.is_some() {
        base.path.clone_from(&override_attrs.path);
    }
    if override_attrs.secure.is_some() {
        base.secure = override_attrs.secure;
    }
    if override_attrs.http_only.is_some() {
        base.http_only = override_attrs.http_only;
    }
    if override_attrs.same_site.is_some() {
        base.same_site.clone_from(&override_attrs.same_site);
    }
    if override_attrs.max_age.is_some() {
        base.max_age = override_attrs.max_age;
    }
    if override_attrs.partitioned.is_some() {
        base.partitioned = override_attrs.partitioned;
    }
    base
}

pub(super) fn merge_options(mut base: CookieOptions, overrides: CookieOptions) -> CookieOptions {
    if overrides.max_age.is_some() {
        base.max_age = overrides.max_age;
    }
    if overrides.expires.is_some() {
        base.expires = overrides.expires;
    }
    if overrides.domain.is_some() {
        base.domain = overrides.domain;
    }
    if overrides.path.is_some() {
        base.path = overrides.path;
    }
    if overrides.secure.is_some() {
        base.secure = overrides.secure;
    }
    if overrides.http_only.is_some() {
        base.http_only = overrides.http_only;
    }
    if overrides.same_site.is_some() {
        base.same_site = overrides.same_site;
    }
    if overrides.partitioned.is_some() {
        base.partitioned = overrides.partitioned;
    }
    base
}