Skip to main content

saml_rs/
constants.rs

1//! SAML 2.0 URN constants and keywords.
2//!
3//! String values follow the SAML and XML-DSig/XML-Enc specifications so
4//! messages and metadata interoperate.
5
6/// Protocol binding.
7///
8/// Combines full binding URNs and short binding keywords in one type.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum Binding {
11    /// HTTP-Redirect binding.
12    Redirect,
13    /// HTTP-POST binding.
14    Post,
15    /// HTTP-POST-SimpleSign binding.
16    SimpleSign,
17    /// HTTP-Artifact binding (not implemented; present for parity).
18    Artifact,
19}
20
21impl Binding {
22    /// Full binding URN.
23    pub fn urn(self) -> &'static str {
24        match self {
25            Binding::Redirect => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
26            Binding::Post => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
27            Binding::SimpleSign => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign",
28            Binding::Artifact => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact",
29        }
30    }
31
32    /// Short keyword (`wording.binding`).
33    pub fn short_name(self) -> &'static str {
34        match self {
35            Binding::Redirect => "redirect",
36            Binding::Post => "post",
37            Binding::SimpleSign => "simpleSign",
38            Binding::Artifact => "artifact",
39        }
40    }
41
42    /// Resolve from a short keyword (`redirect`, `post`, `simpleSign`, `artifact`).
43    pub fn from_short_name(name: &str) -> Option<Self> {
44        match name {
45            "redirect" => Some(Binding::Redirect),
46            "post" => Some(Binding::Post),
47            "simpleSign" => Some(Binding::SimpleSign),
48            "artifact" => Some(Binding::Artifact),
49            _ => None,
50        }
51    }
52
53    /// Resolve from a full binding URN.
54    pub fn from_urn(urn: &str) -> Option<Self> {
55        [
56            Binding::Redirect,
57            Binding::Post,
58            Binding::SimpleSign,
59            Binding::Artifact,
60        ]
61        .into_iter()
62        .find(|b| b.urn() == urn)
63    }
64}
65
66/// Top-level XML namespace URNs (`namespace.names`).
67pub mod namespace {
68    /// SAML 2.0 protocol namespace.
69    pub const PROTOCOL: &str = "urn:oasis:names:tc:SAML:2.0:protocol";
70    /// SAML 2.0 assertion namespace.
71    pub const ASSERTION: &str = "urn:oasis:names:tc:SAML:2.0:assertion";
72    /// SAML 2.0 metadata namespace.
73    pub const METADATA: &str = "urn:oasis:names:tc:SAML:2.0:metadata";
74    /// XML-DSig namespace.
75    pub const DSIG: &str = "http://www.w3.org/2000/09/xmldsig#";
76    /// User-initiated logout reason.
77    pub const USER_LOGOUT: &str = "urn:oasis:names:tc:SAML:2.0:logout:user";
78    /// Admin-initiated logout reason.
79    pub const ADMIN_LOGOUT: &str = "urn:oasis:names:tc:SAML:2.0:logout:admin";
80}
81
82/// NameID formats (`namespace.format`).
83pub mod name_id_format {
84    /// Email address format.
85    pub const EMAIL_ADDRESS: &str = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
86    /// Persistent identifier format.
87    pub const PERSISTENT: &str = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent";
88    /// Transient identifier format.
89    pub const TRANSIENT: &str = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
90    /// Entity identifier format.
91    pub const ENTITY: &str = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity";
92    /// Unspecified format.
93    pub const UNSPECIFIED: &str = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
94    /// Kerberos principal name format.
95    pub const KERBEROS: &str = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos";
96    /// Windows domain qualified name format.
97    pub const WINDOWS_DOMAIN_QUALIFIED_NAME: &str =
98        "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName";
99    /// X.509 subject name format.
100    pub const X509_SUBJECT_NAME: &str = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName";
101}
102
103/// AuthnContext class references (`namespace.authnContextClassRef`).
104pub mod authn_context_class_ref {
105    /// Password class.
106    pub const PASSWORD: &str = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password";
107    /// Password-protected transport class.
108    pub const PASSWORD_PROTECTED_TRANSPORT: &str =
109        "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport";
110}
111
112/// SAML status codes (`StatusCode`), top- and second-tier.
113pub mod status_code {
114    // top-tier
115    /// Success.
116    pub const SUCCESS: &str = "urn:oasis:names:tc:SAML:2.0:status:Success";
117    /// Requester error.
118    pub const REQUESTER: &str = "urn:oasis:names:tc:SAML:2.0:status:Requester";
119    /// Responder error.
120    pub const RESPONDER: &str = "urn:oasis:names:tc:SAML:2.0:status:Responder";
121    /// Version mismatch.
122    pub const VERSION_MISMATCH: &str = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch";
123    // second-tier
124    /// Authentication failed.
125    pub const AUTH_FAILED: &str = "urn:oasis:names:tc:SAML:2.0:status:AuthnFailed";
126    /// Invalid attribute name or value.
127    pub const INVALID_ATTR_NAME_OR_VALUE: &str =
128        "urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue";
129    /// Invalid NameID policy.
130    pub const INVALID_NAME_ID_POLICY: &str =
131        "urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy";
132    /// No authn context.
133    pub const NO_AUTHN_CONTEXT: &str = "urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext";
134    /// No available IdP.
135    pub const NO_AVAILABLE_IDP: &str = "urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP";
136    /// No passive authentication possible.
137    pub const NO_PASSIVE: &str = "urn:oasis:names:tc:SAML:2.0:status:NoPassive";
138    /// No supported IdP.
139    pub const NO_SUPPORTED_IDP: &str = "urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP";
140    /// Partial logout.
141    pub const PARTIAL_LOGOUT: &str = "urn:oasis:names:tc:SAML:2.0:status:PartialLogout";
142    /// Proxy count exceeded.
143    pub const PROXY_COUNT_EXCEEDED: &str = "urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded";
144    /// Request denied.
145    pub const REQUEST_DENIED: &str = "urn:oasis:names:tc:SAML:2.0:status:RequestDenied";
146    /// Request unsupported.
147    pub const REQUEST_UNSUPPORTED: &str = "urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported";
148    /// Request version deprecated.
149    pub const REQUEST_VERSION_DEPRECATED: &str =
150        "urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated";
151    /// Request version too high.
152    pub const REQUEST_VERSION_TOO_HIGH: &str =
153        "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh";
154    /// Request version too low.
155    pub const REQUEST_VERSION_TOO_LOW: &str =
156        "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow";
157    /// Resource not recognized.
158    pub const RESOURCE_NOT_RECOGNIZED: &str =
159        "urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized";
160    /// Too many responses.
161    pub const TOO_MANY_RESPONSES: &str = "urn:oasis:names:tc:SAML:2.0:status:TooManyResponses";
162    /// Unknown attribute profile.
163    pub const UNKNOWN_ATTR_PROFILE: &str = "urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile";
164    /// Unknown principal.
165    pub const UNKNOWN_PRINCIPAL: &str = "urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal";
166    /// Unsupported binding.
167    pub const UNSUPPORTED_BINDING: &str = "urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding";
168}
169
170/// XML signature algorithm URIs (`algorithms.signature`).
171pub mod signature_algorithm {
172    /// RSA-SHA1.
173    pub const RSA_SHA1: &str = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
174    /// RSA-SHA256.
175    pub const RSA_SHA256: &str = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
176    /// RSASSA-PSS with SHA-256 and MGF1.
177    pub const RSA_SHA256_MGF1: &str = "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1";
178    /// RSA-SHA384.
179    pub const RSA_SHA384: &str = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384";
180    /// RSA-SHA512.
181    pub const RSA_SHA512: &str = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512";
182}
183
184/// XML digest algorithm URIs (`algorithms.digest`).
185pub mod digest_algorithm {
186    /// SHA1 digest.
187    pub const SHA1: &str = "http://www.w3.org/2000/09/xmldsig#sha1";
188    /// SHA256 digest.
189    pub const SHA256: &str = "http://www.w3.org/2001/04/xmlenc#sha256";
190    /// SHA384 digest.
191    pub const SHA384: &str = "http://www.w3.org/2001/04/xmldsig-more#sha384";
192    /// SHA512 digest.
193    pub const SHA512: &str = "http://www.w3.org/2001/04/xmlenc#sha512";
194}
195
196/// Map a signature algorithm URI to its digest algorithm URI (`getDigestMethod`).
197pub fn digest_for_signature(sig_alg: &str) -> Option<&'static str> {
198    match sig_alg {
199        signature_algorithm::RSA_SHA1 => Some(digest_algorithm::SHA1),
200        signature_algorithm::RSA_SHA256 => Some(digest_algorithm::SHA256),
201        signature_algorithm::RSA_SHA256_MGF1 => Some(digest_algorithm::SHA256),
202        signature_algorithm::RSA_SHA384 => Some(digest_algorithm::SHA384),
203        signature_algorithm::RSA_SHA512 => Some(digest_algorithm::SHA512),
204        _ => None,
205    }
206}
207
208/// XML encryption data algorithm URIs (`algorithms.encryption.data`).
209pub mod data_encryption_algorithm {
210    /// AES-128-CBC.
211    pub const AES_128: &str = "http://www.w3.org/2001/04/xmlenc#aes128-cbc";
212    /// AES-256-CBC.
213    pub const AES_256: &str = "http://www.w3.org/2001/04/xmlenc#aes256-cbc";
214    /// Triple DES CBC.
215    pub const TRIPLE_DES: &str = "http://www.w3.org/2001/04/xmlenc#tripledes-cbc";
216    /// AES-128-GCM.
217    pub const AES_128_GCM: &str = "http://www.w3.org/2009/xmlenc11#aes128-gcm";
218}
219
220/// XML key encryption algorithm URIs (`algorithms.encryption.key`).
221pub mod key_encryption_algorithm {
222    /// RSA-OAEP-MGF1P.
223    pub const RSA_OAEP_MGF1P: &str = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p";
224    /// RSA 1.5.
225    pub const RSA_1_5: &str = "http://www.w3.org/2001/04/xmlenc#rsa-1_5";
226}
227
228/// XML-DSig transform / canonicalization algorithm URIs.
229pub mod transform_algorithm {
230    /// Enveloped-signature transform.
231    pub const ENVELOPED_SIGNATURE: &str = "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
232    /// Exclusive XML canonicalization (also the canonicalization method).
233    pub const EXC_C14N: &str = "http://www.w3.org/2001/10/xml-exc-c14n#";
234}
235
236/// Query/form parameter names (`wording.urlParams`).
237pub mod url_params {
238    /// `SAMLRequest` parameter.
239    pub const SAML_REQUEST: &str = "SAMLRequest";
240    /// `SAMLResponse` parameter.
241    pub const SAML_RESPONSE: &str = "SAMLResponse";
242    /// `SigAlg` parameter.
243    pub const SIG_ALG: &str = "SigAlg";
244    /// `Signature` parameter.
245    pub const SIGNATURE: &str = "Signature";
246    /// `RelayState` parameter.
247    pub const RELAY_STATE: &str = "RelayState";
248}
249
250/// Certificate use keyword (`wording.certUse`).
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum CertUse {
253    /// Signing certificate.
254    Signing,
255    /// Encryption certificate.
256    Encryption,
257}
258
259impl CertUse {
260    /// Metadata keyword (`signing` / `encryption`).
261    pub fn as_str(self) -> &'static str {
262        match self {
263            CertUse::Signing => "signing",
264            CertUse::Encryption => "encryption",
265        }
266    }
267}
268
269/// Message signing order (`MessageSignatureOrder` / `messageConfigurations.signingOrder`).
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub enum MessageSignatureOrder {
272    /// Sign, then encrypt.
273    SignThenEncrypt,
274    /// Encrypt, then sign.
275    EncryptThenSign,
276}
277
278impl MessageSignatureOrder {
279    /// Keyword form (`sign-then-encrypt` / `encrypt-then-sign`).
280    pub fn as_str(self) -> &'static str {
281        match self {
282            MessageSignatureOrder::SignThenEncrypt => "sign-then-encrypt",
283            MessageSignatureOrder::EncryptThenSign => "encrypt-then-sign",
284        }
285    }
286}
287
288/// SAML message parser type (`ParserType`).
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub enum ParserType {
291    /// `<AuthnRequest>`.
292    SamlRequest,
293    /// `<Response>`.
294    SamlResponse,
295    /// `<LogoutRequest>`.
296    LogoutRequest,
297    /// `<LogoutResponse>`.
298    LogoutResponse,
299}
300
301impl ParserType {
302    /// Local root element name.
303    pub fn as_str(self) -> &'static str {
304        match self {
305            ParserType::SamlRequest => "SAMLRequest",
306            ParserType::SamlResponse => "SAMLResponse",
307            ParserType::LogoutRequest => "LogoutRequest",
308            ParserType::LogoutResponse => "LogoutResponse",
309        }
310    }
311
312    /// Query parameter direction (`getQueryParamByType`): request types map to
313    /// `SAMLRequest`, response types to `SAMLResponse`.
314    pub fn query_param(self) -> &'static str {
315        match self {
316            ParserType::SamlRequest | ParserType::LogoutRequest => url_params::SAML_REQUEST,
317            ParserType::SamlResponse | ParserType::LogoutResponse => url_params::SAML_RESPONSE,
318        }
319    }
320}
321
322/// Metadata element ordering profiles (`elementsOrder`).
323///
324/// Some IdPs restrict the order of elements in entity descriptors.
325pub mod elements_order {
326    /// Default order.
327    pub const DEFAULT: &[&str] = &[
328        "KeyDescriptor",
329        "NameIDFormat",
330        "SingleLogoutService",
331        "AssertionConsumerService",
332    ];
333    /// OneLogin order (same as default).
334    pub const ONELOGIN: &[&str] = DEFAULT;
335    /// Shibboleth order.
336    pub const SHIBBOLETH: &[&str] = &[
337        "KeyDescriptor",
338        "SingleLogoutService",
339        "NameIDFormat",
340        "AssertionConsumerService",
341        "AttributeConsumingService",
342    ];
343
344    /// IdP metadata element ordering profiles.
345    pub mod idp {
346        /// Default IdP order.
347        pub const DEFAULT: &[&str] = &[
348            "KeyDescriptor",
349            "NameIDFormat",
350            "SingleSignOnService",
351            "SingleLogoutService",
352        ];
353        /// OneLogin IdP order.
354        pub const ONELOGIN: &[&str] = &[
355            "KeyDescriptor",
356            "NameIDFormat",
357            "SingleLogoutService",
358            "SingleSignOnService",
359        ];
360        /// Shibboleth IdP order.
361        pub const SHIBBOLETH: &[&str] = &[
362            "KeyDescriptor",
363            "SingleLogoutService",
364            "NameIDFormat",
365            "SingleSignOnService",
366        ];
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn binding_urns_match_saml_spec_values() {
376        assert_eq!(
377            Binding::Redirect.urn(),
378            "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
379        );
380        assert_eq!(
381            Binding::Post.urn(),
382            "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
383        );
384        assert_eq!(
385            Binding::SimpleSign.urn(),
386            "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"
387        );
388        assert_eq!(
389            Binding::Artifact.urn(),
390            "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
391        );
392    }
393
394    #[test]
395    fn binding_short_name_round_trip() {
396        for b in [
397            Binding::Redirect,
398            Binding::Post,
399            Binding::SimpleSign,
400            Binding::Artifact,
401        ] {
402            assert_eq!(Binding::from_short_name(b.short_name()), Some(b));
403            assert_eq!(Binding::from_urn(b.urn()), Some(b));
404        }
405        assert_eq!(Binding::from_short_name("nope"), None);
406        assert_eq!(Binding::from_urn("nope"), None);
407    }
408
409    #[test]
410    fn parser_type_query_param() {
411        assert_eq!(ParserType::SamlRequest.query_param(), "SAMLRequest");
412        assert_eq!(ParserType::LogoutRequest.query_param(), "SAMLRequest");
413        assert_eq!(ParserType::SamlResponse.query_param(), "SAMLResponse");
414        assert_eq!(ParserType::LogoutResponse.query_param(), "SAMLResponse");
415    }
416
417    #[test]
418    fn digest_mapping_matches_xmlsig_values() {
419        assert_eq!(
420            digest_for_signature(signature_algorithm::RSA_SHA256),
421            Some(digest_algorithm::SHA256)
422        );
423        assert_eq!(
424            digest_for_signature(signature_algorithm::RSA_SHA256_MGF1),
425            Some(digest_algorithm::SHA256)
426        );
427        assert_eq!(
428            digest_for_signature(signature_algorithm::RSA_SHA1),
429            Some("http://www.w3.org/2000/09/xmldsig#sha1")
430        );
431        assert_eq!(digest_for_signature("unknown"), None);
432    }
433
434    #[test]
435    fn status_success_value() {
436        assert_eq!(
437            status_code::SUCCESS,
438            "urn:oasis:names:tc:SAML:2.0:status:Success"
439        );
440    }
441
442    #[test]
443    fn elements_order_profiles() {
444        assert_eq!(elements_order::DEFAULT.len(), 4);
445        assert_eq!(elements_order::ONELOGIN, elements_order::DEFAULT);
446        assert_eq!(elements_order::SHIBBOLETH[1], "SingleLogoutService");
447    }
448}