saml_rs/api/idp.rs
1use crate::browser::{BrowserInput, Outbound, SsoRequestBinding};
2use crate::config::SpDescriptor;
3use crate::error::SamlError as Error;
4use crate::flow::HttpRequest;
5use crate::idp::{IdentityProvider, LoginResponseOptions, LoginResponseOverrides};
6use crate::model::{
7 AuthnRequest, Received, RelayStateParam, ReplayKey, SamlValidationContext, SsoResponse, Subject,
8};
9
10use super::raw_mapping::{
11 ensure_entity_id, input_binding, raw_sp_descriptor, relay_state_from_input, response_target,
12};
13use super::{Idp, RespondSso, Saml, SamlError};
14
15impl Saml<Idp> {
16 /// Local IdP metadata XML.
17 pub fn metadata_xml(&self) -> &str {
18 self.raw_identity_provider().metadata_xml()
19 }
20
21 /// Raw compatibility Identity Provider.
22 pub fn raw_identity_provider(&self) -> &IdentityProvider {
23 &self.0.identity_provider
24 }
25
26 /// Receive an SP AuthnRequest.
27 ///
28 /// # Errors
29 ///
30 /// Returns [`SamlError`] when browser input or relay state is invalid, the
31 /// request binding is unsupported, SP metadata cannot be parsed, XML
32 /// parsing or signature/trust validation fails, the request destination
33 /// does not match local metadata, an enabled [`crate::AuthnRequestAgePolicy`]
34 /// rejects `IssueInstant`, or replay validation detects a duplicate or
35 /// expired request.
36 ///
37 /// # Examples
38 ///
39 /// ```no_run
40 /// use saml_rs::{
41 /// AuthnRequest, BrowserInput, FormField, ReplayPolicy, RespondSso, Saml,
42 /// SamlValidationContext, SpDescriptor, Subject,
43 /// };
44 /// use std::time::SystemTime;
45 ///
46 /// # fn respond(
47 /// # idp: &Saml<saml_rs::Idp>,
48 /// # sp: &SpDescriptor,
49 /// # fields: Vec<FormField>,
50 /// # subject: Subject,
51 /// # ) -> Result<(), saml_rs::SamlError> {
52 /// let validation = SamlValidationContext::new(
53 /// SystemTime::now(),
54 /// ReplayPolicy::DisabledForCompatibility,
55 /// );
56 /// let input = BrowserInput::<AuthnRequest>::post(fields);
57 /// let request = idp.receive_sso(sp, input, validation)?;
58 /// let response = idp.respond_sso(sp, &request, subject, RespondSso::post())?;
59 ///
60 /// let form = response.post_form()?;
61 /// # let _ = form;
62 /// # Ok(()) }
63 /// ```
64 pub fn receive_sso(
65 &self,
66 sp: &SpDescriptor,
67 input: BrowserInput<AuthnRequest>,
68 validation: SamlValidationContext<'_>,
69 ) -> Result<Received<AuthnRequest>, SamlError> {
70 let relay_state = relay_state_from_input(&input)?;
71 let binding = SsoRequestBinding::try_from(input_binding(&input))?;
72 let raw_sp = raw_sp_descriptor(sp)?;
73 let request = HttpRequest::try_from(input)?;
74 let (flow, message_authenticated) = self.raw_identity_provider().parse_login_request_at(
75 &raw_sp,
76 binding.as_binding(),
77 &request,
78 validation.now(),
79 validation.clock_skew().as_millis(),
80 )?;
81 let authn = AuthnRequest::try_from(flow)?;
82 let verify_present_signature = self
83 .raw_identity_provider()
84 .setting
85 .verify_authn_request_signature_if_present;
86 if authn.destination().is_some() || (message_authenticated && verify_present_signature) {
87 let expected = self
88 .raw_identity_provider()
89 .metadata
90 .get_single_sign_on_service(binding.as_binding())
91 .ok_or_else(|| Error::MissingMetadata("SingleSignOnService".into()))?;
92 if authn.destination().map(|destination| destination.as_str())
93 != Some(expected.as_str())
94 {
95 return Err(Error::destination_mismatch(
96 &expected,
97 authn.destination().map(|destination| destination.as_str()),
98 ));
99 }
100 }
101 let mut validation = validation;
102 validation.check_authn_request_issue_instant(authn.issue_instant())?;
103 validation.check_and_store_message_replay(ReplayKey::AuthnRequestId(authn.id().clone()))?;
104 Ok(Received::new(authn).with_relay_state(relay_state))
105 }
106
107 /// Respond to a received SP AuthnRequest.
108 ///
109 /// # Errors
110 ///
111 /// Returns [`SamlError`] when the request issuer does not match the SP
112 /// descriptor, relay state is invalid, the request ACS selection conflicts
113 /// with the response binding or SP metadata, required metadata or signing
114 /// keys are missing, the configured issuance expiration overflows the
115 /// supported timestamp range, producer rules are combined with a custom
116 /// login response template, an error status is combined with a login
117 /// response template, or response creation fails.
118 pub fn respond_sso(
119 &self,
120 sp: &SpDescriptor,
121 request: &Received<AuthnRequest>,
122 subject: Subject,
123 options: RespondSso,
124 ) -> Result<Outbound<SsoResponse>, SamlError> {
125 ensure_entity_id(request.message().issuer(), sp.entity_id())?;
126 self.issue_sso(sp, Some(request), subject, options)
127 }
128
129 /// Initiate IdP-initiated SSO.
130 ///
131 /// # Errors
132 ///
133 /// Returns [`SamlError`] when relay state is invalid, SP metadata cannot be
134 /// parsed, a compatible ACS endpoint or signing key is missing, the
135 /// selected binding is unsupported, the configured issuance expiration
136 /// overflows the supported timestamp range, producer rules are combined
137 /// with a custom login response template, an error status is combined with
138 /// a login response template, or response creation fails.
139 ///
140 /// # Examples
141 ///
142 /// ```no_run
143 /// use saml_rs::{
144 /// BrowserInput, FormField, IdpDescriptor, ReplayPolicy, RespondSso, Saml,
145 /// SamlValidationContext, SpDescriptor, SsoResponse, Subject,
146 /// };
147 /// use std::time::SystemTime;
148 ///
149 /// # fn initiate(
150 /// # idp: &Saml<saml_rs::Idp>,
151 /// # sp: &Saml<saml_rs::Sp>,
152 /// # sp_descriptor: &SpDescriptor,
153 /// # idp_descriptor: &IdpDescriptor,
154 /// # subject: Subject,
155 /// # form_fields: Vec<FormField>,
156 /// # ) -> Result<(), saml_rs::SamlError> {
157 /// let response = idp.initiate_sso(sp_descriptor, subject, RespondSso::post())?;
158 /// let form = response.post_form()?;
159 /// # let _ = form;
160 ///
161 /// let validation = SamlValidationContext::new(
162 /// SystemTime::now(),
163 /// ReplayPolicy::DisabledForCompatibility,
164 /// );
165 /// let session = sp.accept_unsolicited_sso(
166 /// idp_descriptor,
167 /// BrowserInput::<SsoResponse>::post(form_fields),
168 /// validation,
169 /// )?;
170 /// let issuer = session.issuer().as_str();
171 /// # let _ = issuer;
172 /// # Ok(()) }
173 /// ```
174 pub fn initiate_sso(
175 &self,
176 sp: &SpDescriptor,
177 subject: Subject,
178 options: RespondSso,
179 ) -> Result<Outbound<SsoResponse>, SamlError> {
180 self.issue_sso(sp, None, subject, options)
181 }
182 fn issue_sso(
183 &self,
184 sp: &SpDescriptor,
185 request: Option<&Received<AuthnRequest>>,
186 subject: Subject,
187 options: RespondSso,
188 ) -> Result<Outbound<SsoResponse>, SamlError> {
189 let idp_setting = &self.raw_identity_provider().setting;
190 let sign_response = options.should_sign_response(
191 idp_setting.is_assertion_encrypted,
192 &idp_setting.data_encryption_algorithm,
193 );
194 let relay_state = options.relay_state.unwrap_or_else(|| {
195 request.map_or_else(RelayStateParam::absent, |request| {
196 request.relay_state().clone()
197 })
198 });
199 relay_state.validate()?;
200 let mut raw_sp = raw_sp_descriptor(sp)?;
201 raw_sp.setting.want_message_signed = sign_response;
202 let (binding, explicit_acs) = match request {
203 Some(request) => response_target(&raw_sp, request.message(), options.binding)?,
204 None => (options.binding, None),
205 };
206 let name_id_format = subject
207 .name_id()
208 .format()
209 .map(|format| format.as_uri().to_string());
210 let user = user_from_subject(subject);
211 let raw_options = LoginResponseOptions {
212 in_response_to: request.map(|request| request.message().id().as_str()),
213 relay_state: relay_state.as_deref(),
214 encrypt_then_sign: false,
215 custom: None,
216 };
217 let context = self
218 .raw_identity_provider()
219 .create_login_response_with_overrides(
220 &raw_sp,
221 binding.as_binding(),
222 &user,
223 &raw_options,
224 LoginResponseOverrides {
225 acs: explicit_acs.as_deref(),
226 name_id_format: name_id_format.as_deref(),
227 issuance_lifetime: Some(self.0.issuance_lifetime),
228 web_browser_sso_producer: options.web_browser_sso_producer,
229 status: options.status.as_ref(),
230 },
231 )?;
232 Outbound::<SsoResponse>::try_from(context)
233 }
234}
235fn user_from_subject(subject: Subject) -> crate::entity::User {
236 let name_id = subject.name_id().value().to_string();
237 crate::entity::User::new(name_id)
238}