ezidam: refactor jwt and refresh token generate in "tokens" mod

This commit is contained in:
Philippe Loctaux 2023-03-26 19:25:50 +02:00
parent 23b1e3ea4f
commit 9687116063
13 changed files with 179 additions and 59 deletions

View file

@ -0,0 +1,6 @@
update keys
set revoked_at = CURRENT_TIMESTAMP
where revoked_at is null
and id is not (?)

View file

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

View file

@ -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": [],

View file

@ -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(()))
}
}

View file

@ -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,

View file

@ -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 {

View file

@ -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();

View file

@ -12,6 +12,7 @@ mod page;
mod response_timer;
mod routes;
mod shutdown;
mod tokens;
#[cfg(test)]
mod tests;

View file

@ -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?;

View file

@ -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() {

View 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(())
}

View file

@ -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,

View file

@ -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,