Skip to main content

ordinary_auth/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![warn(clippy::all, clippy::pedantic)]
4#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
5
6// Copyright (C) 2026 Ordinary Labs, LLC.
7//
8// SPDX-License-Identifier: AGPL-3.0-only
9
10use anyhow::bail;
11use regex::Regex;
12
13/// `^[a-z][a-z0-9_]{0,13}[a-z0-9]$`
14///
15/// allows lowercase ascii letters, numbers 0-9 (but not leading), underscores (but not leading
16/// or trailing) and at least 2 chars but no more than 15.
17pub static ACCOUNT_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
18    Regex::new(r"^[a-z][a-z0-9_]{0,13}[a-z0-9]$").expect("failed to create regex")
19});
20
21/// `^[a-z][a-z0-9\-]{0,35}[a-z0-9][.]$`
22///
23/// allows lowercase ascii letters, numbers 0-9 (but not leading), hyphens (but not leading
24/// or trailing) and at least 2 chars but no more than 37 (accommodates a stringified
25/// UUID with leading "a-z").
26///
27/// note: includes a check for "." at the end of the subdomain, if using in another application
28/// that strips or splits on the "." it will need to be re-appended before `.is_match` is called.
29pub static SUBDOMAIN_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
30    Regex::new(r"^[a-z][a-z0-9\-]{0,35}[a-z0-9][.]$").expect("failed to create regex")
31});
32
33use argon2::Argon2;
34use opaque_ke::ciphersuite::CipherSuite;
35
36pub use chacha20poly1305::aead::OsRng;
37pub use opaque_ke::ServerSetup;
38
39pub(crate) const ZEROED_KEY: [u8; 32] = [0u8; 32];
40pub const EXP_LEN: usize = 8;
41#[cfg(feature = "core")]
42pub(crate) const SIG_LEN: usize = 64;
43pub(crate) const MAC_LEN: usize = 32;
44
45#[cfg(feature = "core")]
46pub mod keys;
47
48pub mod login;
49pub mod registration;
50pub mod token;
51
52#[cfg(feature = "core")]
53mod core;
54#[cfg(feature = "core")]
55pub use core::Auth;
56
57#[cfg(feature = "core")]
58pub mod recovery;
59
60#[cfg(feature = "client")]
61mod client;
62#[cfg(feature = "client")]
63pub use client::AuthClient;
64
65pub struct DefaultCipherSuite;
66
67impl CipherSuite for DefaultCipherSuite {
68    type OprfCs = opaque_ke::Ristretto255;
69    type KeyExchange = opaque_ke::TripleDh<opaque_ke::Ristretto255, opaque_sha2::Sha512>;
70    type Ksf = Argon2<'static>;
71}
72
73pub fn validate_account(account: &str) -> anyhow::Result<String> {
74    let lowercase_account = account.to_ascii_lowercase();
75
76    if !ACCOUNT_REGEX.is_match(&lowercase_account) {
77        bail!("invalid account name");
78    }
79
80    Ok(lowercase_account)
81}
82
83pub fn validate_domain(base_domains: &Vec<String>, domain: &str) -> bool {
84    for base_domain in base_domains {
85        if domain.ends_with(base_domain) {
86            let mut domain = domain.to_string();
87            domain.truncate(domain.len() - base_domain.len());
88
89            return SUBDOMAIN_REGEX.is_match(&domain);
90        }
91    }
92
93    false
94}
95
96#[cfg(test)]
97mod test {
98    use super::*;
99
100    #[test]
101    fn validate_account_test() -> anyhow::Result<()> {
102        // valid
103        let account = validate_account("my_account")?;
104        assert_eq!(account, "my_account");
105
106        let account = validate_account("MyAccount")?;
107        assert_eq!(account, "myaccount");
108
109        let account = validate_account("myAccount123")?;
110        assert_eq!(account, "myaccount123");
111
112        // invalid
113        let account = validate_account("_my_account");
114        assert!(account.is_err());
115
116        let account = validate_account("my_account_");
117        assert!(account.is_err());
118
119        let account = validate_account("1my_account");
120        assert!(account.is_err());
121
122        let account = validate_account("my-account");
123        assert!(account.is_err());
124
125        let account = validate_account("@#$%1234");
126        assert!(account.is_err());
127
128        Ok(())
129    }
130
131    #[test]
132    fn validate_domain_test() {
133        let app_domains = vec!["example.com".to_string(), "sub.example.com".to_string()];
134
135        // valid
136        assert!(validate_domain(&app_domains, "my.example.com"));
137        assert!(validate_domain(&app_domains, "asdf.example.com"));
138        assert!(validate_domain(&app_domains, "as-df.example.com"));
139
140        // invalid
141        assert!(!validate_domain(&app_domains, "asdf..example.com"));
142        assert!(!validate_domain(&app_domains, "asdf-example.com"));
143        assert!(!validate_domain(&app_domains, "asdf-.example.com"));
144        assert!(!validate_domain(&app_domains, "asdf-.example.com"));
145        assert!(!validate_domain(&app_domains, "asdf.sub.example.com"));
146        assert!(!validate_domain(&app_domains, "a-sdf.sub.example.com"));
147    }
148}