ezidam: refactor jwt and refresh token generate in "tokens" mod
This commit is contained in:
parent
23b1e3ea4f
commit
9687116063
13 changed files with 179 additions and 59 deletions
6
crates/database/queries/keys/revoke_all_except_one.sql
Normal file
6
crates/database/queries/keys/revoke_all_except_one.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
update keys
|
||||
|
||||
set revoked_at = CURRENT_TIMESTAMP
|
||||
|
||||
where revoked_at is null
|
||||
and id is not (?)
|
||||
5
crates/database/queries/refresh_tokens/revoke_all.sql
Normal file
5
crates/database/queries/refresh_tokens/revoke_all.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
update refresh_tokens
|
||||
|
||||
set revoked_at = CURRENT_TIMESTAMP
|
||||
|
||||
where revoked_at is null
|
||||
|
|
@ -338,6 +338,16 @@
|
|||
},
|
||||
"query": "select id,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n is_admin as \"is_admin: bool\",\n username,\n name,\n email,\n password,\n password_recover,\n paper_key,\n is_archived as \"is_archived: bool\"\nfrom users\n\nwhere email is (?)\n"
|
||||
},
|
||||
"7b7f2430b2a719b3d5ce504c0a9302731b3ff82da99ba7771c2728d88aee642a": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "update keys\n\nset revoked_at = CURRENT_TIMESTAMP\n\nwhere revoked_at is null\n and id is not (?)\n"
|
||||
},
|
||||
"7f26b73408318040f94fb6574d5cc25482cef1a57ba4c467fa0bc0fdf25bf39c": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
|
|
@ -358,6 +368,16 @@
|
|||
},
|
||||
"query": "update settings\n\nset url = ?\n\nwhere id is 0\n"
|
||||
},
|
||||
"9f1885c4786f73335b4d614f562bb7cad49c91bfe7f084d8c25c6c571673ab90": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
},
|
||||
"query": "update refresh_tokens\n\nset revoked_at = CURRENT_TIMESTAMP\n\nwhere revoked_at is null"
|
||||
},
|
||||
"a55b17a3a70e6445517f19536220f0dafc78a0e8b69221dee4715f84841839da": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
|
|
|
|||
|
|
@ -63,4 +63,17 @@ impl Keys {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn revoke_all_except_one(
|
||||
conn: impl SqliteExecutor<'_>,
|
||||
exception: &str,
|
||||
) -> Result<Option<()>, Error> {
|
||||
let query: SqliteQueryResult =
|
||||
sqlx::query_file!("queries/keys/revoke_all_except_one.sql", exception)
|
||||
.execute(conn)
|
||||
.await
|
||||
.map_err(handle_error)?;
|
||||
|
||||
Ok((query.rows_affected() >= 1).then_some(()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,15 @@ impl RefreshTokens {
|
|||
Ok((query.rows_affected() == 1).then_some(()))
|
||||
}
|
||||
|
||||
pub async fn revoke_all(conn: impl SqliteExecutor<'_>) -> Result<Option<()>, Error> {
|
||||
let query: SqliteQueryResult = sqlx::query_file!("queries/refresh_tokens/revoke_all.sql")
|
||||
.execute(conn)
|
||||
.await
|
||||
.map_err(handle_error)?;
|
||||
|
||||
Ok((query.rows_affected() >= 1).then_some(()))
|
||||
}
|
||||
|
||||
pub async fn revoke_all_for_user(
|
||||
conn: impl SqliteExecutor<'_>,
|
||||
user: &str,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ mod admin;
|
|||
mod user;
|
||||
|
||||
use crate::guards::refresh_token::get_refresh_token_from_cookie;
|
||||
use crate::tokens::{
|
||||
JWT_COOKIE_NAME, JWT_DURATION_MINUTES, REFRESH_TOKEN_COOKIE_NAME, REFRESH_TOKEN_DURATION_DAYS,
|
||||
};
|
||||
pub use admin::JwtAdmin;
|
||||
use id::KeyID;
|
||||
pub use user::JwtUser;
|
||||
|
|
@ -50,7 +53,7 @@ pub enum Error {
|
|||
pub(super) fn get_access_token_from_cookie(request: &Request) -> Option<String> {
|
||||
request
|
||||
.cookies()
|
||||
.get("access_token")
|
||||
.get(JWT_COOKIE_NAME)
|
||||
.map(|cookie| cookie.value().to_string())
|
||||
}
|
||||
|
||||
|
|
@ -261,9 +264,6 @@ pub async fn use_refresh_token(
|
|||
}
|
||||
};
|
||||
|
||||
// Refresh token duration in days
|
||||
let refresh_token_duration = 21;
|
||||
|
||||
// Attempt to get ip address
|
||||
let ip_address = match request.client_ip() {
|
||||
Some(ip) => ip.to_string(),
|
||||
|
|
@ -278,7 +278,7 @@ pub async fn use_refresh_token(
|
|||
new_refresh_token.as_ref(),
|
||||
ip_address,
|
||||
user.id(),
|
||||
refresh_token_duration,
|
||||
REFRESH_TOKEN_DURATION_DAYS,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
@ -286,11 +286,14 @@ pub async fn use_refresh_token(
|
|||
}
|
||||
|
||||
// Add refresh token as a cookie
|
||||
let mut cookie = Cookie::new("refresh_token", new_refresh_token.as_ref().to_string());
|
||||
let mut cookie = Cookie::new(
|
||||
REFRESH_TOKEN_COOKIE_NAME,
|
||||
new_refresh_token.as_ref().to_string(),
|
||||
);
|
||||
cookie.set_secure(true);
|
||||
cookie.set_http_only(true);
|
||||
cookie.set_same_site(SameSite::Strict);
|
||||
cookie.set_max_age(Duration::days(refresh_token_duration));
|
||||
cookie.set_max_age(Duration::days(REFRESH_TOKEN_DURATION_DAYS));
|
||||
cookie_jar.add(cookie);
|
||||
|
||||
// Get latest key from database
|
||||
|
|
@ -336,14 +339,11 @@ pub async fn use_refresh_token(
|
|||
// TODO: get user roles
|
||||
let roles = vec![];
|
||||
|
||||
// Access token duration in minutes
|
||||
let access_token_duration = 1;
|
||||
|
||||
// Create jwt, sign and serialize
|
||||
let jwt_claims = JwtClaims::new(home_page.clone(), "ezidam", &user, roles);
|
||||
let jwt = match jwt_claims
|
||||
.clone()
|
||||
.sign_serialize(&private_key, access_token_duration)
|
||||
.sign_serialize(&private_key, JWT_DURATION_MINUTES)
|
||||
{
|
||||
Ok(jwt) => jwt,
|
||||
Err(e) => {
|
||||
|
|
@ -352,11 +352,11 @@ pub async fn use_refresh_token(
|
|||
};
|
||||
|
||||
// Add jwt as a cookie
|
||||
let mut cookie = Cookie::new("access_token", jwt);
|
||||
let mut cookie = Cookie::new(JWT_COOKIE_NAME, jwt);
|
||||
cookie.set_secure(true);
|
||||
cookie.set_http_only(true);
|
||||
cookie.set_same_site(SameSite::Strict);
|
||||
cookie.set_max_age(Duration::minutes(access_token_duration));
|
||||
cookie.set_max_age(Duration::minutes(JWT_DURATION_MINUTES));
|
||||
cookie_jar.add(cookie);
|
||||
|
||||
if let Err(_e) = transaction.commit().await {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use crate::tokens::REFRESH_TOKEN_COOKIE_NAME;
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use rocket::Request;
|
||||
|
||||
pub struct RefreshToken(pub String);
|
||||
|
||||
pub fn get_refresh_token_from_cookie(request: &Request) -> Option<String> {
|
||||
match request.cookies().get("refresh_token") {
|
||||
match request.cookies().get(REFRESH_TOKEN_COOKIE_NAME) {
|
||||
Some(cookie) => {
|
||||
let value = cookie.value();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ mod page;
|
|||
mod response_timer;
|
||||
mod routes;
|
||||
mod shutdown;
|
||||
mod tokens;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
use crate::routes::prelude::*;
|
||||
use crate::tokens::{generate_jwt, generate_refresh_token};
|
||||
use apps::App;
|
||||
use authorization_codes::AuthorizationCode;
|
||||
use hash::SecretString;
|
||||
use jwt::database::Key;
|
||||
use jwt::{JwtClaims, PrivateKey};
|
||||
use jwt::PrivateKey;
|
||||
use refresh_tokens::RefreshToken;
|
||||
use rocket::http::{Cookie, CookieJar, SameSite};
|
||||
use rocket::time::Duration;
|
||||
use rocket::http::CookieJar;
|
||||
use rocket::{get, UriDisplayQuery};
|
||||
use settings::Settings;
|
||||
use std::net::IpAddr;
|
||||
|
|
@ -80,28 +79,9 @@ pub async fn redirect_page(
|
|||
// TODO: refactor for "code" route
|
||||
|
||||
// Generate refresh token
|
||||
let refresh_token = task::spawn_blocking(|| SecretString::new(64)).await?;
|
||||
|
||||
// Refresh token duration in days
|
||||
let refresh_token_duration = 21;
|
||||
|
||||
// Insert refresh token in database
|
||||
RefreshToken::insert(
|
||||
&mut transaction,
|
||||
refresh_token.as_ref(),
|
||||
ip_address.to_string(),
|
||||
user.id(),
|
||||
refresh_token_duration,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add refresh token as a cookie
|
||||
let mut cookie = Cookie::new("refresh_token", refresh_token.as_ref().to_string());
|
||||
cookie.set_secure(true);
|
||||
cookie.set_http_only(true);
|
||||
cookie.set_same_site(SameSite::Strict);
|
||||
cookie.set_max_age(Duration::days(refresh_token_duration));
|
||||
cookie_jar.add(cookie);
|
||||
generate_refresh_token(&mut transaction, ip_address, user.id(), cookie_jar)
|
||||
.await
|
||||
.map_err(Error::internal_server_error)?;
|
||||
|
||||
// Get latest key from database
|
||||
let key = Key::get_most_recent(&mut transaction)
|
||||
|
|
@ -118,23 +98,17 @@ pub async fn redirect_page(
|
|||
task::spawn_blocking(move || PrivateKey::from_der(key.private_der(), key.key_id()))
|
||||
.await??;
|
||||
|
||||
// TODO: get user roles
|
||||
let roles = vec![];
|
||||
|
||||
// Access token duration in minutes
|
||||
let access_token_duration = 15;
|
||||
|
||||
// Create jwt, sign and serialize
|
||||
let jwt = JwtClaims::new(home_page.clone(), app.id().as_ref(), &user, roles)
|
||||
.sign_serialize(&private_key, access_token_duration)?;
|
||||
|
||||
// Add jwt as a cookie
|
||||
let mut cookie = Cookie::new("access_token", jwt);
|
||||
cookie.set_secure(true);
|
||||
cookie.set_http_only(true);
|
||||
cookie.set_same_site(SameSite::Strict);
|
||||
cookie.set_max_age(Duration::minutes(access_token_duration));
|
||||
cookie_jar.add(cookie);
|
||||
// Generate jwt
|
||||
generate_jwt(
|
||||
&mut transaction,
|
||||
&private_key,
|
||||
&home_page,
|
||||
&app.id().0,
|
||||
&user,
|
||||
cookie_jar,
|
||||
)
|
||||
.await
|
||||
.map_err(Error::internal_server_error)?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use super::prelude::*;
|
||||
use crate::tokens::{JWT_COOKIE_NAME, REFRESH_TOKEN_COOKIE_NAME};
|
||||
use rocket::http::{Cookie, CookieJar};
|
||||
use rocket::{get, post};
|
||||
use settings::Settings;
|
||||
|
|
@ -124,8 +125,8 @@ async fn logout(
|
|||
.ok_or_else(|| Error::not_found("Unknown refresh token"))?;
|
||||
|
||||
// Delete cookies
|
||||
cookie_jar.remove(Cookie::named("access_token"));
|
||||
cookie_jar.remove(Cookie::named("refresh_token"));
|
||||
cookie_jar.remove(Cookie::named(JWT_COOKIE_NAME));
|
||||
cookie_jar.remove(Cookie::named(REFRESH_TOKEN_COOKIE_NAME));
|
||||
|
||||
// If refresh token has already been used
|
||||
if refresh_token.has_been_used() {
|
||||
|
|
|
|||
79
crates/ezidam/src/tokens.rs
Normal file
79
crates/ezidam/src/tokens.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
use hash::SecretString;
|
||||
use id::UserID;
|
||||
use jwt::{JwtClaims, PrivateKey};
|
||||
use refresh_tokens::RefreshToken;
|
||||
use rocket::http::{Cookie, CookieJar, SameSite};
|
||||
use rocket::time::Duration;
|
||||
use rocket::tokio::task;
|
||||
use rocket_db_pools::sqlx::SqliteExecutor;
|
||||
use std::net::IpAddr;
|
||||
use users::User;
|
||||
|
||||
pub const REFRESH_TOKEN_DURATION_DAYS: i64 = 21;
|
||||
pub const REFRESH_TOKEN_COOKIE_NAME: &str = "refresh_token";
|
||||
|
||||
pub async fn generate_refresh_token(
|
||||
conn: impl SqliteExecutor<'_>,
|
||||
ip_address: IpAddr,
|
||||
user_id: &UserID,
|
||||
cookie_jar: &CookieJar<'_>,
|
||||
) -> Result<(), String> {
|
||||
// Generate refresh token
|
||||
let refresh_token = task::spawn_blocking(|| SecretString::new(64))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Insert refresh token in database
|
||||
RefreshToken::insert(
|
||||
conn,
|
||||
refresh_token.as_ref(),
|
||||
ip_address.to_string(),
|
||||
user_id,
|
||||
REFRESH_TOKEN_DURATION_DAYS,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Add refresh token as a cookie
|
||||
let mut cookie = Cookie::new(
|
||||
REFRESH_TOKEN_COOKIE_NAME,
|
||||
refresh_token.as_ref().to_string(),
|
||||
);
|
||||
cookie.set_secure(true);
|
||||
cookie.set_http_only(true);
|
||||
cookie.set_same_site(SameSite::Strict);
|
||||
cookie.set_max_age(Duration::days(REFRESH_TOKEN_DURATION_DAYS));
|
||||
cookie_jar.add(cookie);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const JWT_DURATION_MINUTES: i64 = 15;
|
||||
pub const JWT_COOKIE_NAME: &str = "access_token";
|
||||
|
||||
pub async fn generate_jwt(
|
||||
conn: impl SqliteExecutor<'_>,
|
||||
private_key: &PrivateKey,
|
||||
issuer: &str,
|
||||
audience: &str,
|
||||
user: &User,
|
||||
cookie_jar: &CookieJar<'_>,
|
||||
) -> Result<(), String> {
|
||||
// TODO: get user roles
|
||||
let roles = vec![];
|
||||
|
||||
// Create jwt, sign and serialize
|
||||
let jwt = JwtClaims::new(issuer, audience, user, roles)
|
||||
.sign_serialize(private_key, JWT_DURATION_MINUTES)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Add jwt as a cookie
|
||||
let mut cookie = Cookie::new(JWT_COOKIE_NAME, jwt);
|
||||
cookie.set_secure(true);
|
||||
cookie.set_http_only(true);
|
||||
cookie.set_same_site(SameSite::Strict);
|
||||
cookie.set_max_age(Duration::minutes(JWT_DURATION_MINUTES));
|
||||
cookie_jar.add(cookie);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -19,6 +19,13 @@ pub async fn save_new_keys(
|
|||
.await?)
|
||||
}
|
||||
|
||||
pub async fn revoke_all_except_one(
|
||||
conn: impl SqliteExecutor<'_>,
|
||||
exception: &KeyID,
|
||||
) -> Result<Option<()>, Error> {
|
||||
Ok(DatabaseKeys::revoke_all_except_one(conn, &exception.0).await?)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Key {
|
||||
id: KeyID,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ impl RefreshToken {
|
|||
Ok(DatabaseRefreshTokens::revoke(conn, &self.token).await?)
|
||||
}
|
||||
|
||||
pub async fn revoke_all(conn: impl SqliteExecutor<'_>) -> Result<Option<()>, Error> {
|
||||
Ok(DatabaseRefreshTokens::revoke_all(conn).await?)
|
||||
}
|
||||
|
||||
pub async fn revoke_all_for_user(
|
||||
conn: impl SqliteExecutor<'_>,
|
||||
user: &UserID,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue