Skip to content

Commit 2acc01b

Browse files
authored
Requested authn context (#50)
* Add `RequestedAuthnContext` to `AuthnRequest` * Update CHANGELOG * Implement `FromStr` for Assertion and add a test for it --------- Co-authored-by: Daniel Wiesenberg <[email protected]>
1 parent d5d0fff commit 2acc01b

File tree

4 files changed

+180
-1
lines changed

4 files changed

+180
-1
lines changed

CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
### Unreleased
4+
- Add support for basic `RequestedAuthnContext` de-/serialization in `AuthnRequest`
5+
36
### 0.0.15
47

58
- Updates dependencies

src/schema/authn_request.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::schema::{Conditions, Issuer, NameIdPolicy, Subject};
1+
use crate::schema::{Conditions, Issuer, NameIdPolicy, RequestedAuthnContext, Subject};
22
use crate::signature::Signature;
33
use chrono::prelude::*;
44
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
@@ -36,6 +36,8 @@ pub struct AuthnRequest {
3636
pub name_id_policy: Option<NameIdPolicy>,
3737
#[serde(rename = "Conditions")]
3838
pub conditions: Option<Conditions>,
39+
#[serde(rename = "RequestedAuthnContext")]
40+
pub requested_authn_context: Option<RequestedAuthnContext>,
3941
#[serde(rename = "@ForceAuthn")]
4042
pub force_authn: Option<bool>,
4143
#[serde(rename = "@IsPassive")]
@@ -65,6 +67,7 @@ impl Default for AuthnRequest {
6567
subject: None,
6668
name_id_policy: None,
6769
conditions: None,
70+
requested_authn_context: None,
6871
force_authn: None,
6972
is_passive: None,
7073
assertion_consumer_service_index: None,
@@ -219,6 +222,10 @@ impl TryFrom<&AuthnRequest> for Event<'_> {
219222
let event: Event<'_> = conditions.try_into()?;
220223
writer.write_event(event)?;
221224
}
225+
if let Some(requested_authn_context) = &value.requested_authn_context {
226+
let event: Event<'_> = requested_authn_context.try_into()?;
227+
writer.write_event(event)?;
228+
}
222229

223230
writer.write_event(Event::End(BytesEnd::new(NAME)))?;
224231

src/schema/mod.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ pub mod authn_request;
22
mod conditions;
33
mod issuer;
44
mod name_id_policy;
5+
mod requested_authn_context;
56
mod response;
67
mod subject;
78

89
pub use authn_request::AuthnRequest;
910
pub use conditions::*;
1011
pub use issuer::Issuer;
1112
pub use name_id_policy::NameIdPolicy;
13+
pub use requested_authn_context::{AuthnContextComparison, RequestedAuthnContext};
1214
pub use response::Response;
1315
pub use subject::*;
1416

@@ -197,6 +199,14 @@ impl Assertion {
197199
}
198200
}
199201

202+
impl FromStr for Assertion {
203+
type Err = Box<dyn std::error::Error>;
204+
205+
fn from_str(s: &str) -> Result<Self, Self::Err> {
206+
Ok(quick_xml::de::from_str(s)?)
207+
}
208+
}
209+
200210
impl TryFrom<Assertion> for Event<'_> {
201211
type Error = Box<dyn std::error::Error>;
202212

@@ -483,6 +493,47 @@ impl TryFrom<&AuthnContextClassRef> for Event<'_> {
483493
}
484494
}
485495

