diff options
author | Yves Fischer <yvesf-git@xapek.org> | 2018-11-26 01:35:11 +0100 |
---|---|---|
committer | Yves Fischer <yvesf-git@xapek.org> | 2018-11-26 01:35:11 +0100 |
commit | 3b89dc69da0f88cf8e2290523fa50656ac2ebb5d (patch) | |
tree | 105313b862ca7d8a123a37c279508081744a90d9 /src/auth_handler | |
download | nginx-auth-totp-3b89dc69da0f88cf8e2290523fa50656ac2ebb5d.tar.gz nginx-auth-totp-3b89dc69da0f88cf8e2290523fa50656ac2ebb5d.zip |
Proof of concept with totp
Diffstat (limited to 'src/auth_handler')
-rw-r--r-- | src/auth_handler/handler_info.rs | 30 | ||||
-rw-r--r-- | src/auth_handler/handler_login.rs | 142 | ||||
-rw-r--r-- | src/auth_handler/mod.rs | 170 | ||||
-rw-r--r-- | src/auth_handler/urls.rs | 18 |
4 files changed, 360 insertions, 0 deletions
diff --git a/src/auth_handler/handler_info.rs b/src/auth_handler/handler_info.rs new file mode 100644 index 0000000..0aeaa09 --- /dev/null +++ b/src/auth_handler/handler_info.rs @@ -0,0 +1,30 @@ +use std::io; + +use tokio::prelude::*; + +use http::{Request, Response, StatusCode}; +use bytes::Bytes; + +use ::ApplicationState; +use super::AuthHandler; + +pub(in super) fn respond<'a>(auth_handler: &AuthHandler, state: &ApplicationState, req: &Request<Bytes>, + path_rest: &'a str) -> Response<String> { + let body = html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "Hello world!"; + } + body { + h1(id = "heading") { + : "Hello! This is <html />"; + : "And path rest is: "; + : path_rest; + : "... ok :)"; + } + } + } + }; + Response::builder().body(body.to_string()).unwrap() +}
\ No newline at end of file diff --git a/src/auth_handler/handler_login.rs b/src/auth_handler/handler_login.rs new file mode 100644 index 0000000..83d5214 --- /dev/null +++ b/src/auth_handler/handler_login.rs @@ -0,0 +1,142 @@ +use std::io; +use std::borrow::Cow; + +use tokio::prelude::*; + +use http::{Request, Response, StatusCode, Method}; +use http::header::{SET_COOKIE, COOKIE}; +use url::form_urlencoded; + +use ::ApplicationState; +use ::totp; +use super::*; + + +pub(in super) fn GET<'a>(header_infos: &HeaderExtract, state: &ApplicationState, path_rest: &'a str) + -> Response<String> { + let body = if is_logged_in(&header_infos.cookies, &state.cookie_store) { + format!("{}", html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "TOTP Login"; + } + body { + h1(id = "heading") { + : "Currently logged in" + } + } + } + }) + } else { + format!("{}", html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "TOTP Login"; + } + body { + h1(id = "heading") { + : "Login" + } + form(method="POST") { + label(for="token") { + : "Enter TOTP token" + } + input(name="token",id="token",type="text"); + input(name="redirect", type="hidden", value=path_rest); + input(name="send",type="submit",value="Submit"); + } + } + } + }) + }; + make_response(StatusCode::OK, format!("{}", body)) +} + +fn test_secrets(secrets: &Vec<&str>, token: &String) -> bool { + secrets.iter() + .any(|secret| { + match totp::verify(secret, token) { + Ok(true) => true, + Ok(false) => false, + Err(e) => { + error!("Error from totp::verify: {}", e); + false + } + } + }) +} + +pub(in super) fn POST<'a>(header_infos: &HeaderExtract, state: &ApplicationState, req: &Request<Bytes>) + -> Response<String> { + let mut token = None; + let mut redirect = None; + for (key, val) in form_urlencoded::parse(req.body()) { + if key == "token" { + token = Some(val.into_owned()) + } else if key == "redirect" { + redirect = Some(val.into_owned()) + } + } + if token.is_none() { + return error_handler_internal("missing argument 'token'".to_string()); + } + let redirect = redirect.unwrap_or(Default::default()); + + if header_infos.totp_secrets.is_empty() { + return error_handler_internal("no secrets configured".to_string()) + } + + let mut ret = Response::builder(); + let body = if test_secrets(&header_infos.totp_secrets, &token.unwrap()) { + let cookie_value = state.cookie_store.create_authenticated_cookie(); + let cookie = CookieBuilder::new(COOKIE_NAME, cookie_value.to_string()) + .http_only(true) + .path("/") + .max_age(state.cookie_max_age) + .finish(); + ret.header(SET_COOKIE, cookie.to_string()); + warn!("Authenticated user with cookie {}", cookie); + format!("{}", html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "TOTP Successful"; + meta(http-equiv="refresh", content=format!("3; URL={}", redirect)) + } + body { + h1(id = "heading") { + : "Login succesful" + } + a(href="login") { + : "Try again... redirecting to "; + } + span { + : format!("{}", redirect) + } + } + } + }) + } else { + format!("{}", html! { + : horrorshow::helper::doctype::HTML; + html { + head { + title: "TOTP Login failed"; + meta(http-equiv="refresh", content="1") + } + body { + h1(id = "heading") { + : "Login failed" + } + a(href="login") { + : "Try again... " + } + } + } + }) + }; + + ret.body(body).unwrap() +}
\ No newline at end of file diff --git a/src/auth_handler/mod.rs b/src/auth_handler/mod.rs new file mode 100644 index 0000000..b4e02d1 --- /dev/null +++ b/src/auth_handler/mod.rs @@ -0,0 +1,170 @@ +#![allow(warnings)] + +use std::cell::Cell; +use std::collections::HashMap; +use std::io; +use std::marker::Sync; +use std::str; +use std::str::FromStr; +use std::sync::{Arc, RwLock, Mutex, MutexGuard}; +use std::cell::RefCell; + +use time::{Tm, Duration}; +use http::{Request, Response, StatusCode, Method}; +use tokio::prelude::*; +use horrorshow; +use cookie::{Cookie,CookieBuilder}; +use bytes::Bytes; + +use router; +use cookie_store::CookieStore; +use cookie_store::to_cookie; +use http_server::HttpHandler; + +mod urls; +mod handler_login; +mod handler_info; + +pub(in auth_handler) struct HeaderExtract<'a> { + totp_secrets: Vec<&'a str>, + cookies: Vec<Cookie<'a>>, +} + +pub struct HeaderMissing { + name: &'static str, +} + +static HTTP_HEADER_AUTHORIZATION: &'static str = r"Authorization"; +static HTTP_HEADER_X_ORIGINAL_URL: &'static str = r"X-Original-Url"; +static HTTP_HEADER_WWW_AUTHENTICATE: &'static str = r"WWW-Authenticate"; +static HTTP_HEADER_X_TOTP_SECRET: &'static str = r"X-Totp-Secret"; +static COOKIE_NAME: &'static str = r"totp_cookie"; + +#[derive(Clone)] +pub struct AuthHandler { + routing_table: router::RoutingTable<urls::Route>, +} + +pub(in auth_handler) fn make_response(code: StatusCode, body: String) -> Response<String> { + Response::builder().status(code).body(body).unwrap() +} + +pub(in auth_handler) fn error_handler_internal(body: String) -> Response<String> { + Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(body).unwrap() +} + +impl HttpHandler<super::ApplicationState> for AuthHandler { + fn respond(&self, state: &super::ApplicationState, req: Request<Bytes>) -> Response<String> { + match self.routing_table.match_path(req.uri().path()) { + Ok((urls::Route::Info, rest)) => handler_info::respond(self, state, &req, rest), + Ok((urls::Route::Login, rest)) => self.login(state, &req, rest), + Ok((urls::Route::Logout, rest)) => self.logout(state, &req, rest), + Ok((urls::Route::Check, rest)) => self.check(state, &req, rest), + Err(error) => match error { + router::NoMatchingRoute => + make_response(StatusCode::NOT_FOUND, "Resource not found".to_string()), + } + } + } +} + +pub fn is_logged_in(cookies: &Vec<Cookie>, cookie_store: &CookieStore) -> bool { + for cookie in cookies { + if cookie.name() == COOKIE_NAME { + let cookie_value = to_cookie(cookie.value()); + if cookie_value.is_some() && cookie_store.is_cookie_authenticated(&cookie_value.unwrap()) { + return true; + } + } + } + false +} + + +impl AuthHandler { + pub fn make() -> AuthHandler { + AuthHandler { routing_table: urls::create_routing_table() } + } + + fn login<'a>(&self, state: &super::ApplicationState, req: &Request<Bytes>, path_rest: &'a str, + ) -> Response<String> { + let header_infos = match Self::parse_header_infos(req) { + Ok(infos) => infos, + Err(message) => return error_handler_internal(message), + }; + match *req.method() { + Method::GET => handler_login::GET(&header_infos, state, path_rest), + Method::POST => handler_login::POST(&header_infos, state, req), + _ => error_handler_internal("Wrong method".to_string()), + } + } + + fn logout<'a>(&self, state: &super::ApplicationState, req: &Request<Bytes>, path_rest: &'a str, + ) -> Response<String> { + let header_infos = match Self::parse_header_infos(req) { + Ok(infos) => infos, + Err(message) => return error_handler_internal(message), + }; + + let body = format!("Rest: {}", path_rest); + Response::builder().body(body.to_string()).unwrap() + } + + + fn check<'a>(&self, state: &super::ApplicationState, req: &Request<Bytes>, path_rest: &'a str) -> Response<String> { + let header_infos = match Self::parse_header_infos(req) { + Ok(infos) => infos, + Err(message) => return error_handler_internal(message), + }; + if is_logged_in(&header_infos.cookies, &state.cookie_store) { + make_response(StatusCode::OK, "".to_string()) + } else { + make_response(StatusCode::UNAUTHORIZED, "Cookie expired".to_string()) + } + } + + + fn parse_header_infos(req: &Request<Bytes>) -> Result<HeaderExtract, String> { + let mut totp_secrets = Vec::new(); + for header_value in req.headers().get_all(HTTP_HEADER_X_TOTP_SECRET) { + let value = header_value.to_str().or(Err("Failed to read totp-secret header value"))?; + totp_secrets.push(value); + } + + let mut cookies = Vec::new(); + for header_value in req.headers().get_all(::http::header::COOKIE) { + let value = header_value.to_str().or(Err("Failed to read cookie value"))?; + let cookie = Cookie::parse(value).or(Err("Failed to parse cookie value"))?; + cookies.push(cookie); + } + + Ok(HeaderExtract { totp_secrets, cookies }) + } +} + +#[cfg(test)] +mod test1 { + // use super::*; + use test::Bencher; + // use horrorshow::prelude::*; + use horrorshow::helper::doctype; + + #[bench] + fn bench_1(_: &mut Bencher) { + let _ = format!("{}", html! { +: doctype::HTML; +html { +head { +title: "Hello world!"; +} +body { +// attributes +h1(id = "heading") { +// Insert escaped text +: "Hello! This is <html />" +} +} +} +}); + } +} diff --git a/src/auth_handler/urls.rs b/src/auth_handler/urls.rs new file mode 100644 index 0000000..fa7c76f --- /dev/null +++ b/src/auth_handler/urls.rs @@ -0,0 +1,18 @@ +use ::router; + +#[derive(Clone, Copy)] +pub(in auth_handler) enum Route { + Login, + Logout, + Info, + Check +} + +pub(in auth_handler) fn create_routing_table() -> router::RoutingTable<Route> { + let mut r = router::RoutingTable::new(); + r.insert("/info", Route::Info); + r.insert("/login", Route::Login); + r.insert("/logout", Route::Logout); + r.insert("/check", Route::Check); + r +}
\ No newline at end of file |