From ef8d75eceeebaa82b7de9d82b3b6bedec78c6b03 Mon Sep 17 00:00:00 2001 From: Philippe Loctaux Date: Sat, 18 Mar 2023 15:16:15 +0100 Subject: [PATCH] ezidam + refresh tokens: create and insert refresh token --- Cargo.lock | 21 +++++++++ .../20230318135414_refresh_tokens.down.sql | 1 + .../20230318135414_refresh_tokens.up.sql | 13 ++++++ .../queries/refresh_tokens/insert.sql | 2 + crates/database/sqlx-data.json | 10 +++++ crates/database/src/tables.rs | 2 + crates/database/src/tables/refresh_tokens.rs | 41 ++++++++++++++++++ crates/ezidam/Cargo.toml | 2 + crates/ezidam/src/error/conversion.rs | 6 +++ crates/ezidam/src/routes/oauth/redirect.rs | 22 ++++++++++ crates/refresh_tokens/Cargo.toml | 12 ++++++ crates/refresh_tokens/src/database.rs | 43 +++++++++++++++++++ crates/refresh_tokens/src/error.rs | 8 ++++ crates/refresh_tokens/src/lib.rs | 21 +++++++++ 14 files changed, 204 insertions(+) create mode 100644 crates/database/migrations/20230318135414_refresh_tokens.down.sql create mode 100644 crates/database/migrations/20230318135414_refresh_tokens.up.sql create mode 100644 crates/database/queries/refresh_tokens/insert.sql create mode 100644 crates/database/src/tables/refresh_tokens.rs create mode 100644 crates/refresh_tokens/Cargo.toml create mode 100644 crates/refresh_tokens/src/database.rs create mode 100644 crates/refresh_tokens/src/error.rs create mode 100644 crates/refresh_tokens/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5fa1c30..d84d121 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -803,7 +803,9 @@ dependencies = [ "infer", "jwt", "openid", + "refresh_tokens", "rocket", + "rocket-client-addr", "rocket_db_pools", "rocket_dyn_templates", "settings", @@ -2447,6 +2449,16 @@ dependencies = [ "syn", ] +[[package]] +name = "refresh_tokens" +version = "0.0.0" +dependencies = [ + "chrono", + "database", + "id", + "thiserror", +] + [[package]] name = "regex" version = "1.7.1" @@ -2538,6 +2550,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "rocket-client-addr" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb54a6e3dd696465c8a47fa61603b49e28cf475133a3d082902c2d4eaae9df1" +dependencies = [ + "rocket", +] + [[package]] name = "rocket_codegen" version = "0.5.0-rc.2" diff --git a/crates/database/migrations/20230318135414_refresh_tokens.down.sql b/crates/database/migrations/20230318135414_refresh_tokens.down.sql new file mode 100644 index 0000000..c840649 --- /dev/null +++ b/crates/database/migrations/20230318135414_refresh_tokens.down.sql @@ -0,0 +1 @@ +drop table if exists refresh_tokens; diff --git a/crates/database/migrations/20230318135414_refresh_tokens.up.sql b/crates/database/migrations/20230318135414_refresh_tokens.up.sql new file mode 100644 index 0000000..0a897a2 --- /dev/null +++ b/crates/database/migrations/20230318135414_refresh_tokens.up.sql @@ -0,0 +1,13 @@ +create table if not exists refresh_tokens +( + -- info + token TEXT not null primary key, + ip_address TEXT not null, + user TEXT not null references users (id), + + -- timings + created_at TEXT not null default CURRENT_TIMESTAMP, + expires_at TEXT not null, + used_at TEXT, + revoked_at TEXT +); diff --git a/crates/database/queries/refresh_tokens/insert.sql b/crates/database/queries/refresh_tokens/insert.sql new file mode 100644 index 0000000..ee496a9 --- /dev/null +++ b/crates/database/queries/refresh_tokens/insert.sql @@ -0,0 +1,2 @@ +insert into refresh_tokens (token, ip_address, user, expires_at) +values (?, ?, ?, datetime(?, 'unixepoch')) diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json index b7d3c13..431b660 100644 --- a/crates/database/sqlx-data.json +++ b/crates/database/sqlx-data.json @@ -322,6 +322,16 @@ }, "query": "insert into authorization_codes (code, app, user, expires_at)\nvalues (?, ?, ?, datetime(?, 'unixepoch'))\n" }, + "aa88eb27d38ba4cfb539e4b4d7a86770c24221109e8fcc188a7d38f41e674817": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 4 + } + }, + "query": "insert into refresh_tokens (token, ip_address, user, expires_at)\nvalues (?, ?, ?, datetime(?, 'unixepoch'))\n" + }, "aae93a39c5a9f46235b5ef871b45ba76d7efa1677bfe8291a62b8cbf9cd9e0d5": { "describe": { "columns": [], diff --git a/crates/database/src/tables.rs b/crates/database/src/tables.rs index 63d7382..d52ac8a 100644 --- a/crates/database/src/tables.rs +++ b/crates/database/src/tables.rs @@ -1,11 +1,13 @@ mod apps; mod authorization_codes; mod keys; +mod refresh_tokens; mod settings; mod users; pub use apps::Apps; pub use authorization_codes::AuthorizationCodes; pub use keys::Keys; +pub use refresh_tokens::RefreshTokens; pub use settings::Settings; pub use users::Users; diff --git a/crates/database/src/tables/refresh_tokens.rs b/crates/database/src/tables/refresh_tokens.rs new file mode 100644 index 0000000..53f240c --- /dev/null +++ b/crates/database/src/tables/refresh_tokens.rs @@ -0,0 +1,41 @@ +use crate::error::{handle_error, Error}; +use sqlx::sqlite::SqliteQueryResult; +use sqlx::types::chrono::{DateTime, Utc}; +use sqlx::{FromRow, SqliteExecutor}; + +#[derive(FromRow)] +pub struct RefreshTokens { + // Info + pub token: String, + pub ip_address: String, + pub user: String, + + // Timings + pub created_at: DateTime, + pub expires_at: DateTime, + pub used_at: Option>, + pub revoked_at: Option>, +} + +impl RefreshTokens { + pub async fn insert( + conn: impl SqliteExecutor<'_>, + token: &str, + ip_address: &str, + user: &str, + expires_at: i64, + ) -> Result, Error> { + let query: SqliteQueryResult = sqlx::query_file!( + "queries/refresh_tokens/insert.sql", + token, + ip_address, + user, + expires_at + ) + .execute(conn) + .await + .map_err(handle_error)?; + + Ok((query.rows_affected() == 1).then_some(())) + } +} diff --git a/crates/ezidam/Cargo.toml b/crates/ezidam/Cargo.toml index 5c3f596..66bde84 100644 --- a/crates/ezidam/Cargo.toml +++ b/crates/ezidam/Cargo.toml @@ -12,6 +12,7 @@ erased-serde = "0.3" url = { workspace = true } identicon-rs = "4.0.1" futures = "0.3.26" +rocket-client-addr = "0.5.2" # local crates database_pool = { path = "../database_pool" } @@ -23,3 +24,4 @@ openid = { path = "../openid" } jwt = { path = "../jwt" } apps = { path = "../apps" } authorization_codes = { path = "../authorization_codes" } +refresh_tokens = { path = "../refresh_tokens" } diff --git a/crates/ezidam/src/error/conversion.rs b/crates/ezidam/src/error/conversion.rs index 7b671b3..2b5e450 100644 --- a/crates/ezidam/src/error/conversion.rs +++ b/crates/ezidam/src/error/conversion.rs @@ -74,3 +74,9 @@ impl From for Error { Error::internal_server_error(e) } } + +impl From for Error { + fn from(e: refresh_tokens::Error) -> Self { + Error::internal_server_error(e) + } +} diff --git a/crates/ezidam/src/routes/oauth/redirect.rs b/crates/ezidam/src/routes/oauth/redirect.rs index aaae0c2..5b82a37 100644 --- a/crates/ezidam/src/routes/oauth/redirect.rs +++ b/crates/ezidam/src/routes/oauth/redirect.rs @@ -1,6 +1,9 @@ use crate::routes::prelude::*; use authorization_codes::AuthorizationCode; +use hash::SecretString; +use refresh_tokens::RefreshToken; use rocket::{get, UriDisplayQuery}; +use rocket_client_addr::ClientRealAddr; use settings::Settings; use users::User; @@ -14,6 +17,7 @@ pub struct RedirectRequest<'r> { pub async fn redirect_page( mut db: Connection, redirect_request: RedirectRequest<'_>, + ip_address: &ClientRealAddr, ) -> Result { let mut transaction = db.begin().await?; @@ -55,6 +59,24 @@ pub async fn redirect_page( .map(String::from) .ok_or_else(|| Error::bad_request("Server url is not set"))?; + // TODO: refactor for "code" route + + // Generate refresh token + let refresh_token = task::spawn_blocking(|| SecretString::new(64)).await?; + + // Insert refresh token in database + RefreshToken::insert( + &mut transaction, + refresh_token.as_ref(), + ip_address.get_ipv6_string().as_str(), + user.id(), + ) + .await?; + + // TODO: generate access token + + // TODO: store tokens in secure, http only cookies + transaction.commit().await?; // HTTP Response diff --git a/crates/refresh_tokens/Cargo.toml b/crates/refresh_tokens/Cargo.toml new file mode 100644 index 0000000..ad9949e --- /dev/null +++ b/crates/refresh_tokens/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "refresh_tokens" +version = "0.0.0" +edition = "2021" + +[dependencies] +thiserror = { workspace = true } +chrono = { workspace = true } + +# local crates +database = { path = "../database" } +id = { path = "../id" } diff --git a/crates/refresh_tokens/src/database.rs b/crates/refresh_tokens/src/database.rs new file mode 100644 index 0000000..d2ca171 --- /dev/null +++ b/crates/refresh_tokens/src/database.rs @@ -0,0 +1,43 @@ +use crate::error::Error; +use crate::RefreshToken; +use chrono::{Duration, Utc}; +use database::sqlx::SqliteExecutor; +use database::RefreshTokens as DatabaseRefreshTokens; +use id::UserID; + +impl From for RefreshToken { + fn from(db: DatabaseRefreshTokens) -> Self { + Self { + // Info + token: db.token, + ip_address: db.ip_address, + user: UserID(db.user), + + // Timings + created_at: db.created_at, + expires_at: db.expires_at, + used_at: db.used_at, + revoked_at: db.revoked_at, + } + } +} + +impl RefreshToken { + pub async fn insert( + conn: impl SqliteExecutor<'_>, + token: &str, + ip_address: &str, + user: &UserID, + ) -> Result, Error> { + let expires_at = Utc::now() + Duration::days(21); + + Ok(DatabaseRefreshTokens::insert( + conn, + token, + ip_address, + user.as_ref(), + expires_at.timestamp(), + ) + .await?) + } +} diff --git a/crates/refresh_tokens/src/error.rs b/crates/refresh_tokens/src/error.rs new file mode 100644 index 0000000..47def78 --- /dev/null +++ b/crates/refresh_tokens/src/error.rs @@ -0,0 +1,8 @@ +// error +#[derive(thiserror::Error)] +// the rest +#[derive(Debug)] +pub enum Error { + #[error("Database: {0}")] + Database(#[from] database::Error), +} diff --git a/crates/refresh_tokens/src/lib.rs b/crates/refresh_tokens/src/lib.rs new file mode 100644 index 0000000..6421dff --- /dev/null +++ b/crates/refresh_tokens/src/lib.rs @@ -0,0 +1,21 @@ +mod database; +mod error; + +use chrono::{DateTime, Utc}; +use id::UserID; + +pub use crate::error::Error; + +#[derive(Debug)] +pub struct RefreshToken { + // Info + token: String, + ip_address: String, + user: UserID, + + // Timings + created_at: DateTime, + expires_at: DateTime, + used_at: Option>, + revoked_at: Option>, +}