Menu
AkurAI-Build
publicLatest 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"
);
}
}