ordinary-app 0.7.0

Application server for Ordinary
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use anyhow::bail;
use axum::body::Bytes;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Redirect};
use axum_extra::extract::CookieJar;
use axum_extra::extract::cookie::{Cookie, SameSite};
use cookie::time::Duration;
use std::sync::Arc;

use base64::{Engine as B64Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use hyper::HeaderMap;
use ordinary_config::AuthConfig;

fn get_token_from_headers(headers: &HeaderMap) -> anyhow::Result<Bytes> {
    if let Some(val) = headers.get("authorization")
        && let Ok(str_val) = val.to_str()
        && let Some(b64_token) = str_val.strip_prefix("Bearer ")
        && let Ok(token) = b64.decode(b64_token)
    {
        Ok(Bytes::copy_from_slice(&token))
    } else {
        bail!("no valid token")
    }
}

pub async fn access(
    State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
    headers: HeaderMap,
) -> impl IntoResponse {
    let span = tracing::info_span!("token", flv = %"fetch");

    span.in_scope(|| {
        let Ok(token) = get_token_from_headers(&headers) else {
            return StatusCode::UNAUTHORIZED.into_response();
        };

        match state.auth.access_get(&token) {
            Ok(token) => (StatusCode::OK, token).into_response(),
            Err(err) => {
                tracing::warn!(%err);
                StatusCode::UNAUTHORIZED.into_response()
            }
        }
    })
}

// ?? evaluate creating read-only tokens to be used for templates ('__Host-' sent on every
// ?? request), and action tokens ('__Secure-' to be sent for only specific paths corresponding
// ?? to actions).
fn access_token_cookie(
    jar: CookieJar,
    token: &Bytes,
    auth_config: &AuthConfig,
    secure: bool,
) -> CookieJar {
    let name = if secure {
        tracing::info!("generated host access cookie");
        "__Host-ORDINARY-ACCESS-TOKEN"
    } else {
        tracing::warn!("generated insecure access cookie");
        "ORDINARY-ACCESS-TOKEN"
    };

    jar.add(
        Cookie::build((name, b64.encode(token)))
            .secure(secure)
            .http_only(true)
            .path("/")
            .same_site(SameSite::Strict)
            .max_age(Duration::seconds(i64::from(
                auth_config.access_token.lifetime,
            ))),
    )
}

pub async fn access_cookies(
    State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
    jar: CookieJar,
    headers: HeaderMap,
) -> impl IntoResponse {
    let span = tracing::info_span!("token", flv = %"cookie");

    span.in_scope(|| {
        let Ok(token) = get_token_from_headers(&headers) else {
            return StatusCode::UNAUTHORIZED.into_response();
        };

        match state.auth.access_get(&token) {
            Ok(token) => {
                let cookie = if state.auth.config.cookies_enabled {
                    Some(access_token_cookie(
                        jar,
                        &token,
                        &state.auth.config,
                        state.secure_cookies,
                    ))
                } else {
                    None
                };

                (StatusCode::OK, cookie, token).into_response()
            }
            Err(err) => {
                tracing::warn!(%err);
                StatusCode::UNAUTHORIZED.into_response()
            }
        }
    })
}

pub async fn access_redirect(
    State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
    jar: CookieJar,
) -> Result<(CookieJar, Redirect), StatusCode> {
    let span = tracing::info_span!("token", flv = %"redirect");

    let cookie_name = if state.secure_cookies {
        "__Secure-ORDINARY-REFRESH-TOKEN"
    } else {
        "ORDINARY-REFRESH-TOKEN"
    };

    span.in_scope(|| {
        if let Some(refresh_token) = jar.get(cookie_name) {
            if let Ok(refresh_token) = b64.decode(refresh_token.value()) {
                if let Ok(access_token) = state
                    .auth
                    .access_get(&Bytes::copy_from_slice(&refresh_token[..]))
                {
                    return Ok((
                        access_token_cookie(
                            jar,
                            &access_token,
                            &state.auth.config,
                            state.secure_cookies,
                        ),
                        Redirect::to("/"),
                    ));
                }
            } else {
                tracing::error!("failed to decode refresh cookie");
            }
        } else {
            tracing::error!("no refresh cookie");
        }

        Err(StatusCode::UNAUTHORIZED)
    })
}