496+
#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)]
497+
pub struct AuthnContextDeclRef {
498+
#[serde(rename = "$value")]
499+
pub value: Option<String>,
500+
}
501+
502+
impl AuthnContextDeclRef {
503+
fn name() -> &'static str {
504+
"saml2:AuthnContextDeclRef"
505+
}
506+
}
507+
508+
impl TryFrom<AuthnContextDeclRef> for Event<'_> {
509+
type Error = Box<dyn std::error::Error>;
510+
511+
fn try_from(value: AuthnContextDeclRef) -> Result<Self, Self::Error> {
512+
(&value).try_into()
513+
}
514+
}
515+
516+
impl TryFrom<&AuthnContextDeclRef> for Event<'_> {
517+
type Error = Box<dyn std::error::Error>;
518+
519+
fn try_from(value: &AuthnContextDeclRef) -> Result<Self, Self::Error> {
520+
if let Some(value) = &value.value {
521+
let mut write_buf = Vec::new();
522+
let mut writer = Writer::new(Cursor::new(&mut write_buf));
523+
let root = BytesStart::new(AuthnContextDeclRef::name());
524+
525+
writer.write_event(Event::Start(root))?;
526+
writer.write_event(Event::Text(BytesText::from_escaped(value)))?;
527+
writer.write_event(Event::End(BytesEnd::new(AuthnContextDeclRef::name())))?;
528+
Ok(Event::Text(BytesText::from_escaped(String::from_utf8(
529+
write_buf,
530+
)?)))
531+
} else {
532+
Ok(Event::Text(BytesText::from_escaped(String::new())))
533+
}
534+
}
535+
}
536+
486537
#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)]
487538
pub struct Status {
488539
#[serde(rename = "StatusCode")]

src/schema/requested_authn_context.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use crate::schema::{AuthnContextClassRef, AuthnContextDeclRef};
2+
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
3+
use quick_xml::Writer;
4+
use serde::Deserialize;
5+
use std::io::Cursor;
6+
use std::str::FromStr;
7+
8+
const NAME: &str = "saml2p:RequestedAuthnContext";
9+
const SCHEMA: (&str, &str) = ("xmlns:saml2", "urn:oasis:names:tc:SAML:2.0:assertion");
10+
11+
#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)]
12+
pub struct RequestedAuthnContext {
13+
#[serde(rename = "AuthnContextClassRef")]
14+
pub authn_context_class_refs: Option<Vec<AuthnContextClassRef>>,
15+
#[serde(rename = "AuthnContextDeclRef")]
16+
pub authn_context_decl_refs: Option<Vec<AuthnContextDeclRef>>,
17+
#[serde(rename = "@Comparison")]
18+
pub comparison: Option<AuthnContextComparison>,
19+
}
20+
21+
impl TryFrom<RequestedAuthnContext> for Event<'_> {
22+
type Error = Box<dyn std::error::Error>;
23+
24+
fn try_from(value: RequestedAuthnContext) -> Result<Self, Self::Error> {
25+
(&value).try_into()
26+
}
27+
}
28+
29+
impl TryFrom<&RequestedAuthnContext> for Event<'_> {
30+
type Error = Box<dyn std::error::Error>;
31+
32+
fn try_from(value: &RequestedAuthnContext) -> Result<Self, Self::Error> {
33+
let mut write_buf = Vec::new();
34+
let mut writer = Writer::new(Cursor::new(&mut write_buf));
35+
let mut root = BytesStart::from_content(NAME, NAME.len());
36+
root.push_attribute(SCHEMA);
37+
38+
if let Some(comparison) = &value.comparison {
39+
root.push_attribute(("Comparison", comparison.value()));
40+
}
41+
writer.write_event(Event::Start(root))?;
42+
43+
if let Some(authn_context_class_refs) = &value.authn_context_class_refs {
44+
for authn_context_class_ref in authn_context_class_refs {
45+
let event: Event<'_> = authn_context_class_ref.try_into()?;
46+
writer.write_event(event)?;
47+
}
48+
} else if let Some(authn_context_decl_refs) = &value.authn_context_decl_refs {
49+
for authn_context_decl_ref in authn_context_decl_refs {
50+
let event: Event<'_> = authn_context_decl_ref.try_into()?;
51+
writer.write_event(event)?;
52+
}
53+
}
54+
55+
writer.write_event(Event::End(BytesEnd::new(NAME)))?;
56+
Ok(Event::Text(BytesText::from_escaped(String::from_utf8(
57+
write_buf,
58+
)?)))
59+
}
60+
}
61+
62+
#[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd)]
63+
#[serde(rename_all = "lowercase")]
64+
pub enum AuthnContextComparison {
65+
Exact,
66+
Minimum,
67+
Maximum,
68+
Better,
69+
}
70+
71+
impl AuthnContextComparison {
72+
pub fn value(&self) -> &'static str {
73+
match self {
74+
AuthnContextComparison::Exact => "exact",
75+
AuthnContextComparison::Minimum => "minimum",
76+
AuthnContextComparison::Maximum => "maximum",
77+
AuthnContextComparison::Better => "better",
78+
}
79+
}
80+
}
81+
82+
impl FromStr for AuthnContextComparison {
83+
type Err = quick_xml::DeError;
84+
85+
fn from_str(s: &str) -> Result<Self, Self::Err> {
86+
Ok(match s {
87+
"exact" => AuthnContextComparison::Exact,
88+
"minimum" => AuthnContextComparison::Minimum,
89+
"maximum" => AuthnContextComparison::Maximum,
90+
"better" => AuthnContextComparison::Better,
91+
_ => {
92+
return Err(quick_xml::DeError::Custom("Illegal comparison! Must be one of `exact`, `minimum`, `maximum` or `better`".to_string()));
93+
}
94+
})
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod test {
100+
use crate::traits::ToXml;
101+
102+
use super::*;
103+
104+
#[test]
105+
pub fn test_deserialize_serialize_requested_authn_context() {
106+
let xml_context = r#"<saml2p:RequestedAuthnContext xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" Comparison="exact"><saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef></saml2p:RequestedAuthnContext>"#;
107+
108+
let expected_context: RequestedAuthnContext = quick_xml::de::from_str(xml_context)
109+
.expect("failed to parse RequestedAuthnContext");
110+
let serialized_context = expected_context
111+
.to_xml()
112+
.expect("failed to convert RequestedAuthnContext to xml");
113+
let actual_context: RequestedAuthnContext = quick_xml::de::from_str(&serialized_context)
114+
.expect("failed to re-parse RequestedAuthnContext");
115+
116+
assert_eq!(expected_context, actual_context);
117+
}
118+
}

0 commit comments

Comments
 (0)