ordinary-app 0.8.2

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

use std::sync::Arc;

use axum::body::Bytes;
use axum::extract::{Form, 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 ordinary_auth::AuthClient;
use ordinary_config::{AuthConfig, ClientPasswordHash};
use serde::Deserialize;

use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use sha2::{Digest, Sha256};

fn refresh_token_cookie(
    jar: CookieJar,
    token: &Bytes,
    auth_config: &AuthConfig,
    secure: bool,
) -> CookieJar {
    let name = if secure {
        tracing::info!("generated secure refresh cookie");
        "__Secure-ORDINARY-REFRESH-TOKEN"
    } else {
        tracing::warn!("generated insecure refresh cookie");
        "ORDINARY-REFRESH-TOKEN"
    };

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

pub async fn start(
    State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
    body: Bytes,
) -> impl IntoResponse {
    let span = tracing::info_span!("auth", flv = %"wasm");

    span.in_scope(|| match state.auth.login_start(body) {
        Ok(v) => (StatusCode::OK, v),
        Err(e) => {
            tracing::error!("{e}");
            (StatusCode::INTERNAL_SERVER_ERROR, Bytes::new())
        }
    })
}

pub async fn finish(
    State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
    body: Bytes,
) -> impl IntoResponse {
    let span = tracing::info_span!("auth", flv = %"wasm");

    span.in_scope(|| match state.auth.login_finish(body, true) {
        Ok((token, _, _)) => (StatusCode::OK, token),
        Err(e) => {
            tracing::error!("{e}");
            (StatusCode::INTERNAL_SERVER_ERROR, Bytes::new())
        }
    })
}

#[derive(Deserialize, Debug)]
pub struct LoginForm {
    account: String,
    password: String,
    mfa_code: String,
}

/// for <noscript> users
pub async fn form(
    State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
    jar: CookieJar,
    Form(login_form): Form<LoginForm>,
) -> Result<(CookieJar, Redirect), StatusCode> {
    let span = tracing::info_span!("auth", flv = %"noscript");

    span.in_scope(|| {
        let account = login_form.account.as_bytes();

        if account.len() > 255 {
            return Err(StatusCode::INTERNAL_SERVER_ERROR);
        }

        let password = login_form.password.as_bytes();
        let mfa_code = login_form.mfa_code.as_bytes();

        let app_name = state.config.domain.as_bytes();

        let mut input = app_name.to_vec();
        input.extend_from_slice(account);

        let mut password_input = input.clone();
        let mut mfa_input = input.clone();
        password_input.extend_from_slice(password);
        mfa_input.extend_from_slice(mfa_code);

        let mut password = vec![];
        let mut mfa_code = vec![];

        if let Some(auth) = &state.config.auth {
            match &auth.client_hash {
                ClientPasswordHash::Sha256 => {
                    let mut hasher = Sha256::new();
                    hasher.update(&password_input);
                    password = hasher.finalize().to_vec();

                    let mut hasher = Sha256::new();
                    hasher.update(&mfa_input);
                    mfa_code = hasher.finalize().to_vec();
                }
            }
        }

        match AuthClient::login_start_req(account, &password[..]) {
            Ok((client_state, req)) => match state.auth.login_start(req) {
                Ok(server_message) => match AuthClient::login_finish_req(
                    account,
                    &password[..],
                    &mfa_code[..],
                    &client_state,
                    &server_message[..],
                    None,
                ) {
                    Ok((req, session_key)) => match state.auth.login_finish(req, true) {
                        Ok((res, _, _)) => match AuthClient::decrypt_token(&res, &session_key) {
                            Ok(token) => {
                                return Ok((
                                    refresh_token_cookie(
                                        jar,
                                        &token,
                                        &state.auth.config,
                                        state.secure_cookies,
                                    ),
                                    Redirect::to("/accounts/access/redirect"),
                                ));
                            }
                            Err(err) => tracing::error!("{err}"),
                        },
                        Err(err) => tracing::error!("{err}"),
                    },
                    Err(err) => tracing::error!("{err}"),
                },
                Err(err) => tracing::error!("{err}"),
            },
            Err(err) => tracing::error!("{err}"),
        }

        Err(StatusCode::INTERNAL_SERVER_ERROR)
    })
}

pub async fn hash_only(
    State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
    jar: CookieJar,
    body: Bytes,
) -> impl IntoResponse {
    let span = tracing::info_span!("auth", flv = %"js");

    span.in_scope(|| {
        if let Some(account_len) = body.first() {
            let account_len = *account_len as usize;

            if body.len() < account_len + 1 + 32 + 32 {
                return (StatusCode::INTERNAL_SERVER_ERROR, None, Bytes::new());
            }

            let account = &body[1..=account_len];
            let mfa_code = &body[account_len + 1..account_len + 1 + 32];
            let password = &body[account_len + 1 + 32..account_len + 1 + 32 + 32];

            let mut client_verifier = None;

            if body.len() == account_len + 1 + 32 + 32 + 32 && !state.auth.config.cookies_enabled {
                client_verifier =
                    Some(&body[account_len + 1 + 32 + 32..account_len + 1 + 32 + 32 + 32]);
            }

            match AuthClient::login_start_req(account, password) {
                Ok((client_state, req)) => match state.auth.login_start(req) {
                    Ok(server_message) => match AuthClient::login_finish_req(
                        account,
                        password,
                        mfa_code,
                        &client_state,
                        &server_message[..],
                        client_verifier,
                    ) {
                        Ok((req, session_key)) => match state.auth.login_finish(req, true) {
                            Ok((res, _, _)) => {
                                match AuthClient::decrypt_token(&res, &session_key) {
                                    Ok(token) => {
                                        let cookie = if state.auth.config.cookies_enabled {
                                            Some(refresh_token_cookie(
                                                jar,
                                                &token,
                                                &state.auth.config,
                                                state.secure_cookies,
                                            ))
                                        } else {
                                            None
                                        };

                                        return (StatusCode::OK, cookie, token);
                                    }
                                    Err(err) => tracing::error!("{err}"),
                                }
                            }
                            Err(err) => tracing::error!("{err}"),
                        },
                        Err(err) => tracing::error!("{err}"),
                    },
                    Err(err) => tracing::error!("{err}"),
                },
                Err(err) => tracing::error!("{err}"),
            }
        }

        (StatusCode::INTERNAL_SERVER_ERROR, None, Bytes::new())
    })
}