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, OutstandingLogout, SamlValidationContext, SsoResponse, SsoSession,
6    VerifiedXmlSignature,
7};
8use crate::sp::{LoginRequestOptions, LoginResponseParseOptions, ServiceProvider};
9
10use super::raw_mapping::{
11    ensure_entity_id, ensure_relay_state, ensure_sso_response_binding, input_binding,
12    raw_idp_descriptor, relay_state_from_input, selected_acs,
13};
14use super::{ForceAuthn, Saml, SamlError, Sp, StartSso};
15
16fn session_from_verified_flow(
17    result: FlowResultWithSignatureEvidence,
18) -> Result<SsoSession, SamlError> {
19    let (flow, evidence) = result.into_parts();
20    let verified_xml_signatures = evidence
21        .into_iter()
22        .filter_map(|signature| {
23            VerifiedXmlSignature::from_verified_coverage(
24                signature.algorithm_uri().to_string(),
25                signature.response_covered(),
26                signature.assertion_directly_covered(),
27            )
28        })
29        .collect();
30    SsoSession::try_from_with_verified_xml_signatures(flow, verified_xml_signatures)
31}
32
33fn finish_validated_session(
34    session: SsoSession,
35    validation: &mut SamlValidationContext<'_>,
36    outstanding_logout: Option<&OutstandingLogout>,
37) -> Result<SsoSession, SamlError> {
38    let matches_outstanding_logout = match outstanding_logout {
39        Some(logout) => logout.matches_unexpired(&session, validation)?,
40        None => false,
41    };
42    session.check_and_store_replay(validation)?;
43    if matches_outstanding_logout {
44        return Err(SamlError::AssertionMatchesOutstandingLogout);
45    }
46    Ok(session)
47}
48
49impl Saml<Sp> {
50    /// Local SP metadata XML.
51    pub fn metadata_xml(&self) -> &str {
52        self.raw_service_provider().metadata_xml()
53    }
54
55    /// Raw compatibility Service Provider.
56    pub fn raw_service_provider(&self) -> &ServiceProvider {
57        &self.0.service_provider
58    }
59
60    /// Start SP-initiated Web SSO.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`SamlError`] when relay state is invalid, the IdP metadata
65    /// cannot be parsed or trusted, the requested ACS is missing or conflicts
66    /// with the selected binding, producer rules are combined with a custom
67    /// AuthnRequest template, or request creation fails because required
68    /// metadata, signing keys, or supported bindings are unavailable.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use saml_rs::{
74    ///     AcsEndpoint, EntityId, IdpConfig, IdpDescriptor, IdpValidationPolicy,
75    ///     MetadataTrustPolicy, RelayStateParam, Saml, SpConfig, SpValidationPolicy,
76    ///     SsoEndpoint, StartSso,
77    /// };
78    ///
79    /// # fn main() -> Result<(), saml_rs::SamlError> {
80    /// let sp_config = SpConfig::builder(EntityId::try_new("https://sp.example.com/metadata")?)
81    ///     .acs_endpoint(AcsEndpoint::post("https://sp.example.com/acs")?)
82    ///     .validation(SpValidationPolicy::compatibility())
83    ///     .build()?;
84    /// let idp_config = IdpConfig::builder(EntityId::try_new("https://idp.example.com/metadata")?)
85    ///     .sso_endpoint(SsoEndpoint::redirect("https://idp.example.com/sso")?)
86    ///     .validation(IdpValidationPolicy::compatibility())
87    ///     .build()?;
88    ///
89    /// let sp = Saml::sp(sp_config)?;
90    /// let idp = Saml::idp(idp_config)?;
91    /// let idp = IdpDescriptor::from_metadata_xml(
92    ///     idp.metadata_xml(),
93    ///     MetadataTrustPolicy::UnsignedForCompatibility,
94    /// )?;
95    /// let relay_state = RelayStateParam::try_from_option(Some("state".to_string()))?;
96    /// let started = sp.start_sso(&idp, StartSso::redirect().relay_state(relay_state))?;
97    ///
98    /// let redirect_url = started.outbound.redirect_url()?;
99    /// # let _ = redirect_url;
100    /// # Ok(()) }
101    /// ```
102    pub fn start_sso(
103        &self,
104        idp: &IdpDescriptor,
105        options: StartSso,
106    ) -> Result<Started<AuthnRequest>, SamlError> {
107        options.relay_state.validate()?;
108        let raw_idp = raw_idp_descriptor(idp)?;
109        let acs = selected_acs(
110            self.raw_service_provider(),
111            options.response_binding,
112            options.acs_index,
113        )?;
114        let response_binding = acs.binding();
115        let raw_options = LoginRequestOptions {
116            relay_state: options.relay_state.as_deref(),
117            force_authn: options.force_authn.map(ForceAuthn::as_bool),
118            assertion_consumer_service_index: options.acs_index,
119            response_binding: Some(response_binding.as_binding()),
120            ..Default::default()
121        };
122        let context = self.raw_service_provider().create_typed_login_request(
123            &raw_idp,
124            options.binding.as_binding(),
125            &raw_options,
126            options.web_browser_sso_producer,
127        )?;
128        let outbound = Outbound::<AuthnRequest>::try_from(context)?;
129        let pending = PendingAuthnRequest::try_new(
130            outbound.id().clone(),
131            options.relay_state,
132            acs,
133            response_binding,
134            idp.entity_id().clone(),
135        )?
136        .with_request_binding(options.binding);
137        Ok(Started { pending, outbound })
138    }
139
140    /// Finish SP-initiated SSO using stored pending AuthnRequest state.
141    ///
142    /// This accepts the response without applying an outstanding logout. Use
143    /// [`Self::finish_sso_with_outstanding_logout`] to reject a later assertion
144    /// that matches one.
145    ///
146    /// # Errors
147    ///
148    /// Returns [`SamlError`] when the response does not match the pending
149    /// request, including issuer, binding, relay state, destination, recipient,
150    /// or `InResponseTo` mismatches; when XML, signature, certificate trust,
151    /// audience, condition, or time validation fails; when the assertions do
152    /// not share one issuer and one principal; or when replay validation
153    /// returns `ReplayDetected` or `TimeWindowInvalid`.
154    ///
155    /// # Examples
156    ///
157    /// ```no_run
158    /// use saml_rs::{
159    ///     BrowserInput, FormField, IdpDescriptor, PendingAuthnRequest, ReplayPolicy, Saml,
160    ///     SamlValidationContext, SsoResponse,
161    /// };
162    /// use std::time::SystemTime;
163    ///
164    /// # fn finish(
165    /// #     sp: &Saml<saml_rs::Sp>,
166    /// #     idp: &IdpDescriptor,
167    /// #     pending: &PendingAuthnRequest,
168    /// #     fields: Vec<FormField>,
169    /// # ) -> Result<(), saml_rs::SamlError> {
170    /// let validation = SamlValidationContext::new(
171    ///     SystemTime::now(),
172    ///     ReplayPolicy::DisabledForCompatibility,
173    /// );
174    /// let input = BrowserInput::<SsoResponse>::post(fields);
175    /// let session = sp.finish_sso(idp, pending, input, validation)?;
176    ///
177    /// let name_id = session.name_id().value();
178    /// # let _ = name_id;
179    /// # Ok(()) }
180    /// ```
181    pub fn finish_sso(
182        &self,
183        idp: &IdpDescriptor,
184        pending: &PendingAuthnRequest,
185        input: BrowserInput<SsoResponse>,
186        mut validation: SamlValidationContext<'_>,
187    ) -> Result<SsoSession, SamlError> {
188        let session = self.parsed_sso_session(idp, pending, input, &validation)?;
189        finish_validated_session(session, &mut validation, None)
190    }
191
192    /// Finish SP-initiated SSO and apply an [`OutstandingLogout`].
193    ///
194    /// A match is rejected. The assertion identifier is still recorded for
195    /// replay.
196    ///
197    /// # Errors
198    ///
199    /// Returns [`SamlError::AssertionMatchesOutstandingLogout`] when the logout
200    /// matches. Also returns the errors documented on [`Self::finish_sso`].
201    pub fn finish_sso_with_outstanding_logout(
202        &self,
203        idp: &IdpDescriptor,
204        pending: &PendingAuthnRequest,
205        input: BrowserInput<SsoResponse>,
206        mut validation: SamlValidationContext<'_>,
207        outstanding_logout: &OutstandingLogout,
208    ) -> Result<SsoSession, SamlError> {
209        let session = self.parsed_sso_session(idp, pending, input, &validation)?;
210        finish_validated_session(session, &mut validation, Some(outstanding_logout))
211    }
212
213    /// Accept an IdP-initiated SSO response explicitly.
214    ///
215    /// This accepts the response without applying an outstanding logout. Use
216    /// [`Self::accept_unsolicited_sso_with_outstanding_logout`] to reject a
217    /// later assertion that matches one.
218    ///
219    /// # Errors
220    ///
221    /// Returns [`SamlError`] when the browser binding is not valid for SSO
222    /// responses, `RelayState` exceeds 80 bytes, the IdP metadata cannot be
223    /// parsed or trusted, XML parsing or signature verification fails,
224    /// destination, recipient, audience, condition, or time validation fails,
225    /// the assertions do not share one issuer and one principal, or replay
226    /// validation returns `ReplayDetected` or `TimeWindowInvalid`.
227    pub fn accept_unsolicited_sso(
228        &self,
229        idp: &IdpDescriptor,
230        input: BrowserInput<SsoResponse>,
231        mut validation: SamlValidationContext<'_>,
232    ) -> Result<SsoSession, SamlError> {
233        let session = self.parsed_unsolicited_session(idp, input, &validation)?;
234        finish_validated_session(session, &mut validation, None)
235    }
236
237    /// Accept an IdP-initiated SSO response and apply an [`OutstandingLogout`].
238    ///
239    /// # Errors
240    ///
241    /// Returns the errors documented on [`Self::finish_sso_with_outstanding_logout`]
242    /// and [`Self::accept_unsolicited_sso`].
243    pub fn accept_unsolicited_sso_with_outstanding_logout(
244        &self,
245        idp: &IdpDescriptor,
246        input: BrowserInput<SsoResponse>,
247        mut validation: SamlValidationContext<'_>,
248        outstanding_logout: &OutstandingLogout,
249    ) -> Result<SsoSession, SamlError> {
250        let session = self.parsed_unsolicited_session(idp, input, &validation)?;
251        finish_validated_session(session, &mut validation, Some(outstanding_logout))
252    }
253
254    fn parsed_sso_session(
255        &self,
256        idp: &IdpDescriptor,
257        pending: &PendingAuthnRequest,
258        input: BrowserInput<SsoResponse>,
259        validation: &SamlValidationContext<'_>,
260    ) -> Result<SsoSession, SamlError> {
261        ensure_entity_id(pending.idp_entity_id(), idp.entity_id())?;
262        ensure_sso_response_binding(input_binding(&input), pending.response_binding())?;
263        ensure_relay_state(pending.relay_state(), &relay_state_from_input(&input)?)?;
264        let raw_idp = raw_idp_descriptor(idp)?;
265        let request = HttpRequest::try_from(input)?;
266        let flow = self
267            .raw_service_provider()
268            .parse_login_response_with_request_id_at(
269                &raw_idp,
270                pending.response_binding().as_binding(),
271                &request,
272                pending.request_id().as_str(),
273                LoginResponseParseOptions::at(
274                    validation.now(),
275                    validation.clock_skew().as_millis(),
276                )
277                .with_expected_recipient(pending.acs().location().as_str()),
278            )?;
279        session_from_verified_flow(flow)
280    }
281
282    fn parsed_unsolicited_session(
283        &self,
284        idp: &IdpDescriptor,
285        input: BrowserInput<SsoResponse>,
286        validation: &SamlValidationContext<'_>,
287    ) -> Result<SsoSession, SamlError> {
288        relay_state_from_input(&input)?;
289        let binding = SsoResponseBinding::try_from(input_binding(&input))?;
290        let raw_idp = raw_idp_descriptor(idp)?;
291        let request = HttpRequest::try_from(input)?;
292        let flow = self
293            .raw_service_provider()
294            .parse_unsolicited_login_response_at(
295                &raw_idp,
296                binding.as_binding(),
297                &request,
298                validation.now(),
299                validation.clock_skew().as_millis(),
300            )?;
301        session_from_verified_flow(flow)
302    }
303}