use crate::session::extractor::AuthSession;
pub(super) fn normalize_issuer(issuer: &str) -> String {
let trimmed = issuer.trim_end_matches('/');
match url::Url::parse(trimmed) {
Ok(parsed) => {
let mut normalized =
format!("{}://{}", parsed.scheme(), parsed.host_str().unwrap_or(""));
if let Some(port) = parsed.port() {
normalized.push_str(&format!(":{port}"));
}
let path = parsed.path().trim_end_matches('/');
if !path.is_empty() {
normalized.push_str(path);
}
normalized
}
Err(_) => trimmed.to_string(),
}
}
pub(super) fn is_valid_pkce_verifier(verifier: &str) -> bool {
axess_factors::pkce::is_valid_verifier(verifier)
}
pub(super) async fn compute_claim_lock(
provider: &str,
subject: &str,
session: &AuthSession,
) -> String {
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use sha2::{Digest, Sha256};
let sid = session.session_id().await;
let mut hasher = Sha256::new();
hasher.update(provider.as_bytes());
hasher.update(b":");
hasher.update(subject.as_bytes());
hasher.update(b":");
hasher.update(sid.as_bytes());
URL_SAFE_NO_PAD.encode(hasher.finalize())
}
#[cfg(test)]
mod pkce_verifier_tests {
use super::is_valid_pkce_verifier;
#[test]
fn accepts_43_char_unreserved() {
let v = "a".repeat(43);
assert!(is_valid_pkce_verifier(&v));
}
#[test]
fn accepts_128_char_unreserved() {
let v = "Z".repeat(128);
assert!(is_valid_pkce_verifier(&v));
}
#[test]
fn rejects_42_char() {
let v = "a".repeat(42);
assert!(!is_valid_pkce_verifier(&v));
}
#[test]
fn rejects_129_char() {
let v = "a".repeat(129);
assert!(!is_valid_pkce_verifier(&v));
}
#[test]
fn accepts_all_unreserved_punctuation() {
let body = "abcdefghij0123456789-._~ABCDEFGHIJKLMNOPQR";
let v = format!("{body}x");
assert_eq!(v.len(), 43);
assert!(is_valid_pkce_verifier(&v));
}
#[test]
fn rejects_plus_character() {
let mut v = "a".repeat(42);
v.push('+');
assert_eq!(v.len(), 43);
assert!(!is_valid_pkce_verifier(&v));
}
#[test]
fn rejects_space() {
let mut v = "a".repeat(42);
v.push(' ');
assert!(!is_valid_pkce_verifier(&v));
}
#[test]
fn rejects_non_ascii() {
let mut v = "a".repeat(40);
v.push('é');
v.push_str("xx");
assert!(!is_valid_pkce_verifier(&v));
}
}
#[cfg(test)]
mod normalize_issuer_tests {
use super::normalize_issuer;
#[test]
fn strips_single_trailing_slash() {
assert_eq!(
normalize_issuer("https://accounts.google.com/"),
"https://accounts.google.com"
);
}
#[test]
fn passes_through_already_normalised_issuer() {
assert_eq!(
normalize_issuer("https://accounts.google.com"),
"https://accounts.google.com"
);
}
#[test]
fn preserves_path_component() {
assert_eq!(
normalize_issuer("https://idp.example.com/realms/main"),
"https://idp.example.com/realms/main"
);
}
#[test]
fn preserves_explicit_port() {
assert_eq!(
normalize_issuer("https://idp.example.com:8443/"),
"https://idp.example.com:8443"
);
}
#[test]
fn trailing_slash_variants_are_equal() {
let a = normalize_issuer("https://accounts.google.com/");
let b = normalize_issuer("https://accounts.google.com");
assert_eq!(a, b);
}
#[test]
fn strips_trailing_slash_on_path() {
assert_eq!(
normalize_issuer("https://idp.example.com/realms/main/"),
"https://idp.example.com/realms/main"
);
}
#[test]
fn root_path_normalises_to_host_only() {
assert_eq!(normalize_issuer("https://host/"), "https://host");
assert_eq!(normalize_issuer("https://host"), "https://host");
}
}