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"
|
"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": [],
|
||||||
|
|
|
||||||
|
|
@ -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(()))
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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?;
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
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?)
|
.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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue