1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
//! A library for doing SAML things, terribly, in rust.
//!
//! My main aim at the moment is to provide IdP capabilities for the [Kanidm](https://github.com/kanidm/kanidm) project.
//!
//! `#![deny(unsafe_code)]` is used everywhere to avoid unsafe code tricks. This is why we're using rust, after all! 🦀
//! //!
//! If you would like to help - please log PRs/Issues against [terminaloutcomes/saml-rs](https://github.com/terminaloutcomes/saml-rs).
//!
//! There's a test application [saml_test_server](../saml_test_server/index.html) based on [tide](https://docs.rs/tide/) to allow one to test functionality.
//!
//! # Current progress:
//!
//! - Compiles, most of the time
//! - `saml_test_server` runs on HTTP and HTTPS, parses Redirect requests as-needed. Doesn't parse them well... or validate them if they're signed, but it's a start!
//! - Parses and ... seems to handle SP XML data so we can store a representation of it and match them up later
//!
//! # Next steps:
//!
//! - Support the SAML 2.0 Web Browser SSO (SP Redirect Bind/ IdP POST Response) flow
//! - Sign responses
//! - Support Signed AuthN Redirect Requests
//!
//! # SAML 2.0 Web Browser SSO (SP Redirect Bind/ IdP POST Response) flow
//!
//! 1. User attempts to access the SP resource (eg `https://example.com/application`)
//! 2. User is HTTP 302 redirected to the IdP (that's us!)
//!    - The URL is provided in the SAML2.0 metadata from the IdP
//!    - There should be two query parameters, [SAMLRequest](SamlQuery::SAMLRequest) and [RelayState](SamlQuery::RelayState) details about them are available in [SamlQuery]
//! 3. The SSO Service validates the request and responds with a document containing an XHTML form:
//!
//!       NOTE: POSTed assertions MUST be signed
//!
//! ```html
//! <form method="post" action="https://example.com/SAML2/SSO/POST" ...>
//!   <input type="hidden" name="SAMLResponse" value="response" />
//!   <input type="hidden" name="RelayState" value="token" />
//! etc etc...
//! <input type="submit" value="Submit" />
//! </form>
//! ```
//! 4. Request the Assertion Consumer Service at the SP. The user agent issues a POST request to the Assertion Consumer Service at the service provider:
//!
//! ```html
//! POST /SAML2/SSO/POST HTTP/1.1
//! Host: sp.example.com
//! Content-Type: application/x-www-form-urlencoded
//! Content-Length: nnn
//! SAMLResponse=response&RelayState=token
//! ```
//!
//! To automate the submission of the form, the following line of JavaScript may appear anywhere on the XHTML page:
//!
//! ```javascript
//! window.onload = function () { document.forms[0].submit(); }
//! ```
//!
//! # Testing tools:
//!
//! * Idp/SP online tester - <https://samltest.id/>
//! * Parser for requests and responses: <https://samltool.io>
//! * OneLogin SAMLTool - <https://www.samltool.com/validate_xml.php> great for validating things against schema.

#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![deny(missing_debug_implementations)]

#[macro_use]
extern crate log;

// use xmlparser;
use serde::Serialize;
use std::fmt;
use xmlparser::{StrSpan, Token};

use inflate::inflate_bytes;
use std::str::from_utf8;

pub mod assertion;
pub mod cert;
pub mod constants;
pub mod metadata;
pub mod response;
pub mod sign;
pub mod sp;
pub mod test_samples;
pub mod utils;
pub mod xml;

// #[cfg(feature = "enable_tide")]
// pub mod tide_helpers;
use serde::Deserialize;

use chrono::{DateTime, SecondsFormat, Utc};

/// Stores the values one would expect in an AuthN Request
#[derive(Debug, Serialize)]
pub struct AuthnRequest {
    #[serde(rename = "ID")]
    /// RelayState provided as part of the request
    pub relay_state: String,
    #[serde(rename = "IssueInstant")]
    /// AuthN request issue time, generated by the SP - or shoved in by the IdP when parsing if missing
    // TODO: find out if the IdP can just decide on this
    pub issue_instant: DateTime<Utc>,
    #[serde(rename = "AssertionConsumerServiceURL")]
    /// Consumer URL - where the SP wants you to send responses to
    pub consumer_service_url: String,
    /// Issuer of the request - used for matching to the SP-provided metadata.
    ///
    /// This is a nested element inside a `<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">`
    pub issuer: String,
    #[serde(rename = "Version")]
    /// This better be 2.0!
    pub version: String,
    #[serde(rename = "Destination")]
    /// Destination...
    /// TODO: work out if/why this is different to the ACS
    pub destination: String,

