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}