opensaml 0.1.2

SAML 2.0 Service Provider library (bergshamra for XML crypto).
Documentation

opensaml-rs

crates.io docs.rs MIT licensed unsafe forbidden

Pure-Rust SAML 2.0 — Service Provider and Identity Provider in one crate. No libxml2, no xmlsec1, no OpenSSL build chain for the protocol layer. XML cryptography (XML-DSig, XML-Enc, C14N) is delegated to bergshamra behind an optional feature that is on by default.

Behavior is ported to parity with npm samlify v2.10.2, with the upstream test suite carried over 1:1 and additional hardening (audience restriction, InResponseTo binding, XML Signature Wrapping tests, metadata key pinning, signed-metadata verification).

[dependencies]
opensaml = "0.1"
# Crypto-free protocol layer only:
# opensaml = { version = "0.1", default-features = false }

Alias crates (samlify, open-saml, rust-saml, rustsaml, saml-rs, samlrs) are thin pub use opensaml::*; re-exports.


Why opensaml vs. samael?

opensaml samael
Native C/XML stack (libxml2, xmlsec1, libxslt, …) None — protocol in Rust; crypto via bergshamra Required with default xmlsec feature
OpenSSL at build time No Yes (typical xmlsec setup)
System packages to install None libiconv, libtool, libxml2, libxslt, libclang, openssl, pkg-config, xmlsec1, …
Cross-compile (musl, distroless, arm64) Straightforward Native libs needed on every target
cargo audit for the SAML stack Rust dependency graph Split: crates + OS packages
SP + IdP in one library First-class ServiceProvider + IdentityProvider SP-oriented; IdP-initiated SSO supported
HTTP bindings POST, Redirect, POST-SimpleSign Redirect → POST (primary documented path)
Single Logout Create + parse, all three bindings Limited / evolving
Metadata Parse + generate (SP and IdP) Deserialize-focused
Signed metadata verification Yes
XSW / reference hardening Explicit guard + strict verification via bergshamra Inherits xmlsec behavior
#![forbid(unsafe_code)] Yes No
API shape Ported from npm samlify v2.10.2 Builder-style ServiceProvider

Takeaway: samael is the established SAML crate on crates.io, but signing and verification pull in libxml2 + xmlsec1 + OpenSSL and a host of system libraries. opensaml keeps the protocol layer and XML crypto on a Rust-only path (no C XML stack), ships SP and IdP with three bindings and Single Logout, and adds hardening beyond the samlify port (audience, InResponseTo, XSW tests, metadata key pinning).


What you can do

Area Highlights
Web SSO Signed AuthnRequest / Response, HTTP-POST and HTTP-Redirect, POST-SimpleSign
Metadata Parse peer metadata, generate SP/IdP descriptors, verify signed aggregates
Single Logout LogoutRequest / LogoutResponse create and parse (all three bindings)
Validation Issuer, <Audience>, assertion time window, status codes, optional InResponseTo
Crypto (default) XML-DSig sign/verify, XML-Enc encrypt/decrypt, detached redirect signatures, anti-wrapping
Extraction quick-xml DOM + local-name field sets (samlify-compatible extract engine)

Quick start

Service Provider — login request

use opensaml::constants::Binding;
use opensaml::entity::EntitySetting;
use opensaml::metadata::{Endpoint, SpMetadataConfig};
use opensaml::ServiceProvider;

let sp = ServiceProvider::from_config(
    &SpMetadataConfig {
        entity_id: "https://sp.example.com/metadata".into(),
        assertion_consumer_service: vec![Endpoint::new(
            Binding::Post,
            "https://sp.example.com/acs",
        )],
        ..Default::default()
    },
    EntitySetting::default(),
)?;

// Binding::Redirect for DEFLATE + query-string dispatch
let request = sp.create_login_request(&idp, Binding::Post, None)?;
// POST: request.context is the base64 SAMLRequest
// Redirect: use binding helpers to build the redirect URL

Identity Provider — login response

use opensaml::constants::Binding;
use opensaml::entity::{EntitySetting, User};
use opensaml::flow::HttpRequest;
use opensaml::idp::LoginResponseOptions;
use opensaml::IdentityProvider;