    // Example value http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256
    #[serde(rename = "SigAlg")]
    /// Signature algorithm, if the request is signed
    pub sigalg: Option<String>,

    // Example signature value Signature=HlQbshvUcfvRY1DYo3B8PJfu%2F32pkFnKNkVtQ%2Fjn%2Bl9DurSUa4DrZH76StCwH1qgJ34v%2FXEfXBPy%2BK79ryojzUs5JR7R1KvlMR%2Fzfvgz7LFGv1fGUIFA9vnbbMsn7G%2FI0%2FXSaFkWiXp9%2BmqTmiBBBhFLsd9A8shXIEjnLVWZNUGR73HwUhEiURhGGAmVkPPGDRW1gU%2BwVdy4YUcsGusqTNEcKvUHZeOe0FC%2BggZ%2BRmCCjr2lTVrAxlXMeNU4NkgBk9VimMFCLA2A6LZ9mtLDn20CHaMEkCbSIessWKfXfz7aXd1VaY6lO1K0aSZ0h3%2BAYRcXcNVl3uvZQUslxh48Nw%3D%3D
    #[serde(rename = "Signature")]
    /// Signature, if signed
    pub signature: Option<String>,
}

impl AuthnRequest {
    /// Allows one to turn a [AuthnRequestParser] into a Request object
    ///
    /// TODO: doctest for AuthnRequest::From<AuthnRequestParser>
    #[allow(clippy::or_fun_call)]
    pub fn from(parser: AuthnRequestParser) -> Self {
        AuthnRequest {
            relay_state: parser.relay_state.unwrap(),
            issue_instant: parser.issue_instant.unwrap_or(Utc::now()),
            consumer_service_url: parser.consumer_service_url.unwrap_or(String::from("unset")),
            issuer: parser.issuer.unwrap(),
            version: parser.version,
            destination: parser.destination.unwrap_or(String::from("unset")),
            sigalg: parser.sigalg,
            signature: parser.signature,
        }
    }

    /// Return the issue instant in the required form
    pub fn issue_instant_string(&self) -> String {
        self.issue_instant
            .to_rfc3339_opts(SecondsFormat::Secs, true)
    }
}

/// Used to pull apart a SAML AuthN Request and build a [AuthnRequest]
#[derive(Debug, Default)]
pub struct AuthnRequestParser {
    /// Request ID / RelayState as provided by the SP
    pub relay_state: Option<String>,
    /// AuthN request issue time, generated by the SP - or shoved in by the IdP when parsing if missing
    // TODO: find out if the IdP can just decide on this
    pub issue_instant: Option<DateTime<Utc>>,
    /// Consumer URL - where the SP wants you to send responses to
    pub consumer_service_url: Option<String>,
    /// Issuer of the request - used for matching to the SP-provided metadata.
    ///
    /// This is a nested element inside a `<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">`
    pub issuer: Option<String>,
    /// This better be 2.0!
    pub version: String,
    /// Internal state id for the issuer
    // TODO: see if I can remove this because I think I replaced this with previous_name
    pub issuer_state: i8,
    /// Destination...
    // TODO: work out if/why this is different to the ACS
    pub destination: Option<String>,
    // TODO: need to parse the name-id policy ```<samlp:NameIDPolicy AllowCreate="true" Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>```
    /// Signature algorithm, if the request is signed
    pub sigalg: Option<String>,
    /// Signature algorithm, if the request is signed
    ///
    /// We leave this as the string while returning from the parser, so the [AuthnRequest] can verify it.
    pub signature: Option<String>,
}

impl AuthnRequestParser {
    /// Generate a new [AuthnRequestParser]
    pub fn new() -> Self {
        AuthnRequestParser {
            relay_state: None,
            issue_instant: None,
            consumer_service_url: None,
            issuer: None,
            version: String::from("2.0"),
            issuer_state: 0,
            destination: None,
            sigalg: None,
            signature: None,
        }
    }
}

/// Custom error for failing to parse an AuthN request
pub struct AuthnDecodeError {
    /// Error message
    pub message: String,
}

impl AuthnDecodeError {
    /// Generatin' a new AuthDecodeError
    pub fn new(message: String) -> AuthnDecodeError {
        AuthnDecodeError { message }
    }
}

// A unique format for dubugging output
impl fmt::Debug for AuthnDecodeError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "AuthnDecodeError {{ message: {} }}", self.message)
    }
}

