openauth-plugins 0.0.5

Official OpenAuth plugin modules.
Documentation
use std::sync::Arc;

use http::{header, HeaderMap, HeaderValue, Method, StatusCode};
use openauth_core::api::{create_auth_endpoint, response, AuthEndpointOptions};
use openauth_core::cookies::sign_cookie_value;
use openauth_core::error::OpenAuthError;
use openauth_core::plugin::AuthPlugin;
use serde_json::Value;

use super::common::{
    assert_exposes_header, auth_token_header, bearer_request, exposed_auth_token_count,
    json_request, percent_encode_component, router, router_with_plugins, seed_user_and_session,
    sign_up_and_tokens, TestAdapter,
};

#[tokio::test]
async fn bearer_scheme_is_case_insensitive_and_allows_extra_whitespace(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router(adapter, openauth_plugins::bearer::bearer())?;
    let tokens = sign_up_and_tokens(&router).await?;

    for scheme in ["bearer", "BEARER", "BeArEr", "Bearer "] {
        let mut headers = HeaderMap::new();
        headers.insert(
            header::AUTHORIZATION,
            HeaderValue::from_str(&format!("{scheme}  {}", tokens.signed))?,
        );
        let response = router
            .handle_async(json_request(
                Method::GET,
                "/api/auth/get-session",
                "",
                None,
                headers,
            )?)
            .await?;
        let body: Value = serde_json::from_slice(response.body())?;
        assert_eq!(body["session"]["token"], tokens.raw);
    }
    Ok(())
}

#[tokio::test]
async fn signed_bearer_token_may_be_percent_encoded() -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router(adapter, openauth_plugins::bearer::bearer())?;
    let tokens = sign_up_and_tokens(&router).await?;
    let encoded = percent_encode_component(&tokens.signed);

    let response = router
        .handle_async(bearer_request(
            Method::GET,
            "/api/auth/get-session",
            &encoded,
            None,
        )?)
        .await?;

    assert_eq!(response.status(), StatusCode::OK);
    let body: Value = serde_json::from_slice(response.body())?;
    assert_eq!(body["session"]["token"], tokens.raw);
    Ok(())
}

#[tokio::test]
async fn invalid_bearer_token_does_not_override_valid_cookie(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router(adapter, openauth_plugins::bearer::bearer())?;
    let tokens = sign_up_and_tokens(&router).await?;
    let cookie = format!("open-auth.session_token={}", tokens.signed);

    let response = router
        .handle_async(bearer_request(
            Method::GET,
            "/api/auth/get-session",
            "invalid.token",
            Some(&cookie),
        )?)
        .await?;

    assert_eq!(response.status(), StatusCode::OK);
    let body: Value = serde_json::from_slice(response.body())?;
    assert_eq!(body["session"]["token"], tokens.raw);
    Ok(())
}

#[tokio::test]
async fn missing_malformed_and_empty_bearer_headers_are_ignored(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    seed_user_and_session(&adapter).await;
    let router = router(adapter, openauth_plugins::bearer::bearer())?;

    for value in [None, Some("Basic token_1"), Some("Bearer    ")] {
        let mut headers = HeaderMap::new();
        if let Some(value) = value {
            headers.insert(header::AUTHORIZATION, HeaderValue::from_static(value));
        }
        let response = router
            .handle_async(json_request(
                Method::GET,
                "/api/auth/get-session",
                "",
                None,
                headers,
            )?)
            .await?;
        let body: Value = serde_json::from_slice(response.body())?;
        assert!(body.is_null());
    }
    Ok(())
}

#[tokio::test]
async fn existing_exposed_headers_are_preserved_when_auth_token_is_added(
) -> Result<(), Box<dyn std::error::Error>> {
    let response = issue_cookie_response("x-existing", false).await?;

    assert!(auth_token_header(&response).is_some());
    assert_exposes_header(&response, "x-existing")?;
    assert_exposes_header(&response, "set-auth-token")?;
    Ok(())
}

#[tokio::test]
async fn existing_auth_token_exposure_is_not_duplicated() -> Result<(), Box<dyn std::error::Error>>
{
    let response = issue_cookie_response("x-existing, set-auth-token", false).await?;

    assert_eq!(exposed_auth_token_count(&response)?, 1);
    Ok(())
}

#[tokio::test]
async fn session_cookie_is_found_when_multiple_set_cookie_headers_exist(
) -> Result<(), Box<dyn std::error::Error>> {
    let response = issue_cookie_response("", true).await?;

    assert_eq!(
        auth_token_header(&response).as_deref(),
        Some("issued.token")
    );
    Ok(())
}

async fn issue_cookie_response(
    exposed_headers: &'static str,
    include_other_cookie_first: bool,
) -> Result<http::Response<Vec<u8>>, Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router_with_plugins(
        adapter,
        vec![
            openauth_plugins::bearer::bearer(),
            issue_cookie_plugin(exposed_headers, include_other_cookie_first),
        ],
    )?;
    Ok(router
        .handle_async(json_request(
            Method::GET,
            "/api/auth/issue-cookie",
            "",
            None,
            HeaderMap::new(),
        )?)
        .await?)
}

fn issue_cookie_plugin(
    exposed_headers: &'static str,
    include_other_cookie_first: bool,
) -> AuthPlugin {
    let endpoint = create_auth_endpoint(
        "/issue-cookie",
        Method::GET,
        AuthEndpointOptions::default(),
        move |context, _request| {
            Box::pin(async move {
                let mut response = response(StatusCode::OK, b"OK".to_vec())?;
                if include_other_cookie_first {
                    response.headers_mut().append(
                        header::SET_COOKIE,
                        HeaderValue::from_static("unrelated=value; Path=/"),
                    );
                }
                let token = if include_other_cookie_first {
                    "issued.token".to_owned()
                } else {
                    sign_cookie_value("issued_token", &context.secret)?
                };
                let cookie = format!(
                    "{}={token}; Path=/",
                    context.auth_cookies.session_token.name
                );
                let cookie = HeaderValue::from_str(&cookie)
                    .map_err(|error| OpenAuthError::Api(error.to_string()))?;
                response.headers_mut().append(header::SET_COOKIE, cookie);
                if !exposed_headers.is_empty() {
                    response.headers_mut().insert(
                        header::ACCESS_CONTROL_EXPOSE_HEADERS,
                        HeaderValue::from_static(exposed_headers),
                    );
                }
                Ok(response)
            })
        },
    );
    AuthPlugin::new("issuer").with_endpoint(endpoint)
}