use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DiscoveryDocument {
pub issuer: String,
pub authorization_endpoint: String,
pub token_endpoint: String,
pub userinfo_endpoint: String,
pub jwks_uri: String,
pub registration_endpoint: String,
pub introspection_endpoint: String,
pub revocation_endpoint: String,
pub end_session_endpoint: String,
pub scopes_supported: Vec<String>,
pub response_types_supported: Vec<String>,
pub response_modes_supported: Vec<String>,
pub grant_types_supported: Vec<String>,
pub subject_types_supported: Vec<String>,
pub id_token_signing_alg_values_supported: Vec<String>,
pub token_endpoint_auth_methods_supported: Vec<String>,
pub claims_supported: Vec<String>,
pub code_challenge_methods_supported: Vec<String>,
pub dpop_signing_alg_values_supported: Vec<String>,
pub authorization_response_iss_parameter_supported: bool,
pub solid_oidc_supported: String,
}
pub fn build_discovery(issuer: &str) -> DiscoveryDocument {
let normalised_issuer = if issuer.ends_with('/') {
issuer.to_string()
} else {
format!("{issuer}/")
};
let base = issuer.trim_end_matches('/');
DiscoveryDocument {
issuer: normalised_issuer,
authorization_endpoint: format!("{base}/idp/auth"),
token_endpoint: format!("{base}/idp/token"),
userinfo_endpoint: format!("{base}/idp/me"),
jwks_uri: format!("{base}/.well-known/jwks.json"),
registration_endpoint: format!("{base}/idp/reg"),
introspection_endpoint: format!("{base}/idp/token/introspection"),
revocation_endpoint: format!("{base}/idp/token/revocation"),
end_session_endpoint: format!("{base}/idp/session/end"),
scopes_supported: vec![
"openid".into(),
"webid".into(),
"profile".into(),
"email".into(),
"offline_access".into(),
],
response_types_supported: vec!["code".into()],
response_modes_supported: vec![
"query".into(),
"fragment".into(),
"form_post".into(),
],
grant_types_supported: vec![
"authorization_code".into(),
"refresh_token".into(),
"client_credentials".into(),
],
subject_types_supported: vec!["public".into()],
id_token_signing_alg_values_supported: vec!["ES256".into()],
token_endpoint_auth_methods_supported: vec![
"none".into(),
"client_secret_basic".into(),
"client_secret_post".into(),
],
claims_supported: vec![
"sub".into(),
"webid".into(),
"name".into(),
"email".into(),
"email_verified".into(),
],
code_challenge_methods_supported: vec!["S256".into()],
dpop_signing_alg_values_supported: vec!["ES256".into()],
authorization_response_iss_parameter_supported: true,
solid_oidc_supported: "https://solidproject.org/TR/solid-oidc".into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discovery_contains_required_fields() {
let d = build_discovery("https://pod.example");
assert_eq!(d.issuer, "https://pod.example/");
assert_eq!(d.authorization_endpoint, "https://pod.example/idp/auth");
assert_eq!(d.token_endpoint, "https://pod.example/idp/token");
assert_eq!(d.jwks_uri, "https://pod.example/.well-known/jwks.json");
assert_eq!(
d.registration_endpoint,
"https://pod.example/idp/reg"
);
assert!(d.scopes_supported.iter().any(|s| s == "webid"));
assert!(d
.token_endpoint_auth_methods_supported
.iter()
.any(|s| s == "none"));
assert!(d.grant_types_supported.iter().any(|s| s == "authorization_code"));
assert!(!d.dpop_signing_alg_values_supported.is_empty());
assert!(d.code_challenge_methods_supported.iter().any(|s| s == "S256"));
assert!(d.solid_oidc_supported.contains("solid-oidc"));
}
#[test]
fn discovery_normalises_issuer_trailing_slash() {
let a = build_discovery("https://pod.example");
let b = build_discovery("https://pod.example/");
assert_eq!(a.issuer, b.issuer);
assert_eq!(a.issuer, "https://pod.example/");
assert_eq!(a.authorization_endpoint, b.authorization_endpoint);
assert!(!a.authorization_endpoint.contains("//idp"));
}
}