#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
/// Used in the SAML Redirect GET request to pull out the query values
///
/// Snake case is needed to allow the fields to be pulled out correctly
pub struct SamlQuery {
    /// The value of the SAMLRequest parameter is a deflated, base64-encoded and URL-encoded value of an `<samlp:[AuthnRequest]>` element. The SAMLRequest *may* be signed using the SP signing key.
    pub SAMLRequest: Option<String>,
    /// The RelayState token is an opaque reference to state information maintained at the service provider.
    pub RelayState: Option<String>,
    /// Stores a base64-encoded signature...
    /// TODO: is Signature *just* a base64 encoded string? Is there other decoding needed? Can we store this as a better type?
    pub Signature: Option<String>,
    /// Stores the signature type - this should be URL-decoded by the web frontend
    // TODO: uhh... signature enums?
    pub SigAlg: Option<String>,
}

/// Does the decoding to hand the signature to the verifier
// TODO: is string the best retval for decode_authn_request_signature
pub fn decode_authn_request_signature(signature: String) -> String {
    debug!("signature: {:?}", signature);
    signature
}

/// Removes base64 encoding and also deflates the input String.
pub fn decode_authn_request_base64_encoded(req: String) -> Result<String, AuthnDecodeError> {
    let base64_decoded_samlrequest: Vec<u8> = match base64::decode(req) {
        Ok(value) => {
            debug!("Successfully Base64 Decoded the SAMLRequest");
            value
        }
        Err(err) => {
            return Err(AuthnDecodeError::new(format!(
                "Failed to base64 decode SAMLRequest in saml_redirect_get {:?}",
                err
            )));
        }
    };
    // here we try and use libflate to deflate the base64-decoded bytes because compression is used
    let inflated_result = match inflate_bytes(&base64_decoded_samlrequest) {
        Ok(value) => {
            debug!(
                "Successfully inflated the base64-decoded bytes: {:?}",
                from_utf8(&value).unwrap_or("Couldn't utf-8 decode this mess")
            );
            value
        }
        // if it fails, it's probably fine to return the bare bytes as they're already a string?
        Err(error) => {
            debug!("Failed to inflate bytes ({:?})", error);
            base64_decoded_samlrequest
        }
    };
    match from_utf8(&inflated_result) {
        Ok(value) => Ok(value.to_string()),
        _ => Err(AuthnDecodeError::new(format!(
            "Failed to utf-8 encode the result: {:?}",
            inflated_result
        ))),
    }
}

/// Used inside AuthnRequestParser to help parse the AuthN request
fn parse_authn_tokenizer_attribute(
    local: StrSpan,
    value: StrSpan,
    mut req: AuthnRequestParser,
) -> Result<AuthnRequestParser, AuthnDecodeError> {
    match local.to_lowercase().as_str() {
        "destination" => {
            req.destination = Some(value.to_string());
        }
        "id" => {
            req.relay_state = Some(value.to_string());
        }
        "issueinstant" => {
            debug!("Found issueinstant: {}", value.to_string());

            // Date parsing... 2021-07-19T12:06:25Z
            let parsed_datetime = DateTime::parse_from_rfc3339(&value);
            debug!("parsed_datetime: {:?}", parsed_datetime);
            match parsed_datetime {
                Ok(value) => {
                    debug!("Setting issue_instant");
                    let result: DateTime<Utc> = value.into();
                    req.issue_instant = Some(result);
                }
                Err(error) => {
                    eprintln!(
                        "Failed to cast datetime source={:?}, error=\"{}\"",
                        value.to_string(),
                        error
                    );
                }
            };
        }
        "assertionconsumerserviceurl" => {
            req.consumer_service_url = Some(value.to_string());
        }
        "version" => {
            if value.to_string() != "2.0" {
                return Err(AuthnDecodeError::new(format!(
                    "SAML Request where version!=2.0 ({}), this is probably bad.",
                    value.to_string()
                )));
            }
            req.version = value.to_string();
        }
        _ => debug!(
            "Found tokenizer attribute={}, value={}",
            local.to_lowercase().as_str(),
            value.to_string()
        ),
    }

    //eprintln!("after block {:?}", req.issue_instant);
    Ok(req)
}

