From b5bca1de636f27037cd3b4c8ff947dcb4229a0c2 Mon Sep 17 00:00:00 2001 From: Dan Callahan Date: Fri, 24 Jun 2016 10:23:11 -0500 Subject: [PATCH] Add serde_codegen and build script for stable Rust This is necessary because stable Rust does not yet support custom #[derive] implementations, which are needed for Serde's Serialize/Deserialize traits. Serde has a macro package and compiler plugin which handle those, but compiler plugins are *also* not availble in stable Rust, so we instead have to generate code at build time using serde_codegen. Bug for `custom_derive` feature: https://github.com/rust-lang/rust/issues/29644 Bug for compiler plugins: https://github.com/rust-lang/rust/issues/29597 --- Cargo.toml | 7 +- build.rs | 16 +++ src/lib.rs | 376 +------------------------------------------------- src/lib.rs.in | 372 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 400 insertions(+), 371 deletions(-) create mode 100644 build.rs create mode 100644 src/lib.rs.in diff --git a/Cargo.toml b/Cargo.toml index 3e63d628..869b3d67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,10 @@ name = "ladaemon" version = "0.1.0" authors = ["Dirkjan Ochtman "] +build = "build.rs" + +[build-dependencies] +serde_codegen = "0.7.10" [dependencies] emailaddress = "0.3.0" @@ -13,7 +17,8 @@ rand = "0.3.14" redis = "0.5.3" router = "0.1.1" rustc-serialize = "0.3.19" -serde_json = "0.7.0" +serde = "0.7.10" +serde_json = "0.7.1" time = "0.1.35" url = "1.1.0" urlencoded = "0.3.0" diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..0defb83e --- /dev/null +++ b/build.rs @@ -0,0 +1,16 @@ +// Stable Rust 1.9 doesn't support the custom_derive feature, so we generate +// code for structures which #[derive] Serde's Serialize/Deserialize traits. + +extern crate serde_codegen; + +use std::env; +use std::path::Path; + +pub fn main() { + let out_dir = env::var_os("OUT_DIR").unwrap(); + + let src = Path::new("src/lib.rs.in"); + let dst = Path::new(&out_dir).join("lib.rs"); + + serde_codegen::expand(&src, &dst).unwrap(); +} diff --git a/src/lib.rs b/src/lib.rs index e022c236..72bab68d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,372 +1,8 @@ -extern crate emailaddress; -extern crate iron; -extern crate openssl; -extern crate rand; -extern crate redis; -extern crate rustc_serialize; -extern crate serde_json; -extern crate time; -extern crate url; -extern crate urlencoded; +// Stable Rust 1.9 doesn't support the custom_derive feature, so we generate +// code for structures which #[derive] Serde's Serialize/Deserialize traits. +// +// See ../build.rs for the use of serde_codegen. -pub mod email; -pub mod oidc; +extern crate serde; -use emailaddress::EmailAddress; -use iron::headers::ContentType; -use iron::middleware::Handler; -use iron::modifiers; -use iron::prelude::*; -use iron::status; -use openssl::bn::BigNum; -use openssl::crypto::hash; -use openssl::crypto::pkey::PKey; -use serde_json::builder::ObjectBuilder; -use serde_json::de::from_reader; -use serde_json::value::Value; -use rand::{OsRng, Rng}; -use redis::Client; -use rustc_serialize::base64::{self, ToBase64}; -use std::collections::BTreeMap; -use std::fs::File; -use std::io::{BufReader, Write}; -use std::iter::Iterator; -use time::now_utc; -use urlencoded::{UrlEncodedBody, UrlEncodedQuery}; - - -/// Helper function for returning an Iron response with JSON data. -/// -/// Serializes the argument value to JSON and returns a HTTP 200 response -/// code with the serialized JSON as the body. -fn json_response(obj: &Value) -> IronResult { - let content = serde_json::to_string(&obj).unwrap(); - let mut rsp = Response::with((status::Ok, content)); - rsp.headers.set(ContentType::json()); - Ok(rsp) -} - - -/// Configuration data for a "famous" identity provider. -/// -/// Used as part of the `AppConfig` struct. -#[derive(Clone)] -struct ProviderConfig { - /// URL pointing to the OpenID configuration document. - discovery: String, - /// Client ID issued for this daemon instance by the provider. - client_id: String, - /// Secret issued for this daemon instance by the provider. - secret: String, - /// Issuer origin as used in identity tokens issued by the provider. - /// Used to check that the issuer in the token matches expectations. - issuer: String, -} - - -/// Configuration data for this daemon instance. -#[derive(Clone)] -pub struct AppConfig { - /// Version of the daemon (used in the `WelcomeHandler`). - version: String, - /// Base URL where this daemon can be found. This is used to construct - /// callback URLs. - base_url: String, - /// Private key used to sign identity tokens. - priv_key: PKey, - /// Redis database connector. - store: Client, - /// Duration in seconds for expiry of data stored in Redis. - expire_keys: usize, - /// Email sender (email address, then human-readable name). - sender: (String, String), - /// Duration in seconds for expiration of identity tokens. - token_validity: i64, - /// A map of "famous" identity providers. Each key is an email domain, - /// the value holds the configuration required to act as an application - /// doing OpenID Connect against the provider. - providers: BTreeMap, -} - - -/// Implementation with single method to read configuration from JSON. -impl AppConfig { - pub fn from_json_file(file_name: &str) -> AppConfig { - - let config_file = File::open(file_name).unwrap(); - let config_value: Value = from_reader(BufReader::new(config_file)).unwrap(); - let config = config_value.as_object().unwrap(); - - // The `private_key_file` key in the JSON object should hold a file - // name pointing to a PEM-encoded private RSA key. - let key_file_name = config["private_key_file"].as_string().unwrap(); - let priv_key_file = File::open(key_file_name).unwrap(); - let mut reader = BufReader::new(priv_key_file); - let sender = config["sender"].as_object().unwrap(); - AppConfig { - // Use the crate's version as defined in Cargo.toml. - version: env!("CARGO_PKG_VERSION").to_string(), - base_url: config["base_url"].as_string().unwrap().to_string(), - priv_key: PKey::private_key_from_pem(&mut reader).unwrap(), - store: Client::open(config["redis"].as_string().unwrap()).unwrap(), - expire_keys: config["expire_keys"].as_u64().unwrap() as usize, - sender: ( - sender["address"].as_string().unwrap().to_string(), - sender["name"].as_string().unwrap().to_string(), - ), - token_validity: config["token_validity"].as_i64().unwrap(), - providers: config["providers"].as_object().unwrap().iter() - .map(|(host, params)| { - let pobj = params.as_object().unwrap(); - (host.clone(), ProviderConfig { - discovery: pobj["discovery"].as_string().unwrap().to_string(), - client_id: pobj["client_id"].as_string().unwrap().to_string(), - secret: pobj["secret"].as_string().unwrap().to_string(), - issuer: pobj["issuer"].as_string().unwrap().to_string(), - }) - }) - .collect::>(), - } - - } -} - - -/// Iron handler for the root path, returns human-friendly message. -/// -/// This is not actually used in the protocol. -pub struct WelcomeHandler { pub app: AppConfig } -impl Handler for WelcomeHandler { - fn handle(&self, _: &mut Request) -> IronResult { - json_response(&ObjectBuilder::new() - .insert("ladaemon", "Welcome") - .insert("version", &self.app.version) - .unwrap()) - } -} - - -/// Iron handler to return the OpenID Discovery document. -/// -/// Most of this is hard-coded for now, although the URLs are constructed by -/// using the base URL as configured in the `base_url` configuration value. -pub struct OIDConfigHandler { pub app: AppConfig } -impl Handler for OIDConfigHandler { - fn handle(&self, _: &mut Request) -> IronResult { - json_response(&ObjectBuilder::new() - .insert("issuer", &self.app.base_url) - .insert("authorization_endpoint", - format!("{}/auth", self.app.base_url)) - .insert("jwks_uri", format!("{}/keys.json", self.app.base_url)) - .insert("scopes_supported", vec!["openid", "email"]) - .insert("claims_supported", - vec!["aud", "email", "email_verified", "exp", "iat", "iss", "sub"]) - .insert("response_types_supported", vec!["id_token"]) - .insert("response_modes_supported", vec!["form_post"]) - .insert("grant_types_supported", vec!["implicit"]) - .insert("subject_types_supported", vec!["public"]) - .insert("id_token_signing_alg_values_supported", vec!["RS256"]) - .unwrap()) - } -} - - -/// Helper function to encode a `BigNum` as URL-safe base64-encoded bytes. -/// -/// This is used for the public RSA key components returned by the -/// `KeysHandler`. -fn json_big_num(n: &BigNum) -> String { - n.to_vec().to_base64(base64::URL_SAFE) -} - - -/// Helper function to build a session ID for a login attempt. -/// -/// Put the email address, the client ID (RP origin) and some randomness into -/// a SHA256 hash, and encode it with URL-safe bas64 encoding. This is used -/// as the key in Redis, as well as the state for OAuth authentication. -fn session_id(email: &EmailAddress, client_id: &str) -> String { - let mut rng = OsRng::new().unwrap(); - let mut bytes_iter = rng.gen_iter(); - let rand_bytes: Vec = (0..16).map(|_| bytes_iter.next().unwrap()).collect(); - - let mut hasher = hash::Hasher::new(hash::Type::SHA256); - hasher.write(email.to_string().as_bytes()).unwrap(); - hasher.write(client_id.as_bytes()).unwrap(); - hasher.write(&rand_bytes).unwrap(); - hasher.finish().to_base64(base64::URL_SAFE) -} - - -/// Iron handler for the JSON Web Key Set document. -/// -/// Currently only supports a single RSA key (as configured), which is -/// published with the `"base"` key ID, scoped to signing usage. Relying -/// Parties will need to fetch this data to be able to verify identity tokens -/// issued by this daemon instance. -pub struct KeysHandler { pub app: AppConfig } -impl Handler for KeysHandler { - fn handle(&self, _: &mut Request) -> IronResult { - let rsa = self.app.priv_key.get_rsa(); - json_response(&ObjectBuilder::new() - .insert_array("keys", |builder| { - builder.push_object(|builder| { - builder.insert("kty", "RSA") - .insert("alg", "RS256") - .insert("use", "sig") - .insert("kid", "base") - .insert("n", json_big_num(&rsa.n().unwrap())) - .insert("e", json_big_num(&rsa.e().unwrap())) - }) - }) - .unwrap()) - } -} - - -/// Iron handler for authentication requests from the RP. -/// -/// Calls the `oidc::request()` function if the provided email address's -/// domain matches one of the configured famous providers. Otherwise, calls the -/// `email::request()` function to allow authentication through the email loop. -pub struct AuthHandler { pub app: AppConfig } -impl Handler for AuthHandler { - fn handle(&self, req: &mut Request) -> IronResult { - let params = req.get_ref::().unwrap(); - let email_addr = EmailAddress::new(¶ms.get("login_hint").unwrap()[0]).unwrap(); - if self.app.providers.contains_key(&email_addr.domain) { - - // OIDC authentication. Using 302 Found for redirection here. Note - // that, per RFC 7231, a user agent MAY change the request method - // from POST to GET for the subsequent request. - let auth_url = oidc::request(&self.app, params); - Ok(Response::with((status::Found, modifiers::Redirect(auth_url)))) - - } else { - - // Email loop authentication. For now, returns a JSON response; - // empty if successful, otherwise contains an error. - let obj = email::request(&self.app, params); - json_response(&obj) - - } - } -} - - - -/// Helper method to create a JWT for a given email address and origin. -/// -/// Builds the JSON payload and header, encoding with (URL-safe) -/// base64-encoding, then hashing and signing with the provided private key. -/// Returns the full JWT. -fn create_jwt(app: &AppConfig, email: &str, origin: &str) -> String { - - let now = now_utc().to_timespec().sec; - let payload = serde_json::to_string( - &ObjectBuilder::new() - .insert("aud", origin) - .insert("email", email) - .insert("email_verified", email) - .insert("exp", now + app.token_validity) - .insert("iat", now) - .insert("iss", &app.base_url) - .insert("sub", email) - .unwrap() - ).unwrap(); - let header = serde_json::to_string( - &ObjectBuilder::new() - .insert("kid", "base") - .insert("alg", "RS256") - .unwrap() - ).unwrap(); - - let mut input = Vec::::new(); - input.extend(header.as_bytes().to_base64(base64::URL_SAFE).into_bytes()); - input.push(b'.'); - input.extend(payload.as_bytes().to_base64(base64::URL_SAFE).into_bytes()); - let sha256 = hash::hash(hash::Type::SHA256, &input); - let sig = app.priv_key.sign(&sha256); - input.push(b'.'); - input.extend(sig.to_base64(base64::URL_SAFE).into_bytes()); - String::from_utf8(input).unwrap() - -} - - -/// HTML template used to have the user agent POST the identity token built -/// by the daemon instance to the RP's `redirect_uri`. -const FORWARD_TEMPLATE: &'static str = r#" - - - Let's Auth - - - -
- -
- -"#; - - -/// Helper function for returning result to the Relying Party. -/// -/// Takes a `Result` from one of the verification functions and embeds it in -/// a form in the `FORWARD_TEMPLATE`, from where it's POSTED to the RP's -/// `redirect_ur` as soon as the page has loaded. Result can either be an error -/// message or a JWT asserting the user's email address identity. -/// TODO: return error to RP instead of in a simple HTTP response. -fn return_to_relier(result: Result<(String, String), &'static str>) - -> IronResult { - - if result.is_err() { - return json_response(&ObjectBuilder::new() - .insert("error", result.unwrap_err()) - .unwrap()); - } - - let (jwt, redirect) = result.unwrap(); - let html = FORWARD_TEMPLATE.replace("{{ return_url }}", &redirect) - .replace("{{ jwt }}", &jwt); - let mut rsp = Response::with((status::Ok, html)); - rsp.headers.set(ContentType::html()); - Ok(rsp) - -} - - - -/// Iron handler for one-time pad email loop confirmation. -/// -/// Retrieves the session based session ID and the expected one-time pad. -/// Verify the code and return the resulting token or error to the RP. -pub struct ConfirmHandler { pub app: AppConfig } -impl Handler for ConfirmHandler { - fn handle(&self, req: &mut Request) -> IronResult { - let params = req.get_ref::().unwrap(); - let session_id = ¶ms.get("session").unwrap()[0]; - let code = ¶ms.get("code").unwrap()[0]; - return_to_relier(email::verify(&self.app, session_id, code)) - } -} - - -/// Iron handler for OAuth callbacks -/// -/// After the user allows or denies the Authentication Request with the famous -/// identity provider, they will be redirected back to the callback handler. -/// Verify the callback data and return the resulting token or error. -pub struct CallbackHandler { pub app: AppConfig } -impl Handler for CallbackHandler { - fn handle(&self, req: &mut Request) -> IronResult { - let params = req.get_ref::().unwrap(); - let session = ¶ms.get("state").unwrap()[0]; - let code = ¶ms.get("code").unwrap()[0]; - return_to_relier(oidc::verify(&self.app, session, code)) - } -} +include!(concat!(env!("OUT_DIR"), "/lib.rs")); diff --git a/src/lib.rs.in b/src/lib.rs.in new file mode 100644 index 00000000..e022c236 --- /dev/null +++ b/src/lib.rs.in @@ -0,0 +1,372 @@ +extern crate emailaddress; +extern crate iron; +extern crate openssl; +extern crate rand; +extern crate redis; +extern crate rustc_serialize; +extern crate serde_json; +extern crate time; +extern crate url; +extern crate urlencoded; + +pub mod email; +pub mod oidc; + +use emailaddress::EmailAddress; +use iron::headers::ContentType; +use iron::middleware::Handler; +use iron::modifiers; +use iron::prelude::*; +use iron::status; +use openssl::bn::BigNum; +use openssl::crypto::hash; +use openssl::crypto::pkey::PKey; +use serde_json::builder::ObjectBuilder; +use serde_json::de::from_reader; +use serde_json::value::Value; +use rand::{OsRng, Rng}; +use redis::Client; +use rustc_serialize::base64::{self, ToBase64}; +use std::collections::BTreeMap; +use std::fs::File; +use std::io::{BufReader, Write}; +use std::iter::Iterator; +use time::now_utc; +use urlencoded::{UrlEncodedBody, UrlEncodedQuery}; + + +/// Helper function for returning an Iron response with JSON data. +/// +/// Serializes the argument value to JSON and returns a HTTP 200 response +/// code with the serialized JSON as the body. +fn json_response(obj: &Value) -> IronResult { + let content = serde_json::to_string(&obj).unwrap(); + let mut rsp = Response::with((status::Ok, content)); + rsp.headers.set(ContentType::json()); + Ok(rsp) +} + + +/// Configuration data for a "famous" identity provider. +/// +/// Used as part of the `AppConfig` struct. +#[derive(Clone)] +struct ProviderConfig { + /// URL pointing to the OpenID configuration document. + discovery: String, + /// Client ID issued for this daemon instance by the provider. + client_id: String, + /// Secret issued for this daemon instance by the provider. + secret: String, + /// Issuer origin as used in identity tokens issued by the provider. + /// Used to check that the issuer in the token matches expectations. + issuer: String, +} + + +/// Configuration data for this daemon instance. +#[derive(Clone)] +pub struct AppConfig { + /// Version of the daemon (used in the `WelcomeHandler`). + version: String, + /// Base URL where this daemon can be found. This is used to construct + /// callback URLs. + base_url: String, + /// Private key used to sign identity tokens. + priv_key: PKey, + /// Redis database connector. + store: Client, + /// Duration in seconds for expiry of data stored in Redis. + expire_keys: usize, + /// Email sender (email address, then human-readable name). + sender: (String, String), + /// Duration in seconds for expiration of identity tokens. + token_validity: i64, + /// A map of "famous" identity providers. Each key is an email domain, + /// the value holds the configuration required to act as an application + /// doing OpenID Connect against the provider. + providers: BTreeMap, +} + + +/// Implementation with single method to read configuration from JSON. +impl AppConfig { + pub fn from_json_file(file_name: &str) -> AppConfig { + + let config_file = File::open(file_name).unwrap(); + let config_value: Value = from_reader(BufReader::new(config_file)).unwrap(); + let config = config_value.as_object().unwrap(); + + // The `private_key_file` key in the JSON object should hold a file + // name pointing to a PEM-encoded private RSA key. + let key_file_name = config["private_key_file"].as_string().unwrap(); + let priv_key_file = File::open(key_file_name).unwrap(); + let mut reader = BufReader::new(priv_key_file); + let sender = config["sender"].as_object().unwrap(); + AppConfig { + // Use the crate's version as defined in Cargo.toml. + version: env!("CARGO_PKG_VERSION").to_string(), + base_url: config["base_url"].as_string().unwrap().to_string(), + priv_key: PKey::private_key_from_pem(&mut reader).unwrap(), + store: Client::open(config["redis"].as_string().unwrap()).unwrap(), + expire_keys: config["expire_keys"].as_u64().unwrap() as usize, + sender: ( + sender["address"].as_string().unwrap().to_string(), + sender["name"].as_string().unwrap().to_string(), + ), + token_validity: config["token_validity"].as_i64().unwrap(), + providers: config["providers"].as_object().unwrap().iter() + .map(|(host, params)| { + let pobj = params.as_object().unwrap(); + (host.clone(), ProviderConfig { + discovery: pobj["discovery"].as_string().unwrap().to_string(), + client_id: pobj["client_id"].as_string().unwrap().to_string(), + secret: pobj["secret"].as_string().unwrap().to_string(), + issuer: pobj["issuer"].as_string().unwrap().to_string(), + }) + }) + .collect::>(), + } + + } +} + + +/// Iron handler for the root path, returns human-friendly message. +/// +/// This is not actually used in the protocol. +pub struct WelcomeHandler { pub app: AppConfig } +impl Handler for WelcomeHandler { + fn handle(&self, _: &mut Request) -> IronResult { + json_response(&ObjectBuilder::new() + .insert("ladaemon", "Welcome") + .insert("version", &self.app.version) + .unwrap()) + } +} + + +/// Iron handler to return the OpenID Discovery document. +/// +/// Most of this is hard-coded for now, although the URLs are constructed by +/// using the base URL as configured in the `base_url` configuration value. +pub struct OIDConfigHandler { pub app: AppConfig } +impl Handler for OIDConfigHandler { + fn handle(&self, _: &mut Request) -> IronResult { + json_response(&ObjectBuilder::new() + .insert("issuer", &self.app.base_url) + .insert("authorization_endpoint", + format!("{}/auth", self.app.base_url)) + .insert("jwks_uri", format!("{}/keys.json", self.app.base_url)) + .insert("scopes_supported", vec!["openid", "email"]) + .insert("claims_supported", + vec!["aud", "email", "email_verified", "exp", "iat", "iss", "sub"]) + .insert("response_types_supported", vec!["id_token"]) + .insert("response_modes_supported", vec!["form_post"]) + .insert("grant_types_supported", vec!["implicit"]) + .insert("subject_types_supported", vec!["public"]) + .insert("id_token_signing_alg_values_supported", vec!["RS256"]) + .unwrap()) + } +} + + +/// Helper function to encode a `BigNum` as URL-safe base64-encoded bytes. +/// +/// This is used for the public RSA key components returned by the +/// `KeysHandler`. +fn json_big_num(n: &BigNum) -> String { + n.to_vec().to_base64(base64::URL_SAFE) +} + + +/// Helper function to build a session ID for a login attempt. +/// +/// Put the email address, the client ID (RP origin) and some randomness into +/// a SHA256 hash, and encode it with URL-safe bas64 encoding. This is used +/// as the key in Redis, as well as the state for OAuth authentication. +fn session_id(email: &EmailAddress, client_id: &str) -> String { + let mut rng = OsRng::new().unwrap(); + let mut bytes_iter = rng.gen_iter(); + let rand_bytes: Vec = (0..16).map(|_| bytes_iter.next().unwrap()).collect(); + + let mut hasher = hash::Hasher::new(hash::Type::SHA256); + hasher.write(email.to_string().as_bytes()).unwrap(); + hasher.write(client_id.as_bytes()).unwrap(); + hasher.write(&rand_bytes).unwrap(); + hasher.finish().to_base64(base64::URL_SAFE) +} + + +/// Iron handler for the JSON Web Key Set document. +/// +/// Currently only supports a single RSA key (as configured), which is +/// published with the `"base"` key ID, scoped to signing usage. Relying +/// Parties will need to fetch this data to be able to verify identity tokens +/// issued by this daemon instance. +pub struct KeysHandler { pub app: AppConfig } +impl Handler for KeysHandler { + fn handle(&self, _: &mut Request) -> IronResult { + let rsa = self.app.priv_key.get_rsa(); + json_response(&ObjectBuilder::new() + .insert_array("keys", |builder| { + builder.push_object(|builder| { + builder.insert("kty", "RSA") + .insert("alg", "RS256") + .insert("use", "sig") + .insert("kid", "base") + .insert("n", json_big_num(&rsa.n().unwrap())) + .insert("e", json_big_num(&rsa.e().unwrap())) + }) + }) + .unwrap()) + } +} + + +/// Iron handler for authentication requests from the RP. +/// +/// Calls the `oidc::request()` function if the provided email address's +/// domain matches one of the configured famous providers. Otherwise, calls the +/// `email::request()` function to allow authentication through the email loop. +pub struct AuthHandler { pub app: AppConfig } +impl Handler for AuthHandler { + fn handle(&self, req: &mut Request) -> IronResult { + let params = req.get_ref::().unwrap(); + let email_addr = EmailAddress::new(¶ms.get("login_hint").unwrap()[0]).unwrap(); + if self.app.providers.contains_key(&email_addr.domain) { + + // OIDC authentication. Using 302 Found for redirection here. Note + // that, per RFC 7231, a user agent MAY change the request method + // from POST to GET for the subsequent request. + let auth_url = oidc::request(&self.app, params); + Ok(Response::with((status::Found, modifiers::Redirect(auth_url)))) + + } else { + + // Email loop authentication. For now, returns a JSON response; + // empty if successful, otherwise contains an error. + let obj = email::request(&self.app, params); + json_response(&obj) + + } + } +} + + + +/// Helper method to create a JWT for a given email address and origin. +/// +/// Builds the JSON payload and header, encoding with (URL-safe) +/// base64-encoding, then hashing and signing with the provided private key. +/// Returns the full JWT. +fn create_jwt(app: &AppConfig, email: &str, origin: &str) -> String { + + let now = now_utc().to_timespec().sec; + let payload = serde_json::to_string( + &ObjectBuilder::new() + .insert("aud", origin) + .insert("email", email) + .insert("email_verified", email) + .insert("exp", now + app.token_validity) + .insert("iat", now) + .insert("iss", &app.base_url) + .insert("sub", email) + .unwrap() + ).unwrap(); + let header = serde_json::to_string( + &ObjectBuilder::new() + .insert("kid", "base") + .insert("alg", "RS256") + .unwrap() + ).unwrap(); + + let mut input = Vec::::new(); + input.extend(header.as_bytes().to_base64(base64::URL_SAFE).into_bytes()); + input.push(b'.'); + input.extend(payload.as_bytes().to_base64(base64::URL_SAFE).into_bytes()); + let sha256 = hash::hash(hash::Type::SHA256, &input); + let sig = app.priv_key.sign(&sha256); + input.push(b'.'); + input.extend(sig.to_base64(base64::URL_SAFE).into_bytes()); + String::from_utf8(input).unwrap() + +} + + +/// HTML template used to have the user agent POST the identity token built +/// by the daemon instance to the RP's `redirect_uri`. +const FORWARD_TEMPLATE: &'static str = r#" + + + Let's Auth + + + +
+ +
+ +"#; + + +/// Helper function for returning result to the Relying Party. +/// +/// Takes a `Result` from one of the verification functions and embeds it in +/// a form in the `FORWARD_TEMPLATE`, from where it's POSTED to the RP's +/// `redirect_ur` as soon as the page has loaded. Result can either be an error +/// message or a JWT asserting the user's email address identity. +/// TODO: return error to RP instead of in a simple HTTP response. +fn return_to_relier(result: Result<(String, String), &'static str>) + -> IronResult { + + if result.is_err() { + return json_response(&ObjectBuilder::new() + .insert("error", result.unwrap_err()) + .unwrap()); + } + + let (jwt, redirect) = result.unwrap(); + let html = FORWARD_TEMPLATE.replace("{{ return_url }}", &redirect) + .replace("{{ jwt }}", &jwt); + let mut rsp = Response::with((status::Ok, html)); + rsp.headers.set(ContentType::html()); + Ok(rsp) + +} + + + +/// Iron handler for one-time pad email loop confirmation. +/// +/// Retrieves the session based session ID and the expected one-time pad. +/// Verify the code and return the resulting token or error to the RP. +pub struct ConfirmHandler { pub app: AppConfig } +impl Handler for ConfirmHandler { + fn handle(&self, req: &mut Request) -> IronResult { + let params = req.get_ref::().unwrap(); + let session_id = ¶ms.get("session").unwrap()[0]; + let code = ¶ms.get("code").unwrap()[0]; + return_to_relier(email::verify(&self.app, session_id, code)) + } +} + + +/// Iron handler for OAuth callbacks +/// +/// After the user allows or denies the Authentication Request with the famous +/// identity provider, they will be redirected back to the callback handler. +/// Verify the callback data and return the resulting token or error. +pub struct CallbackHandler { pub app: AppConfig } +impl Handler for CallbackHandler { + fn handle(&self, req: &mut Request) -> IronResult { + let params = req.get_ref::().unwrap(); + let session = ¶ms.get("state").unwrap()[0]; + let code = ¶ms.get("code").unwrap()[0]; + return_to_relier(oidc::verify(&self.app, session, code)) + } +}