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" "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": { "7f26b73408318040f94fb6574d5cc25482cef1a57ba4c467fa0bc0fdf25bf39c": {
"describe": { "describe": {
"columns": [], "columns": [],
@ -358,6 +368,16 @@
}, },
"query": "update settings\n\nset url = ?\n\nwhere id is 0\n" "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": { "a55b17a3a70e6445517f19536220f0dafc78a0e8b69221dee4715f84841839da": {
"describe": { "describe": {
"columns": [], "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(())) 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( pub async fn revoke_all_for_user(
conn: impl SqliteExecutor<'_>, conn: impl SqliteExecutor<'_>,
user: &str, user: &str,

View file

@ -10,6 +10,9 @@ mod admin;
mod user; mod user;
use crate::guards::refresh_token::get_refresh_token_from_cookie; 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; pub use admin::JwtAdmin;
use id::KeyID; use id::KeyID;
pub use user::JwtUser; pub use user::JwtUser;
@ -50,7 +53,7 @@ pub enum Error {
pub(super) fn get_access_token_from_cookie(request: &Request) -> Option<String> { pub(super) fn get_access_token_from_cookie(request: &Request) -> Option<String> {
request request
.cookies() .cookies()
.get("access_token") .get(JWT_COOKIE_NAME)
.map(|cookie| cookie.value().to_string()) .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 // Attempt to get ip address
let ip_address = match request.client_ip() { let ip_address = match request.client_ip() {
Some(ip) => ip.to_string(), Some(ip) => ip.to_string(),
@ -278,7 +278,7 @@ pub async fn use_refresh_token(
new_refresh_token.as_ref(), new_refresh_token.as_ref(),
ip_address, ip_address,
user.id(), user.id(),
refresh_token_duration, REFRESH_TOKEN_DURATION_DAYS,
) )
.await .await
{ {
@ -286,11 +286,14 @@ pub async fn use_refresh_token(
} }
// Add refresh token as a cookie // 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_secure(true);
cookie.set_http_only(true); cookie.set_http_only(true);
cookie.set_same_site(SameSite::Strict); 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); cookie_jar.add(cookie);
// Get latest key from database // Get latest key from database
@ -336,14 +339,11 @@ pub async fn use_refresh_token(
// TODO: get user roles // TODO: get user roles
let roles = vec![]; let roles = vec![];
// Access token duration in minutes
let access_token_duration = 1;
// Create jwt, sign and serialize // Create jwt, sign and serialize
let jwt_claims = JwtClaims::new(home_page.clone(), "ezidam", &user, roles); let jwt_claims = JwtClaims::new(home_page.clone(), "ezidam", &user, roles);
let jwt = match jwt_claims let jwt = match jwt_claims
.clone() .clone()
.sign_serialize(&private_key, access_token_duration) .sign_serialize(&private_key, JWT_DURATION_MINUTES)
{ {
Ok(jwt) => jwt, Ok(jwt) => jwt,
Err(e) => { Err(e) => {
@ -352,11 +352,11 @@ pub async fn use_refresh_token(
}; };
// Add jwt as a cookie // 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_secure(true);
cookie.set_http_only(true); cookie.set_http_only(true);
cookie.set_same_site(SameSite::Strict); 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); cookie_jar.add(cookie);
if let Err(_e) = transaction.commit().await { 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::{FromRequest, Outcome};
use rocket::Request; use rocket::Request;
pub struct RefreshToken(pub String); pub struct RefreshToken(pub String);
pub fn get_refresh_token_from_cookie(request: &Request) -> Option<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) => { Some(cookie) => {
let value = cookie.value(); let value = cookie.value();

View file

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

View file

@ -1,12 +1,11 @@
use crate::routes::prelude::*; use crate::routes::prelude::*;
use crate::tokens::{generate_jwt, generate_refresh_token};
use apps::App; use apps::App;
use authorization_codes::AuthorizationCode; use authorization_codes::AuthorizationCode;
use hash::SecretString;
use jwt::database::Key; use jwt::database::Key;
use jwt::{JwtClaims, PrivateKey}; use jwt::PrivateKey;
use refresh_tokens::RefreshToken; use refresh_tokens::RefreshToken;
use rocket::http::{Cookie, CookieJar, SameSite}; use rocket::http::CookieJar;
use rocket::time::Duration;
use rocket::{get, UriDisplayQuery}; use rocket::{get, UriDisplayQuery};
use settings::Settings; use settings::Settings;
use std::net::IpAddr; use std::net::IpAddr;
@ -80,28 +79,9 @@ pub async fn redirect_page(
// TODO: refactor for "code" route // TODO: refactor for "code" route
// Generate refresh token // Generate refresh token
let refresh_token = task::spawn_blocking(|| SecretString::new(64)).await?; generate_refresh_token(&mut transaction, ip_address, user.id(), cookie_jar)
.await
// Refresh token duration in days .map_err(Error::internal_server_error)?;
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);
// Get latest key from database // Get latest key from database
let key = Key::get_most_recent(&mut transaction) 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())) task::spawn_blocking(move || PrivateKey::from_der(key.private_der(), key.key_id()))
.await??; .await??;
// TODO: get user roles // Generate jwt
let roles = vec![]; generate_jwt(
&mut transaction,
// Access token duration in minutes &private_key,
let access_token_duration = 15; &home_page,
&app.id().0,
// Create jwt, sign and serialize &user,
let jwt = JwtClaims::new(home_page.clone(), app.id().as_ref(), &user, roles) cookie_jar,
.sign_serialize(&private_key, access_token_duration)?; )
.await
// Add jwt as a cookie .map_err(Error::internal_server_error)?;
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);
} }
transaction.commit().await?; transaction.commit().await?;

View file

@ -1,4 +1,5 @@
use super::prelude::*; use super::prelude::*;
use crate::tokens::{JWT_COOKIE_NAME, REFRESH_TOKEN_COOKIE_NAME};
use rocket::http::{Cookie, CookieJar}; use rocket::http::{Cookie, CookieJar};
use rocket::{get, post}; use rocket::{get, post};
use settings::Settings; use settings::Settings;
@ -124,8 +125,8 @@ async fn logout(
.ok_or_else(|| Error::not_found("Unknown refresh token"))?; .ok_or_else(|| Error::not_found("Unknown refresh token"))?;
// Delete cookies // Delete cookies
cookie_jar.remove(Cookie::named("access_token")); cookie_jar.remove(Cookie::named(JWT_COOKIE_NAME));
cookie_jar.remove(Cookie::named("refresh_token")); cookie_jar.remove(Cookie::named(REFRESH_TOKEN_COOKIE_NAME));
// If refresh token has already been used // If refresh token has already been used
if refresh_token.has_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?) .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)] #[derive(Debug)]
pub struct Key { pub struct Key {
id: KeyID, id: KeyID,

View file

@ -55,6 +55,10 @@ impl RefreshToken {
Ok(DatabaseRefreshTokens::revoke(conn, &self.token).await?) 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( pub async fn revoke_all_for_user(
conn: impl SqliteExecutor<'_>, conn: impl SqliteExecutor<'_>,
user: &UserID, user: &UserID,