openauth-plugins 0.0.4

Official OpenAuth plugin modules.
Documentation
use openauth_core::context::request_state::current_new_session;
use openauth_core::context::AuthContext;
use openauth_core::db::DbAdapter;
use openauth_core::error::OpenAuthError;
use openauth_core::plugin::{PluginAfterHookAction, PluginAfterHookFuture};
use openauth_core::session::DbSessionStore;

use super::cookies::{
    append_cookies, expire_multi_cookie_name, multi_cookie_name, multi_session_cookie,
    signed_multi_tokens,
};
use super::options::MultiSessionConfig;

pub fn store_multi_session_cookie(
    config: MultiSessionConfig,
) -> impl for<'a> Fn(
    &'a AuthContext,
    &'a openauth_core::api::ApiRequest,
    openauth_core::api::ApiResponse,
) -> PluginAfterHookFuture<'a>
       + Send
       + Sync
       + 'static {
    move |context, request, response| {
        Box::pin(async move {
            store_multi_session_cookie_inner(context, request, response, config).await
        })
    }
}

pub fn revoke_multi_session_cookies() -> impl for<'a> Fn(
    &'a AuthContext,
    &'a openauth_core::api::ApiRequest,
    openauth_core::api::ApiResponse,
) -> PluginAfterHookFuture<'a>
       + Send
       + Sync
       + 'static {
    move |context, request, response| {
        Box::pin(
            async move { revoke_multi_session_cookies_inner(context, request, response).await },
        )
    }
}

async fn store_multi_session_cookie_inner(
    context: &AuthContext,
    request: &openauth_core::api::ApiRequest,
    mut response: openauth_core::api::ApiResponse,
    config: MultiSessionConfig,
) -> Result<PluginAfterHookAction, OpenAuthError> {
    let Some(created) = current_new_session()? else {
        return Ok(PluginAfterHookAction::Continue(response));
    };
    let cookie_header = request_cookie_header(request);
    let cookie_name = multi_cookie_name(context, &created.session.token);
    if response_has_cookie(&response, &cookie_name)
        || request_has_cookie(&cookie_header, &cookie_name)
    {
        return Ok(PluginAfterHookAction::Continue(response));
    }
    let Some(adapter) = context.adapter() else {
        return Ok(PluginAfterHookAction::Continue(response));
    };

    let current_signed_count = signed_multi_tokens(context, &cookie_header)?.len();
    let deleted = delete_same_user_sessions(
        adapter.as_ref(),
        context,
        &cookie_header,
        &created.user.id,
        &mut response,
    )
    .await?;
    let current_count = current_signed_count.saturating_sub(deleted) + 1;
    if current_count > config.maximum_sessions {
        return Ok(PluginAfterHookAction::Continue(response));
    }
    append_cookies(
        &mut response,
        [multi_session_cookie(context, &created.session.token)?],
    )?;
    Ok(PluginAfterHookAction::Continue(response))
}

async fn revoke_multi_session_cookies_inner(
    context: &AuthContext,
    request: &openauth_core::api::ApiRequest,
    mut response: openauth_core::api::ApiResponse,
) -> Result<PluginAfterHookAction, OpenAuthError> {
    let Some(adapter) = context.adapter() else {
        return Ok(PluginAfterHookAction::Continue(response));
    };
    let cookie_header = request_cookie_header(request);
    let tokens = signed_multi_tokens(context, &cookie_header)?;
    for (key, token) in &tokens {
        append_cookies(&mut response, [expire_multi_cookie_name(context, key)])?;
        DbSessionStore::new(adapter.as_ref())
            .delete_session(token)
            .await?;
    }
    Ok(PluginAfterHookAction::Continue(response))
}

async fn delete_same_user_sessions(
    adapter: &dyn DbAdapter,
    context: &AuthContext,
    cookie_header: &str,
    user_id: &str,
    response: &mut openauth_core::api::ApiResponse,
) -> Result<usize, OpenAuthError> {
    let mut deleted = 0;
    for (key, token) in signed_multi_tokens(context, cookie_header)? {
        let Some(session) = DbSessionStore::new(adapter).find_session(&token).await? else {
            continue;
        };
        if session.user_id != user_id {
            continue;
        }
        DbSessionStore::new(adapter).delete_session(&token).await?;
        append_cookies(response, [expire_multi_cookie_name(context, &key)])?;
        deleted += 1;
    }
    Ok(deleted)
}

fn request_cookie_header(request: &openauth_core::api::ApiRequest) -> String {
    request
        .headers()
        .get(http::header::COOKIE)
        .and_then(|value| value.to_str().ok())
        .unwrap_or_default()
        .to_owned()
}

fn request_has_cookie(cookie_header: &str, name: &str) -> bool {
    openauth_core::cookies::parse_cookies(cookie_header).contains_key(name)
}

fn response_has_cookie(response: &openauth_core::api::ApiResponse, name: &str) -> bool {
    response
        .headers()
        .get_all(http::header::SET_COOKIE)
        .iter()
        .filter_map(|value| value.to_str().ok())
        .any(|cookie| cookie.starts_with(&format!("{name}=")))
}