123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- use crate::{schema, user};
- use microrm::prelude::*;
- use serde::Deserialize;
- use tide::http::Cookie;
- pub(super) struct SessionHelper<'l> {
- qi: &'l microrm::QueryInterface<'static>,
- tmpl: &'l handlebars::Handlebars<'l>,
- realm_str: &'l str,
- }
- type Request = tide::Request<super::ServerStateWrapper>;
- const SESSION_COOKIE_NAME: &'static str = "uauth_session";
- impl<'l> SessionHelper<'l> {
- pub fn new(req: &'l Request) -> Self {
- Self {
- qi: req.state().core.pool.query_interface(),
- tmpl: &req.state().core.templates,
- realm_str: req.param("realm").expect("no realm param?"),
- }
- }
- pub fn get_realm(&self) -> tide::Result<schema::RealmID> {
- self.qi
- .get()
- .by(schema::Realm::Shortname, self.realm_str)
- .one()
- .expect("couldn't query db")
- .map(|r| r.id())
- .ok_or(tide::Error::from_str(404, "No such realm"))
- }
- fn build_session(
- &self,
- ) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
- let rng = ring::rand::SystemRandom::new();
- let session_id: [u8; 32] = ring::rand::generate(&rng)
- .map_err(|_| tide::Error::from_str(500, "Failed to generate session ID"))?
- .expose();
- let session_id = base64::encode_config(session_id, base64::URL_SAFE_NO_PAD);
- let maybe_id = self.qi.add(&schema::Session {
- key: session_id.clone(),
- });
- let session_cookie = Cookie::build(SESSION_COOKIE_NAME, session_id)
- .path("/")
- .finish();
- Ok((
- maybe_id.ok().ok_or(tide::Error::from_str(
- 500,
- "Failed to store session in database",
- ))?,
- Some(session_cookie),
- ))
- }
- pub fn verify_session(&self, req: &Request) -> Option<(schema::RealmID, schema::UserID)> {
- self.get_or_build_session(req)
- .ok()
- .zip(self.get_realm().ok())
- .and_then(|((sid, _cookie), realm)| {
- self.get_auth_for_session(realm, sid).and_then(|auth| {
- if auth.challenges_left.len() == 0 {
- Some((realm, auth.user))
- } else {
- None
- }
- })
- })
- }
- pub fn get_session(&self, req: &Request) -> Option<schema::SessionID> {
- req.cookie(SESSION_COOKIE_NAME)
- .and_then(|sid| {
- self.qi
- .get()
- .by(schema::Session::Key, sid.value())
- .one()
- .expect("couldn't query db")
- })
- .and_then(|session| Some(session.id()))
- }
- pub fn get_or_build_session(
- &self,
- req: &Request,
- ) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
- match self.get_session(&req) {
- Some(sid) => Ok((sid, None)),
- None => self.build_session(),
- }
- }
- pub fn get_auth_for_session(
- &self,
- realm: schema::RealmID,
- session: schema::SessionID,
- ) -> Option<microrm::WithID<schema::SessionAuthentication>> {
- use schema::SessionAuthentication as SAC;
- self.qi
- .get()
- .by(SAC::Realm, &realm)
- .by(SAC::Session, &session)
- .one()
- .expect("couldn't query db")
- }
- pub fn destroy_auth(&self, realm: schema::RealmID, session: schema::SessionID) {
- use schema::SessionAuthentication as SAC;
- self.qi
- .delete()
- .by(SAC::Realm, &realm)
- .by(SAC::Session, &session)
- .exec()
- .expect("couldn't query db")
- }
- }
- impl<'l> SessionHelper<'l> {
- fn render_login_from_auth(
- &self,
- mut response: tide::Response,
- redirect: String,
- auth: Option<schema::SessionAuthentication>,
- error_msg: Option<String>,
- ) -> tide::Response {
- log::info!("rendering login response... auth is {:?}", auth);
- let to_present: Option<schema::AuthChallengeType> = match auth {
- None => Some(schema::AuthChallengeType::Username),
- Some(auth) => auth.challenges_left.first().copied(),
- };
- if to_present.is_none() {
- response.set_status(302);
- tide::Redirect::new("../..").into()
- } else {
- self.render_login_page(response, redirect, to_present.unwrap(), error_msg)
- }
- }
- fn render_login_page(
- &self,
- mut response: tide::Response,
- redirect: String,
- to_present: schema::AuthChallengeType,
- error_msg: Option<String>,
- ) -> tide::Response {
- let do_challenge = |ty, ch| {
- self.tmpl
- .render(
- "id_v1_login",
- &serde_json::json!(
- {
- "challenge":
- format!(r#"
- <input type="hidden" name="challenge_type" value="{:?}" />
- <div class="challenge-type">{}</div>
- <div class="challenge-content">{}</div>
- "#,
- to_present, ty, ch),
- "redirect": redirect,
- "error_msg": error_msg.iter().collect::<Vec<_>>()
- }
- ),
- )
- .unwrap()
- };
- response.set_content_type("text/html");
- match to_present {
- schema::AuthChallengeType::Username => {
- response.set_body(do_challenge(
- "Username",
- r#"<input name="challenge" type="text" autofocus />"#,
- ));
- }
- schema::AuthChallengeType::Password => {
- response.set_body(do_challenge(
- "Password",
- r#"<input name="challenge" type="password" autofocus />"#,
- ));
- }
- _ => todo!(),
- }
- response
- }
- }
- async fn v1_login(req: Request) -> tide::Result<tide::Response> {
- let mut response = tide::Response::builder(200).build();
- let shelper = SessionHelper::new(&req);
- let realm = shelper.get_realm()?;
- let (session_id, cookie) = shelper.get_or_build_session(&req)?;
- cookie.map(|c| response.insert_cookie(c));
- let auth = shelper.get_auth_for_session(realm, session_id);
- #[derive(serde::Deserialize)]
- struct LoginQuery {
- redirect: Option<String>,
- }
- let query: LoginQuery = req.query().unwrap();
- Ok(shelper.render_login_from_auth(
- response,
- query.redirect.unwrap_or_else(|| "../..".to_string()),
- auth.map(|a| a.wrapped()),
- None,
- ))
- }
- async fn v1_login_post(mut req: Request) -> tide::Result<tide::Response> {
- let mut response = tide::Response::builder(200).build();
- #[derive(Deserialize)]
- struct ResponseBody {
- challenge_type: String,
- challenge: String,
- reset: Option<String>,
- redirect: String,
- }
- let body: ResponseBody = req.body_form().await?;
- let shelper = SessionHelper::new(&req);
- let realm = shelper.get_realm()?;
- let (session_id, cookie) = shelper.get_or_build_session(&req)?;
- cookie.map(|c| response.insert_cookie(c));
- let mut auth = shelper.get_auth_for_session(realm, session_id);
- // check if a login reset was requested; if so, we start again from the top
- if body.reset.is_some() {
- if let Some(_) = auth {
- shelper.destroy_auth(realm, session_id);
- response.set_status(302);
- return Ok(tide::Redirect::new("login").into());
- }
- }
- use schema::AuthChallengeType as ChallengeType;
- let challenge: schema::AuthChallengeType = match body.challenge_type.as_str() {
- "Username" => ChallengeType::Username,
- "Password" => ChallengeType::Password,
- _ => Err(tide::Error::from_str(400, "Unknown challenge type"))?,
- };
- let mut error = None;
- // check that the response matches what we're expecting next
- let to_be_presented: Option<schema::AuthChallengeType> = match &auth {
- None => Some(schema::AuthChallengeType::Username),
- Some(auth) => auth.challenges_left.first().copied(),
- };
- if to_be_presented != Some(challenge) {
- Err(tide::Error::from_str(400, "Unexpected challenge type"))?
- }
- match challenge {
- ChallengeType::Username => {
- let qi = req.state().core.pool.query_interface();
- shelper.destroy_auth(realm, session_id);
- let user = qi
- .get()
- .by(schema::User::Realm, &realm)
- .by(schema::User::Username, &body.challenge)
- .one()
- .expect("couldn't query db");
- if user.is_none() {
- error = Some(format!("No such user {}", body.challenge));
- } else {
- let user = user.unwrap();
- // TODO: set list of challenges to be whatever else this user has set up
- let sa = schema::SessionAuthentication {
- session: session_id,
- realm: realm,
- user: user.id(),
- challenges_left: vec![schema::AuthChallengeType::Password],
- };
- let id = qi.add(&sa).unwrap();
- auth = Some(microrm::WithID::new(sa, id));
- }
- }
- ct => {
- if let Some(auth) = &mut auth {
- let qi = req.state().core.pool.query_interface();
- let user = qi.get().by_id(&auth.user).one().expect("couldn't query db");
- if let Some(user) = user {
- let user = user::User::from_model(user);
- let verification = user.verify_challenge(&qi, ct, body.challenge.as_bytes());
- match verification {
- Some(true) => {
- auth.challenges_left.remove(0);
- qi.update()
- .to(auth.as_ref())
- .by_id(&auth.id())
- .exec()
- .expect("couldn't update auth status?");
- }
- Some(false) => {
- error = Some("Incorrect response. Please try again".into());
- }
- None => {
- error = Some(
- "User no longer exists. Please contact an administrator.".into(),
- );
- }
- }
- } else {
- error = Some(format!("User is not configured correctly: either it was deleted or it lacks a required authentication challenge type. Please contact an administrator."));
- }
- } else {
- error = Some(format!("Please restart login process."));
- }
- }
- };
- Ok(shelper.render_login_from_auth(response, body.redirect, auth.map(|a| a.wrapped()), error))
- }
- async fn v1_logout(req: Request) -> tide::Result<tide::Response> {
- let shelper = SessionHelper::new(&req);
- #[derive(serde::Deserialize)]
- struct LogoutQuery {
- redirect: Option<String>,
- }
- let query: LogoutQuery = req.query().unwrap();
- let realm = shelper.get_realm()?;
- shelper
- .get_session(&req)
- .map(|sid| shelper.destroy_auth(realm, sid));
- Ok(tide::Redirect::new(query.redirect.unwrap_or_else(|| "../..".into())).into())
- }
- pub(super) fn session_v1_server(mut route: tide::Route<super::ServerStateWrapper>) {
- route.at("login").get(v1_login).post(v1_login_post);
- route.at("logout").get(v1_logout);
- }
|