Skip to content

Commit

Permalink
Merge a3786c0 into 2a16a47
Browse files Browse the repository at this point in the history
  • Loading branch information
Stéphan Kochen committed Oct 27, 2017
2 parents 2a16a47 + a3786c0 commit 5543ef4
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 277 deletions.
339 changes: 173 additions & 166 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ path = "src/main.rs"
glob = "0.2.11"

[dependencies]
base64 = "0.6.0"
base64 = "0.7.0"
docopt = "0.8.1"
env_logger = "0.4.3"
futures = "0.1.14"
Expand Down
35 changes: 34 additions & 1 deletion lang/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,37 @@ msgid "Use the link in that email to login to"
msgstr "Use the link in that email to login to"

msgid "Alternatively, enter the code from the email to continue in this browser tab:"
msgstr "Alternatively, enter the code from the email to continue in this browser tab:"
msgstr "Alternatively, enter the code from the email to continue in this browser tab:"

msgid "The request is invalid, and could not be completed."
msgstr "The request is invalid, and could not be completed."

msgid "This indicates an issue with the site you're trying to login to. Contact the site administrator to get the issue resolved."
msgstr "This indicates an issue with the site you're trying to login to. Contact the site administrator to get the issue resolved."

msgid "Failed to connect with your email domain."
msgstr "Failed to connect with your email domain."

msgid "Contact the administrator of your email domain to get the issue resolved."
msgstr "Contact the administrator of your email domain to get the issue resolved."

msgid "Something went wrong, and we cannot complete your request at this time."
msgstr "Something went wrong, and we cannot complete your request at this time."

msgid "An internal error occurred, which has been logged with the below reference number."
msgstr "An internal error occurred, which has been logged with the below reference number."

msgid "Too many login attempts."
msgstr "Too many login attempts."

msgid "We've received too many requests in a short amount of time. Please try again later."
msgstr "We've received too many requests in a short amount of time. Please try again later."

msgid "The session has expired."
msgstr "The session has expired."

msgid "Your login attempt may have taken too long, or you tried to follow an old link. Please try again."
msgstr "Your login attempt may have taken too long, or you tried to follow an old link. Please try again."

msgid "Technical description"
msgstr "Technical description"
33 changes: 33 additions & 0 deletions lang/nl.po
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,36 @@ msgstr "Gebruik de link in die email om in te loggen op"

msgid "Alternatively, enter the code from the email to continue in this browser tab:"
msgstr "Als alternatief kunt u ook de code uit de email invoeren om in deze browser tab verder te gaan:"

msgid "The request is invalid, and could not be completed."
msgstr "De aanvraag is ongeldig, en kon niet worden verwerkt."

msgid "This indicates an issue with the site you're trying to login to. Contact the site administrator to get the issue resolved."
msgstr "Deze melding betreft een probleem met de site waar u probeert in te loggen. Neem contact op met de administrator van de site."

msgid "Failed to connect with your email domain."
msgstr "Kon geen verbinding maken met het email domein."

msgid "Contact the administrator of your email domain to get the issue resolved."
msgstr "Neem contact op met de administrator van uw email domein."

msgid "Something went wrong, and we cannot complete your request at this time."
msgstr "De aanvraag kon niet worden verwerkt door een onverwachtse fout."

msgid "An internal error occurred, which has been logged with the below reference number."
msgstr "Deze melding betreft een interne fout. De fout staat genoteerd met het onderstaande referentienummer."

msgid "Too many login attempts."
msgstr "Te veel inlog pogingen."

msgid "We've received too many requests in a short amount of time. Please try again later."
msgstr "We hebben te veel aanvragen ontvangen in een kort tijdsbestek. Probeer het later nog eens."

msgid "The session has expired."
msgstr "De sessie is verlopen."

msgid "Your login attempt may have taken too long, or you tried to follow an old link. Please try again."
msgstr "Uw inlogpoging heeft wellicht te lang geduurd, of u probeerde een oude link te volgen. Probeer het nog eens."

msgid "Technical description"
msgstr "Technische omschrijving"
22 changes: 8 additions & 14 deletions src/bridges/email.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bridges::{BridgeData, complete_auth};
use crypto::{random_zbase32};
use email_address::EmailAddress;
use error::BrokerError;
use futures::future;
Expand All @@ -8,16 +9,10 @@ use hyper::header::ContentType;
use lettre::email::EmailBuilder;
use lettre::transport::EmailTransport;
use lettre::transport::smtp::SmtpTransportBuilder;
use rand;
use std::iter::Iterator;
use std::rc::Rc;
use url::percent_encoding::{utf8_percent_encode, QUERY_ENCODE_SET};


