AkurAI Build
Menu

AkurAI-Build

public

Latest change fe39ce51fc9c202d5411f39fd9a8a2feeca4a85e - Harden test suite: 16→179 hermetic tests, race-free integration harness by Ólafur Búi Ólafsson

use std::env;

use serde::Deserialize;

#[derive(Clone, Debug)]
pub struct IdpConfig {
    pub issuer: String,
    pub internal_url: String,
    pub client_id: String,
    pub client_secret: String,
    pub redirect_uri: String,
    pub admin_emails: Vec<String>,
}

#[derive(Clone, Debug, Deserialize)]
pub struct UserInfo {
    pub sub: String,
    pub email: String,
}

impl IdpConfig {
    pub fn from_env(public_origin: &str) -> Self {
        let issuer = env::var("AKURAI_BUILD_IDP_ISSUER")
            .unwrap_or_else(|_| "https://auth.olibuijr.com".into());
        let internal_url =
            env::var("AKURAI_BUILD_IDP_INTERNAL_URL").unwrap_or_else(|_| issuer.clone());
        Self {
            internal_url: if internal_url.starts_with("http://127.0.0.1") {
                issuer.clone()
            } else {
                internal_url
            },
            issuer,
            client_id: env::var("AKURAI_BUILD_IDP_CLIENT_ID").unwrap_or_default(),
            client_secret: env::var("AKURAI_BUILD_IDP_CLIENT_SECRET").unwrap_or_default(),
            redirect_uri: env::var("AKURAI_BUILD_IDP_REDIRECT")
                .unwrap_or_else(|_| format!("{public_origin}/auth/callback")),
            admin_emails: env::var("AKURAI_BUILD_ADMIN_EMAILS")
                .unwrap_or_default()
                .split(',')
                .map(str::trim)
                .filter(|email| !email.is_empty())
                .map(str::to_owned)
                .collect(),
        }
    }

    pub fn authorize_url(&self, state: &str) -> String {
        format!(
            "{}/authorize?client_id={}&redirect_uri={}&response_type=code&scope=openid%20profile%20email&state={}",
            self.issuer,
            encode(&self.client_id),
            encode(&self.redirect_uri),
            encode(state),
        )
    }

    pub fn token_url(&self) -> String {
        format!("{}/token", self.internal_url.trim_end_matches('/'))
    }

    pub fn userinfo_url(&self) -> String {
        format!("{}/userinfo", self.internal_url.trim_end_matches('/'))
    }
}

pub fn encode(value: &str) -> String {
    value
        .bytes()
        .flat_map(|byte| match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
                vec![byte as char]
            }
            _ => format!("%{byte:02X}").chars().collect(),
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn authorize_url_escapes_callback_and_state() {
        let config = IdpConfig {
            issuer: "https://auth.example".into(),
            internal_url: String::new(),
            client_id: "build client".into(),
            client_secret: String::new(),
            redirect_uri: "https://build.example/auth/callback".into(),
            admin_emails: vec![],
        };
        let url = config.authorize_url("a b");
        assert!(url.contains("client_id=build%20client"));
        assert!(url.contains("state=a%20b"));
    }

    #[test]
    fn encode_preserves_unreserved_characters() {
        assert_eq!(
            encode("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"),
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~",
        );
    }

    #[test]
    fn encode_empty_string_remains_empty() {
        assert_eq!(encode(""), "");
    }

    #[test]
    fn encode_escapes_special_characters_as_uppercase_percent_hex() {
        assert_eq!(encode(" "), "%20");
        assert_eq!(encode("a b"), "a%20b");
        assert_eq!(encode(":"), "%3A");
        assert_eq!(encode("/"), "%2F");
        assert_eq!(encode("?"), "%3F");
        assert_eq!(encode("#"), "%23");
        assert_eq!(encode("&"), "%26");
        assert_eq!(encode("="), "%3D");
        assert_eq!(encode("%"), "%25");
        // Non-ASCII byte sequences are each byte individually escaped
        assert_eq!(encode("\u{00e9}"), "%C3%A9"); // é => 0xC3 0xA9
    }

    #[test]
    fn idp_config_authorize_url_includes_all_expected_parameters() {
        let config = IdpConfig {
            issuer: "https://idp.example.com".into(),
            internal_url: String::new(),
            client_id: "my-app".into(),
            client_secret: String::new(),
            redirect_uri: "https://app.example/cb".into(),
            admin_emails: vec![],
        };
        let url = config.authorize_url("s1");
        assert!(url.starts_with("https://idp.example.com/authorize?"));
        assert!(url.contains("client_id=my-app"));
        assert!(url.contains("redirect_uri=https%3A%2F%2Fapp.example%2Fcb"));
        assert!(url.contains("response_type=code"));
        assert!(url.contains("scope=openid%20profile%20email"));
        assert!(url.contains("state=s1"));
    }

    #[test]
    fn idp_config_token_and_userinfo_urls_trim_slash() {
        let config = IdpConfig {
            issuer: "https://idp.example.com".into(),
            internal_url: "https://internal.example.com/".into(),
            client_id: String::new(),
            client_secret: String::new(),
            redirect_uri: String::new(),
            admin_emails: vec![],
        };
        assert_eq!(config.token_url(), "https://internal.example.com/token");
        assert_eq!(
            config.userinfo_url(),
            "https://internal.example.com/userinfo"
        );
    }
}