session.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. use crate::{schema, user};
  2. use microrm::prelude::*;
  3. use serde::Deserialize;
  4. use tide::http::Cookie;
  5. pub(super) struct SessionHelper<'l> {
  6. qi: &'l microrm::QueryInterface<'static>,
  7. tmpl: &'l handlebars::Handlebars<'l>,
  8. realm_str: &'l str,
  9. }
  10. type Request = tide::Request<super::ServerStateWrapper>;
  11. const SESSION_COOKIE_NAME: &'static str = "uauth_session";
  12. impl<'l> SessionHelper<'l> {
  13. pub fn new(req: &'l Request) -> Self {
  14. Self {
  15. qi: req.state().core.pool.query_interface(),
  16. tmpl: &req.state().core.templates,
  17. realm_str: req.param("realm").expect("no realm param?"),
  18. }
  19. }
  20. pub fn get_realm(&self) -> tide::Result<schema::RealmID> {
  21. self.qi
  22. .get()
  23. .by(schema::Realm::Shortname, self.realm_str)
  24. .one()
  25. .expect("couldn't query db")
  26. .map(|r| r.id())
  27. .ok_or(tide::Error::from_str(404, "No such realm"))
  28. }
  29. fn build_session(
  30. &self,
  31. ) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
  32. let rng = ring::rand::SystemRandom::new();
  33. let session_id: [u8; 32] = ring::rand::generate(&rng)
  34. .map_err(|_| tide::Error::from_str(500, "Failed to generate session ID"))?
  35. .expose();
  36. let session_id = base64::encode_config(session_id, base64::URL_SAFE_NO_PAD);
  37. let maybe_id = self.qi.add(&schema::Session {
  38. key: session_id.clone(),
  39. });
  40. let session_cookie = Cookie::build(SESSION_COOKIE_NAME, session_id)
  41. .path("/")
  42. .finish();
  43. Ok((
  44. maybe_id.ok().ok_or(tide::Error::from_str(
  45. 500,
  46. "Failed to store session in database",
  47. ))?,
  48. Some(session_cookie),
  49. ))
  50. }
  51. pub fn verify_session(&self, req: &Request) -> Option<(schema::RealmID, schema::UserID)> {
  52. self.get_or_build_session(req)
  53. .ok()
  54. .zip(self.get_realm().ok())
  55. .and_then(|((sid, _cookie), realm)| {
  56. self.get_auth_for_session(realm, sid).and_then(|auth| {
  57. if auth.challenges_left.len() == 0 {
  58. Some((realm, auth.user))
  59. } else {
  60. None
  61. }
  62. })
  63. })
  64. }
  65. pub fn get_session(&self, req: &Request) -> Option<schema::SessionID> {
  66. req.cookie(SESSION_COOKIE_NAME)
  67. .and_then(|sid| {
  68. self.qi
  69. .get()
  70. .by(schema::Session::Key, sid.value())
  71. .one()
  72. .expect("couldn't query db")
  73. })
  74. .and_then(|session| Some(session.id()))
  75. }
  76. pub fn get_or_build_session(
  77. &self,
  78. req: &Request,
  79. ) -> tide::Result<(schema::SessionID, Option<tide::http::Cookie<'static>>)> {
  80. match self.get_session(&req) {
  81. Some(sid) => Ok((sid, None)),
  82. None => self.build_session(),
  83. }
  84. }
  85. pub fn get_auth_for_session(
  86. &self,
  87. realm: schema::RealmID,
  88. session: schema::SessionID,
  89. ) -> Option<microrm::WithID<schema::SessionAuthentication>> {
  90. use schema::SessionAuthentication as SAC;
  91. self.qi
  92. .get()
  93. .by(SAC::Realm, &realm)
  94. .by(SAC::Session, &session)
  95. .one()
  96. .expect("couldn't query db")
  97. }
  98. pub fn destroy_auth(&self, realm: schema::RealmID, session: schema::SessionID) {
  99. use schema::SessionAuthentication as SAC;
  100. self.qi
  101. .delete()
  102. .by(SAC::Realm, &realm)
  103. .by(SAC::Session, &session)
  104. .exec()
  105. .expect("couldn't query db")
  106. }
  107. }
  108. impl<'l> SessionHelper<'l> {
  109. fn render_login_from_auth(
  110. &self,
  111. mut response: tide::Response,
  112. redirect: String,
  113. auth: Option<schema::SessionAuthentication>,
  114. error_msg: Option<String>,
  115. ) -> tide::Response {
  116. log::info!("rendering login response... auth is {:?}", auth);
  117. let to_present: Option<schema::AuthChallengeType> = match auth {
  118. None => Some(schema::AuthChallengeType::Username),
  119. Some(auth) => auth.challenges_left.first().copied(),
  120. };
  121. if to_present.is_none() {
  122. response.set_status(302);
  123. tide::Redirect::new("../..").into()
  124. } else {
  125. self.render_login_page(response, redirect, to_present.unwrap(), error_msg)
  126. }
  127. }
  128. fn render_login_page(
  129. &self,
  130. mut response: tide::Response,
  131. redirect: String,
  132. to_present: schema::AuthChallengeType,
  133. error_msg: Option<String>,
  134. ) -> tide::Response {
  135. let do_challenge = |ty, ch| {
  136. self.tmpl
  137. .render(
  138. "id_v1_login",
  139. &serde_json::json!(
  140. {
  141. "challenge":
  142. format!(r#"
  143. <input type="hidden" name="challenge_type" value="{:?}" />
  144. <div class="challenge-type">{}</div>
  145. <div class="challenge-content">{}</div>
  146. "#,
  147. to_present, ty, ch),
  148. "redirect": redirect,
  149. "error_msg": error_msg.iter().collect::<Vec<_>>()
  150. }
  151. ),
  152. )
  153. .unwrap()
  154. };
  155. response.set_content_type("text/html");
  156. match to_present {
  157. schema::AuthChallengeType::Username => {
  158. response.set_body(do_challenge(
  159. "Username",
  160. r#"<input name="challenge" type="text" autofocus />"#,
  161. ));
  162. }
  163. schema::AuthChallengeType::Password => {
  164. response.set_body(do_challenge(
  165. "Password",
  166. r#"<input name="challenge" type="password" autofocus />"#,
  167. ));
  168. }
  169. _ => todo!(),
  170. }
  171. response
  172. }
  173. }
  174. async fn v1_login(req: Request) -> tide::Result<tide::Response> {
  175. let mut response = tide::Response::builder(200).build();
  176. let shelper = SessionHelper::new(&req);
  177. let realm = shelper.get_realm()?;
  178. let (session_id, cookie) = shelper.get_or_build_session(&req)?;
  179. cookie.map(|c| response.insert_cookie(c));
  180. let auth = shelper.get_auth_for_session(realm, session_id);
  181. #[derive(serde::Deserialize)]
  182. struct LoginQuery {
  183. redirect: Option<String>,
  184. }
  185. let query: LoginQuery = req.query().unwrap();
  186. Ok(shelper.render_login_from_auth(
  187. response,
  188. query.redirect.unwrap_or_else(|| "../..".to_string()),
  189. auth.map(|a| a.wrapped()),
  190. None,
  191. ))
  192. }
  193. async fn v1_login_post(mut req: Request) -> tide::Result<tide::Response> {
  194. let mut response = tide::Response::builder(200).build();
  195. #[derive(Deserialize)]
  196. struct ResponseBody {
  197. challenge_type: String,
  198. challenge: String,
  199. reset: Option<String>,
  200. redirect: String,
  201. }
  202. let body: ResponseBody = req.body_form().await?;
  203. let shelper = SessionHelper::new(&req);
  204. let realm = shelper.get_realm()?;
  205. let (session_id, cookie) = shelper.get_or_build_session(&req)?;
  206. cookie.map(|c| response.insert_cookie(c));
  207. let mut auth = shelper.get_auth_for_session(realm, session_id);
  208. // check if a login reset was requested; if so, we start again from the top
  209. if body.reset.is_some() {
  210. if let Some(_) = auth {
  211. shelper.destroy_auth(realm, session_id);
  212. response.set_status(302);
  213. return Ok(tide::Redirect::new("login").into());
  214. }
  215. }
  216. use schema::AuthChallengeType as ChallengeType;
  217. let challenge: schema::AuthChallengeType = match body.challenge_type.as_str() {
  218. "Username" => ChallengeType::Username,
  219. "Password" => ChallengeType::Password,
  220. _ => Err(tide::Error::from_str(400, "Unknown challenge type"))?,
  221. };
  222. let mut error = None;
  223. // check that the response matches what we're expecting next
  224. let to_be_presented: Option<schema::AuthChallengeType> = match &auth {
  225. None => Some(schema::AuthChallengeType::Username),
  226. Some(auth) => auth.challenges_left.first().copied(),
  227. };
  228. if to_be_presented != Some(challenge) {
  229. Err(tide::Error::from_str(400, "Unexpected challenge type"))?
  230. }
  231. match challenge {
  232. ChallengeType::Username => {
  233. let qi = req.state().core.pool.query_interface();
  234. shelper.destroy_auth(realm, session_id);
  235. let user = qi
  236. .get()
  237. .by(schema::User::Realm, &realm)
  238. .by(schema::User::Username, &body.challenge)
  239. .one()
  240. .expect("couldn't query db");
  241. if user.is_none() {
  242. error = Some(format!("No such user {}", body.challenge));
  243. } else {
  244. let user = user.unwrap();
  245. // TODO: set list of challenges to be whatever else this user has set up
  246. let sa = schema::SessionAuthentication {
  247. session: session_id,
  248. realm: realm,
  249. user: user.id(),
  250. challenges_left: vec![schema::AuthChallengeType::Password],
  251. };
  252. let id = qi.add(&sa).unwrap();
  253. auth = Some(microrm::WithID::new(sa, id));
  254. }
  255. }
  256. ct => {
  257. if let Some(auth) = &mut auth {
  258. let qi = req.state().core.pool.query_interface();
  259. let user = qi.get().by_id(&auth.user).one().expect("couldn't query db");
  260. if let Some(user) = user {
  261. let user = user::User::from_model(user);
  262. let verification = user.verify_challenge(&qi, ct, body.challenge.as_bytes());
  263. match verification {
  264. Some(true) => {
  265. auth.challenges_left.remove(0);
  266. qi.update()
  267. .to(auth.as_ref())
  268. .by_id(&auth.id())
  269. .exec()
  270. .expect("couldn't update auth status?");
  271. }
  272. Some(false) => {
  273. error = Some("Incorrect response. Please try again".into());
  274. }
  275. None => {
  276. error = Some(
  277. "User no longer exists. Please contact an administrator.".into(),
  278. );
  279. }
  280. }
  281. } else {
  282. error = Some(format!("User is not configured correctly: either it was deleted or it lacks a required authentication challenge type. Please contact an administrator."));
  283. }
  284. } else {
  285. error = Some(format!("Please restart login process."));
  286. }
  287. }
  288. };
  289. Ok(shelper.render_login_from_auth(response, body.redirect, auth.map(|a| a.wrapped()), error))
  290. }
  291. async fn v1_logout(req: Request) -> tide::Result<tide::Response> {
  292. let shelper = SessionHelper::new(&req);
  293. #[derive(serde::Deserialize)]
  294. struct LogoutQuery {
  295. redirect: Option<String>,
  296. }
  297. let query: LogoutQuery = req.query().unwrap();
  298. let realm = shelper.get_realm()?;
  299. shelper
  300. .get_session(&req)
  301. .map(|sid| shelper.destroy_auth(realm, sid));
  302. Ok(tide::Redirect::new(query.redirect.unwrap_or_else(|| "../..".into())).into())
  303. }
  304. pub(super) fn session_v1_server(mut route: tide::Route<super::ServerStateWrapper>) {
  305. route.at("login").get(v1_login).post(v1_login_post);
  306. route.at("logout").get(v1_logout);
  307. }