/// The z-base-32 character set, from which we select characters for the one-time pad.
const CODE_CHARS: &'static [u8] = b"13456789abcdefghijkmnopqrstuwxyz";


/// Data we store in the session.
#[derive(Serialize,Deserialize)]
pub struct EmailBridgeData {
Expand All @@ -38,9 +33,7 @@ pub fn auth(ctx_handle: &ContextHandle, email_addr: &Rc<EmailAddress>) -> Handle
let mut ctx = ctx_handle.borrow_mut();

// Generate a 12-character one-time pad.
let chars = String::from_utf8((0..12).map(|_| {
CODE_CHARS[rand::random::<usize>() % CODE_CHARS.len()]
}).collect()).expect("failed to build one-time pad");
let chars = random_zbase32(12);
// For display, we split it in two groups of 6.
let chars_fmt = [&chars[0..6], &chars[6..12]].join(" ");

Expand Down Expand Up @@ -123,19 +116,20 @@ pub fn auth(ctx_handle: &ContextHandle, email_addr: &Rc<EmailAddress>) -> Handle
///
/// Retrieves the session based session ID and the expected one-time pad. Verifies the code and
/// returns the resulting token to the relying party.
pub fn confirmation(ctx_handle: ContextHandle) -> HandlerResult {
pub fn confirmation(ctx_handle: &ContextHandle) -> HandlerResult {
let mut ctx = ctx_handle.borrow_mut();

let session_id = try_get_param!(ctx, "session");
let session_id = try_get_provider_param!(ctx, "session");
let bridge_data = match ctx.load_session(&session_id) {
Ok(BridgeData::Email(bridge_data)) => Rc::new(bridge_data),
Ok(_) => return Box::new(future::err(BrokerError::Input("invalid session".to_owned()))),
Ok(_) => return Box::new(future::err(BrokerError::ProviderInput("invalid session".to_owned()))),
Err(e) => return Box::new(future::err(e)),
};

let code = try_get_param!(ctx, "code").replace(|c: char| c.is_whitespace(), "").to_lowercase();
let code = try_get_provider_param!(ctx, "code")
.replace(|c: char| c.is_whitespace(), "").to_lowercase();
if code != bridge_data.code {
return Box::new(future::err(BrokerError::Input("incorrect code".to_owned())));
return Box::new(future::err(BrokerError::ProviderInput("incorrect code".to_owned())));
}

Box::new(future::result(complete_auth(&*ctx)))
Expand Down
40 changes: 20 additions & 20 deletions src/bridges/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ pub fn auth(ctx_handle: &ContextHandle, email_addr: &Rc<EmailAddress>, link: &Li
///
/// For providers that don't support `response_mode=form_post`, we capture the fragment parameters
/// in javascript and emulate the POST request.
pub fn fragment_callback(ctx_handle: ContextHandle) -> HandlerResult {
pub fn fragment_callback(ctx_handle: &ContextHandle) -> HandlerResult {
let ctx = ctx_handle.borrow();

let res = Response::new()
Expand All @@ -204,26 +204,26 @@ pub fn fragment_callback(ctx_handle: ContextHandle) -> HandlerResult {
/// Match the returned email address and nonce against our session data, then extract the identity
/// token returned by the provider and verify it. Return an identity token for the relying party if
/// successful, or an error message otherwise.
pub fn callback(ctx_handle: ContextHandle) -> HandlerResult {
pub fn callback(ctx_handle: &ContextHandle) -> HandlerResult {
let (bridge_data, id_token) = {
let mut ctx = ctx_handle.borrow_mut();

let session_id = try_get_param!(ctx, "state");
let session_id = try_get_provider_param!(ctx, "state");
let bridge_data = match ctx.load_session(&session_id) {
Ok(BridgeData::Oidc(bridge_data)) => Rc::new(bridge_data),
Ok(_) => return Box::new(future::err(BrokerError::Input("invalid session".to_owned()))),
Ok(_) => return Box::new(future::err(BrokerError::ProviderInput("invalid session".to_owned()))),
Err(e) => return Box::new(future::err(e)),
};

let id_token = try_get_param!(ctx, "id_token");
let id_token = try_get_provider_param!(ctx, "id_token");
(bridge_data, id_token)
};

// Retrieve the provider's configuration.
let f = fetch_config(&ctx_handle, &bridge_data);
let f = fetch_config(ctx_handle, &bridge_data);

// Grab the keys from the provider, then verify the signature.
let ctx_handle2 = Rc::clone(&ctx_handle);
let ctx_handle2 = Rc::clone(ctx_handle);
let bridge_data2 = Rc::clone(&bridge_data);
let f = f.and_then(move |provider_config: ProviderConfig| {
let ctx = ctx_handle2.borrow();
Expand All @@ -233,12 +233,12 @@ pub fn callback(ctx_handle: ContextHandle) -> HandlerResult {
.then(move |result| {
let key_set: ProviderKeys = result.map_err(|e| BrokerError::Provider(
format!("could not fetch {}'s keys: {}", &bridge_data2.origin, e)))?;
crypto::verify_jws(&id_token, &key_set.keys).map_err(|_| BrokerError::Provider(
crypto::verify_jws(&id_token, &key_set.keys).map_err(|_| BrokerError::ProviderInput(
format!("could not verify the token received from {}", &bridge_data2.origin)))
})
});

let ctx_handle = Rc::clone(&ctx_handle);
let ctx_handle = Rc::clone(ctx_handle);
let f = f.and_then(move |jwt_payload| {
let ctx = ctx_handle.borrow();
let data = ctx.session_data.as_ref().expect("session vanished");
Expand All @@ -248,27 +248,27 @@ pub fn callback(ctx_handle: ContextHandle) -> HandlerResult {

// Extract the token claims.
let descr = format!("{}'s token payload", email_addr.domain());
let iss = try_get_json_field!(jwt_payload, "iss", descr);
let aud = try_get_json_field!(jwt_payload, "aud", descr);
let token_addr = try_get_json_field!(jwt_payload, "email", descr);
let exp = try_get_json_field!(jwt_payload, "exp", |v| v.as_i64(), descr);
let nonce = try_get_json_field!(jwt_payload, "nonce", descr);
let iss = try_get_token_field!(jwt_payload, "iss", descr);
let aud = try_get_token_field!(jwt_payload, "aud", descr);
let token_addr = try_get_token_field!(jwt_payload, "email", descr);
let exp = try_get_token_field!(jwt_payload, "exp", |v| v.as_i64(), descr);
let nonce = try_get_token_field!(jwt_payload, "nonce", descr);

// Normalize the token email address too.
let token_addr: EmailAddress = match token_addr.parse() {
Ok(addr) => bridge_data.normalization.apply(addr),
Err(_) => return Err(BrokerError::Provider(format!(
Err(_) => return Err(BrokerError::ProviderInput(format!(
"failed to parse email from {}", descr))),
};

// Verify the token claims.
check_field!(iss == bridge_data.origin, "iss", descr);
check_field!(aud == *bridge_data.client_id, "aud", descr);
check_field!(nonce == *bridge_data.nonce, "nonce", descr);
check_field!(token_addr == email_addr, "email", descr);
check_token_field!(iss == bridge_data.origin, "iss", descr);
check_token_field!(aud == *bridge_data.client_id, "aud", descr);
check_token_field!(nonce == *bridge_data.nonce, "nonce", descr);
check_token_field!(token_addr == email_addr, "email", descr);

let now = now_utc().to_timespec().sec;
check_field!(now < exp, "exp", descr);
check_token_field!(now < exp, "exp", descr);

// If everything is okay, build a new identity token and send it
// to the relying party.
Expand Down
14 changes: 13 additions & 1 deletion src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ use openssl::hash::{Hasher, MessageDigest};
use openssl::rsa::Rsa;
use openssl::pkey::PKey;
use openssl::sign::{Signer, Verifier};
use rand::{OsRng, Rng};
use rand::{OsRng, Rng, random};
use serde_json as json;
use std::fs::File;
use std::io::{Read, Error as IoError};
use std::iter::Iterator;
use time::now_utc;


Expand Down Expand Up @@ -147,13 +148,24 @@ pub fn session_id(email: &EmailAddress, client_id: &str) -> String {
}


/// Helper function to create a secure nonce.
pub fn nonce() -> String {
let mut rng = OsRng::new().expect("unable to create rng");
let rand_bytes: Vec<u8> = (0..16).map(|_| rng.gen()).collect();
base64url_encode(&rand_bytes)
}


/// Helper function to create a random string consisting of
/// characters from the z-base-32 set.
pub fn random_zbase32(len: usize) -> String {
const CHARSET: &'static [u8] = b"13456789abcdefghijkmnopqrstuwxyz";
String::from_utf8((0..len).map(|_| {
CHARSET[random::<usize>() % CHARSET.len()]
}).collect()).expect("failed to build one-time pad")
}


/// Helper function to deserialize key from JWK Key Set.
///
/// Searches the provided JWK Key Set Value for the key matching the given
Expand Down
75 changes: 65 additions & 10 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crypto::random_zbase32;
use hyper::{StatusCode};
use std::error::Error;
use std::fmt;

Expand All @@ -7,27 +9,79 @@ use std::fmt;
pub enum BrokerError {
/// User input error, which results in 400
Input(String),
/// Identity provider error, which results in 503 or email loop fallback
/// Identity provider error, which results in 503
Provider(String),
/// Identity provider request error, which results in 400
ProviderInput(String),
/// Internal errors, which result in 500
Internal(String),
/// User was rate limited, results in 413
RateLimited,
/// User session not found, results in 400
SessionExpired,
/// Result status used by bridges to cancel a request
ProviderCancelled,
}

impl BrokerError {
/// Log this error at the appropriate log level.
pub fn log(&self) {
/// Internal errors return a reference number for the error.
pub fn log(&self) -> Option<String> {
match *self {
BrokerError::Input(ref description) => debug!("{}", description),
BrokerError::Provider(ref description) => info!("{}", description),
BrokerError::Internal(ref description) => error!("{}", description),
// Silent errors
// User errors only at debug level.
ref err @ BrokerError::Input(_)
| ref err @ BrokerError::ProviderInput(_)
| ref err @ BrokerError::RateLimited
| ref err @ BrokerError::SessionExpired
| ref err @ BrokerError::ProviderCancelled
=> {
debug!("{}", err.description());
None
},
// Provider errors can be noteworthy, especially when
// the issue is network related.
ref err @ BrokerError::Provider(_) => {
info!("{}", err.description());
None
},
// Internal errors should ring alarm bells.
ref err @ BrokerError::Internal(_) => {
let reference = random_zbase32(6);
error!("[REF:{}] {}", err.description(), reference);
Some(reference)
},
}
}

/// Get the HTTP status code for this error.
pub fn http_status_code(&self) -> StatusCode {
match *self {
BrokerError::Input(_)
| BrokerError::ProviderInput(_)
| BrokerError::SessionExpired
=> StatusCode::BadRequest,
BrokerError::Provider(_) => StatusCode::ServiceUnavailable,
BrokerError::Internal(_) => StatusCode::InternalServerError,
BrokerError::RateLimited => StatusCode::TooManyRequests,
// Internal status that should never bubble this far
BrokerError::ProviderCancelled => unreachable!(),
}
}

/// Get the OAuth2 error code for this error
pub fn oauth_error_code(&self) -> &str {
match *self {
BrokerError::Input(_) => "invalid_request",
BrokerError::Provider(_)
| BrokerError::ProviderInput(_)
=> "temporarily_unavailable",
BrokerError::Internal(_) => "server_error",
// We will never redirect for these types of errors
BrokerError::RateLimited
| BrokerError::SessionExpired
// Internal status that should never bubble this far
| BrokerError::ProviderCancelled
=> {},
=> unreachable!(),
}
}
}
Expand All @@ -37,11 +91,12 @@ impl Error for BrokerError {
match *self {
BrokerError::Input(ref description)
| BrokerError::Provider(ref description)
| BrokerError::ProviderInput(ref description)
| BrokerError::Internal(ref description)
=> description,
BrokerError::RateLimited
| BrokerError::ProviderCancelled
=> unreachable!(),
BrokerError::RateLimited => "too many requests",
BrokerError::SessionExpired => "session has expired",
BrokerError::ProviderCancelled => "bridge cancelled the request",
}
}
}
Expand Down

0 comments on commit 5543ef4

Please sign in to comment.