From 7cbd5c3f58318ae8c94f0d5b670a0611c0fe46a9 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Thu, 21 Oct 2021 00:55:06 +0100 Subject: [PATCH] Make the logout button destroy sessions in the database --- chartered-db/src/users.rs | 17 ++++++++++++++++- chartered-frontend/src/dark.sass | 2 +- chartered-frontend/src/useAuth.tsx | 36 ++++++++++++++++++++++++++++++++++-- chartered-web/src/config.rs | 2 +- chartered-frontend/src/pages/Login.tsx | 2 +- chartered-web/src/middleware/auth.rs | 7 +++++-- chartered-web/src/endpoints/web_api/mod.rs | 3 ++- chartered-web/src/endpoints/web_api/auth/logout.rs | 38 ++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/auth/mod.rs | 24 ++++++++++++++++++++++-- 9 files changed, 109 insertions(+), 22 deletions(-) diff --git a/chartered-db/src/users.rs b/chartered-db/src/users.rs index 75236c4..80391eb 100644 --- a/chartered-db/src/users.rs +++ a/chartered-db/src/users.rs @@ -85,7 +85,7 @@ pub async fn find_by_session_key( conn: ConnectionPool, given_session_key: String, - ) -> Result> { + ) -> Result> { use crate::schema::user_sessions::dsl::{expires_at, session_key}; tokio::task::spawn_blocking(move || { @@ -99,7 +99,7 @@ ) .filter(session_key.eq(given_session_key)) .inner_join(users::table) - .select(users::all_columns) + .select((user_sessions::all_columns, users::all_columns)) .get_result(&conn) .optional()?) }) @@ -334,6 +334,19 @@ Ok(crate::schema::user_sessions::table .filter(session_key.eq(generated_session_key)) .get_result(&conn)?) + }) + .await? + } + + pub async fn delete(self: Arc, conn: ConnectionPool) -> Result { + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + let res = diesel::delete(user_sessions::table) + .filter(user_sessions::id.eq(self.id)) + .execute(&conn)?; + + Ok(res > 0) }) .await? } diff --git a/chartered-frontend/src/dark.sass b/chartered-frontend/src/dark.sass index 245dbd0..356f030 100644 --- a/chartered-frontend/src/dark.sass +++ a/chartered-frontend/src/dark.sass @@ -100,7 +100,7 @@ code background: $card-border-color !important - .form-control + .form-control, .form-control:focus background: transparent !important color: white border-color: $card-border-color !important diff --git a/chartered-frontend/src/useAuth.tsx b/chartered-frontend/src/useAuth.tsx index 29fd9df..cf90e5e 100644 --- a/chartered-frontend/src/useAuth.tsx +++ a/chartered-frontend/src/useAuth.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useContext, createContext } from "react"; import { useLocation, Redirect } from "react-router-dom"; -import { unauthenticatedEndpoint } from "./util"; +import {authenticatedEndpoint, BASE_URL, unauthenticatedEndpoint} from "./util"; import LoadingPage from "./pages/Loading"; export interface OAuthProviders { @@ -40,7 +40,7 @@ useEffect(async () => { try { let result = await fetch( - unauthenticatedEndpoint(`login/oauth/complete${location.search}`) + unauthenticatedEndpoint(`auth/login/oauth/complete${location.search}`) ); let json = await result.json(); @@ -101,7 +101,7 @@ }; const login = async (username: string, password: string) => { - let res = await fetch(unauthenticatedEndpoint("login/password"), { + let res = await fetch(unauthenticatedEndpoint("auth/login/password"), { method: "POST", headers: { "Content-Type": "application/json", @@ -116,7 +116,7 @@ const oauthLogin = async (provider: string) => { let res = await fetch( - unauthenticatedEndpoint(`login/oauth/${provider}/begin`), + unauthenticatedEndpoint(`auth/login/oauth/${provider}/begin`), { method: "GET", headers: { @@ -134,16 +134,34 @@ window.location.href = json.redirect_url; }; - const logout = async () => { - // todo call the service so we can purge the key from the db - setAuth(null); - }; - const getAuthKey = () => { if (auth?.[2] > new Date()) { return auth?.[1]; } else if (auth) { return null; + } + }; + + const logout = async () => { + if (auth === null) { + return; + } + + try { + await fetch( + `${BASE_URL}/a/${getAuthKey()}/web/v1/auth/logout`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + "User-Agent": window.navigator.userAgent, + }, + } + ); + } catch (e) { + console.error("Failed to fully log user out of session", e) + } finally { + setAuth(null); } }; diff --git a/chartered-web/src/config.rs b/chartered-web/src/config.rs index 20fbacb..e8e45a5 100644 --- a/chartered-web/src/config.rs +++ a/chartered-web/src/config.rs @@ -46,7 +46,7 @@ DiscoveredClient::discover( config.client_id.to_string(), config.client_secret.to_string(), - Some("http://127.0.0.1:1234/login/oauth".to_string()), + Some("http://127.0.0.1:1234/auth/login/oauth".to_string()), config.discovery_uri.clone(), ) .await?, diff --git a/chartered-frontend/src/pages/Login.tsx b/chartered-frontend/src/pages/Login.tsx index 299ef2a..e81a4bf 100644 --- a/chartered-frontend/src/pages/Login.tsx +++ a/chartered-frontend/src/pages/Login.tsx @@ -28,7 +28,7 @@ const { response: oauthProviders } = useUnauthenticatedRequest({ - endpoint: "login/oauth/providers", + endpoint: "auth/login/oauth/providers", }); useEffect(() => { diff --git a/chartered-web/src/middleware/auth.rs b/chartered-web/src/middleware/auth.rs index cde7c22..2544106 100644 --- a/chartered-web/src/middleware/auth.rs +++ a/chartered-web/src/middleware/auth.rs @@ -9,7 +9,9 @@ collections::HashMap, task::{Context, Poll}, }; +use std::sync::Arc; use tower::Service; +use chartered_db::users::User; use crate::endpoints::ErrorResponse; @@ -52,11 +54,11 @@ .unwrap() .clone(); - let user = match chartered_db::users::User::find_by_session_key(db, String::from(key)) + let (session, user) = match User::find_by_session_key(db, String::from(key)) .await .unwrap() { - Some(user) => std::sync::Arc::new(user), + Some((session, user)) => (Arc::new(session), Arc::new(user)), None => { return Ok(Response::builder() .status(StatusCode::UNAUTHORIZED) @@ -71,6 +73,7 @@ }; req.extensions_mut().unwrap().insert(user); + req.extensions_mut().unwrap().insert(session); let response: Response = inner.call(req.try_into_request().unwrap()).await?; diff --git a/chartered-web/src/endpoints/web_api/mod.rs b/chartered-web/src/endpoints/web_api/mod.rs index 407d039..08335b0 100644 --- a/chartered-web/src/endpoints/web_api/mod.rs +++ a/chartered-web/src/endpoints/web_api/mod.rs @@ -26,6 +26,7 @@ .nest("/organisations", organisations::routes()) .nest("/crates", crates::routes()) .nest("/users", users::routes()) + .nest("/auth", auth::authenticated_routes()) .route("/ssh-key", get(ssh_key::handle_get)) .route("/ssh-key", put(ssh_key::handle_put)) .route("/ssh-key/:id", delete(ssh_key::handle_delete))) @@ -40,5 +41,5 @@ > + Clone + Send, > { - crate::axum_box_after_every_route!(Router::new().nest("/login", auth::routes())) + crate::axum_box_after_every_route!(Router::new().nest("/auth", auth::unauthenticated_routes())) } diff --git a/chartered-web/src/endpoints/web_api/auth/logout.rs b/chartered-web/src/endpoints/web_api/auth/logout.rs new file mode 100644 index 0000000..3d0696e 100644 --- /dev/null +++ a/chartered-web/src/endpoints/web_api/auth/logout.rs @@ -1,0 +1,38 @@ +use std::sync::Arc; +use axum::{extract, Json}; +use chartered_db::ConnectionPool; +use serde::Serialize; +use thiserror::Error; +use chartered_db::users::UserSession; + +pub async fn handle( + extract::Extension(session): extract::Extension>, + extract::Extension(db): extract::Extension, +) -> Result, Error> { + session.delete(db).await?; + + Ok(Json(LogoutResponse { + success: true, + })) +} + +#[derive(Debug, Serialize)] +pub struct LogoutResponse { + success: bool, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + } + } +} + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/auth/mod.rs b/chartered-web/src/endpoints/web_api/auth/mod.rs index 1b3303e..1445227 100644 --- a/chartered-web/src/endpoints/web_api/auth/mod.rs +++ a/chartered-web/src/endpoints/web_api/auth/mod.rs @@ -16,9 +16,23 @@ pub mod openid; pub mod password; +pub mod logout; -pub fn routes() -> Router< +pub fn authenticated_routes() -> Router< impl tower::Service< + Request, + Response = Response, + Error = Infallible, + Future = impl Future, Infallible>> + Send, + > + Clone + + Send, +> { + crate::axum_box_after_every_route!(Router::new() + .route("/logout", get(logout::handle))) +} + +pub fn unauthenticated_routes() -> Router< + impl tower::Service< Request, Response = Response, Error = Infallible, @@ -27,10 +41,10 @@ + Send, > { crate::axum_box_after_every_route!(Router::new() - .route("/password", post(password::handle)) - .route("/oauth/:provider/begin", get(openid::begin_oidc)) - .route("/oauth/complete", get(openid::complete_oidc)) - .route("/oauth/providers", get(openid::list_providers))) + .route("/login/password", post(password::handle)) + .route("/login/oauth/:provider/begin", get(openid::begin_oidc)) + .route("/login/oauth/complete", get(openid::complete_oidc)) + .route("/login/oauth/providers", get(openid::list_providers))) } #[derive(Serialize)] -- rgit 0.1.3