use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
sync::Arc,
time::{Duration, SystemTime},
};
use tokio::sync::RwLock;
use url::Url;
use uuid::Uuid;
use crate::{auth::User, error::FusekiResult};
use super::saml_helpers::{write_xml_attr, xml_escape};
#[derive(Debug, Clone)]
pub struct SamlConfig {
pub sp: ServiceProviderConfig,
pub idp: IdentityProviderConfig,
pub attribute_mapping: AttributeMapping,
pub session: SessionConfig,
}
pub type SamlSpConfig = ServiceProviderConfig;
pub type SamlAttributeMappings = AttributeMapping;
#[derive(Debug, Clone)]
pub struct ServiceProviderConfig {
pub entity_id: String,
pub acs_url: Url,
pub sls_url: Option<Url>,
pub certificate: Option<String>,
pub private_key: Option<String>,
}
#[derive(Debug, Clone)]
pub struct IdentityProviderConfig {
pub entity_id: String,
pub sso_url: Url,
pub slo_url: Option<Url>,
pub certificate: String,
pub metadata_url: Option<Url>,
}
#[derive(Debug, Clone)]
pub struct AttributeMapping {
pub username: String,
pub email: Option<String>,
pub display_name: Option<String>,
pub groups: Option<String>,
pub custom: HashMap<String, String>,
}
impl Default for AttributeMapping {
fn default() -> Self {
Self {
username: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name".to_string(),
email: Some(
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress".to_string(),
),
display_name: Some(
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname".to_string(),
),
groups: Some("http://schemas.xmlsoap.org/claims/Group".to_string()),
custom: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub timeout: Duration,
pub allow_idp_initiated: bool,
pub force_authn: bool,
pub track_session_index: bool,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(3600), allow_idp_initiated: false,
force_authn: false,
track_session_index: true,
}
}
}
pub struct SamlProvider {
pub config: SamlConfig,
pub(super) sessions: Arc<RwLock<HashMap<String, SamlSession>>>,
pub(super) pending_requests: Arc<RwLock<HashMap<String, PendingRequest>>>,
}
#[derive(Debug, Clone)]
pub(super) struct SamlSession {
pub(super) user: User,
pub(super) session_index: Option<String>,
pub(super) created_at: SystemTime,
pub(super) expires_at: SystemTime,
pub(super) attributes: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone)]
pub(super) struct PendingRequest {
pub(super) id: String,
pub(super) relay_state: Option<String>,
pub(super) timestamp: SystemTime,
}
#[derive(Debug, Serialize)]
pub struct AuthnRequest {
pub id: String,
pub issue_instant: DateTime<Utc>,
pub destination: Url,
pub issuer: String,
pub acs_url: Url,
pub protocol_binding: String,
pub force_authn: bool,
}
impl AuthnRequest {
pub fn new(config: &SamlConfig) -> Self {
Self {
id: format!("_{}", Uuid::new_v4()),
issue_instant: Utc::now(),
destination: config.idp.sso_url.clone(),
issuer: config.sp.entity_id.clone(),
acs_url: config.sp.acs_url.clone(),
protocol_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".to_string(),
force_authn: config.session.force_authn,
}
}
pub fn to_xml(&self) -> FusekiResult<String> {
let mut buf = String::with_capacity(1024);
buf.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
buf.push('\n');
buf.push_str("<samlp:AuthnRequest");
write_xml_attr(
&mut buf,
"xmlns:samlp",
"urn:oasis:names:tc:SAML:2.0:protocol",
);
write_xml_attr(
&mut buf,
"xmlns:saml",
"urn:oasis:names:tc:SAML:2.0:assertion",
);
write_xml_attr(&mut buf, "ID", &self.id);
write_xml_attr(&mut buf, "Version", "2.0");
write_xml_attr(&mut buf, "IssueInstant", &self.issue_instant.to_rfc3339());
write_xml_attr(&mut buf, "Destination", self.destination.as_str());
write_xml_attr(&mut buf, "ProtocolBinding", &self.protocol_binding);
write_xml_attr(
&mut buf,
"AssertionConsumerServiceURL",
self.acs_url.as_str(),
);
write_xml_attr(
&mut buf,
"ForceAuthn",
if self.force_authn { "true" } else { "false" },
);
buf.push('>');
buf.push('\n');
buf.push_str(" <saml:Issuer>");
buf.push_str(&xml_escape(&self.issuer));
buf.push_str("</saml:Issuer>\n");
buf.push_str(" <samlp:NameIDPolicy");
write_xml_attr(
&mut buf,
"Format",
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
);
write_xml_attr(&mut buf, "AllowCreate", "true");
buf.push_str("/>\n");
buf.push_str("</samlp:AuthnRequest>\n");
Ok(buf)
}
}
#[derive(Debug, Deserialize)]
pub struct SamlResponse {
pub status: ResponseStatus,
pub assertions: Vec<Assertion>,
pub in_response_to: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ResponseStatus {
pub code: String,
pub message: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Assertion {
pub subject: Subject,
pub attributes: Vec<Attribute>,
pub conditions: Option<Conditions>,
pub authn_statement: Option<AuthnStatement>,
pub audiences: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct Subject {
pub name_id: String,
pub format: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Attribute {
pub name: String,
pub values: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct Conditions {
pub not_before: Option<DateTime<Utc>>,
pub not_on_or_after: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize)]
pub struct AuthnStatement {
pub session_index: Option<String>,
pub authn_instant: DateTime<Utc>,
pub session_not_on_or_after: Option<DateTime<Utc>>,
}