Skip to main content

saml_rs/api/
sp.rs

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