From ecf1da29782ef42e17068fc2f9a90b2aa95af2e5 Mon Sep 17 00:00:00 2001 From: Matthieu Bessat Date: Sat, 9 Nov 2024 19:57:15 +0100 Subject: [PATCH] feat(oauth2): authorize form and redirection --- Cargo.lock | 280 ++++++++++++++++++++++++++- Cargo.toml | 4 +- TODO.md | 2 + config.toml | 4 + http_integration_tests/.curl-cookies | 2 +- http_integration_tests/authorize.sh | 2 +- migrations/all.sql | 1 + src/controllers/ui/authorize.rs | 192 ++++++++++++++---- src/models/authorization.rs | 3 + src/models/config.rs | 8 +- src/router.rs | 1 + src/services/mod.rs | 1 + src/services/oauth2.rs | 7 + src/templates/pages/authorize.html | 27 ++- src/utils.rs | 8 + 15 files changed, 480 insertions(+), 62 deletions(-) create mode 100644 src/services/oauth2.rs diff --git a/Cargo.lock b/Cargo.lock index 912cf72..a1d4a20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -606,6 +606,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1063,6 +1074,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1071,12 +1200,23 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1178,6 +1318,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -1255,6 +1401,7 @@ dependencies = [ "log", "minijinja", "minijinja-embed", + "rand", "rand_core", "redis", "serde", @@ -1265,6 +1412,7 @@ dependencies = [ "toml", "totp-rs", "tower-http", + "url", "uuid", ] @@ -2136,6 +2284,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "stringprep" version = "0.1.5" @@ -2206,6 +2360,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "tempfile" version = "3.13.0" @@ -2270,6 +2435,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2536,9 +2711,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", "idna", @@ -2551,6 +2726,18 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2822,6 +3009,42 @@ dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -2843,8 +3066,51 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] diff --git a/Cargo.toml b/Cargo.toml index 52e0240..97d3b58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,6 @@ tower-http = { version = "0.6.1", features = ["fs"] } totp-rs = "5.6" minijinja-embed = "2.3.1" axum-macros = "0.4.2" -rand_core = { version = "0.6.4", features = ["std"] } jwt = "0.16.0" dotenvy = "0.15.7" frank_jwt = "3.1.3" @@ -55,6 +54,9 @@ jsonwebtoken = "9.3.0" axum-extra = { version = "0.9.4", features = ["cookie"] } axum_typed_multipart = "0.13.1" base64 = "0.22.1" +rand = "0.8.5" +rand_core = { version = "0.6.4", features = ["std"] } +url = "2.5.3" [build-dependencies] minijinja-embed = "2.3.1" diff --git a/TODO.md b/TODO.md index 1b5612e..ecb287b 100644 --- a/TODO.md +++ b/TODO.md @@ -9,3 +9,5 @@ - Verify authorize - Select by app client secret - Upload picture + +- [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1 diff --git a/config.toml b/config.toml index dce54a2..a596b4d 100644 --- a/config.toml +++ b/config.toml @@ -5,8 +5,12 @@ logo_uri = "https://example.org/logo.png" [[applications]] slug = "demo_app" name = "Demo app" +description = "A super application where you can do everything you want." client_id = "a1785786-8be1-443c-9a6f-35feed703609" client_secret = "49c6c16a-0a8a-4981-a60d-5cb96582cc1a" +allowed_redirect_uris = [ + "http://localhost:9090/authorize" +] [[roles]] slug = "basic" diff --git a/http_integration_tests/.curl-cookies b/http_integration_tests/.curl-cookies index 5a2fba3..a2cff5b 100644 --- a/http_integration_tests/.curl-cookies +++ b/http_integration_tests/.curl-cookies @@ -2,4 +2,4 @@ # https://curl.se/docs/http-cookies.html # This file was generated by libcurl! Edit at your own risk. -localhost FALSE / FALSE 1731761080 minauth_jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwZGZkOThlYy1mYjc3LTRjMWEtOTk5NS00Njg0Y2Y5NDM2NjYiLCJleHAiOjE3MzEyNDI2ODB9.Qnu8UiryN-NZIMk2-YorCuqY5g0ZJwRdszeBe_Y5S3E +localhost FALSE / FALSE 1731769020 minauth_jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiZDQzM2Y1MS1kOWViLTRlNjAtODM1OC03NTMyYzJjMGY0NmIiLCJleHAiOjE3MzEyNTA2MjB9.KOPkOd-c-UlBZ4JwWT8XeZKCbgNzSM0Hu2udTzb-rIY diff --git a/http_integration_tests/authorize.sh b/http_integration_tests/authorize.sh index a4a6f3d..2a54a1e 100755 --- a/http_integration_tests/authorize.sh +++ b/http_integration_tests/authorize.sh @@ -5,6 +5,6 @@ curl -v http://localhost:8085/authorize \ --cookie ".curl-cookies" \ -d client_id="a1785786-8be1-443c-9a6f-35feed703609" \ -d response_type="code" \ - -d redirect_uri="https://localhost:9090/authorize" \ + -d redirect_uri="http://localhost:9090/authorize" \ -d scope="read_basics" \ -d state="qxYAfk4kf6pbZkms78jM" diff --git a/migrations/all.sql b/migrations/all.sql index f024839..8bc9612 100644 --- a/migrations/all.sql +++ b/migrations/all.sql @@ -21,6 +21,7 @@ CREATE TABLE authorizations ( user_id TEXT NOT NULL, client_id TEXT NOT NULL, scopes TEXT, -- json array of app scope (permissions) + code TEXT, last_used_at DATETIME, created_at DATETIME NOT NULL diff --git a/src/controllers/ui/authorize.rs b/src/controllers/ui/authorize.rs index f68a594..6d44528 100644 --- a/src/controllers/ui/authorize.rs +++ b/src/controllers/ui/authorize.rs @@ -1,14 +1,18 @@ -use axum::{extract::{Query, State}, http::StatusCode, response::{Html, IntoResponse}, Extension, Form}; +use axum::{extract::{Query, State}, http::{HeaderMap, HeaderValue, StatusCode}, response::{Html, IntoResponse}, Extension, Form}; +use chrono::{SecondsFormat, Utc}; use fully_pub::fully_pub; +use log::{debug, error, info}; use minijinja::context; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use url::Url; +use uuid::Uuid; -use crate::{models::authorization::Authorization, renderer::TemplateRenderer, server::AppState, services::session::TokenClaims}; +use crate::{models::authorization::Authorization, renderer::TemplateRenderer, server::AppState, services::{oauth2::verify_redirect_uri, session::TokenClaims}, utils::get_random_alphanumerical}; -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[fully_pub] /// query params described in [RFC6759 section 4.1.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1) -struct AuthorizeQueryParams { +struct AuthorizationParams { response_type: String, client_id: String, scope: String, @@ -17,65 +21,175 @@ struct AuthorizeQueryParams { state: String, } +fn redirect_to_client( + authorization_code: &str, + authorization_params: &AuthorizationParams +) -> impl IntoResponse { + let target_url = format!("{}?code={}&state={}", + authorization_params.redirect_uri, + authorization_code, + authorization_params.state, + ); + debug!("Redirecting to {}", target_url); + + let mut headers = HeaderMap::new(); + headers.insert("Location", HeaderValue::from_str(&target_url).unwrap()); + ( + StatusCode::FOUND, + headers, + Html("Redirecting to client…") + ).into_response() +} + +/// The authorization endpoint is used to interact with the resource owner and obtain an authorization grant. +/// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1 pub async fn authorize_form( State(app_state): State, Extension(token_claims): Extension, Extension(renderer): Extension, - query_params: Query + query_params: Query ) -> impl IntoResponse { - // 1. Verify the app details - let app_res = app_state.config.applications - .iter() - .find(|a| a.client_id == query_params.client_id); + let Query(authorization_params) = query_params; - if app_res.is_none() { + // 1. Verify the app details + let app = match app_state.config.applications + .iter() + .find(|a| a.client_id == authorization_params.client_id) { + Some(app) => app, + None => { + return ( + StatusCode::BAD_REQUEST, + Html("Invalid client_id query params, app not found.") + ).into_response(); + } + }; + + // 1.1. Verify that the app redirect_uri is authorized + if !verify_redirect_uri(app, &authorization_params.redirect_uri) { return ( StatusCode::BAD_REQUEST, - Html("Invalid client_id query params, app not found.") + Html("Unauthorized redirect_uri.") ).into_response(); } + // 1.3. Parse and validate redirect_uri. + // Note: for now, we only support HTTP(s) redirect URLs. + let parsed_redirect_uri: Url = match Url::parse(&authorization_params.redirect_uri) { + Ok(url) => url, + Err(_) => { + return ( + StatusCode::BAD_REQUEST, + Html("Invalid redirect URL. Could not parse as HTTP(S) URL.") + ).into_response(); + } + }; // 2. Check if the app is already authorized - let authorizations_res = sqlx::query_as::<_, Authorization>("SELECT * FROM authorizations WHERE user_id = $1 AND -app_id = $2") + let authorizations_res = sqlx::query_as::<_, Authorization>( + "SELECT * FROM authorizations WHERE user_id = $1 AND client_id = $2" + ) .bind(&token_claims.sub) - .bind(&query_params.client_id) + .bind(&authorization_params.client_id) .fetch_one(&app_state.db) - .await - .expect("To get authorizations"); + .await; + + match authorizations_res { + Ok(existing_authorization) => { + info!("Reusing existing authorization {}", &existing_authorization.id); + + // Update last used timestamp for this authorization + let _result = sqlx::query("UPDATE authorizations SET last_used_at = $2 WHERE id = $1") + .bind(existing_authorization.id) + .bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)) + .execute(&app_state.db) + .await.unwrap(); + + // Authorization already given, just redirect to the app + return redirect_to_client( + &existing_authorization.code, + &authorization_params + ).into_response() + }, + Err(sqlx::Error::RowNotFound) => { + debug!("Authorization not found."); + }, + Err(err) => { + error!("Cannot get existing authorization. {}", err); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Html("Internal server error: Failed to verify conditions.") + ).into_response(); + } + } - dbg!(authorizations_res); - - // 3. Verify scopes - // 4. Show form that POST to authorize - - renderer .render( "pages/authorize", - context!() + context!( + app => app, + authorization_params => authorization_params, + redirect_uri_host => parsed_redirect_uri.host_str() + ) ) .into_response() } -#[derive(Debug, Deserialize)] -#[fully_pub] -struct AuthorizeForm { - /// client_id - client_id: String, - scopes: Vec -} - - pub async fn perform_authorize( State(app_state): State, - Extension(renderer): Extension, - Form(authorize_form): Form + Extension(token_claims): Extension, + Form(authorize_form): Form ) -> impl IntoResponse { - // Save authorization in DB - // 4.1. Create an authorization code - // 4.2. Redirect to the app with a token - (StatusCode::FOUND, Html("Redirecting…")) + // 1. Get the app details + let app = match app_state.config.applications + .iter() + .find(|a| a.client_id == authorize_form.client_id) { + Some(app) => app, + None => { + return ( + StatusCode::BAD_REQUEST, + Html("Invalid client_id, app not found.") + ).into_response(); + } + }; + + // 2. Create an authorizaton code + let authorization_code = get_random_alphanumerical(32); + + let authorization = Authorization { + id: Uuid::new_v4().to_string(), + user_id: token_claims.sub, + client_id: app.client_id.clone(), + scopes: sqlx::types::Json(Vec::new()), + code: authorization_code.clone(), + last_used_at: Some(Utc::now()), + created_at: Utc::now(), + }; + + // 3. Save authorization in DB with state + let res = sqlx::query(" + INSERT INTO authorizations + (id, user_id, client_id, scopes, code, last_used_at, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ") + .bind(authorization.id.clone()) + .bind(authorization.user_id) + .bind(authorization.client_id) + .bind(authorization.scopes) + .bind(authorization.code) + .bind(authorization.last_used_at.map(|x| x.to_rfc3339_opts(SecondsFormat::Millis, true))) + .bind(authorization.created_at.to_rfc3339_opts(SecondsFormat::Millis, true)) + .execute(&app_state.db) + .await; + match res { + Err(err) => { + error!("Failed to save authorization in DB. {}", err); + return (StatusCode::INTERNAL_SERVER_ERROR, Html("Internal server error: Failed to process authorization form.")).into_response(); + }, + _ => {} + } + info!("Created authorization {}", &authorization.id); + + // 4. Redirect to the app with the authorization code and state + redirect_to_client(&authorization_code, &authorize_form).into_response() } diff --git a/src/models/authorization.rs b/src/models/authorization.rs index d89eb94..b4cd9cb 100644 --- a/src/models/authorization.rs +++ b/src/models/authorization.rs @@ -5,6 +5,7 @@ use sqlx::types::Json; #[derive(sqlx::Type, Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(strum_macros::Display)] +#[fully_pub] enum AuthorizationScope { ReadBasics } @@ -19,6 +20,8 @@ struct Authorization { client_id: String, scopes: Json>, + /// defined in https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2 + code: String, last_used_at: Option>, created_at: DateTime } diff --git a/src/models/config.rs b/src/models/config.rs index 183d598..a065ec4 100644 --- a/src/models/config.rs +++ b/src/models/config.rs @@ -1,9 +1,5 @@ -use std::collections::HashMap; - -use chrono::{DateTime, Utc}; use fully_pub::fully_pub; use serde::{Deserialize, Serialize}; -use uuid::Uuid; const fn _default_true() -> bool { true } @@ -20,8 +16,10 @@ struct InstanceConfig { struct Application { slug: String, name: String, + description: String, client_id: String, - client_secret: String + client_secret: String, + allowed_redirect_uris: Vec } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/router.rs b/src/router.rs index 6997e32..167cb58 100644 --- a/src/router.rs +++ b/src/router.rs @@ -17,6 +17,7 @@ pub fn build_router(server_config: &ServerConfig, app_state: AppState) -> Router .route("/me/details-form", post(ui::me::me_perform_update_details)) .route("/logout", get(ui::logout::perform_logout)) .route("/authorize", get(ui::authorize::authorize_form)) + .route("/authorize", post(ui::authorize::perform_authorize)) .layer(middleware::from_fn_with_state(app_state.clone(), enforce_auth_middleware)); Router::new() diff --git a/src/services/mod.rs b/src/services/mod.rs index 5348a28..569b260 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,2 +1,3 @@ pub mod password; pub mod session; +pub mod oauth2; diff --git a/src/services/oauth2.rs b/src/services/oauth2.rs new file mode 100644 index 0000000..a041e8d --- /dev/null +++ b/src/services/oauth2.rs @@ -0,0 +1,7 @@ +use crate::models::config::Application; + +pub fn verify_redirect_uri(app: &Application, input_redirect_uri: &str) -> bool { + app.allowed_redirect_uris + .iter() + .find(|uri| **uri == input_redirect_uri).is_some() +} diff --git a/src/templates/pages/authorize.html b/src/templates/pages/authorize.html index 7003d68..a096feb 100644 --- a/src/templates/pages/authorize.html +++ b/src/templates/pages/authorize.html @@ -1,21 +1,32 @@ {% extends "layouts/base.html" %} {% block body %} - + {% if error %}
Error: {{ error }}
{% endif %} -
+

Do you authorize this app?

+

+ You're about to log-in and give some of your personal data to an application. + If you accept, you will be redirected to "{{ redirect_uri_host }}". +

    -
  • App name:
  • -
  • Permisions: read basics
  • +
  • App name: {{ app.name }}
  • +
  • App description: {{ app.description }}
  • +
  • Permisions: {{ authorization_params.scope }}
- - - + + + + + - +
+ + Don't authorize + +
{% endblock %} diff --git a/src/utils.rs b/src/utils.rs index c9e246b..b654302 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,6 +6,7 @@ use argon2::{ }, Argon2 }; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; pub fn get_password_hash(password: String) -> Result<(String, String)> { let salt = SaltString::generate(&mut OsRng); @@ -33,3 +34,10 @@ pub fn verify_password_hash(password_hash: String, password: String) -> Result<( } } +pub fn get_random_alphanumerical(length: usize) -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +}