feat(oauth2): authorize form and redirection

This commit is contained in:
Matthieu Bessat 2024-11-09 19:57:15 +01:00
parent c277ab3bd9
commit ecf1da2978
15 changed files with 480 additions and 62 deletions

280
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -606,6 +606,17 @@ dependencies = [
"subtle", "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]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
@ -1063,6 +1074,124 @@ dependencies = [
"cc", "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]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
@ -1071,12 +1200,23 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [ dependencies = [
"unicode-bidi", "idna_adapter",
"unicode-normalization", "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]] [[package]]
@ -1178,6 +1318,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litemap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@ -1255,6 +1401,7 @@ dependencies = [
"log", "log",
"minijinja", "minijinja",
"minijinja-embed", "minijinja-embed",
"rand",
"rand_core", "rand_core",
"redis", "redis",
"serde", "serde",
@ -1265,6 +1412,7 @@ dependencies = [
"toml", "toml",
"totp-rs", "totp-rs",
"tower-http", "tower-http",
"url",
"uuid", "uuid",
] ]
@ -2136,6 +2284,12 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.5" version = "0.1.5"
@ -2206,6 +2360,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.13.0" version = "3.13.0"
@ -2270,6 +2435,16 @@ dependencies = [
"time-core", "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]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.8.0" version = "1.8.0"
@ -2536,9 +2711,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.2" version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
@ -2551,6 +2726,18 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 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]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
@ -2822,6 +3009,42 @@ dependencies = [
"memchr", "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]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.35"
@ -2843,8 +3066,51 @@ dependencies = [
"syn 2.0.79", "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]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 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",
]

View file

@ -47,7 +47,6 @@ tower-http = { version = "0.6.1", features = ["fs"] }
totp-rs = "5.6" totp-rs = "5.6"
minijinja-embed = "2.3.1" minijinja-embed = "2.3.1"
axum-macros = "0.4.2" axum-macros = "0.4.2"
rand_core = { version = "0.6.4", features = ["std"] }
jwt = "0.16.0" jwt = "0.16.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
frank_jwt = "3.1.3" frank_jwt = "3.1.3"
@ -55,6 +54,9 @@ jsonwebtoken = "9.3.0"
axum-extra = { version = "0.9.4", features = ["cookie"] } axum-extra = { version = "0.9.4", features = ["cookie"] }
axum_typed_multipart = "0.13.1" axum_typed_multipart = "0.13.1"
base64 = "0.22.1" base64 = "0.22.1"
rand = "0.8.5"
rand_core = { version = "0.6.4", features = ["std"] }
url = "2.5.3"
[build-dependencies] [build-dependencies]
minijinja-embed = "2.3.1" minijinja-embed = "2.3.1"

View file

@ -9,3 +9,5 @@
- Verify authorize - Verify authorize
- Select by app client secret - Select by app client secret
- Upload picture - Upload picture
- [ ] Support error responses by https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1

View file

@ -5,8 +5,12 @@ logo_uri = "https://example.org/logo.png"
[[applications]] [[applications]]
slug = "demo_app" slug = "demo_app"
name = "Demo app" name = "Demo app"
description = "A super application where you can do everything you want."
client_id = "a1785786-8be1-443c-9a6f-35feed703609" client_id = "a1785786-8be1-443c-9a6f-35feed703609"
client_secret = "49c6c16a-0a8a-4981-a60d-5cb96582cc1a" client_secret = "49c6c16a-0a8a-4981-a60d-5cb96582cc1a"
allowed_redirect_uris = [
"http://localhost:9090/authorize"
]
[[roles]] [[roles]]
slug = "basic" slug = "basic"

View file

@ -2,4 +2,4 @@
# https://curl.se/docs/http-cookies.html # https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk. # 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

View file

@ -5,6 +5,6 @@ curl -v http://localhost:8085/authorize \
--cookie ".curl-cookies" \ --cookie ".curl-cookies" \
-d client_id="a1785786-8be1-443c-9a6f-35feed703609" \ -d client_id="a1785786-8be1-443c-9a6f-35feed703609" \
-d response_type="code" \ -d response_type="code" \
-d redirect_uri="https://localhost:9090/authorize" \ -d redirect_uri="http://localhost:9090/authorize" \
-d scope="read_basics" \ -d scope="read_basics" \
-d state="qxYAfk4kf6pbZkms78jM" -d state="qxYAfk4kf6pbZkms78jM"

View file

@ -21,6 +21,7 @@ CREATE TABLE authorizations (
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
client_id TEXT NOT NULL, client_id TEXT NOT NULL,
scopes TEXT, -- json array of app scope (permissions) scopes TEXT, -- json array of app scope (permissions)
code TEXT,
last_used_at DATETIME, last_used_at DATETIME,
created_at DATETIME NOT NULL created_at DATETIME NOT NULL

View file

@ -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 fully_pub::fully_pub;
use log::{debug, error, info};
use minijinja::context; 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] #[fully_pub]
/// query params described in [RFC6759 section 4.1.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1) /// 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, response_type: String,
client_id: String, client_id: String,
scope: String, scope: String,
@ -17,65 +21,175 @@ struct AuthorizeQueryParams {
state: String, 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( pub async fn authorize_form(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Extension(token_claims): Extension<TokenClaims>, Extension(token_claims): Extension<TokenClaims>,
Extension(renderer): Extension<TemplateRenderer>, Extension(renderer): Extension<TemplateRenderer>,
query_params: Query<AuthorizeQueryParams> query_params: Query<AuthorizationParams>
) -> impl IntoResponse { ) -> impl IntoResponse {
// 1. Verify the app details let Query(authorization_params) = query_params;
let app_res = app_state.config.applications
.iter()
.find(|a| a.client_id == query_params.client_id);
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 ( return (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
Html("Invalid client_id query params, app not found.") Html("Unauthorized redirect_uri.")
).into_response(); ).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 // 2. Check if the app is already authorized
let authorizations_res = sqlx::query_as::<_, Authorization>("SELECT * FROM authorizations WHERE user_id = $1 AND let authorizations_res = sqlx::query_as::<_, Authorization>(
app_id = $2") "SELECT * FROM authorizations WHERE user_id = $1 AND client_id = $2"
)
.bind(&token_claims.sub) .bind(&token_claims.sub)
.bind(&query_params.client_id) .bind(&authorization_params.client_id)
.fetch_one(&app_state.db) .fetch_one(&app_state.db)
.await .await;
.expect("To get authorizations");
dbg!(authorizations_res); match authorizations_res {
Ok(existing_authorization) => {
info!("Reusing existing authorization {}", &existing_authorization.id);
// 3. Verify scopes // 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();
}
}
// 4. Show form that POST to authorize // 4. Show form that POST to authorize
renderer renderer
.render( .render(
"pages/authorize", "pages/authorize",
context!() context!(
app => app,
authorization_params => authorization_params,
redirect_uri_host => parsed_redirect_uri.host_str()
)
) )
.into_response() .into_response()
} }
#[derive(Debug, Deserialize)]
#[fully_pub]
struct AuthorizeForm {
/// client_id
client_id: String,
scopes: Vec<String>
}
pub async fn perform_authorize( pub async fn perform_authorize(
State(app_state): State<AppState>, State(app_state): State<AppState>,
Extension(renderer): Extension<TemplateRenderer>, Extension(token_claims): Extension<TokenClaims>,
Form(authorize_form): Form<AuthorizeForm> Form(authorize_form): Form<AuthorizationParams>
) -> impl IntoResponse { ) -> impl IntoResponse {
// Save authorization in DB // 1. Get the app details
// 4.1. Create an authorization code let app = match app_state.config.applications
// 4.2. Redirect to the app with a token .iter()
(StatusCode::FOUND, Html("Redirecting…")) .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()
} }

View file

@ -5,6 +5,7 @@ use sqlx::types::Json;
#[derive(sqlx::Type, Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(sqlx::Type, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[derive(strum_macros::Display)] #[derive(strum_macros::Display)]
#[fully_pub]
enum AuthorizationScope { enum AuthorizationScope {
ReadBasics ReadBasics
} }
@ -19,6 +20,8 @@ struct Authorization {
client_id: String, client_id: String,
scopes: Json<Vec<AuthorizationScope>>, scopes: Json<Vec<AuthorizationScope>>,
/// defined in https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
code: String,
last_used_at: Option<DateTime<Utc>>, last_used_at: Option<DateTime<Utc>>,
created_at: DateTime<Utc> created_at: DateTime<Utc>
} }

View file

@ -1,9 +1,5 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use fully_pub::fully_pub; use fully_pub::fully_pub;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
const fn _default_true() -> bool { true } const fn _default_true() -> bool { true }
@ -20,8 +16,10 @@ struct InstanceConfig {
struct Application { struct Application {
slug: String, slug: String,
name: String, name: String,
description: String,
client_id: String, client_id: String,
client_secret: String client_secret: String,
allowed_redirect_uris: Vec<String>
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View file

@ -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("/me/details-form", post(ui::me::me_perform_update_details))
.route("/logout", get(ui::logout::perform_logout)) .route("/logout", get(ui::logout::perform_logout))
.route("/authorize", get(ui::authorize::authorize_form)) .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)); .layer(middleware::from_fn_with_state(app_state.clone(), enforce_auth_middleware));
Router::new() Router::new()

View file

@ -1,2 +1,3 @@
pub mod password; pub mod password;
pub mod session; pub mod session;
pub mod oauth2;

7
src/services/oauth2.rs Normal file
View file

@ -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()
}

View file

@ -1,21 +1,32 @@
{% extends "layouts/base.html" %} {% extends "layouts/base.html" %}
{% block body %} {% block body %}
<!-- Login form --> <!-- Authorize form -->
{% if error %} {% if error %}
<div> <div>
Error: {{ error }} Error: {{ error }}
</div> </div>
{% endif %} {% endif %}
<form id="authorize-form" method="post"> <form id="authorize-form" method="post" action="/authorize">
<h1>Do you authorize this app?</h1> <h1>Do you authorize this app?</h1>
<p>
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 }}".
</p>
<ul> <ul>
<li>App name: </li> <li>App name: {{ app.name }}</li>
<li>Permisions: read basics</li> <li>App description: <i>{{ app.description }}</i></li>
<li>Permisions: {{ authorization_params.scope }}</li>
</ul> </ul>
<input type="hidden" name="client_id" value="" /> <input type="hidden" name="client_id" value="{{ authorization_params.client_id }}" />
<input type="hidden" name="scope" value="" /> <input type="hidden" name="scope" value="{{ authorization_params.scope }}" />
<input type="hidden" name="state" value="" /> <input type="hidden" name="state" value="{{ authorization_params.state }}" />
<input type="hidden" name="response_type" value="{{ authorization_params.response_type }}" />
<input type="hidden" name="redirect_uri" value="{{ authorization_params.redirect_uri }}" />
<button type="submit" class="btn btn-primary">Authorize</button> <div class="d-flex justify-content-end">
<!-- TODO: implements authorization rejection -->
<a href="/me" class="btn btn-outlined">Don't authorize</a>
<button type="submit" class="btn btn-primary">Authorize</button>
</div>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -6,6 +6,7 @@ use argon2::{
}, },
Argon2 Argon2
}; };
use rand::{distributions::Alphanumeric, thread_rng, Rng};
pub fn get_password_hash(password: String) -> Result<(String, String)> { pub fn get_password_hash(password: String) -> Result<(String, String)> {
let salt = SaltString::generate(&mut OsRng); 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()
}