Skip to main content

saml_rs/
lib.rs

1//! `saml-rs` - SAML 2.0 Service Provider and Identity Provider support.
2//!
3//! # Start here
4//!
5//! Start new browser SSO/SLO integrations with [`Saml`]. The typed facade keeps
6//! local role state in [`Saml<Sp>`] or [`Saml<Idp>`], accepts peer metadata
7//! through typed descriptors, and returns pending transaction values that
8//! callers can store with browser session state.
9//!
10//! [`SpValidationPolicy::recommended`] is the preset for claimed features.
11//! [`SpValidationPolicy::compatibility`] keeps the samlify-port behavior.
12//! Config builders still start on the deprecated `strict()` bundle. `Default`,
13//! `new`, and `try_new` stay on `compatibility()`.
14//!
15//! Where the compact flow examples below use
16//! [`ReplayPolicy::DisabledForCompatibility`] or unsigned metadata, treat those
17//! as explicit interoperability choices. Production-shaped inbound flows should
18//! use [`ReplayPolicy::RequireCache`] with a caller-owned [`ReplayCache`] and,
19//! when protocol timestamps are not enough for expiry,
20//! [`SamlValidationContext::with_replay_retention`].
21//!
22//! ```
23//! use saml_rs::{AcsEndpoint, EntityId, SpConfig, SpValidationPolicy};
24//!
25//! # fn main() -> Result<(), saml_rs::SamlError> {
26//! let config = SpConfig::builder(EntityId::try_new("https://sp.example.com/metadata")?)
27//!     .acs_endpoint(AcsEndpoint::post("https://sp.example.com/acs")?)
28//!     .validation(SpValidationPolicy::compatibility())
29//!     .build()?;
30//!
31//! assert_eq!(config.entity_id.as_str(), "https://sp.example.com/metadata");
32//! # Ok(()) }
33//! ```
34//!
35//! # SP-initiated SSO
36//!
37//! [`Saml<Sp>::start_sso`] creates the browser action and [`PendingAuthnRequest`].
38//! Store the pending value and pass it back to [`Saml<Sp>::finish_sso`] when the
39//! ACS endpoint receives the SAML response.
40//!
41//! ```no_run
42//! use saml_rs::{
43//!     AcsEndpoint, BrowserInput, EntityId, FormField, IdpDescriptor,
44//!     MetadataTrustPolicy, ReplayPolicy, Saml, SamlValidationContext, SpConfig,
45//!     SpValidationPolicy, SsoResponse, StartSso,
46//! };
47//! use std::time::SystemTime;
48//!
49//! # fn run(
50//! #     idp_metadata_xml: &str,
51//! #     form_fields: Vec<FormField>,
52//! # ) -> Result<(), saml_rs::SamlError> {
53//! let sp = Saml::sp(
54//!     SpConfig::builder(EntityId::try_new("https://sp.example.com/metadata")?)
55//!         .acs_endpoint(AcsEndpoint::post("https://sp.example.com/acs")?)
56//!         .validation(SpValidationPolicy::compatibility())
57//!         .build()?,
58//! )?;
59//! let idp = IdpDescriptor::from_metadata_xml_for(
60//!     EntityId::try_new("https://idp.example.com/metadata")?,
61//!     idp_metadata_xml,
62//!     MetadataTrustPolicy::UnsignedForCompatibility,
63//! )?;
64//!
65//! let started = sp.start_sso(&idp, StartSso::redirect())?;
66//! let redirect_url = started.outbound.redirect_url()?;
67//! # let _ = redirect_url;
68//!
69//! let validation = SamlValidationContext::new(
70//!     SystemTime::now(),
71//!     ReplayPolicy::DisabledForCompatibility,
72//! );
73//! let session = sp.finish_sso(
74//!     &idp,
75//!     &started.pending,
76//!     BrowserInput::<SsoResponse>::post(form_fields),
77//!     validation,
78//! )?;
79//! let name_id = session.name_id().value();
80//! # let _ = name_id;
81//! # Ok(()) }
82//! ```
83//!
84//! [`SsoSession::verified_xml_signatures`] preserves structured evidence for
85//! each successfully verified embedded XML signature that covers the Response
86//! root or consumed Assertion. Its exact algorithm URI comes from the same
87//! verifier-owned signature node as the successful verdict. This is separate
88//! from [`SsoSession::sig_alg`], which retains its singular detached
89//! HTTP-Redirect or HTTP-POST-SimpleSign meaning; neither accessor imposes an
90//! application algorithm allowlist.
91//!
92//! # IdP-initiated SSO
93//!
94//! Use [`Saml<Sp>::accept_unsolicited_sso`] for IdP-initiated responses. This
95//! method is separate from `finish_sso` so unsolicited responses are an explicit
96//! caller choice rather than a missing pending request.
97//!
98//! ```no_run
99//! use saml_rs::{
100//!     BrowserInput, FormField, IdpDescriptor, ReplayPolicy, Saml,
101//!     SamlValidationContext, SsoResponse,
102//! };
103//! use std::time::SystemTime;
104//!
105//! # fn accept(
106//! #     sp: &Saml<saml_rs::Sp>,
107//! #     idp: &IdpDescriptor,
108//! #     form_fields: Vec<FormField>,
109//! # ) -> Result<(), saml_rs::SamlError> {
110//! let validation = SamlValidationContext::new(
111//!     SystemTime::now(),
112//!     ReplayPolicy::DisabledForCompatibility,
113//! );
114//! let session = sp.accept_unsolicited_sso(
115//!     idp,
116//!     BrowserInput::<SsoResponse>::post(form_fields),
117//!     validation,
118//! )?;
119//! let issuer = session.issuer().as_str();
120//! # let _ = issuer;
121//! # Ok(()) }
122//! ```
123//!
124//! # Identity Provider flows
125//!
126//! [`Saml<Idp>::receive_sso`] parses an SP `AuthnRequest`; [`Saml<Idp>::respond_sso`]
127//! returns the typed browser response.
128//!
129//! ```no_run
130//! use saml_rs::{
131//!     AuthnRequest, BrowserInput, FormField, NameId, ReplayPolicy, RespondSso,
132//!     Saml, SamlValidationContext, SpDescriptor, Subject,
133//! };
134//! use std::time::SystemTime;
135//!
136//! # fn respond(
137//! #     idp: &Saml<saml_rs::Idp>,
138//! #     sp: &SpDescriptor,
139//! #     request_fields: Vec<FormField>,
140//! # ) -> Result<(), saml_rs::SamlError> {
141//! let validation = SamlValidationContext::new(
142//!     SystemTime::now(),
143//!     ReplayPolicy::DisabledForCompatibility,
144//! );
145//! let request = idp.receive_sso(
146//!     sp,
147//!     BrowserInput::<AuthnRequest>::post(request_fields),
148//!     validation,
149//! )?;
150//! let response = idp.respond_sso(
151//!     sp,
152//!     &request,
153//!     Subject::new(NameId::new("alice@example.com", None), Vec::new()),
154//!     RespondSso::post(),
155//! )?;
156//! let form = response.post_form()?;
157//! # let _ = form;
158//! # Ok(()) }
159//! ```
160//!
161//! # Single Logout
162//!
163//! Typed SLO uses the same pattern: start with a [`LogoutSubject`], store the
164//! returned [`PendingLogoutRequest`], and finish only with the matching
165//! [`LogoutResponse`]. Receiving and responding to peer-initiated logout uses
166//! [`Received<LogoutRequest>`] instead of free-form request ID strings.
167//! Inbound LogoutRequest messages require a UTC `IssueInstant`, but saml-rs
168//! applies no library-selected maximum age to it. Optional UTC
169//! `NotOnOrAfter` values are rejected at their skew-adjusted exclusive
170//! deadline as a fail-closed saml-rs policy, not an OASIS receiver `MUST`.
171//! A new [`SamlValidationContext`] allows five minutes of skew on that
172//! deadline. [`ClockSkew::strict`] keeps the tolerance at zero, and replay
173//! storage uses the same effective deadline when it is present.
174//!
175//! ```no_run
176//! use saml_rs::{
177//!     BrowserInput, FormField, IdpDescriptor, LogoutResponse, ReplayPolicy,
178//!     Saml, SamlValidationContext, SsoSession, StartSlo,
179//! };
180//! use std::time::SystemTime;
181//!
182//! # fn logout(
183//! #     sp: &Saml<saml_rs::Sp>,
184//! #     idp: &IdpDescriptor,
185//! #     session: &SsoSession,
186//! #     response_fields: Vec<FormField>,
187//! # ) -> Result<(), saml_rs::SamlError> {
188//! if let Some(subject) = session.logout_subject() {
189//!     let started = sp.start_slo(idp, subject, StartSlo::post())?;
190//!     let validation = SamlValidationContext::new(
191//!         SystemTime::now(),
192//!         ReplayPolicy::DisabledForCompatibility,
193//!     );
194//!     let completed = sp.finish_slo(
195//!         idp,
196//!         &started.pending,
197//!         BrowserInput::<LogoutResponse>::post(response_fields),
198//!         validation,
199//!     )?;
200//!     let peer = completed.peer_entity_id().as_str();
201//!     # let _ = peer;
202//! }
203//! # Ok(()) }
204//! ```
205//!
206//! # Compile-time flow boundaries
207//!
208//! SSO and SLO pending values are different types. A logout pending value cannot
209//! be used to finish Web SSO:
210//!
211//! ```compile_fail
212//! use saml_rs::{
213//!     BrowserInput, IdpDescriptor, PendingLogoutRequest, Saml,
214//!     SamlValidationContext, SsoResponse,
215//! };
216//!
217//! fn wrong(
218//!     sp: &Saml<saml_rs::Sp>,
219//!     idp: &IdpDescriptor,
220//!     pending: &PendingLogoutRequest,
221//!     input: BrowserInput<SsoResponse>,
222//!     validation: SamlValidationContext<'_>,
223//! ) -> Result<(), saml_rs::SamlError> {
224//!     let _ = sp.finish_sso(idp, pending, input, validation)?;
225//!     Ok(())
226//! }
227//! ```
228//!
229//! SLO responses are correlated through [`Received<LogoutRequest>`], not
230//! arbitrary request ID strings:
231//!
232//! ```compile_fail
233//! use saml_rs::{RespondSlo, Saml, SpDescriptor};
234//!
235//! fn wrong(
236//!     idp: &Saml<saml_rs::Idp>,
237//!     sp: &SpDescriptor,
238//!     request_id: &str,
239//! ) -> Result<(), saml_rs::SamlError> {
240//!     let _ = idp.respond_slo(sp, request_id, RespondSlo::post())?;
241//!     Ok(())
242//! }
243//! ```
244//!
245//! # Metadata trust
246//!
247//! Metadata trust is explicit and caller-pinned. [`MetadataTrustPolicy`] can
248//! accept unsigned metadata with
249//! [`MetadataTrustPolicy::UnsignedForCompatibility`] to preserve a samlify-port
250//! or raw import, or require a signature from caller-provided certificates with
251//! [`MetadataTrustPolicy::RequireSignature`].
252//! [`MetadataTrustPolicy::RequireSignatureAllowingOtherCanonicalization`]
253//! also accepts inclusive canonicalization. Prefer signed metadata with
254//! pinned certificates for production trust decisions; the crate does not treat
255//! the public web PKI CA store as SAML metadata trust.
256//!
257//! # Raw samlify-port API
258//!
259//! The [`raw`] module is the samlify-port surface. [`SpValidationPolicy::compatibility`]
260//! keeps that behavior when leaving the raw API. The two surfaces keep their
261//! own defaults. Config builders still start on the deprecated `strict()`
262//! bundle. Advanced callers should import
263//! [`raw::ServiceProvider`], [`raw::IdentityProvider`], [`raw::HttpRequest`],
264//! and [`raw::BindingContext`] from there rather than using the root re-exports.
265//!
266//! Visible docs.rs modules and crate-root re-exports are the supported public
267//! documentation surface. The [`raw`] module is the supported samlify-port
268//! API. Hidden modules are lower-level implementation paths and should not be
269//! the first choice for new integrations.
270//!
271//! # Unsupported profiles
272//!
273//! The high-level [`Saml`] API focuses on browser Web SSO, metadata-driven SP/IdP
274//! setup, XML signature/encryption through `bergshamra`, and Single Logout. It
275//! does not yet implement Artifact resolution, SOAP/back-channel profiles,
276//! ECP/PAOS, SAML query protocols, NameID management, or metadata federation. If
277//! you need one of those profiles for a real interoperability target, please
278//! open an issue with the profile, binding, IdP/SP product, and a minimal
279//! expected flow so we can consider the implementation.
280//!
281//! XML cryptography (XML-DSig sign/verify with anti-wrapping, XML-Enc, detached
282//! message signatures) is delegated to `bergshamra`. The default
283//! `crypto-bergshamra` feature selects RustCrypto; applications
284//! can instead select `crypto-aws-lc` or `crypto-fips` with default features
285//! disabled. Configure assertion encryption and the RustCrypto XML-Enc exception
286//! through [`XmlEncryptionPolicy`].
287//! Disable default features to build the crypto-free protocol layer; crypto
288//! operations then fail closed with [`SamlError::Unsupported`].
289
290#![forbid(unsafe_code)]
291
292#[cfg(any(
293    all(feature = "crypto-rustcrypto", feature = "crypto-aws-lc"),
294    all(feature = "crypto-rustcrypto", feature = "crypto-fips"),
295    all(feature = "crypto-aws-lc", feature = "crypto-fips")
296))]
297compile_error!(
298    "crypto-rustcrypto, crypto-aws-lc, and crypto-fips are mutually exclusive; select exactly one"
299);
300
301#[cfg(all(
302    any(
303        feature = "crypto-legacy-algorithms",
304        feature = "crypto-post-quantum",
305        feature = "crypto-pkcs11"
306    ),
307    not(any(
308        feature = "crypto-rustcrypto",
309        feature = "crypto-aws-lc",
310        feature = "crypto-fips"
311    ))
312))]
313compile_error!(
314    "crypto capability features require one provider: crypto-rustcrypto, crypto-aws-lc, or crypto-fips"
315);
316
317#[doc(hidden)]
318pub mod api;
319mod assertion_acceptance;
320#[doc(hidden)]
321pub mod binding;
322pub mod browser;
323pub mod config;
324pub mod constants;
325#[doc(hidden)]
326pub mod context;
327#[doc(hidden)]
328pub mod crypto;
329#[doc(hidden)]
330pub mod entity;
331pub mod error;
332#[doc(hidden)]
333pub mod flow;
334#[doc(hidden)]
335pub mod idp;
336#[doc(hidden)]
337pub mod logout;
338pub mod metadata;
339pub mod model;
340pub mod raw;
341#[doc(hidden)]
342pub mod sp;
343#[doc(hidden)]
344pub mod template;
345#[doc(hidden)]
346pub mod util;
347#[doc(hidden)]
348pub mod validator;
349#[doc(hidden)]
350pub mod xml;
351
352pub use api::{
353    ForceAuthn, Idp, IsPassive, LogoutSigning, RespondSlo, RespondSso, Saml, SamlError, Sp,
354    StartSlo, StartSso, Unknown,
355};
356pub use browser::{
357    AcsEndpoint, BrowserInput, EndpointUrl, FormField, LogoutBinding, Outbound, Pending,
358    PendingAuthnRequest, PendingLogoutRequest, PendingSnapshot, PostForm, SloEndpoint, SsoEndpoint,
359    SsoRequestBinding, SsoResponseBinding, Started,
360};
361pub use config::{
362    AlgorithmPolicy, AssertionEncryptionPolicy, AssertionSignaturePolicy, AudienceValidationPolicy,
363    AuthnRequestSigningPolicy, AuthnRequestValidationPolicy, CertificatePem, Credentials,
364    DataEncryptionAlgorithm, DigestAlgorithm, EntityId, IdpConfig, IdpConfigBuilder, IdpDescriptor,
365    IdpMetadataConfig, IdpValidationPolicy, KeyEncryptionAlgorithm, LogoutPolicy,
366    LogoutSignaturePolicy, MetadataTrustPolicy, NameIdCreationPolicy, NameIdFormat, Passphrase,
367    PrivateKeyPem, ResponseSignaturePolicy, SignatureAlgorithm, SpConfig, SpConfigBuilder,
368    SpDescriptor, SpMetadataConfig, SpValidationPolicy, TemplatePolicy, TransformAlgorithm,
369    XmlEncryptionPolicy, XmlPolicy, XmlSignatureProfile,
370};
371#[cfg(any(
372    feature = "crypto-rustcrypto",
373    feature = "crypto-aws-lc",
374    feature = "crypto-fips"
375))]
376pub use crypto::{
377    crypto_provider_info, initialize_crypto_provider, CryptoFipsStatus, CryptoProvider,
378    CryptoProviderInfo,
379};
380#[doc = "Re-export kept for older crate-root imports of the raw samlify-port API. Use `Saml` for new integrations; advanced raw callers should import `raw::EntitySetting`."]
381pub use entity::EntitySetting;
382#[doc = "Re-export kept for older crate-root imports of the raw samlify-port API. Use `Saml` for new integrations; advanced raw callers should import `raw::IdentityProvider`."]
383pub use idp::IdentityProvider;
384#[cfg(any(
385    feature = "crypto-rustcrypto",
386    feature = "crypto-aws-lc",
387    feature = "crypto-fips"
388))]
389pub use metadata::MetadataSignatureVerification;
390pub use model::{
391    Assertion, AssertionId, Attribute, AttributeValue, Attributes, AuthnRequest,
392    AuthnRequestAgePolicy, AuthnSession, ClockSkew, LogoutCompleted, LogoutRequest, LogoutResponse,
393    LogoutSubject, MessageId, NameId, NameIdCreationRequest, NameIdPolicy, OutstandingLogout,
394    Received, RelayState, RelayStateParam, ReplayCache, ReplayKey, ReplayPolicy, RequestedSubject,
395    RequestedSubjectIdentifier, SamlInstant, SamlValidationContext, SessionIndex, SsoResponse,
396    SsoSession, Status, Subject, SubjectConfirmation, SubordinateStatusCode, TopLevelStatusCode,
397    VerifiedXmlSignature, VerifiedXmlSignatureCoverage, MAX_RELAY_STATE_BYTES,
398};
399#[doc = "Re-export kept for older crate-root imports of the raw samlify-port API. Use `Saml` for new integrations; advanced raw callers should import `raw::ServiceProvider`."]
400pub use sp::ServiceProvider;