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"
|
"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": {
|
"aa88eb27d38ba4cfb539e4b4d7a86770c24221109e8fcc188a7d38f41e674817": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"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"
|
"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": {
|
"cf624c4e122477228e3bab09f7cd0dedf4776f73e7a86f19e06772a0adf83406": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,24 @@ impl RefreshTokens {
|
||||||
|
|
||||||
Ok((query.rows_affected() == 1).then_some(()))
|
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 completed_setup;
|
||||||
mod need_setup;
|
mod need_setup;
|
||||||
|
mod refresh_token;
|
||||||
|
|
||||||
pub use completed_setup::CompletedSetup;
|
pub use completed_setup::CompletedSetup;
|
||||||
pub use need_setup::NeedSetup;
|
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 super::prelude::*;
|
||||||
use rocket::get;
|
use rocket::http::{Cookie, CookieJar};
|
||||||
|
use rocket::{get, post};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use users::User;
|
use users::User;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![logo, avatar, homepage, redirect_to_setup]
|
routes![logo, avatar, homepage, redirect_to_setup, logout]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/logo")]
|
#[get("/logo")]
|
||||||
|
|
@ -91,3 +92,46 @@ async fn homepage() -> Page {
|
||||||
abc: "string".to_string(),
|
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">
|
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||||
<a href="./settings" class="dropdown-item">Settings</a>
|
<a href="./settings" class="dropdown-item">Settings</a>
|
||||||
<div class="dropdown-divider"></div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,17 @@ impl RefreshToken {
|
||||||
)
|
)
|
||||||
.await?)
|
.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>>,
|
used_at: Option<DateTime<Utc>>,
|
||||||
revoked_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