ezidam: added logout page, added RefreshToken guard
This commit is contained in:
parent
49b3a3d1fe
commit
5100aa1b4e
10 changed files with 205 additions and 3 deletions
14
crates/database/queries/refresh_tokens/get_one.sql
Normal file
14
crates/database/queries/refresh_tokens/get_one.sql
Normal 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 (?)
|
||||
5
crates/database/queries/refresh_tokens/revoke.sql
Normal file
5
crates/database/queries/refresh_tokens/revoke.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
update refresh_tokens
|
||||
|
||||
set revoked_at = CURRENT_TIMESTAMP
|
||||
|
||||
where token is ?
|
||||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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(()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
24
crates/ezidam/src/guards/refresh_token.rs
Normal file
24
crates/ezidam/src/guards/refresh_token.rs
Normal 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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue