Skip to main content

flp_saml2/idp/
authn_response.rs

1// This library provides SAML2 implementation in Rust
2// Copyright (C) 2026  Hakukaze Shikano
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17use base64::prelude::*;
18use flate2::read::DeflateDecoder;
19use openssl::x509::X509;
20use quick_xml::de::from_str as from_xml_str;
21use serde::Deserialize;
22use std::io::Read;
23
24use crate::{
25    error::{Error, Result},
26    idp::IdentityProvider,
27};
28
29#[derive(Deserialize)]
30pub struct AttributeValue {
31    #[serde(rename = "xsi:type")]
32    pub typ: String,
33    #[serde(rename = "$value")]
34    pub value: String,
35}
36
37#[derive(Deserialize)]
38pub struct Attribute {
39    #[serde(rename = "Name")]
40    pub name: String,
41    #[serde(rename = "NameFormat")]
42    pub name_format: String,
43    #[serde(rename = "AttributeValue")]
44    pub attribute_values: Vec<AttributeValue>,
45}
46
47#[derive(Deserialize)]
48pub struct AttributeStatement {
49    #[serde(rename = "Attribute")]
50    pub attributes: Vec<Attribute>,
51}
52
53#[derive(Deserialize)]
54pub struct AuthnContextClassRef {
55    #[serde(rename = "$value")]
56    pub value: String,
57}
58
59#[derive(Deserialize)]
60pub struct AuthnContext {
61    #[serde(rename = "AuthnContextClassRef")]
62    pub authn_context_class_ref: AuthnContextClassRef,
63}
64
65#[derive(Deserialize)]
66pub struct AuthnStatement {
67    #[serde(rename = "AuthnInstant")]
68    pub authn_instant: String,
69    #[serde(rename = "SessionNotOnOrAfter")]
70    pub session_not_on_or_after: String,
71    #[serde(rename = "SessionIndex")]
72    pub session_index: String,
73    #[serde(rename = "AuthnContext")]
74    pub authn_context: AuthnContext,
75}
76
77#[derive(Deserialize)]
78pub struct Audience {
79    #[serde(rename = "$value")]
80    pub value: String,
81}
82
83#[derive(Deserialize)]
84pub struct AudienceRestriction {
85    #[serde(rename = "Audience")]
86    pub audience: Audience,
87}
88
89#[derive(Deserialize)]
90pub struct Conditions {
91    #[serde(rename = "NotBefore")]
92    pub not_before: String,
93    #[serde(rename = "NotOnOrAfter")]
94    pub not_on_or_after: String,
95    #[serde(rename = "AudienceRestriction")]
96    pub audience_restriction: AudienceRestriction,
97}
98
99#[derive(Deserialize)]
100pub struct SubjectConfirmationData {
101    #[serde(rename = "NotOnOrAfter")]
102    pub not_on_or_after: String,
103    #[serde(rename = "Recipient")]
104    pub recipient: String,
105    #[serde(rename = "InResponseTo")]
106    pub in_response_to: String,
107}
108
109#[derive(Deserialize)]
110pub struct SubjectConfirmation {
111    #[serde(rename = "Method")]
112    pub method: String,
113    #[serde(rename = "SubjectConfirmationData")]
114    pub subject_confirmation_data: SubjectConfirmationData,
115}
116
117#[derive(Deserialize)]
118pub struct NameID {
119    #[serde(rename = "SPNameQualifier")]
120    pub sp_name_qualifier: Option<String>,
121    #[serde(rename = "Format")]
122    pub format: String,
123    #[serde(rename = "$value")]
124    pub value: String,
125}
126
127#[derive(Deserialize)]
128pub struct Subject {
129    #[serde(rename = "NameID")]
130    pub name_id: NameID,
131    #[serde(rename = "SubjectConfirmation")]
132    pub subject_confirmation: SubjectConfirmation,
133}
134
135#[derive(Deserialize)]
136pub struct StatusCode {
137    #[serde(rename = "Value")]
138    pub value: String,
139}
140
141#[derive(Deserialize)]
142pub struct Status {
143    #[serde(rename = "StatusCode")]
144    pub status_code: StatusCode,
145}
146
147#[derive(Deserialize)]
148pub struct X509Certificate {
149    #[serde(rename = "$value")]
150    pub value: String,
151}
152
153#[derive(Deserialize)]
154pub struct X509Data {
155    #[serde(rename = "X509Certificate")]
156    pub x509_certificate: X509Certificate,
157}
158
159#[derive(Deserialize)]
160pub struct KeyInfo {
161    #[serde(rename = "xmlns:ds")]
162    pub ds: Option<String>,
163    #[serde(rename = "X509Data")]
164    pub x509_data: X509Data,
165}
166
167#[derive(Deserialize)]
168pub struct SignatureValue {
169    #[serde(rename = "$value")]
170    pub value: String,
171}
172
173#[derive(Deserialize)]
174pub struct DigestValue {
175    #[serde(rename = "$value")]
176    pub value: String,
177}
178
179#[derive(Deserialize)]
180pub struct DigestMethod {
181    #[serde(rename = "Algorithm")]
182    pub algorithm: String,
183}
184
185#[derive(Deserialize)]
186pub struct Transform {
187    #[serde(rename = "Algorithm")]
188    pub algorithm: String,
189}
190
191#[derive(Deserialize)]
192pub struct Transforms {
193    #[serde(rename = "Transform")]
194    pub transforms: Vec<Transform>,
195}
196
197#[derive(Deserialize)]
198pub struct Reference {
199    #[serde(rename = "URI")]
200    pub uri: String,
201    #[serde(rename = "Transforms")]
202    pub transforms: Transforms,
203    #[serde(rename = "DigestMethod")]
204    pub digest_method: DigestMethod,
205    #[serde(rename = "DigestValue")]
206    pub digest_value: DigestValue,
207}
208
209#[derive(Deserialize)]
210pub struct SignatureMethod {
211    #[serde(rename = "Algorithm")]
212    pub algorithm: String,
213}
214
215#[derive(Deserialize)]
216pub struct CanonicalizationMethod {
217    #[serde(rename = "Algorithm")]
218    pub algorithm: String,
219}
220
221#[derive(Deserialize)]
222pub struct SignedInfo {
223    #[serde(rename = "CanonicalizationMethod")]
224    pub canonicalization_method: CanonicalizationMethod,
225    #[serde(rename = "SignatureMethod")]
226    pub signature_method: SignatureMethod,
227    #[serde(rename = "Reference")]
228    pub reference: Reference,
229}
230
231#[derive(Deserialize)]
232pub struct Signature {
233    #[serde(rename = "xmlns:ds")]
234    pub ds: Option<String>,
235    #[serde(rename = "SignedInfo")]
236    pub signed_info: SignedInfo,
237    #[serde(rename = "SignatureValue")]
238    pub signature_value: SignatureValue,
239    #[serde(rename = "KeyInfo")]
240    pub key_info: KeyInfo,
241}
242
243#[derive(Deserialize)]
244pub struct Issuer {
245    #[serde(rename = "$value")]
246    pub value: String,
247}
248
249#[derive(Deserialize)]
250pub struct Assertion {
251    #[serde(rename = "xmlns:xsi")]
252    pub xsi: Option<String>,
253    #[serde(rename = "xmlns:xs")]
254    pub xs: Option<String>,
255    #[serde(rename = "ID")]
256    pub id: String,
257    #[serde(rename = "Version")]
258    pub version: String,
259    #[serde(rename = "IssueInstant")]
260    pub issue_instant: String,
261    #[serde(rename = "Issuer")]
262    pub issuer: Issuer,
263    #[serde(rename = "Signature")]
264    pub signature: Option<Signature>,
265    #[serde(rename = "Subject")]
266    pub subject: Subject,
267    #[serde(rename = "Conditions")]
268    pub conditions: Conditions,
269    #[serde(rename = "AuthnStatement")]
270    pub authn_statement: AuthnStatement,
271    #[serde(rename = "AttributeStatement")]
272    pub attribute_statement: AttributeStatement,
273}
274
275#[derive(Deserialize)]
276#[serde(rename = "samlp:Response")]
277pub struct Response {
278    #[serde(rename = "xmlns:samlp")]
279    pub samlp: String,
280    #[serde(rename = "xmlns:saml")]
281    pub saml: String,
282    #[serde(rename = "ID")]
283    pub id: String,
284    #[serde(rename = "Version")]
285    pub version: String,
286    #[serde(rename = "IssueInstant")]
287    pub issue_instant: String,
288    #[serde(rename = "Destination")]
289    pub destination: String,
290    #[serde(rename = "InResponseTo")]
291    pub in_response_to: String,
292    #[serde(rename = "Issuer")]
293    pub issuer: Issuer,
294    #[serde(rename = "Signature")]
295    pub signature: Option<Signature>,
296    #[serde(rename = "Status")]
297    pub status: Status,
298    #[serde(rename = "Assertion")]
299    pub assertion: Assertion,
300}
301
302pub fn decode_authn_response(encoded: &str) -> Result<String> {
303    let deflated = BASE64_STANDARD.decode(encoded).map_err(|err| {
304        Error::InvalidResponse(format!("SAMLResponse is not encoded to base64: {}", err))
305    })?;
306    String::from_utf8(deflated).map_err(|err| {
307        Error::InvalidResponse(format!("SAMLResponse contains invalid chars: {}", err))
308    })
309}
310
311pub fn decode_inflate_authn_response(deflated_encoded: &str) -> Result<String> {
312    let deflated = BASE64_STANDARD.decode(deflated_encoded).map_err(|err| {
313        Error::InvalidResponse(format!("SAMLResponse is not encoded to base64: {}", err))
314    })?;
315    let mut inflater = DeflateDecoder::new(deflated.as_slice());
316    let mut inflated = String::new();
317    inflater.read_to_string(&mut inflated)?;
318    Ok(inflated)
319}
320
321impl IdentityProvider {
322    pub fn authn_response(&self, xml: &str) -> Result<Response> {
323        let response = from_xml_str::<Response>(xml)?;
324        if let Some(signature) = response.signature.as_ref() {
325            let cert = X509::from_pem(
326                format!(
327                    "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----",
328                    signature.key_info.x509_data.x509_certificate.value
329                )
330                .as_bytes(),
331            )?;
332            let mut is_valid = false;
333            for public_key in self.certificates.iter() {
334                if cert.verify(public_key.public_key()?.as_ref())? {
335                    is_valid = true;
336                    break;
337                }
338            }
339            if !is_valid {
340                return Err(Error::InvalidCert(
341                    "SAML response contains invalid cert".into(),
342                ));
343            }
344        }
345        Ok(response)
346    }
347}