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}