Skip to main content

saml_rs/api/
sp.rs

1use crate::browser::{BrowserInput, Outbound, PendingAuthnRequest, SsoResponseBinding, Started};
2use crate::config::IdpDescriptor;
3use crate::flow::{FlowResultWithSignatureEvidence, HttpRequest};
4use crate::model::{
5    AuthnRequest, SamlValidationContext, SsoResponse, SsoSession, VerifiedXmlSignature,
6};
7use crate::sp::{LoginRequestOptions, LoginResponseParseOptions, ServiceProvider};
8
9use super::raw_mapping::{
10    ensure_entity_id, ensure_relay_state, ensure_sso_response_binding, input_binding,
11    raw_idp_descriptor, relay_state_from_input, selected_acs,
12};
13use super::{ForceAuthn, Saml, SamlError, Sp, StartSso};
14
15fn session_from_verified_flow(
16    result: FlowResultWithSignatureEvidence,
17) -> Result<SsoSession, SamlError> {
18    let (flow, evidence) = result.into_parts();
19    let verified_xml_signatures = evidence
20        .into_iter()
21        .filter_map(|signature| {
22            VerifiedXmlSignature::from_verified_coverage(
23                signature.algorithm_uri().to_string(),
24                signature.response_covered(),
25                signature.assertion_directly_covered(),
26            )
27        })
28        .collect();
29    SsoSession::try_from_with_verified_xml_signatures(flow, verified_xml_signatures)
30}
31
32impl Saml<Sp> {
33    /// Local SP metadata XML.
34    pub fn metadata_xml(&self) -> &str {
35        self.raw_service_provider().metadata_xml()
36    }
37
38    /// Raw compatibility Service Provider.
39    pub fn raw_service_provider(&self) -> &ServiceProvider {
40        &self.0.service_provider
41    }
42
43    /// Start SP-initiated Web SSO.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`SamlError`] when relay state is invalid, the IdP metadata
48    /// cannot be parsed or trusted, the requested ACS is missing or conflicts
49    /// with the selected binding, or request creation fails because required
50    /// metadata, signing keys, or supported bindings are unavailable.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use saml_rs::{
56    ///     AcsEndpoint, EntityId, IdpConfig, IdpDescriptor, IdpValidationPolicy,
57    ///     MetadataTrustPolicy, RelayStateParam, Saml, SpConfig, SpValidationPolicy,
58    ///     SsoEndpoint, StartSso,
59    /// };
60    ///
61    /// # fn main() -> Result<(), saml_rs::SamlError> {
62    /// let sp_config = SpConfig::builder(EntityId::try_new("https://sp.example.com/metadata")?)
63    ///     .acs_endpoint(AcsEndpoint::post("https://sp.example.com/acs")?)
64    ///     .validation(SpValidationPolicy::compatibility())
65    ///     .build()?;
66    /// let idp_config = IdpConfig::builder(EntityId::try_new("https://idp.example.com/metadata")?)
67    ///     .sso_endpoint(SsoEndpoint::redirect("https://idp.example.com/sso")?)
68    ///     .validation(IdpValidationPolicy::compatibility())
69    ///     .build()?;
70    ///
71    /// let sp = Saml::sp(sp_config)?;
72    /// let idp = Saml::idp(idp_config)?;
73    /// let idp = IdpDescriptor::from_metadata_xml(
74    ///     idp.metadata_xml(),
75    ///     MetadataTrustPolicy::UnsignedForCompatibility,
76    /// )?;
77    /// let relay_state = RelayStateParam::try_from_option(Some("state".to_string()))?;
78    /// let started = sp.start_sso(&idp, StartSso::redirect().relay_state(relay_state))?;
79    ///
80    /// let redirect_url = started.outbound.redirect_url()?;
81    /// # let _ = redirect_url;
82    /// # Ok(()) }
83    /// ```
84    pub fn start_sso(
85        &self,
86        idp: &IdpDescriptor,
87        options: StartSso,
88    ) -> Result<Started<AuthnRequest>, SamlError> {
89        options.relay_state.validate()?;
90        let raw_idp = raw_idp_descriptor(idp)?;
91        let acs = selected_acs(
92            self.raw_service_provider(),
93            options.response_binding,
94            options.acs_index,
95        )?;
96        let response_binding = acs.binding();
97        let raw_options = LoginRequestOptions {
98            relay_state: options.relay_state.as_deref(),
99            force_authn: options.force_authn.map(ForceAuthn::as_bool),
100            assertion_consumer_service_index: options.acs_index,
101            response_binding: Some(response_binding.as_binding()),
102            ..Default::default()
103        };
104        let context = self
105            .raw_service_provider()
106            .create_login_request_with_options(
107                &raw_idp,
108                options.binding.as_binding(),
109                &raw_options,
110            )?;
111        let outbound = Outbound::<AuthnRequest>::try_from(context)?;
112        let pending = PendingAuthnRequest::try_new(
113            outbound.id().clone(),
114            options.relay_state,
115            acs,
116            response_binding,
117            idp.entity_id().clone(),
118        )?
119        .with_request_binding(options.binding);
120        Ok(Started { pending, outbound })
121    }
122
123    /// Finish SP-initiated SSO using stored pending AuthnRequest state.
124    ///
125    /// # Errors
126    ///
127    /// Returns [`SamlError`] when the response does not match the pending
128    /// request, including issuer, binding, relay state, destination, recipient,
129    /// or `InResponseTo` mismatches; when XML, signature, certificate trust,
130    /// audience, or time-window validation fails; or when replay validation
131    /// returns `ReplayDetected` or `TimeWindowInvalid`.
132    ///
133    /// # Examples
134    ///
135    /// ```no_run
136    /// use saml_rs::{
137    ///     BrowserInput, FormField, IdpDescriptor, PendingAuthnRequest, ReplayPolicy, Saml,
138    ///     SamlValidationContext, SsoResponse,
139    /// };
140    /// use std::time::SystemTime;
141    ///
142    /// # fn finish(
143    /// #     sp: &Saml<saml_rs::Sp>,
144    /// #     idp: &IdpDescriptor,
145    /// #     pending: &PendingAuthnRequest,
146    /// #     fields: Vec<FormField>,
147    /// # ) -> Result<(), saml_rs::SamlError> {
148    /// let validation = SamlValidationContext::new(
149    ///     SystemTime::now(),
150    ///     ReplayPolicy::DisabledForCompatibility,
151    /// );
152    /// let input = BrowserInput::<SsoResponse>::post(fields);
153    /// let session = sp.finish_sso(idp, pending, input, validation)?;
154    ///
155    /// let name_id = session.name_id().value();
156    /// # let _ = name_id;
157    /// # Ok(()) }
158    /// ```
159    pub fn finish_sso(
160        &self,
161        idp: &IdpDescriptor,
162        pending: &PendingAuthnRequest,
163        input: BrowserInput<SsoResponse>,
164        mut validation: SamlValidationContext<'_>,
165    ) -> Result<SsoSession, SamlError> {
166        ensure_entity_id(pending.idp_entity_id(), idp.entity_id())?;
167        ensure_sso_response_binding(input_binding(&input), pending.response_binding())?;
168        ensure_relay_state(pending.relay_state(), &relay_state_from_input(&input)?)?;
169        let raw_idp = raw_idp_descriptor(idp)?;
170        let request = HttpRequest::try_from(input)?;
171        let flow = self
172            .raw_service_provider()
173            .parse_login_response_with_request_id_at(
174                &raw_idp,
175                pending.response_binding().as_binding(),
176                &request,
177                pending.request_id().as_str(),
178                LoginResponseParseOptions::at(
179                    validation.now(),
180                    validation.clock_skew().as_millis(),
181                )
182                .with_expected_recipient(pending.acs().location().as_str()),
183            )?;
184        let session = session_from_verified_flow(flow)?;
185        session.check_and_store_replay(&mut validation)?;
186        Ok(session)
187    }
188
189    /// Accept an IdP-initiated SSO response explicitly.
190    ///
191    /// # Errors
192    ///
193    /// Returns [`SamlError`] when the browser binding is not valid for SSO
194    /// responses, the IdP metadata cannot be parsed or trusted, XML parsing or
195    /// signature verification fails, destination, recipient, audience, or time
196    /// validation fails, or replay validation returns `ReplayDetected` or
197    /// `TimeWindowInvalid`.
198    pub fn accept_unsolicited_sso(
199        &self,
200        idp: &IdpDescriptor,
201        input: BrowserInput<SsoResponse>,
202        mut validation: SamlValidationContext<'_>,
203    ) -> Result<SsoSession, SamlError> {
204        let binding = SsoResponseBinding::try_from(input_binding(&input))?;
205        let raw_idp = raw_idp_descriptor(idp)?;
206        let request = HttpRequest::try_from(input)?;
207        let flow = self
208            .raw_service_provider()
209            .parse_unsolicited_login_response_at(
210                &raw_idp,
211                binding.as_binding(),
212                &request,
213                validation.now(),
214                validation.clock_skew().as_millis(),
215            )?;
216        let session = session_from_verified_flow(flow)?;
217        session.check_and_store_replay(&mut validation)?;
218        Ok(session)
219    }
220}