/// Used inside AuthnRequestParser to help parse the AuthN request
fn parse_authn_tokenizer_element_start(
    local: StrSpan,
    mut req: AuthnRequestParser,
) -> AuthnRequestParser {
    debug!(
        "parse_authn_tokenizer_element_start: {}",
        local.to_lowercase().as_str()
    );
    if local.to_lowercase().as_str() == "issuer" {
        if req.issuer_state == 0 {
            req.issuer_state = 1;
            debug!("Found a text tag called issuer, moving to issuer-finding state machine state 1")
        } else {
            debug!(
                "Found issuer tag and not at issuer_state==0 {}",
                req.issuer_state
            );
            // TODO: throw an error?
        }
    } else {
        debug!("Found elementStart text={}", local.to_lowercase().as_str());
    }
    req
}

/// Give it a string full of XML and it'll give you back a [AuthnRequest] object which has the details
pub fn parse_authn_request(request_data: &str) -> Result<AuthnRequest, String> {
    // more examples here
    // https://developers.onelogin.com/saml/examples/authnrequest

    let mut saml_request = AuthnRequestParser::new();
    let tokenizer = xmlparser::Tokenizer::from(request_data);
    for token in tokenizer {
        match token {
            Ok(token_value) => {
                saml_request = match token_value {
                    Token::Attribute {
                        prefix: _,
                        local,
                        value,
                        span: _,
                    } => match parse_authn_tokenizer_attribute(local, value, saml_request) {
                        Ok(value) => value,
                        Err(error) => {
                            return Err(format!("Failed to parse authn request: {:?}", error))
                        }
                    },
                    Token::ElementStart {
                        prefix: _,
                        local,
                        span: _,
                    } => parse_authn_tokenizer_element_start(local, saml_request),
                    Token::Text { text } => {
                        // if issuer_state == -1 { continue }
                        if saml_request.issuer_state == 1 {
                            let issuer = text.as_str();
                            debug!("Found issuer: {}", issuer);
                            saml_request.issuer = Some(issuer.to_string());
                            saml_request.issuer_state = -1; // reset the state machine so we don't try and do this again
                        } else {
                            debug!(
                                "Found issuer text and not at issuer_state==1 ({}) text={:?}",
                                saml_request.issuer_state, text
                            );
                        }
                        saml_request
                    }
                    _ => saml_request,
                };
            }
            Err(ref error) => {
                return Err(format!(
                    "Error parsing token: {:?}\n{:?}",
                    error, request_data
                ));
            }
        }
    }

    println!("found relay_state={:?}", &saml_request.relay_state);
    Ok(AuthnRequest::from(saml_request))
}

// TODO: This has some interesting code for parsing and handling assertions etc
// https://docs.rs/crate/saml2aws-auto/1.10.1/source/src/saml/mod.rs
// use crate::prelude::*;

fn _get_private_key() {
    println!("Generating private key");
    // let rsa = Rsa::generate(2048).unwrap()
    // println!("Dumping RSA Cert {:?}", rsa.private_key_to_der());
    // let data = b"foobar";
    // let mut buf = vec![0; rsa.size() as usize];
    // let encrypted_result = rsa.public_encrypt(data, &mut buf, Padding::PKCS1);
    // println!("Dumping encrypted thing: {:?}", &encrypted_result);
    // let encrypted_len = &encrypted_result.unwrap();

    // println!("Length of encrypted thing: {:?}", encrypted_len);
}

// fn get_public_cert_base64(cert_path: std::string::String) -> Result<Certificate, ()> {
//     let mut buf = Vec::new();
//     let file = match File::open("certpath") {
//         Ok(file) => file,
//         Err(_) => Err
//     }
//     .read_to_end(&mut buf)?;
//     let cert = Certificate::from_der(&buf)?;
//     // cert.to_string()?
//     // .read_to_end(&mut buf)?;

//     // let mut encoded_cert = String::from("hello world");
//     // encoded_cert.push_str(&cert_path);
//     // return encoded_cert

// }

// fn encode_cert_as_base64_der() -> std::string::String{

//     use std::io::Write;
//     let mut buf = String::new();

//     let mut base64_encoder = base64::write::EncoderStringWriter::from(&mut buf, base64::STANDARD);

//     // enc.write_all(b"asdf").unwrap();
//     base64_encoder.write_all(generate_cert("www.example.com")).unwrap();

//     // release the &mut reference on buf
//     let _ = base64_encoder.into_inner();
//     buf
//     // assert_eq!("base64: YXNkZg==", &buf);
//     /*
//     pub fn Rsa.private_key_to_der(&self) -> Result<Vec<u8>, ErrorStack>
// Serializes the private key to a DER-encoded PKCS#1 RSAPrivateKey structure.

// This corresponds to i2d_RSAPrivateKey.
// */
// }