openauth-plugins 0.0.4

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

use http::{header, Method, Request, StatusCode};
use openauth_core::api::AuthRouter;
use openauth_core::context::create_auth_context_with_adapter;
use openauth_core::cookies::{get_cookies, set_session_cookie, Cookie, SessionCookieOptions};
use openauth_core::db::{DbFieldType, DbValue, MemoryAdapter};
use openauth_core::error::OpenAuthError;
use openauth_core::options::{AdvancedOptions, OpenAuthOptions};
use openauth_core::session::{CreateSessionInput, DbSessionStore};
use openauth_core::user::{CreateUserInput, DbUserStore};
use openauth_plugins::device_authorization::{
    device_authorization_with_options, DeviceAuthorizationOptions,
};
use serde_json::Value;
use time::{Duration, OffsetDateTime};

mod code;
mod decision;
mod options;
mod schema;
mod token;
mod verify;

type TestAdapter = MemoryAdapter;

fn router(
    adapter: Arc<TestAdapter>,
    options: DeviceAuthorizationOptions,
) -> Result<AuthRouter, OpenAuthError> {
    router_with_openauth_options(adapter, options, OpenAuthOptions::default())
}

fn router_with_openauth_options(
    adapter: Arc<TestAdapter>,
    plugin_options: DeviceAuthorizationOptions,
    mut auth_options: OpenAuthOptions,
) -> Result<AuthRouter, OpenAuthError> {
    auth_options.plugins = vec![device_authorization_with_options(plugin_options)];
    auth_options.secret = Some(secret().to_owned());
    auth_options.base_url = Some("http://localhost:3000".to_owned());
    auth_options.advanced = AdvancedOptions {
        disable_csrf_check: true,
        disable_origin_check: true,
        ..auth_options.advanced
    };
    let context = create_auth_context_with_adapter(auth_options, adapter)?;
    AuthRouter::with_async_endpoints(context, Vec::new(), Vec::new())
}

fn json_request(
    method: Method,
    path: &str,
    body: &str,
    cookie: Option<&str>,
) -> Result<Request<Vec<u8>>, http::Error> {
    let mut builder = Request::builder()
        .method(method)
        .uri(format!("http://localhost:3000{path}"));
    if !body.is_empty() {
        builder = builder.header(header::CONTENT_TYPE, "application/json");
    }
    if let Some(cookie) = cookie {
        builder = builder.header(header::COOKIE, cookie);
    }
    builder.body(body.as_bytes().to_vec())
}

fn form_request(
    method: Method,
    path: &str,
    body: &str,
    cookie: Option<&str>,
) -> Result<Request<Vec<u8>>, http::Error> {
    let mut builder = Request::builder()
        .method(method)
        .uri(format!("http://localhost:3000{path}"))
        .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded");
    if let Some(cookie) = cookie {
        builder = builder.header(header::COOKIE, cookie);
    }
    builder.body(body.as_bytes().to_vec())
}

async fn create_device_code(
    router: &AuthRouter,
    client_id: &str,
    scope: Option<&str>,
) -> Result<Value, Box<dyn std::error::Error>> {
    let body = match scope {
        Some(scope) => format!(r#"{{"client_id":"{client_id}","scope":"{scope}"}}"#),
        None => format!(r#"{{"client_id":"{client_id}"}}"#),
    };
    let response = router
        .handle_async(json_request(
            Method::POST,
            "/api/auth/device/code",
            &body,
            None,
        )?)
        .await?;
    assert_eq!(response.status(), StatusCode::OK);
    Ok(serde_json::from_slice(response.body())?)
}

async fn create_user_session(
    adapter: &TestAdapter,
) -> Result<(String, String), Box<dyn std::error::Error>> {
    let user = DbUserStore::new(adapter)
        .create_user(
            CreateUserInput::new("Ada", "ada@example.com")
                .id("user_1")
                .email_verified(true),
        )
        .await?;
    let session = DbSessionStore::new(adapter)
        .create_session(
            CreateSessionInput::new(
                user.id.clone(),
                OffsetDateTime::now_utc() + Duration::days(7),
            )
            .token("session_token_1"),
        )
        .await?;
    Ok((user.id, signed_session_cookie(&session.token)?))
}

fn signed_session_cookie(token: &str) -> Result<String, OpenAuthError> {
    let cookies = set_session_cookie(
        &get_cookies(&OpenAuthOptions {
            secret: Some(secret().to_owned()),
            ..OpenAuthOptions::default()
        })?,
        secret(),
        token,
        SessionCookieOptions::default(),
    )?;
    Ok(cookie_header(&cookies))
}

fn cookie_header(cookies: &[Cookie]) -> String {
    cookies
        .iter()
        .map(|cookie| format!("{}={}", cookie.name, cookie.value))
        .collect::<Vec<_>>()
        .join("; ")
}

fn secret() -> &'static str {
    "test-secret-123456789012345678901234"
}

fn string_field<'a>(body: &'a Value, name: &str) -> &'a str {
    body[name].as_str().unwrap_or_default()
}

async fn device_record(adapter: &TestAdapter) -> Option<indexmap::IndexMap<String, DbValue>> {
    adapter.records("deviceCode").await.into_iter().next()
}