From 827bba041a4b736320286f2f9c3d575adca7b06c Mon Sep 17 00:00:00 2001 From: Philippe Loctaux Date: Sat, 18 Mar 2023 00:40:11 +0100 Subject: [PATCH] ezidam: oauth: redirect: get and check code, get user info, mark code as used, display html template --- crates/authorization_codes/src/database.rs | 17 ++- crates/authorization_codes/src/lib.rs | 11 +- .../queries/authorization_codes/get_one.sql | 9 ++ .../queries/authorization_codes/use_code.sql | 5 + .../users/get_one_from_authorization_code.sql | 16 +++ crates/database/sqlx-data.json | 136 ++++++++++++++++++ .../src/tables/authorization_codes.rs | 17 +++ crates/database/src/tables/users.rs | 14 ++ crates/ezidam/src/page.rs | 10 +- crates/ezidam/src/routes/oauth.rs | 9 ++ crates/ezidam/src/routes/oauth/authorize.rs | 4 +- crates/ezidam/src/routes/oauth/redirect.rs | 56 +++++++- .../templates/pages/oauth/redirect.html.tera | 10 +- crates/users/src/database.rs | 9 ++ crates/users/src/lib.rs | 6 + 15 files changed, 310 insertions(+), 19 deletions(-) create mode 100644 crates/database/queries/authorization_codes/get_one.sql create mode 100644 crates/database/queries/authorization_codes/use_code.sql create mode 100644 crates/database/queries/users/get_one_from_authorization_code.sql diff --git a/crates/authorization_codes/src/database.rs b/crates/authorization_codes/src/database.rs index 836ef0f..0a86cd2 100644 --- a/crates/authorization_codes/src/database.rs +++ b/crates/authorization_codes/src/database.rs @@ -1,11 +1,11 @@ use crate::error::Error; -use crate::AuthorizationCodes; +use crate::AuthorizationCode; use chrono::{Duration, Utc}; use database::sqlx::SqliteExecutor; use database::AuthorizationCodes as DatabaseAuthorizationCodes; use id::{AppID, UserID}; -impl From for AuthorizationCodes { +impl From for AuthorizationCode { fn from(db: DatabaseAuthorizationCodes) -> Self { Self { // Info @@ -21,7 +21,7 @@ impl From for AuthorizationCodes { } } -impl AuthorizationCodes { +impl AuthorizationCode { pub async fn insert( conn: impl SqliteExecutor<'_>, code: &str, @@ -39,4 +39,15 @@ impl AuthorizationCodes { ) .await?) } + + pub async fn get_one(conn: impl SqliteExecutor<'_>, code: &str) -> Result, Error> { + Ok(DatabaseAuthorizationCodes::get_one(conn, code) + .await? + .map(Self::from)) + } + + /// Consume AuthorizationCode, mark as used + pub async fn use_code(self, conn: impl SqliteExecutor<'_>) -> Result, Error> { + Ok(DatabaseAuthorizationCodes::use_code(conn, &self.code).await?) + } } diff --git a/crates/authorization_codes/src/lib.rs b/crates/authorization_codes/src/lib.rs index 1ea677e..361c545 100644 --- a/crates/authorization_codes/src/lib.rs +++ b/crates/authorization_codes/src/lib.rs @@ -7,7 +7,7 @@ use id::{AppID, UserID}; pub use crate::error::Error; #[derive(Debug)] -pub struct AuthorizationCodes { +pub struct AuthorizationCode { // Info code: String, app: AppID, @@ -18,3 +18,12 @@ pub struct AuthorizationCodes { expires_at: DateTime, used_at: Option>, } + +impl AuthorizationCode { + pub fn has_been_used(&self) -> bool { + self.used_at.is_some() + } + pub fn has_expired(&self) -> bool { + self.expires_at < Utc::now() + } +} diff --git a/crates/database/queries/authorization_codes/get_one.sql b/crates/database/queries/authorization_codes/get_one.sql new file mode 100644 index 0000000..6e1cb14 --- /dev/null +++ b/crates/database/queries/authorization_codes/get_one.sql @@ -0,0 +1,9 @@ +select code, + app, + user, + created_at as "created_at: DateTime", + expires_at as "expires_at: DateTime", + used_at as "used_at: DateTime" +from authorization_codes + +where code is (?) diff --git a/crates/database/queries/authorization_codes/use_code.sql b/crates/database/queries/authorization_codes/use_code.sql new file mode 100644 index 0000000..46d787d --- /dev/null +++ b/crates/database/queries/authorization_codes/use_code.sql @@ -0,0 +1,5 @@ +update authorization_codes + +set used_at = CURRENT_TIMESTAMP + +where code is ? \ No newline at end of file diff --git a/crates/database/queries/users/get_one_from_authorization_code.sql b/crates/database/queries/users/get_one_from_authorization_code.sql new file mode 100644 index 0000000..4c12823 --- /dev/null +++ b/crates/database/queries/users/get_one_from_authorization_code.sql @@ -0,0 +1,16 @@ +select u.id, + u.created_at as "created_at: DateTime", + u.updated_at as "updated_at: DateTime", + u.is_admin as "is_admin: bool", + u.username, + u.name, + u.email, + u.password, + u.password_recover, + u.paper_key, + u.is_archived as "is_archived: bool" +from users u + + inner join authorization_codes ac on u.id = ac.user + +where ac.code is ? \ No newline at end of file diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json index fdcc0f3..b7d3c13 100644 --- a/crates/database/sqlx-data.json +++ b/crates/database/sqlx-data.json @@ -292,6 +292,16 @@ }, "query": "select id,\n created_at as \"created_at: DateTime\",\n revoked_at as \"revoked_at: DateTime\",\n private_der,\n public_der\n\nfrom keys\nwhere revoked_at is null\norder by created_at desc\nlimit 1\n" }, + "7f26b73408318040f94fb6574d5cc25482cef1a57ba4c467fa0bc0fdf25bf39c": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 1 + } + }, + "query": "update authorization_codes\n\nset used_at = CURRENT_TIMESTAMP\n\nwhere code is ?" + }, "87906834faa6f185aee0e4d893b9754908b1c173e9dce383663d723891a89cd1": { "describe": { "columns": [], @@ -478,6 +488,54 @@ }, "query": "select u.id,\n u.created_at as \"created_at: DateTime\",\n u.updated_at as \"updated_at: DateTime\",\n u.is_admin as \"is_admin: bool\",\n u.username,\n u.name,\n u.email,\n u.password,\n u.password_recover,\n u.paper_key,\n u.is_archived as \"is_archived: bool\"\nfrom users u\n\n inner join settings s on u.id = s.first_admin\n\nwhere u.is_admin is 1\n and u.is_archived is 0\n and u.id is s.first_admin\n\nlimit 1" }, + "cf624c4e122477228e3bab09f7cd0dedf4776f73e7a86f19e06772a0adf83406": { + "describe": { + "columns": [ + { + "name": "code", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "app", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "user", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "expires_at: DateTime", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "used_at: DateTime", + "ordinal": 5, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + true + ], + "parameters": { + "Right": 1 + } + }, + "query": "select code,\n app,\n user,\n created_at as \"created_at: DateTime\",\n expires_at as \"expires_at: DateTime\",\n used_at as \"used_at: DateTime\"\nfrom authorization_codes\n\nwhere code is (?)\n" + }, "d166553746afb2d3eaa1ddcb9986b7b9723258f4051bce8287038e3dd1ac928a": { "describe": { "columns": [ @@ -737,5 +795,83 @@ } }, "query": "insert into keys (id, private_der, public_der)\nvalues (?, ?, ?)\n" + }, + "f745e4df7b92e295f31f95b17563fd67684736b61adb37289fdcd34114b12d12": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "created_at: DateTime", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "updated_at: DateTime", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "is_admin: bool", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "username", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "name", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "email", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "password", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "password_recover", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "paper_key", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "is_archived: bool", + "ordinal": 10, + "type_info": "Int64" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "select u.id,\n u.created_at as \"created_at: DateTime\",\n u.updated_at as \"updated_at: DateTime\",\n u.is_admin as \"is_admin: bool\",\n u.username,\n u.name,\n u.email,\n u.password,\n u.password_recover,\n u.paper_key,\n u.is_archived as \"is_archived: bool\"\nfrom users u\n\n inner join authorization_codes ac on u.id = ac.user\n\nwhere ac.code is ?" } } \ No newline at end of file diff --git a/crates/database/src/tables/authorization_codes.rs b/crates/database/src/tables/authorization_codes.rs index f6f2922..3e69a73 100644 --- a/crates/database/src/tables/authorization_codes.rs +++ b/crates/database/src/tables/authorization_codes.rs @@ -37,4 +37,21 @@ impl AuthorizationCodes { Ok((query.rows_affected() == 1).then_some(())) } + + pub async fn get_one(conn: impl SqliteExecutor<'_>, code: &str) -> Result, Error> { + sqlx::query_file_as!(Self, "queries/authorization_codes/get_one.sql", code) + .fetch_optional(conn) + .await + .map_err(handle_error) + } + + pub async fn use_code(conn: impl SqliteExecutor<'_>, code: &str) -> Result, Error> { + let query: SqliteQueryResult = + sqlx::query_file!("queries/authorization_codes/use_code.sql", code) + .execute(conn) + .await + .map_err(handle_error)?; + + Ok((query.rows_affected() == 1).then_some(())) + } } diff --git a/crates/database/src/tables/users.rs b/crates/database/src/tables/users.rs index 77a3dd9..cc334e6 100644 --- a/crates/database/src/tables/users.rs +++ b/crates/database/src/tables/users.rs @@ -71,4 +71,18 @@ impl Users { .await .map_err(handle_error) } + + pub async fn get_one_from_authorization_code( + conn: impl SqliteExecutor<'_>, + code: &str, + ) -> Result, Error> { + sqlx::query_file_as!( + Self, + "queries/users/get_one_from_authorization_code.sql", + code + ) + .fetch_optional(conn) + .await + .map_err(handle_error) + } } diff --git a/crates/ezidam/src/page.rs b/crates/ezidam/src/page.rs index 92d6a0a..5c6b9be 100644 --- a/crates/ezidam/src/page.rs +++ b/crates/ezidam/src/page.rs @@ -14,7 +14,7 @@ pub enum Page { Setup, Homepage(Homepage), Authorize(Authorize), - Redirect, + Redirect(Redirect), } impl Page { @@ -25,7 +25,7 @@ impl Page { Page::Setup => "pages/setup", Page::Homepage(_) => "pages/homepage", Page::Authorize(_) => "pages/oauth/authorize", - Page::Redirect => "pages/oauth/redirect", + Page::Redirect(_) => "pages/oauth/redirect", } } @@ -36,7 +36,7 @@ impl Page { Page::Setup => "Setup", Page::Homepage(_) => "Home", Page::Authorize(_) => "Authorize app", - Page::Redirect => "Redirecting", + Page::Redirect(_) => "Redirecting", } } @@ -47,7 +47,7 @@ impl Page { Page::Setup => None, Page::Homepage(_) => Some(Item::Home.into()), Page::Authorize(_) => None, - Page::Redirect => None, + Page::Redirect(_) => None, } } @@ -58,7 +58,7 @@ impl Page { Page::Setup => Box::new(()), Page::Homepage(homepage) => Box::new(homepage), Page::Authorize(authorize) => Box::new(authorize), - Page::Redirect => Box::new(()), + Page::Redirect(redirect) => Box::new(redirect), } } } diff --git a/crates/ezidam/src/routes/oauth.rs b/crates/ezidam/src/routes/oauth.rs index 991c038..1dac486 100644 --- a/crates/ezidam/src/routes/oauth.rs +++ b/crates/ezidam/src/routes/oauth.rs @@ -24,4 +24,13 @@ pub mod content { pub app_name: String, pub business_name: String, } + + #[derive(Serialize)] + #[serde(crate = "rocket::serde")] + #[derive(Clone)] + pub struct Redirect { + pub user_id: String, + pub name: Option, + pub username: String, + } } diff --git a/crates/ezidam/src/routes/oauth/authorize.rs b/crates/ezidam/src/routes/oauth/authorize.rs index 04d958f..171be92 100644 --- a/crates/ezidam/src/routes/oauth/authorize.rs +++ b/crates/ezidam/src/routes/oauth/authorize.rs @@ -1,6 +1,6 @@ use crate::routes::prelude::*; use apps::App; -use authorization_codes::AuthorizationCodes; +use authorization_codes::AuthorizationCode; use hash::SecretString; use rocket::{get, post}; use settings::Settings; @@ -142,7 +142,7 @@ pub async fn authorize_form( // Save authorization code let mut transaction = db.begin().await?; - AuthorizationCodes::insert(&mut transaction, code.as_ref(), app.id(), user.id()).await?; + AuthorizationCode::insert(&mut transaction, code.as_ref(), app.id(), user.id()).await?; transaction.commit().await?; // Construct uri to redirect to diff --git a/crates/ezidam/src/routes/oauth/redirect.rs b/crates/ezidam/src/routes/oauth/redirect.rs index 9b00fe3..7da499e 100644 --- a/crates/ezidam/src/routes/oauth/redirect.rs +++ b/crates/ezidam/src/routes/oauth/redirect.rs @@ -1,9 +1,53 @@ use crate::routes::prelude::*; -use rocket::get; +use authorization_codes::AuthorizationCode; +use rocket::{get, UriDisplayQuery}; +use users::User; -#[get("/oauth/redirect")] -pub async fn redirect_page(mut db: Connection) -> Result { - // TODO: make request to oauth token - - Ok(Page::Redirect) +#[derive(Debug, FromForm, UriDisplayQuery)] +pub struct RedirectRequest<'r> { + pub code: &'r str, + pub state: &'r str, +} + +#[get("/oauth/redirect?")] +pub async fn redirect_page( + mut db: Connection, + redirect_request: RedirectRequest<'_>, +) -> Result { + let mut transaction = db.begin().await?; + + // Get authorization code + let code = AuthorizationCode::get_one(&mut transaction, redirect_request.code) + .await? + .ok_or_else(|| Error::not_found("Could not find authorization code"))?; + + // Make sure code has not been used + if code.has_been_used() { + // TODO: revoke all codes and refresh tokens for user + return Err(Error::bad_request( + "Authorization code has already been used", + )); + } + + // Make sure it has not expired + if code.has_expired() { + return Err(Error::bad_request("Authorization code has expired")); + } + + // Get user info + let user = User::get_one_from_authorization_code(&mut transaction, redirect_request.code) + .await? + .ok_or_else(|| Error::not_found("Could not find user"))?; + + // Mark code as used + code.use_code(&mut transaction).await?; + + transaction.commit().await?; + + // HTTP Response + Ok(Page::Redirect(super::content::Redirect { + user_id: user.id().to_string(), + name: user.name().map(String::from), + username: user.username().to_string(), + })) } diff --git a/crates/ezidam/templates/pages/oauth/redirect.html.tera b/crates/ezidam/templates/pages/oauth/redirect.html.tera index d1e7fd1..a50d207 100644 --- a/crates/ezidam/templates/pages/oauth/redirect.html.tera +++ b/crates/ezidam/templates/pages/oauth/redirect.html.tera @@ -18,8 +18,14 @@
-

Fullname or username

+ style="background-image: url(/avatar/{{ user_id }}?size=250)"> +

+ {% if name %} + {{ name }} + {% else %} + {{ username }} + {% endif %} +

diff --git a/crates/users/src/database.rs b/crates/users/src/database.rs index e25695c..929573a 100644 --- a/crates/users/src/database.rs +++ b/crates/users/src/database.rs @@ -87,4 +87,13 @@ impl User { // Get user from username Self::get_by_username(conn, login).await } + + pub async fn get_one_from_authorization_code( + conn: impl SqliteExecutor<'_>, + code: &str, + ) -> Result, Error> { + Ok(DatabaseUsers::get_one_from_authorization_code(conn, code) + .await? + .map(Self::from)) + } } diff --git a/crates/users/src/lib.rs b/crates/users/src/lib.rs index 52ac11c..1ad11ec 100644 --- a/crates/users/src/lib.rs +++ b/crates/users/src/lib.rs @@ -31,4 +31,10 @@ impl User { pub fn password_hashed(&self) -> Option<&str> { self.password.as_deref() } + pub fn username(&self) -> &str { + &self.username + } + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } }