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;