1use core::marker::PhantomData;
2
3use super::forms::{FormField, PostForm};
4use crate::constants::url_params;
5use crate::constants::Binding;
6use crate::entity::BindingContext;
7use crate::error::SamlError;
8use crate::model::{
9 AuthnRequest, EndpointUrl, LogoutRequest, LogoutResponse, MessageId, RelayState, SsoResponse,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13enum OutboundKind {
14 Redirect,
15 Post,
16 SimpleSignPost,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20enum MessageField {
21 Request,
22 Response,
23}
24
25impl MessageField {
26 fn param_name(self) -> &'static str {
27 match self {
28 Self::Request => url_params::SAML_REQUEST,
29 Self::Response => url_params::SAML_RESPONSE,
30 }
31 }
32
33 fn opposite_param_name(self) -> &'static str {
34 match self {
35 Self::Request => url_params::SAML_RESPONSE,
36 Self::Response => url_params::SAML_REQUEST,
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
82pub struct Outbound<Message> {
83 id: MessageId,
84 relay_state: Option<RelayState>,
85 kind: OutboundKind,
86 redirect_url: Option<String>,
87 post_form: Option<PostForm>,
88 raw_context: BindingContext,
89 _message: PhantomData<Message>,
90}
91
92impl<Message> Outbound<Message> {
93 pub fn id(&self) -> &MessageId {
95 &self.id
96 }
97
98 pub fn relay_state(&self) -> Option<&RelayState> {
100 self.relay_state.as_ref()
101 }
102
103 pub fn redirect_url(&self) -> Result<&str, SamlError> {
109 if self.kind == OutboundKind::Redirect {
110 return self
111 .redirect_url
112 .as_deref()
113 .ok_or_else(|| SamlError::Invalid("missing redirect URL".into()));
114 }
115 Err(SamlError::UndefinedBinding)
116 }
117
118 pub fn post_form(&self) -> Result<&PostForm, SamlError> {
124 if matches!(self.kind, OutboundKind::Post | OutboundKind::SimpleSignPost) {
125 return self
126 .post_form
127 .as_ref()
128 .ok_or_else(|| SamlError::Invalid("missing POST form".into()));
129 }
130 Err(SamlError::UndefinedBinding)
131 }
132
133 pub fn raw_context(&self) -> &BindingContext {
135 &self.raw_context
136 }
137
138 pub fn into_raw_context(self) -> BindingContext {
140 self.raw_context
141 }
142}
143
144impl TryFrom<BindingContext> for Outbound<AuthnRequest> {
145 type Error = SamlError;
146
147 fn try_from(raw_context: BindingContext) -> Result<Self, Self::Error> {
148 outbound_from_context(raw_context, true, MessageField::Request)
149 }
150}
151
152impl TryFrom<BindingContext> for Outbound<SsoResponse> {
153 type Error = SamlError;
154
155 fn try_from(raw_context: BindingContext) -> Result<Self, Self::Error> {
156 outbound_from_context(raw_context, false, MessageField::Response)
157 }
158}
159
160impl TryFrom<BindingContext> for Outbound<LogoutRequest> {
161 type Error = SamlError;
162
163 fn try_from(raw_context: BindingContext) -> Result<Self, Self::Error> {
164 outbound_from_context(raw_context, true, MessageField::Request)
165 }
166}
167
168impl TryFrom<BindingContext> for Outbound<LogoutResponse> {
169 type Error = SamlError;
170
171 fn try_from(raw_context: BindingContext) -> Result<Self, Self::Error> {
172 outbound_from_context(raw_context, true, MessageField::Response)
173 }
174}
175
176fn outbound_from_context<Message>(
177 raw_context: BindingContext,
178 allow_redirect: bool,
179 expected_message: MessageField,
180) -> Result<Outbound<Message>, SamlError> {
181 validate_context_message_kind(&raw_context, expected_message)?;
182 let id = MessageId::try_new(raw_context.id.clone())?;
183 let relay_state = raw_context
184 .relay_state
185 .clone()
186 .map(RelayState::try_new)
187 .transpose()?;
188 match raw_context.binding {
189 Binding::Redirect => {
190 if !allow_redirect {
191 return Err(SamlError::UndefinedBinding);
192 }
193 EndpointUrl::try_new(raw_context.context.clone())?;
194 validate_redirect_message_kind(&raw_context.context, expected_message)?;
195 Ok(Outbound {
196 id,
197 relay_state,
198 kind: OutboundKind::Redirect,
199 redirect_url: Some(raw_context.context.clone()),
200 post_form: None,
201 raw_context,
202 _message: PhantomData,
203 })
204 }
205 Binding::Post => {
206 reject_detached_signature_for_post(&raw_context)?;
207 let form = post_form_from_context(&raw_context, false)?;
208 Ok(Outbound {
209 id,
210 relay_state,
211 kind: OutboundKind::Post,
212 redirect_url: None,
213 post_form: Some(form),
214 raw_context,
215 _message: PhantomData,
216 })
217 }
218 Binding::SimpleSign => {
219 require_complete_detached_signature(&raw_context)?;
220 let form = post_form_from_context(&raw_context, true)?;
221 Ok(Outbound {
222 id,
223 relay_state,
224 kind: OutboundKind::SimpleSignPost,
225 redirect_url: None,
226 post_form: Some(form),
227 raw_context,
228 _message: PhantomData,
229 })
230 }
231 Binding::Artifact => Err(SamlError::UndefinedBinding),
232 }
233}
234
235fn validate_context_message_kind(
236 context: &BindingContext,
237 expected_message: MessageField,
238) -> Result<(), SamlError> {
239 let expected_name = expected_message.param_name();
240 if context.request_type == expected_name {
241 return Ok(());
242 }
243 Err(SamlError::Invalid(format!(
244 "outbound context request_type must be {expected_name}"
245 )))
246}
247
248fn validate_redirect_message_kind(
249 raw_url: &str,
250 expected_message: MessageField,
251) -> Result<(), SamlError> {
252 let expected_name = expected_message.param_name();
253 let opposite_name = expected_message.opposite_param_name();
254 let parsed = url::Url::parse(raw_url).map_err(|err| SamlError::Invalid(err.to_string()))?;
255 let (expected_count, opposite_count) =
256 parsed
257 .query_pairs()
258 .fold((0usize, 0usize), |(expected, opposite), (name, _)| {
259 if name == expected_name {
260 (expected + 1, opposite)
261 } else if name == opposite_name {
262 (expected, opposite + 1)
263 } else {
264 (expected, opposite)
265 }
266 });
267
268 match (expected_count, opposite_count) {
269 (1, 0) => Ok(()),
270 (0, 0) => Err(SamlError::Invalid(format!(
271 "missing Redirect field {expected_name}"
272 ))),
273 (count, 0) if count > 1 => Err(SamlError::Invalid(format!(
274 "ambiguous Redirect field {expected_name}"
275 ))),
276 (_, _) => Err(SamlError::Invalid(format!(
277 "expected Redirect field {expected_name}, found {opposite_name}"
278 ))),
279 }
280}
281
282fn reject_partial_detached_signature(context: &BindingContext) -> Result<(), SamlError> {
283 if context.sig_alg.is_some() != context.signature.is_some() {
284 return Err(SamlError::Invalid(
285 "partial detached signature state is invalid".into(),
286 ));
287 }
288 Ok(())
289}
290
291fn reject_detached_signature_for_post(context: &BindingContext) -> Result<(), SamlError> {
292 if context.sig_alg.is_some() || context.signature.is_some() {
293 return Err(SamlError::Invalid(
294 "POST outbound must not carry detached signature fields".into(),
295 ));
296 }
297 Ok(())
298}
299
300fn require_complete_detached_signature(context: &BindingContext) -> Result<(), SamlError> {
301 reject_partial_detached_signature(context)?;
302 match (&context.sig_alg, &context.signature) {
303 (Some(_), Some(_)) => Ok(()),
304 _ => Err(SamlError::Invalid(
305 "SimpleSign requires SigAlg and Signature".into(),
306 )),
307 }
308}
309
310fn post_form_from_context(
311 context: &BindingContext,
312 include_signature: bool,
313) -> Result<PostForm, SamlError> {
314 let action = EndpointUrl::try_new(context.entity_endpoint.clone())?;
315 context.try_post_form()?;
316 let mut fields = vec![FormField::new(
317 context.request_type,
318 context.context.clone(),
319 )];
320 if let Some(relay_state) = &context.relay_state {
321 fields.push(FormField::new("RelayState", relay_state.clone()));
322 }
323 if include_signature {
324 fields.push(FormField::new(
325 "SigAlg",
326 context.sig_alg.clone().unwrap_or_default(),
327 ));
328 fields.push(FormField::new(
329 "Signature",
330 context.signature.clone().unwrap_or_default(),
331 ));
332 }
333 Ok(PostForm::new(action, fields))
334}