Implement logout, renaming sessions::Cookie to sessions::Sesssion
This commit is contained in:
parent
f5946e5ea0
commit
70b417865f
|
@ -72,8 +72,8 @@ pub async fn route(user: Option<LoggedInUser>, cookies: Cookies, state: State<Ro
|
|||
// No cookie, or cookie is invalid. Perform login.
|
||||
if state.check_login(&form.username, &form.password).await.unwrap() {
|
||||
debug!("Login was successful for user {}. Saving cookie now.", &form.username);
|
||||
let (cookie_name, cookie_value) = state.sessions.make_session(COOKIE_NAME, &form.username).await;
|
||||
cookies.add(Cookie::new(COOKIE_NAME, cookie_value));
|
||||
let session = state.sessions.make_session(COOKIE_NAME, &form.username).await;
|
||||
cookies.add(session.cookie());
|
||||
Ok(format!("Welcome {}", &form.username))
|
||||
} else {
|
||||
debug!("Login failed for user {}", &form.username);
|
||||
|
|
20
src/routes/logout.rs
Normal file
20
src/routes/logout.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use axum::extract::State;
|
||||
use tower_cookies::{Cookies, Cookie};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::state::{
|
||||
COOKIE_NAME,
|
||||
RoutableAppState,
|
||||
sessions::LoggedOutUser,
|
||||
};
|
||||
|
||||
pub async fn route(user: Option<LoggedOutUser>, cookies: Cookies, state: State<RoutableAppState>) -> String {
|
||||
if let Some(user) = user {
|
||||
let cookie = user.cookie();
|
||||
cookies.remove(Cookie::new(Cow::Owned(cookie.name().to_string()), Cow::Owned(cookie.value().to_string())));
|
||||
return format!("Goodbye, {}. You are now logged out.", user.username());
|
||||
}
|
||||
|
||||
return format!("You are not logged in.");
|
||||
}
|
|
@ -8,12 +8,14 @@ use crate::state::RoutableAppState;
|
|||
|
||||
mod index;
|
||||
mod login;
|
||||
mod logout;
|
||||
|
||||
/// Build a router for the application, in a specific subpath eg `/yunohost/sso/`
|
||||
pub fn router(subpath: Option<String>, state: RoutableAppState) -> Router {
|
||||
let app = Router::new()
|
||||
.route("/", get(index::route))
|
||||
.route("/login/", post(login::route))
|
||||
.route("/logout/", get(logout::route))
|
||||
.layer(CookieManagerLayer::new())
|
||||
.with_state(state);
|
||||
if let Some(p) = subpath {
|
||||
|
|
|
@ -5,8 +5,9 @@ use axum::{
|
|||
};
|
||||
use ring::{hmac,rand};
|
||||
use snafu::prelude::*;
|
||||
use std::borrow::Cow;
|
||||
use tokio::sync::RwLock;
|
||||
use tower_cookies::Cookies;
|
||||
use tower_cookies::{Cookies, Cookie};
|
||||
use yunohost_api::Username;
|
||||
|
||||
use crate::{
|
||||
|
@ -46,10 +47,10 @@ pub struct SessionManager {
|
|||
pub secret: hmac::Key,
|
||||
/// The list of currently valid cookies. Some of them may have been invalidated,
|
||||
/// but they are stored in a different list to guarantee fast race-free access to this one.
|
||||
pub cookies: RwLock<Vec<Cookie>>,
|
||||
pub cookies: RwLock<Vec<Session>>,
|
||||
/// The list of invalidated cookies, due to logout or other purging mechanisms.
|
||||
/// This list is behind a mutex (RwLock) to prevent race conditions
|
||||
pub invalidated_cookies: RwLock<Vec<Cookie>>,
|
||||
pub invalidated_cookies: RwLock<Vec<Session>>,
|
||||
/// Expiration duration for set cookies, in seconds
|
||||
pub expiration_secs: u64,
|
||||
|
||||
|
@ -71,14 +72,14 @@ impl SessionManager {
|
|||
})
|
||||
}
|
||||
|
||||
/// Validates that a submitted cookie is a valid session. Returns:
|
||||
/// Verifies that a submitted cookie is a valid session. Returns:
|
||||
/// - Err(_) if the cookie format is really wrong
|
||||
/// - Ok(Some(name)) if the user is still logged in
|
||||
/// - Ok(None) if the user is no longer logged in (invalid/expired cookie)
|
||||
pub async fn verify_cookie(&self, cookie_claim: &str) -> Result<Option<Username>, SessionError> {
|
||||
/// - Ok(Some(Session)) if the user is still logged in
|
||||
/// - Ok(None) if the user is no longer lgoged in (invalid/expired cookie)
|
||||
pub async fn verify_cookie_session(&self, cookie_claim: &str) -> Result<Option<Session>, SessionError> {
|
||||
// First check the expiration of the claimed cookie.
|
||||
// If the timestamp was messed with, it will fail further verification.
|
||||
if let Some(claimed_timestamp) = Cookie::has_expired(cookie_claim, self.expiration_secs)? {
|
||||
if let Some(claimed_timestamp) = Session::has_expired(cookie_claim, self.expiration_secs)? {
|
||||
// The claimed timestamp is still valid. Check if we ever had that cookie in memory.
|
||||
// If the server was restarted, user will have to login again.
|
||||
if let Some(valid_cookie) = self.find_cookie(&self.cookies, claimed_timestamp, &cookie_claim).await {
|
||||
|
@ -88,7 +89,7 @@ impl SessionManager {
|
|||
Ok(None)
|
||||
} else {
|
||||
// User is still logged in!
|
||||
Ok(Some(valid_cookie.username()))
|
||||
Ok(Some(valid_cookie))
|
||||
}
|
||||
} else {
|
||||
// User doesn't have an active session
|
||||
|
@ -103,7 +104,7 @@ impl SessionManager {
|
|||
/// Generates a new valid cookie inside the `SessionManager`, and returns:
|
||||
/// - the cookie name to be set
|
||||
/// - the cookie content that can be sent to a client
|
||||
pub async fn make_session(&self, cookie_name: &str, username: &Username) -> (String, String) {
|
||||
pub async fn make_session(&self, cookie_name: &str, username: &Username) -> Session {
|
||||
let now = now();
|
||||
let signable_payload = format!("{now}:{cookie_name}:{username}");
|
||||
let signed_payload = hmac::sign(&self.secret, signable_payload.as_bytes()).as_ref().to_vec();
|
||||
|
@ -113,7 +114,7 @@ impl SessionManager {
|
|||
hex::encode(&signed_payload),
|
||||
);
|
||||
|
||||
let cookie = Cookie {
|
||||
let cookie = Session {
|
||||
cookie_name: cookie_name.to_string(),
|
||||
timestamp: now,
|
||||
username: username.clone(),
|
||||
|
@ -125,14 +126,30 @@ impl SessionManager {
|
|||
// We don't want to block too long the cookie jar
|
||||
// So it's only locked for this block (then dropped)
|
||||
let mut jar = self.cookies.write().await;
|
||||
jar.push(cookie);
|
||||
jar.push(cookie.clone());
|
||||
}
|
||||
|
||||
(cookie_name.to_string(), cookie_payload)
|
||||
cookie
|
||||
}
|
||||
|
||||
/// Invalidated a previously-set session cookie
|
||||
pub async fn invalidate_session(&self, timestamp: i64, content: &str) -> bool {
|
||||
if let Some(cookie) = self.find_cookie(&self.cookies, timestamp, content).await {
|
||||
debug!("[SESSION:{}] User {} is logging out", &cookie.cookie_name, &cookie.username);
|
||||
// Make sure the cookie was not already invalidated to avoid DOSing our invalidated cookie storage
|
||||
if self.find_cookie(&self.invalidated_cookies, timestamp, content).await.is_none() {
|
||||
let mut jar = self.invalidated_cookies.write().await;
|
||||
jar.push(cookie);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// User was not logged in, or session was already invalidated
|
||||
false
|
||||
}
|
||||
|
||||
/// Helper method to find a cookie with a specific timestamp, name and username in a cookie jar
|
||||
async fn find_cookie(&self, jar: &RwLock<Vec<Cookie>>, timestamp: i64, content: &str) -> Option<Cookie> {
|
||||
async fn find_cookie(&self, jar: &RwLock<Vec<Session>>, timestamp: i64, content: &str) -> Option<Session> {
|
||||
jar.read().await.iter().find(|cookie| {
|
||||
// First compare the timestamp (cheapest operation to invalidate the match)
|
||||
cookie.timestamp == timestamp
|
||||
|
@ -143,7 +160,7 @@ impl SessionManager {
|
|||
|
||||
/// A signed/encrypted as stored in memory.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Cookie {
|
||||
pub struct Session {
|
||||
/// The POSIX timetamp this cookie was created at
|
||||
pub timestamp: i64,
|
||||
/// The cookie name
|
||||
|
@ -156,7 +173,7 @@ pub struct Cookie {
|
|||
pub content: String,
|
||||
}
|
||||
|
||||
impl Cookie {
|
||||
impl Session {
|
||||
/// Extrats the timestamp from a stringy cookie
|
||||
pub fn timestamp(cookie: &str) -> Result<i64, SessionError> {
|
||||
let (timestamp, _rest) = cookie.split_once(':')
|
||||
|
@ -187,31 +204,61 @@ impl Cookie {
|
|||
}
|
||||
|
||||
/// The typed [`yunohost_api::Username`] for which this cookie is deemed valid.
|
||||
pub fn username(&self) -> Username {
|
||||
self.username.clone()
|
||||
pub fn username(&self) -> &Username {
|
||||
&self.username
|
||||
}
|
||||
|
||||
pub fn cookie(&self) -> Cookie<'static> {
|
||||
Cookie::build(
|
||||
Cow::Owned(self.cookie_name.to_string()),
|
||||
Cow::Owned(self.content.to_string()),
|
||||
).path(
|
||||
Cow::Owned("/".to_string())
|
||||
).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LoggedInUser(Username);
|
||||
pub struct LoggedInUser {
|
||||
username: Username,
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
impl LoggedInUser {
|
||||
pub fn new(username: Username) -> LoggedInUser {
|
||||
LoggedInUser(username)
|
||||
pub fn new(username: Username, timestamp: i64) -> LoggedInUser {
|
||||
LoggedInUser {
|
||||
username,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
self.username.as_str()
|
||||
}
|
||||
|
||||
pub fn as_username(&self) -> &Username {
|
||||
&self.0
|
||||
pub fn username(&self) -> &Username {
|
||||
&self.username
|
||||
}
|
||||
|
||||
pub fn timestamp(&self) -> i64 {
|
||||
self.timestamp
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
impl From<Session> for LoggedInUser {
|
||||
fn from(s: Session) -> LoggedInUser {
|
||||
LoggedInUser {
|
||||
username: s.username,
|
||||
timestamp: s.timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LoggedInUser {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(fmt)
|
||||
self.username.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,16 +267,88 @@ impl FromRequestParts<RoutableAppState> for LoggedInUser {
|
|||
type Rejection = Error;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result<Self, Self::Rejection> {
|
||||
// TODO: error
|
||||
let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?;
|
||||
trace!("[SESSION:{}] Checking if the user has a valid session", COOKIE_NAME);
|
||||
if let Some(session_cookie) = cookies.get(COOKIE_NAME) {
|
||||
trace!("[SESSION:{}] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie);
|
||||
if let Some(username) = state.sessions.verify_cookie(session_cookie.value()).await.context(SessionSnafu)? {
|
||||
debug!("[SESSION:{}] User {} resumed session.", COOKIE_NAME, &username);
|
||||
return Ok(LoggedInUser::new(username));
|
||||
trace!("[SESSION:{}/login] Checking if the user has a valid session", COOKIE_NAME);
|
||||
let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) {
|
||||
trace!("[SESSION:{}/login] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie);
|
||||
session_cookie
|
||||
} else {
|
||||
trace!("[SESSION:{}/login] No current session cookie", COOKIE_NAME);
|
||||
return Err(Error::Session {
|
||||
source: SessionError::NoSession {
|
||||
cookie_name: COOKIE_NAME.to_string(),
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if let Some(session) = state.sessions.verify_cookie_session(session_cookie.value()).await.context(SessionSnafu)? {
|
||||
debug!("[SESSION:{}/login] User {} resumed session.", COOKIE_NAME, &session.username);
|
||||
return Ok(session.into());
|
||||
} else {
|
||||
trace!("[SESSION:{}/login] User session is invalid or has expired.", COOKIE_NAME);
|
||||
return Err(Error::Session {
|
||||
source: SessionError::InvalidOrExpired {
|
||||
cookie_name: COOKIE_NAME.to_string(),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoggedOutUser(Session);
|
||||
|
||||
impl LoggedOutUser {
|
||||
pub fn new(session: Session) -> Self {
|
||||
Self(session)
|
||||
}
|
||||
|
||||
pub fn username(&self) -> &Username {
|
||||
self.0.username()
|
||||
}
|
||||
|
||||
pub fn cookie(&self) -> Cookie {
|
||||
Cookie::build(
|
||||
Cow::Owned(self.0.cookie_name.to_string()),
|
||||
Cow::Owned(self.0.content.to_string()),
|
||||
).path(
|
||||
Cow::Owned("/".to_string())
|
||||
).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Session> for LoggedOutUser {
|
||||
fn from(s: Session) -> LoggedOutUser {
|
||||
LoggedOutUser(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FromRequestParts<RoutableAppState> for LoggedOutUser {
|
||||
type Rejection = Error;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result<Self, Self::Rejection> {
|
||||
let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?;
|
||||
trace!("[SESSION:{}/logout] Checking if the user has a valid session", COOKIE_NAME);
|
||||
trace!("{:#?}", parts.headers);
|
||||
let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) {
|
||||
trace!("[SESSION:{}/logout] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie);
|
||||
session_cookie
|
||||
} else {
|
||||
trace!("[SESSION:{}/logout] No current session cookie", COOKIE_NAME);
|
||||
return Err(Error::Session {
|
||||
source: SessionError::NoSession {
|
||||
cookie_name: COOKIE_NAME.to_string(),
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if let Some(session) = state.sessions.verify_cookie_session(session_cookie.value()).await.context(SessionSnafu)? {
|
||||
debug!("[SESSION:{}/logout] User {} resumed session, requesting to log out.", COOKIE_NAME, &session.username);
|
||||
if state.sessions.invalidate_session(session.timestamp, &session.content).await {
|
||||
return Ok(session.into());
|
||||
} else {
|
||||
trace!("[SESSION:{}] User session is invalid or has expired.", COOKIE_NAME);
|
||||
warn!("[SESSION:{}/logout] User {} requested logout but was already logged out. Here's the session:\n{:#?}", COOKIE_NAME, &session.username, &session);
|
||||
warn!("This is probably a race condition but is completely harmless.");
|
||||
return Err(Error::Session {
|
||||
source: SessionError::InvalidOrExpired {
|
||||
cookie_name: COOKIE_NAME.to_string(),
|
||||
|
@ -237,9 +356,9 @@ impl FromRequestParts<RoutableAppState> for LoggedInUser {
|
|||
});
|
||||
}
|
||||
} else {
|
||||
trace!("[SESSION:{}] No current session cookie", COOKIE_NAME);
|
||||
trace!("[SESSION:{}/logout] User session is invalid or has expired.", COOKIE_NAME);
|
||||
return Err(Error::Session {
|
||||
source: SessionError::NoSession {
|
||||
source: SessionError::InvalidOrExpired {
|
||||
cookie_name: COOKIE_NAME.to_string(),
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user