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//!
158//! ```no_run
159//! use saml_rs::{
160//!     BrowserInput, FormField, IdpDescriptor, LogoutResponse, ReplayPolicy,
161//!     Saml, SamlValidationContext, SsoSession, StartSlo,
162//! };
163//! use std::time::SystemTime;
164//!
165//! # fn logout(
166//! #     sp: &Saml<saml_rs::Sp>,
167//! #     idp: &IdpDescriptor,
168//! #     session: &SsoSession,
169//! #     response_fields: Vec<FormField>,
170//! # ) -> Result<(), saml_rs::SamlError> {
171//! if let Some(subject) = session.logout_subject() {
172//!     let started = sp.start_slo(idp, subject, StartSlo::post())?;
173//!     let validation = SamlValidationContext::new(
174//!         SystemTime::now(),
175//!         ReplayPolicy::DisabledForCompatibility,
176//!     );
177//!     let completed = sp.finish_slo(
178//!         idp,
179//!         &started.pending,
180//!         BrowserInput::<LogoutResponse>::post(response_fields),
181//!         validation,
182//!     )?;
183//!     let peer = completed.peer_entity_id().as_str();
184//!     # let _ = peer;
185//! }
186//! # Ok(()) }
187//! ```
188//!
189//! # Compile-time flow boundaries
190//!
191//! SSO and SLO pending values are different types. A logout pending value cannot
192//! be used to finish Web SSO:
193//!
194//! ```compile_fail
195//! use saml_rs::{
196//!     BrowserInput, IdpDescriptor, PendingLogoutRequest, Saml,
197//!     SamlValidationContext, SsoResponse,
198//! };
199//!
200//! fn wrong(
201//!     sp: &Saml<saml_rs::Sp>,
202//!     idp: &IdpDescriptor,
203//!     pending: &PendingLogoutRequest,
204//!     input: BrowserInput<SsoResponse>,
205//!     validation: SamlValidationContext<'_>,
206//! ) -> Result<(), saml_rs::SamlError> {
207//!     let _ = sp.finish_sso(idp, pending, input, validation)?;
208//!     Ok(())
209//! }
210//! ```
211//!
212//! SLO responses are correlated through [`Received<LogoutRequest>`], not
213//! arbitrary request ID strings:
214//!
215//! ```compile_fail
216//! use saml_rs::{RespondSlo, Saml, SpDescriptor};
217//!
218//! fn wrong(
219//!     idp: &Saml<saml_rs::Idp>,
220//!     sp: &SpDescriptor,
221//!     request_id: &str,
222//! ) -> Result<(), saml_rs::SamlError> {
223//!     let _ = idp.respond_slo(sp, request_id, RespondSlo::post())?;
224//!     Ok(())
225//! }
226//! ```
227//!
228//! # Metadata trust
229//!
230//! Metadata trust is explicit and caller-pinned. [`MetadataTrustPolicy`] can
231//! accept unsigned metadata for explicit legacy compatibility or require a
232//! signature from caller-provided certificates with
233//! [`MetadataTrustPolicy::RequireSignature`]. Prefer signed metadata with pinned
234//! certificates for production trust decisions; the crate does not treat the
235//! public web PKI CA store as SAML metadata trust.
236//!
237//! # Raw compatibility API
238//!
239//! The [`raw`] module contains the low-level compatibility API and protocol
240//! helpers. Advanced callers should import [`raw::ServiceProvider`],
241//! [`raw::IdentityProvider`], [`raw::HttpRequest`], and [`raw::BindingContext`]
242//! from there rather than using root compatibility exports.
243//!
244//! Visible docs.rs modules and crate-root re-exports are the supported public
245//! documentation surface. The [`raw`] module is supported for compatibility;
246//! hidden modules are lower-level implementation or compatibility paths and
247//! should not be the first choice for new integrations.
248//!
249//! # Unsupported profiles
250//!
251//! The high-level [`Saml`] API focuses on browser Web SSO, metadata-driven SP/IdP
252//! setup, XML signature/encryption through `bergshamra`, and Single Logout. It
253//! does not yet implement Artifact resolution, SOAP/back-channel profiles,
254//! ECP/PAOS, SAML query protocols, NameID management, or metadata federation. If
255//! you need one of those profiles for a real interoperability target, please
256//! open an issue with the profile, binding, IdP/SP product, and a minimal
257//! expected flow so we can consider the implementation.
258//!
259//! XML cryptography (XML-DSig sign/verify with anti-wrapping, XML-Enc, detached
260//! message signatures) is delegated to `bergshamra` behind the
261//! `crypto-bergshamra` feature, which is on by default. Configure assertion
262//! encryption and XML-Enc compatibility exceptions through [`XmlEncryptionPolicy`].
263//! Disable default features to build the crypto-free protocol layer; crypto
264//! operations then fail closed with [`SamlError::Unsupported`].
265
266#![forbid(unsafe_code)]
267
268#[doc(hidden)]
269pub mod api;
270#[doc(hidden)]
271pub mod binding;
272pub mod browser;
273pub mod config;
274pub mod constants;
275#[doc(hidden)]
276pub mod context;
277#[doc(hidden)]
278pub mod crypto;
279#[doc(hidden)]
280pub mod entity;
281pub mod error;
282#[doc(hidden)]
283pub mod flow;
284#[doc(hidden)]
285pub mod idp;
286#[doc(hidden)]
287pub mod logout;
288pub mod metadata;
289pub mod model;
290pub mod raw;
291#[doc(hidden)]
292pub mod sp;
293#[doc(hidden)]
294pub mod template;
295#[doc(hidden)]
296pub mod util;
297#[doc(hidden)]
298pub mod validator;
299#[doc(hidden)]
300pub mod xml;
301
302pub use api::{
303    ForceAuthn, Idp, LogoutSigning, RespondSlo, RespondSso, Saml, SamlError, Sp, StartSlo,
304    StartSso, Unknown,
305};
306pub use browser::{
307    AcsEndpoint, BrowserInput, EndpointUrl, FormField, LogoutBinding, Outbound, Pending,
308    PendingAuthnRequest, PendingLogoutRequest, PendingSnapshot, PostForm, SloEndpoint, SsoEndpoint,
309    SsoRequestBinding, SsoResponseBinding, Started,
310};
311pub use config::{
312    AlgorithmPolicy, AssertionEncryptionPolicy, AssertionSignaturePolicy, AudienceValidationPolicy,
313    AuthnRequestSigningPolicy, AuthnRequestValidationPolicy, CertificatePem, Credentials,
314    DataEncryptionAlgorithm, DigestAlgorithm, EntityId, IdpConfig, IdpConfigBuilder, IdpDescriptor,
315    IdpMetadataConfig, IdpValidationPolicy, KeyEncryptionAlgorithm, LogoutPolicy,
316    LogoutSignaturePolicy, MessageSignaturePolicy, MetadataTrustPolicy, NameIdCreationPolicy,
317    NameIdFormat, Passphrase, PrivateKeyPem, SignatureAlgorithm, SpConfig, SpConfigBuilder,
318    SpDescriptor, SpMetadataConfig, SpValidationPolicy, TemplatePolicy, TransformAlgorithm,
319    XmlEncryptionPolicy, XmlPolicy,
320};
321#[doc = "Compatibility export for older crate-root imports. Use `Saml` for new integrations; advanced raw callers should import `raw::EntitySetting`."]
322pub use entity::EntitySetting;
323#[doc = "Compatibility export for older crate-root imports. Use `Saml` for new integrations; advanced raw callers should import `raw::IdentityProvider`."]
324pub use idp::IdentityProvider;
325#[cfg(feature = "crypto-bergshamra")]
326pub use metadata::MetadataSignatureVerification;
327pub use model::{
328    Assertion, AssertionId, Attribute, AttributeValue, Attributes, AuthnRequest, AuthnSession,
329    ClockSkew, LogoutCompleted, LogoutRequest, LogoutResponse, LogoutSubject, MessageId, NameId,
330    NameIdCreationRequest, NameIdPolicy, Received, RelayState, RelayStateParam, ReplayCache,
331    ReplayKey, ReplayPolicy, SamlInstant, SamlValidationContext, SessionIndex, SsoResponse,
332    SsoSession, Subject, SubjectConfirmation, MAX_RELAY_STATE_BYTES,
333};
334#[doc = "Compatibility export for older crate-root imports. Use `Saml` for new integrations; advanced raw callers should import `raw::ServiceProvider`."]
335pub use sp::ServiceProvider;