ezidam: added logout page, added RefreshToken guard

This commit is contained in:
Philippe Loctaux 2023-03-18 21:49:08 +01:00
parent 49b3a3d1fe
commit 5100aa1b4e
10 changed files with 205 additions and 3 deletions

View file

@ -0,0 +1,14 @@
select
-- info
token,
ip_address,
user,
-- timings
created_at as "created_at: DateTime<Utc>",
expires_at as "expires_at: DateTime<Utc>",
used_at as "used_at: DateTime<Utc>",
revoked_at as "revoked_at: DateTime<Utc>"
from refresh_tokens
where token is (?)

View file

@ -0,0 +1,5 @@
update refresh_tokens
set revoked_at = CURRENT_TIMESTAMP
where token is ?

View file

@ -322,6 +322,60 @@
},
"query": "insert into authorization_codes (code, app, user, expires_at)\nvalues (?, ?, ?, datetime(?, 'unixepoch'))\n"
},
"a7405a0479b551ce8e3ea7451fd781214e049a0f12551146ace1e9a2f2f0c06d": {
"describe": {
"columns": [
{
"name": "token",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ip_address",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "user",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "expires_at: DateTime<Utc>",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "used_at: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "revoked_at: DateTime<Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
true,
true
],
"parameters": {
"Right": 1
}
},
"query": "select\n -- info\n token,\n ip_address,\n user,\n\n -- timings\n created_at as \"created_at: DateTime<Utc>\",\n expires_at as \"expires_at: DateTime<Utc>\",\n used_at as \"used_at: DateTime<Utc>\",\n revoked_at as \"revoked_at: DateTime<Utc>\"\nfrom refresh_tokens\n\nwhere token is (?)"
},
"aa88eb27d38ba4cfb539e4b4d7a86770c24221109e8fcc188a7d38f41e674817": {
"describe": {
"columns": [],
@ -498,6 +552,16 @@
},
"query": "select u.id,\n u.created_at as \"created_at: DateTime<Utc>\",\n u.updated_at as \"updated_at: DateTime<Utc>\",\n u.is_admin as \"is_admin: bool\",\n u.username,\n u.name,\n u.email,\n u.password,\n u.password_recover,\n u.paper_key,\n u.is_archived as \"is_archived: bool\"\nfrom users u\n\n inner join settings s on u.id = s.first_admin\n\nwhere u.is_admin is 1\n and u.is_archived is 0\n and u.id is s.first_admin\n\nlimit 1"
},
"c6157ec3928527ec0ac5f493a5a91faff7e3668204a179e827a87d6279a02c40": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 1
}
},
"query": "update refresh_tokens\n\nset revoked_at = CURRENT_TIMESTAMP\n\nwhere token is ?"
},
"cf624c4e122477228e3bab09f7cd0dedf4776f73e7a86f19e06772a0adf83406": {
"describe": {
"columns": [

View file

@ -38,4 +38,24 @@ impl RefreshTokens {
Ok((query.rows_affected() == 1).then_some(()))
}
pub async fn get_one(
conn: impl SqliteExecutor<'_>,
token: &str,
) -> Result<Option<Self>, Error> {
sqlx::query_file_as!(Self, "queries/refresh_tokens/get_one.sql", token)
.fetch_optional(conn)
.await
.map_err(handle_error)
}
pub async fn revoke(conn: impl SqliteExecutor<'_>, token: &str) -> Result<Option<()>, Error> {
let query: SqliteQueryResult =
sqlx::query_file!("queries/refresh_tokens/revoke.sql", token)
.execute(conn)
.await
.map_err(handle_error)?;
Ok((query.rows_affected() == 1).then_some(()))
}
}

View file

@ -1,5 +1,7 @@
mod completed_setup;
mod need_setup;
mod refresh_token;
pub use completed_setup::CompletedSetup;
pub use need_setup::NeedSetup;
pub use refresh_token::RefreshToken;

View file

@ -0,0 +1,24 @@
use rocket::request::{FromRequest, Outcome};
use rocket::Request;
pub struct RefreshToken(pub String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for RefreshToken {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.cookies().get("refresh_token") {
Some(cookie) => {
let value = cookie.value();
if value.len() == 64 {
Outcome::Success(Self(value.to_string()))
} else {
Outcome::Forward(())
}
}
None => Outcome::Forward(()),
}
}
}

View file

@ -1,10 +1,11 @@
use super::prelude::*;
use rocket::get;
use rocket::http::{Cookie, CookieJar};
use rocket::{get, post};
use settings::Settings;
use users::User;
pub fn routes() -> Vec<Route> {
routes![logo, avatar, homepage, redirect_to_setup]
routes![logo, avatar, homepage, redirect_to_setup, logout]
}
#[get("/logo")]
@ -91,3 +92,46 @@ async fn homepage() -> Page {
abc: "string".to_string(),
})
}
#[post("/logout")]
async fn logout(
mut db: Connection<Database>,
refresh_token: RefreshToken,
cookie_jar: &CookieJar<'_>,
) -> Result<Redirect> {
let mut transaction = db.begin().await?;
let refresh_token = refresh_tokens::RefreshToken::get_one(&mut transaction, &refresh_token.0)
.await?
.ok_or_else(|| Error::not_found("Unknown refresh token"))?;
// If refresh token has already been used
if refresh_token.has_been_used() {
// TODO: Revoke all tokens for user
// user.revoke_all_refresh_tokens(&mut transaction).await?;
// transaction.commit().await?;
return Err(Error::forbidden("This refresh token has already been used"));
}
// If token has been revoked
if refresh_token.is_revoked() {
return Err(Error::forbidden("This refresh token has been revoked"));
}
// If token has expired
if refresh_token.has_expired() {
return Err(Error::forbidden("This refresh token has expired"));
}
// Revoke token
refresh_token.revoke(&mut transaction).await?;
transaction.commit().await?;
// Delete cookies
cookie_jar.remove(Cookie::named("access_token"));
cookie_jar.remove(Cookie::named("refresh_token"));
Ok(Redirect::to(uri!(homepage)))
}

View file

@ -30,7 +30,9 @@
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<a href="./settings" class="dropdown-item">Settings</a>
<div class="dropdown-divider"></div>
<a href="#" class="dropdown-item">Logout</a>
<form action="/logout" method="post">
<button type="submit" class="dropdown-item">Logout</button>
</form>
</div>
</div>
</div>

View file

@ -41,4 +41,17 @@ impl RefreshToken {
)
.await?)
}
pub async fn get_one(
conn: impl SqliteExecutor<'_>,
token: &str,
) -> Result<Option<Self>, Error> {
Ok(DatabaseRefreshTokens::get_one(conn, token)
.await?
.map(Self::from))
}
pub async fn revoke(self, conn: impl SqliteExecutor<'_>) -> Result<Option<()>, Error> {
Ok(DatabaseRefreshTokens::revoke(conn, &self.token).await?)
}
}

View file

@ -19,3 +19,17 @@ pub struct RefreshToken {
used_at: Option<DateTime<Utc>>,
revoked_at: Option<DateTime<Utc>>,
}
impl RefreshToken {
pub fn has_expired(&self) -> bool {
self.expires_at < Utc::now()
}
pub fn has_been_used(&self) -> bool {
self.used_at.is_some()
}
pub fn is_revoked(&self) -> bool {
self.revoked_at.is_some()
}
}