#[path = "common/mod.rs"]
mod common;
use std::io::Read;
use std::time::Duration;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use flate2::read::DeflateDecoder;
use saml::attribute::Attribute;
use saml::authn_context::AuthnContextClassRef;
use saml::binding::{Binding, Dispatch, SsoResponseBinding, SsoResponseDispatch};
use saml::idp::{ConsumeAuthnRequest, IssueResponse};
use saml::nameid::{NameId, NameIdFormat};
use saml::sp::StartLogin;
const SP_ENTITY_ID: &str = "https://sp.example.com/idp-flow";
const SP_ACS_URL: &str = "https://sp.example.com/idp-flow/acs";
const IDP_ENTITY_ID: &str = "https://idp.example.com/idp-flow";
const IDP_SSO_URL: &str = "https://idp.example.com/idp-flow/sso";
#[test]
fn idp_consumes_redirect_authn_request_and_emits_response() {
let sp = common::make_sp(SP_ENTITY_ID, SP_ACS_URL, false).expect("sp builds");
let idp = common::make_idp(IDP_ENTITY_ID, IDP_SSO_URL).expect("idp builds");
let idp_descriptor = common::idp_descriptor(&idp).expect("idp descriptor");
let sp_descriptor = common::sp_descriptor(&sp).expect("sp descriptor");
let now = common::fixed_now().expect("fixed_now");
let start = sp
.start_login(
&idp_descriptor,
StartLogin {
relay_state: Some("idp-flow-relay"),
binding: Binding::HttpRedirect,
force_authn: false,
is_passive: false,
requested_name_id_format: Some(NameIdFormat::Persistent),
requested_authn_context: None,
acs_index: None,
acs_url: None,
response_binding: Some(SsoResponseBinding::HttpPost),
},
)
.expect("start_login");
let redirect_url = match start.dispatch {
Dispatch::Redirect(u) => u,
Dispatch::Post(_) => panic!("expected Redirect dispatch from start_login"),
};
let saml_request_param = redirect_url
.query_pairs()
.find(|(k, _)| k == "SAMLRequest")
.map(|(_, v)| v.into_owned())
.expect("SAMLRequest query parameter present");
let deflated = BASE64
.decode(saml_request_param.as_bytes())
.expect("SAMLRequest base64 valid");
let mut decoder = DeflateDecoder::new(deflated.as_slice());
let mut authn_request_xml = Vec::new();
decoder
.read_to_end(&mut authn_request_xml)
.expect("DEFLATE inflate");
let relay_state_on_wire = redirect_url
.query_pairs()
.find(|(k, _)| k == "RelayState")
.map(|(_, v)| v.into_owned())
.expect("RelayState present in redirect URL");
assert_eq!(relay_state_on_wire, "idp-flow-relay");
let parsed = idp
.consume_authn_request(ConsumeAuthnRequest {
sp: &sp_descriptor,
peer_crypto_policy: None,
saml_request: &authn_request_xml,
binding: Binding::HttpRedirect,
relay_state: Some(&relay_state_on_wire),
detached_signature: None,
expected_destination: IDP_SSO_URL,
now,
clock_skew: Duration::from_mins(2),
})
.expect("idp consume_authn_request");
assert_eq!(parsed.issuer, SP_ENTITY_ID);
assert_eq!(parsed.assertion_consumer_service.url, SP_ACS_URL);
assert_eq!(parsed.relay_state.as_deref(), Some("idp-flow-relay"));
assert_eq!(
parsed.requested_name_id_format,
Some(NameIdFormat::Persistent),
);
let dispatch = idp
.issue_response(IssueResponse {
sp: &sp_descriptor,
in_response_to: &parsed,
name_id: NameId::persistent_for_sp("opaque-user-42", SP_ENTITY_ID),
attributes: vec![Attribute::display_name("Idp Flow User")],
authn_instant: now,
session_index: "sess-idp-flow-1".to_owned(),
session_not_on_or_after: Some(
now.checked_add(Duration::from_hours(1))
.expect("session_not_on_or_after fits"),
),
authn_context_class_ref: AuthnContextClassRef::PasswordProtectedTransport,
force_encrypt_assertion: None,
now,
assertion_lifetime: Duration::from_mins(10),
subject_confirmation_lifetime: Duration::from_mins(5),
holder_of_key_cert: None,
})
.expect("idp issue_response");
let form = match dispatch {
SsoResponseDispatch::Post(f) => f,
SsoResponseDispatch::Artifact(_) => {
panic!("expected POST dispatch for SSO Response")
}
};
assert_eq!(form.action.as_str(), SP_ACS_URL);
assert_eq!(form.relay_state.as_deref(), Some("idp-flow-relay"));
let response_xml = BASE64
.decode(form.saml_response.as_bytes())
.expect("Response base64 valid");
let as_str = std::str::from_utf8(&response_xml).expect("UTF-8");
assert!(as_str.contains(":Response"), "carries Response element");
assert!(as_str.contains(":Assertion"), "carries Assertion element");
assert!(
as_str.contains(SP_ENTITY_ID),
"Audience includes SP entityID"
);
let identity = sp
.consume_response(saml::sp::ConsumeResponse {
idp: &idp_descriptor,
peer_crypto_policy: None,
saml_response: &response_xml,
binding: SsoResponseBinding::HttpPost,
relay_state: Some("idp-flow-relay"),
tracker: Some(&start.tracker),
expected_destination: SP_ACS_URL,
now,
clock_skew: Duration::from_mins(2),
replay_cache: None,
replay_mode: saml::replay::ReplayMode::All,
holder_of_key_cert: None,
})
.expect("SP round-trips the IdP-issued Response");
assert_eq!(identity.name_id.value, "opaque-user-42");
}