let req = HttpRequest::post(vec![("SAMLRequest".into(), saml_request_b64)]);
let parsed = idp.parse_login_request(&sp, Binding::Post, &req)?;

let response = idp.create_login_response(
    &sp,
    Binding::Post,
    &User::new("alice@example.com"),
    &LoginResponseOptions {
        in_response_to: parsed.extract.get_str("request.id"),
        ..Default::default()
    },
)?;

Service Provider — consume response

use opensaml::flow::HttpRequest;

let resp = HttpRequest::post(vec![("SAMLResponse".into(), saml_response_b64)]);

// Prefer binding to the outbound AuthnRequest id (replay / CSRF hygiene)
let result = sp.parse_login_response_with_request_id(
    &idp,
    Binding::Post,
    &resp,
    &authn_request_id,
)?;

let name_id = result.extract.get_str("nameID");

Metadata

use opensaml::constants::Binding;
use opensaml::metadata::{generate_sp_metadata, IdpMetadata, SpMetadataConfig};

// Parse peer IdP metadata
let idp_meta = IdpMetadata::from_xml(idp_metadata_xml)?;
let sso_url = idp_meta.get_single_sign_on_service(Binding::Redirect);

// Generate your SP descriptor
let xml = generate_sp_metadata(&SpMetadataConfig {
    entity_id: "https://sp.example.com/metadata".into(),
    ..Default::default()
});

Single Logout

use opensaml::constants::Binding;
use opensaml::entity::{EntitySetting, User};
use opensaml::flow::HttpRequest;
use opensaml::logout::{create_logout_request, parse_logout_response};

let logout = create_logout_request(
    &sp.setting,
    &sp.metadata,
    &idp.metadata,
    Binding::Post,
    &User::new("alice@example.com"),
    None,  // relay_state
    true,  // want_signed
)?;

let resp = HttpRequest::post(vec![("SAMLResponse".into(), saml_response_b64)]);
let parsed = parse_logout_response(&sp.setting, &idp.metadata, Binding::Post, &resp)?;

End-to-end example (signed round-trip)

A runnable SP → IdP → SP flow with RSA-SHA256 signatures:

cargo run -p opensaml --example sso

Source: crates/opensaml/examples/sso.rs.


Crates in this workspace

Crate Role
opensaml Library: constants, XML, templates, metadata, bindings, ServiceProvider / IdentityProvider, flow, logout, validation, crypto (feature).
samlify Thin re-export: pub use opensaml::*; — drop-in crate name only.
open-saml, rust-saml, rustsaml, saml-rs, samlrs Same thin re-export as samlify.

Crate-level API details: docs.rs/opensaml and the opensaml crate rustdoc module map.


Features

[features]
default = ["crypto-bergshamra"]
crypto-bergshamra = ["dep:bergshamra"]   # XML-DSig, XML-Enc, detached signatures

With default-features = false, the protocol layer still builds messages, parses metadata, and runs extraction; any operation that needs signing, verification, or encryption returns OpenSamlError::Unsupported (fail closed).

With crypto-bergshamra enabled (default):

  • Verification can pin to metadata-declared keys (trusted_keys_only).
  • Strict signed-reference placement helps mitigate XML Signature Wrapping (XSW).
  • An additional wrapping guard runs in crypto::verify.

Security

  • #![forbid(unsafe_code)] on the crate root.
  • Inbound responses: signature (when required), issuer, <Audience>, assertion validity window, SAML status; optional InResponseTo via parse_login_response_with_request_id.
  • DOCTYPE / XXE rejection in the XML layer; optional XSD validation via context::set_schema_validator.
  • Pre-1.0 and not externally audited — review crypto and deployment choices before production.

Status

Version 0.1.1 (APIs may change until 1.0)
Reference port npm samlify v2.10.2
Tests Upstream samlify suite ported 1:1 + XSW / audience / metadata-signature cases
Audit None yet

Development

cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo nextest run --workspace

Upstream reference sources (gitignored): ./scripts/fetch-upstream-samlify.shreference/upstream-samlify/2.10.2/.


License

MIT.