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