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}