mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2025-06-27 00:16:36 +02:00
feat(oidc_provider) use askama templates
Implements a custom OidcResponse with CSP headers and oxide-auth processing compatibility.
This commit is contained in:
parent
bfbd7abf44
commit
ec2c2bb27c
19 changed files with 709 additions and 319 deletions
182
Cargo.lock
generated
182
Cargo.lock
generated
|
@ -436,9 +436,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
version = "0.3.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
|
||||
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
|
@ -866,6 +866,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"conduwuit_core",
|
||||
"conduwuit_service",
|
||||
"conduwuit_web",
|
||||
"const-str",
|
||||
"futures",
|
||||
"hmac",
|
||||
|
@ -1060,13 +1061,20 @@ name = "conduwuit_web"
|
|||
version = "0.5.0-rc.5"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"conduwuit_build_metadata",
|
||||
"conduwuit_core",
|
||||
"conduwuit_service",
|
||||
"futures",
|
||||
"oxide-auth",
|
||||
"oxide-auth-axum",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2122,7 +2130,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2183,21 +2191,22 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
|
||||
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid"
|
||||
version = "1.5.0"
|
||||
name = "icu_locale_core"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
|
||||
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
|
@ -2206,31 +2215,11 @@ dependencies = [
|
|||
"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.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "1.5.0"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
|
||||
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
|
@ -2238,67 +2227,54 @@ dependencies = [
|
|||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "1.5.1"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
|
||||
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "1.5.1"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
|
||||
checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locid_transform",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"potential_utf",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "1.5.1"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
|
||||
checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "1.5.0"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
|
||||
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_provider_macros",
|
||||
"icu_locale_core",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
|
@ -2312,9 +2288,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
|
||||
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
|
@ -2624,9 +2600,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
|||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.5"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
|
||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
|
@ -3340,6 +3316,15 @@ version = "1.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
|
@ -3783,7 +3768,7 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
|
@ -4149,11 +4134,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
||||
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
|
||||
dependencies = [
|
||||
"web-time 1.1.0",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4282,7 +4268,7 @@ dependencies = [
|
|||
"sentry-tracing",
|
||||
"tokio",
|
||||
"ureq",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4931,9 +4917,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
|
||||
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
|
@ -5366,7 +5352,7 @@ dependencies = [
|
|||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"url",
|
||||
"webpki-roots",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5393,12 +5379,6 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[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"
|
||||
|
@ -5589,9 +5569,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.10"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
@ -6005,9 +5994,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
|||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.9"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -6031,17 +6020,11 @@ dependencies = [
|
|||
"bitflags 2.9.0",
|
||||
]
|
||||
|
||||
[[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"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
|
||||
[[package]]
|
||||
name = "xml5ever"
|
||||
|
@ -6062,9 +6045,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
|||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.5"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
|
||||
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
|
@ -6074,9 +6057,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.7.5"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
|
||||
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -6132,10 +6115,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.10.4"
|
||||
name = "zerotrie"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
|
||||
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
@ -6144,9 +6138,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.10.3"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -104,6 +104,8 @@ features = [
|
|||
"tokio",
|
||||
"tracing",
|
||||
"query",
|
||||
# Needed for debug_handler.
|
||||
#"macros",
|
||||
]
|
||||
|
||||
[workspace.dependencies.axum-extra]
|
||||
|
|
|
@ -92,6 +92,7 @@ tokio.workspace = true
|
|||
tracing.workspace = true
|
||||
oxide-auth.workspace = true
|
||||
oxide-auth-axum.workspace = true
|
||||
conduwuit-web.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
|
||||
[lints]
|
||||
|
|
|
@ -1,29 +1,13 @@
|
|||
use conduwuit_web::oidc::{oidc_consent_form, oidc_login_form, AuthorizationQuery, OidcRequest, OidcResponse};
|
||||
use oxide_auth_axum::{OAuthResponse, OAuthRequest};
|
||||
use oxide_auth::{
|
||||
endpoint::{OwnerConsent, Solicitation},
|
||||
frontends::simple::endpoint::FnSolicitor,
|
||||
primitives::registrar::PreGrant,
|
||||
};
|
||||
use axum::extract::{Query, State};
|
||||
use serde_html_form;
|
||||
use conduwuit::{Result, err};
|
||||
use reqwest::Url;
|
||||
use percent_encoding::percent_decode_str;
|
||||
|
||||
|
||||
/// The set of query parameters a client needs to get authorization.
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub(crate) struct OAuthQuery {
|
||||
client_id: String,
|
||||
redirect_uri: Url,
|
||||
scope: String,
|
||||
state: String,
|
||||
code_challenge: String,
|
||||
code_challenge_method: String,
|
||||
response_type: String,
|
||||
response_mode: String,
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/unstable/org.matrix.msc2964/authorize`
|
||||
///
|
||||
/// Authenticate a user and device, and solicit the user's consent.
|
||||
|
@ -34,9 +18,9 @@ pub(crate) struct OAuthQuery {
|
|||
/// access to stage two, [authorize_consent].
|
||||
pub(crate) async fn authorize(
|
||||
State(services): State<crate::State>,
|
||||
Query(query): Query<OAuthQuery>,
|
||||
oauth: OAuthRequest,
|
||||
) -> Result<OAuthResponse> {
|
||||
Query(query): Query<AuthorizationQuery>,
|
||||
oauth: OidcRequest,
|
||||
) -> Result<OidcResponse> {
|
||||
tracing::trace!("processing OAuth request: {query:?}");
|
||||
// Enforce MSC2964's restrictions on OAuth2 flow.
|
||||
let Ok(scope) = percent_decode_str(&query.scope).decode_utf8() else {
|
||||
|
@ -60,16 +44,13 @@ pub(crate) async fn authorize(
|
|||
.client
|
||||
.as_ref()
|
||||
.map(|s| s.domain().expect("well-known client should be a domain"));
|
||||
let login_redirect = OAuthResponse::default()
|
||||
.body(&login_form(hostname, &query))
|
||||
.content_type("text/html")
|
||||
.expect("should set Content-Type on OAuth response");
|
||||
let hostname = hostname.unwrap_or("Continuwuity");
|
||||
match oauth.authorization_header() {
|
||||
| None => {
|
||||
return Ok(login_redirect);
|
||||
return Ok(oidc_login_form(hostname, &query));
|
||||
},
|
||||
| Some(token) => if services.users.find_from_token(token).await.is_err() {
|
||||
return Ok(login_redirect);
|
||||
return Ok(oidc_login_form(hostname, &query));
|
||||
}
|
||||
}
|
||||
// TODO register the device ID ?
|
||||
|
@ -77,18 +58,7 @@ pub(crate) async fn authorize(
|
|||
services
|
||||
.oidc
|
||||
.endpoint()
|
||||
.with_solicitor(FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>|
|
||||
OwnerConsent::InProgress(
|
||||
OAuthResponse::default()
|
||||
.body(&consent_page_html(
|
||||
"/_matrix/client/unstable/org.matrix.msc2964/authorize",
|
||||
solicitation,
|
||||
hostname.unwrap_or("conduwuit"),
|
||||
))
|
||||
.content_type("text/html")
|
||||
.expect("set content type on consent form")
|
||||
)
|
||||
))
|
||||
.with_solicitor(oidc_consent_form(hostname, &query))
|
||||
.authorization_flow()
|
||||
.execute(oauth)
|
||||
.map_err(|err| err!("authorization failed: {err:?}"))
|
||||
|
@ -116,99 +86,15 @@ pub(crate) async fn authorize_consent(
|
|||
services
|
||||
.oidc
|
||||
.endpoint()
|
||||
.with_solicitor(FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>|
|
||||
match allowed {
|
||||
| false => OwnerConsent::Denied,
|
||||
| true => OwnerConsent::Authorized(solicitation.pre_grant().client_id.clone())
|
||||
}
|
||||
))
|
||||
.with_solicitor(
|
||||
FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>|
|
||||
match allowed {
|
||||
| false => OwnerConsent::Denied,
|
||||
| true => OwnerConsent::Authorized(solicitation.pre_grant().client_id.clone())
|
||||
}
|
||||
)
|
||||
)
|
||||
.authorization_flow()
|
||||
.execute(oauth)
|
||||
.map_err(|err| err!(Request(Unknown("consent request failed: {err:?}"))))
|
||||
}
|
||||
|
||||
fn login_form(
|
||||
hostname: Option<&str>,
|
||||
OAuthQuery {
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope,
|
||||
state,
|
||||
code_challenge,
|
||||
code_challenge_method,
|
||||
response_type,
|
||||
response_mode,
|
||||
}: &OAuthQuery,
|
||||
) -> String {
|
||||
let hostname = hostname.unwrap_or("");
|
||||
|
||||
format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<center>
|
||||
<h1>{hostname} login</h1>
|
||||
<form action="/_matrix/client/unstable/org.matrix.msc2964/login" method="post">
|
||||
<input type="text" name="username" placeholder="Username" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<input type="hidden" name="client_id" value="{client_id}">
|
||||
<input type="hidden" name="redirect_uri" value="{redirect_uri}">
|
||||
<input type="hidden" name="scope" value="{scope}">
|
||||
<input type="hidden" name="state" value="{state}">
|
||||
<input type="hidden" name="code_challenge" value="{code_challenge}">
|
||||
<input type="hidden" name="code_challenge_method" value="{code_challenge_method}">
|
||||
<input type="hidden" name="response_type" value="{response_type}">
|
||||
<input type="hidden" name="response_mode" value="{response_mode}">
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn consent_page_html(
|
||||
route: &str,
|
||||
solicitation: Solicitation<'_>,
|
||||
hostname: &str,
|
||||
) -> String {
|
||||
let state = solicitation.state();
|
||||
let grant = solicitation.pre_grant();
|
||||
let PreGrant { client_id, redirect_uri, scope } = grant;
|
||||
let mut args = vec![
|
||||
("response_type", "code"),
|
||||
("client_id", client_id.as_str()),
|
||||
("redirect_uri", redirect_uri.as_str()),
|
||||
];
|
||||
if let Some(state) = state {
|
||||
args.push(("state", state));
|
||||
}
|
||||
let args = serde_html_form::to_string(args).unwrap();
|
||||
|
||||
format!(
|
||||
r#"
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
</head>
|
||||
<body>
|
||||
<center>
|
||||
<h1>{hostname} login</h1>
|
||||
'{client_id}' (at {redirect_uri}) is requesting permission for '{scope}'
|
||||
<form method="post">
|
||||
<input type="submit" value="Accept" formaction="{route}?{args}&allow=true">
|
||||
<input type="submit" value="Deny" formaction="{route}?{args}&deny=true">
|
||||
</form>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,57 +1,20 @@
|
|||
use super::authorize::consent_page_html;
|
||||
use oxide_auth_axum::{OAuthRequest, OAuthResponse};
|
||||
use oxide_auth::{
|
||||
endpoint::{OwnerConsent, Solicitation},
|
||||
frontends::simple::endpoint::FnSolicitor,
|
||||
use conduwuit_web::oidc::{
|
||||
oidc_consent_form,
|
||||
AuthorizationQuery,
|
||||
LoginError,
|
||||
LoginQuery,
|
||||
OidcRequest,
|
||||
OidcResponse,
|
||||
};
|
||||
use axum::extract::{Form, FromRequest, State};
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Result,
|
||||
err,
|
||||
utils::hash::verify_password,
|
||||
};
|
||||
use ruma::user_id::UserId;
|
||||
use reqwest::Url;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
|
||||
/// The set of query parameters a client needs to get authorization.
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub(crate) struct LoginForm {
|
||||
username: String,
|
||||
password: String,
|
||||
client_id: String,
|
||||
redirect_uri: Url,
|
||||
scope: String,
|
||||
state: String,
|
||||
code_challenge: String,
|
||||
code_challenge_method: String,
|
||||
response_type: String,
|
||||
response_mode: String,
|
||||
}
|
||||
|
||||
impl From<LoginForm> for String {
|
||||
/// Turn the OAuth parameters from a Form into a GET query, suitable for
|
||||
/// then turning it into oxide-auth's OAuthRequest. Strips the unneeded
|
||||
/// username and password.
|
||||
fn from(value: LoginForm) -> Self {
|
||||
let encode = |text: &str| -> String {
|
||||
utf8_percent_encode(text, NON_ALPHANUMERIC).to_string()
|
||||
};
|
||||
|
||||
format!(
|
||||
"?client_id={}&redirect_uri={}&scope={}&state={}&code_challenge={}&code_challenge_method={}&response_type={}&response_mode={}",
|
||||
encode(&value.client_id),
|
||||
encode(&value.redirect_uri.to_string()),
|
||||
encode(&value.scope),
|
||||
encode(&value.state),
|
||||
encode(&value.code_challenge),
|
||||
encode(&value.code_challenge_method),
|
||||
encode(&value.response_type),
|
||||
encode(&value.response_mode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//#[axum::debug_handler]
|
||||
/// # `POST /_matrix/client/unstable/org.matrix.msc2964/login`
|
||||
///
|
||||
/// Display a login UI to the user and return an authorization code on success.
|
||||
|
@ -60,14 +23,17 @@ impl From<LoginForm> for String {
|
|||
/// [super::authorize::authorize_consent].
|
||||
pub(crate) async fn oidc_login(
|
||||
State(services): State<crate::State>,
|
||||
Form(login_query): Form<LoginForm>,
|
||||
) -> Result<OAuthResponse> {
|
||||
request: OidcRequest,
|
||||
) -> Result<OidcResponse> {
|
||||
let query: LoginQuery = request.clone().try_into().map_err(|LoginError(err)|
|
||||
err!(Request(InvalidParam("Cannot process login form. {err}")))
|
||||
)?;
|
||||
// Only accept local usernames. Mostly to simplify things at first.
|
||||
let user_id = UserId::parse_with_server_name(
|
||||
login_query.username.clone(),
|
||||
query.username.clone(),
|
||||
&services.config.server_name
|
||||
)
|
||||
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
|
||||
.map_err(|e| err!(Request(InvalidUsername("Username is invalid: {e}"))))?;
|
||||
|
||||
if ! services.users.exists(&user_id).await {
|
||||
return Err(err!(Request(Unknown("unknown username"))));
|
||||
|
@ -82,49 +48,26 @@ pub(crate) async fn oidc_login(
|
|||
if valid_hash.is_empty() {
|
||||
return Err(err!(Request(UserDeactivated("the user's hash was not found"))));
|
||||
}
|
||||
if let Err(_) = verify_password(&login_query.password, &valid_hash) {
|
||||
if let Err(_) = verify_password(&query.password, &valid_hash) {
|
||||
return Err(err!(Request(InvalidParam("password does not match"))));
|
||||
}
|
||||
tracing::info!("{user_id:?} passed, forwarding to consent page");
|
||||
|
||||
// Build up a GET query and parse it as an OAuthRequest.
|
||||
let login_query: String = login_query.into();
|
||||
let login_url = http::Uri::builder()
|
||||
.scheme("https")
|
||||
.authority(services.config.server_name.as_str())
|
||||
.path_and_query(login_query)
|
||||
.build()
|
||||
.expect("should be parseable");
|
||||
let req: http::Request<axum::body::Body> = http::Request::builder()
|
||||
.method("GET")
|
||||
.uri(&login_url)
|
||||
.body(axum::body::Body::empty())
|
||||
.expect("login form OAuth parameters parseable as a query");
|
||||
let oauth = OAuthRequest::from_request(req, &"")
|
||||
.await
|
||||
.expect("request parseable as an OAuth query");
|
||||
let hostname = services
|
||||
.config
|
||||
.well_known
|
||||
.client
|
||||
.as_ref()
|
||||
.map(|s| s.domain().expect("well-known client should be a domain"));
|
||||
.map(|s| s.domain().expect("well-known client should have a domain"));
|
||||
|
||||
let hostname = hostname.unwrap_or("Continuwuity");
|
||||
let authorization_query: AuthorizationQuery = query.into();
|
||||
|
||||
services
|
||||
.oidc
|
||||
.endpoint()
|
||||
.with_solicitor(FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>|
|
||||
OwnerConsent::InProgress(
|
||||
OAuthResponse::default()
|
||||
.body(&consent_page_html(
|
||||
"/_matrix/client/unstable/org.matrix.msc2964/authorize",
|
||||
solicitation,
|
||||
hostname.unwrap_or("conduwuit"),
|
||||
))
|
||||
.content_type("text/html")
|
||||
.expect("set content type on consent form")
|
||||
)
|
||||
))
|
||||
.with_solicitor(oidc_consent_form(hostname, &authorization_query))
|
||||
.authorization_flow()
|
||||
.execute(oauth)
|
||||
.execute(request)
|
||||
.map_err(|err| err!(Request(Unknown("authorization failed: {err:?}"))))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
/// OIDC
|
||||
///
|
||||
/// Stands for OpenID Connect, and is an authentication scheme relying on OAuth2.
|
||||
/// The MSC2964 Matrix Spec Proposal describes an authentication process based
|
||||
/// on OIDC with restrictions. See the [sample flow] for details on what's expected.
|
||||
/// The [MSC2964] Matrix Spec Proposal describes an authentication process based
|
||||
/// on the OIDC flow, with restrictions. See the [sample flow] for details on
|
||||
/// what's expected.
|
||||
///
|
||||
/// This module implements the needed endpoints. It relies on [`service::oidc`] and
|
||||
/// the [oxide-auth] crate.
|
||||
/// This module implements the needed endpoints. It relies on the [oxide-auth]
|
||||
/// crate, and the [`service::oidc`] and [`web::oidc`] modules.
|
||||
///
|
||||
/// [MSC2964]: https://github.com/matrix-org/matrix-spec-proposals/pull/2964
|
||||
/// [oxide-auth]: https://docs.rs/oxide-auth
|
||||
/// [sample flow]: https://github.com/sandhose/matrix-spec-proposals/blob/msc/sandhose/oauth2-profile/proposals/2964-oauth2-profile.md#sample-flow
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ pub(crate) struct ClientResponse {
|
|||
|
||||
/// # `GET /_matrix/client/unstable/org.matrix.msc2964/device/register`
|
||||
///
|
||||
/// Register a client, as specified in [MSC2966]. This client, "device" in Oidc parlance,
|
||||
/// Register a client, as specified in [MSC2966]. This client, "device" in OIDC parlance,
|
||||
/// will have the right to submit [super::authorize::authorize] requests.
|
||||
///
|
||||
/// [MSC2966]: https://github.com/matrix-org/matrix-spec-proposals/pull/2966
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
/// Implementation of [MSC2964]'s OAuth2 restricted flow using the [oxide-auth]
|
||||
/// crate. See the MSC for restrictions that apply to this flow.
|
||||
///
|
||||
/// [MSC2965]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965
|
||||
/// [oxide-auth]: https://docs.rs/oxide-auth
|
||||
|
||||
use oxide_auth_axum::{OAuthResponse, OAuthRequest};
|
||||
use oxide_auth::endpoint::QueryParameter;
|
||||
use axum::{
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
/// Provides the registrar, authorizer and issuer needed by [api::client::oidc].
|
||||
/// The whole OAuth2 flow is taken care of by [oxide-auth].
|
||||
///
|
||||
/// TODO At the moment this service provides no method to dynamically add a
|
||||
/// client. That would need a dedicated space in the database.
|
||||
/// TODO this service would need a dedicated space in the database.
|
||||
///
|
||||
/// [oxide-auth]: https://docs.rs/oxide-auth
|
||||
|
||||
|
@ -90,7 +89,6 @@ impl Service {
|
|||
solicitor: Vacant,
|
||||
// Scope configured later.
|
||||
scopes: Vacant,
|
||||
// `rocket::Response` is `Default`, so we don't need more configuration.
|
||||
response: Vacant,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ crate-type = [
|
|||
[dependencies]
|
||||
conduwuit-build-metadata.workspace = true
|
||||
conduwuit-service.workspace = true
|
||||
conduwuit-core.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
||||
askama = "0.14.0"
|
||||
|
||||
|
@ -30,6 +32,11 @@ futures.workspace = true
|
|||
tracing.workspace = true
|
||||
rand.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
url.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
oxide-auth.workspace = true
|
||||
oxide-auth-axum.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
@ -8,6 +8,7 @@ use axum::{
|
|||
};
|
||||
use conduwuit_build_metadata::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL, version_tag};
|
||||
use conduwuit_service::state;
|
||||
pub mod oidc;
|
||||
|
||||
pub fn build() -> Router<state::State> {
|
||||
let router = Router::<state::State>::new();
|
||||
|
|
56
src/web/oidc.rs
Normal file
56
src/web/oidc.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use askama::Template;
|
||||
// Imports needed by askama templates.
|
||||
use crate::{
|
||||
VERSION_EXTRA, GIT_REMOTE_WEB_URL, GIT_REMOTE_COMMIT_URL,
|
||||
};
|
||||
|
||||
mod authorize;
|
||||
mod consent;
|
||||
mod login;
|
||||
mod response;
|
||||
mod request;
|
||||
pub use authorize::AuthorizationQuery;
|
||||
pub use consent::oidc_consent_form;
|
||||
pub use login::{LoginQuery, LoginError, oidc_login_form};
|
||||
pub use request::OidcRequest;
|
||||
pub use response::OidcResponse;
|
||||
|
||||
/// The parameters for the OIDC login page template.
|
||||
#[derive(Template)]
|
||||
#[template(path = "login.html.j2")]
|
||||
pub(crate) struct LoginPageTemplate<'a> {
|
||||
nonce: &'a str,
|
||||
hostname: &'a str,
|
||||
route: &'a str,
|
||||
client_id: &'a str,
|
||||
redirect_uri: &'a str,
|
||||
scope: &'a str,
|
||||
state: &'a str,
|
||||
code_challenge: &'a str,
|
||||
code_challenge_method: &'a str,
|
||||
response_type: &'a str,
|
||||
response_mode: &'a str,
|
||||
}
|
||||
|
||||
|
||||
/// The parameters for the OIDC consent page template.
|
||||
#[derive(Template)]
|
||||
#[template(path = "consent.html.j2")]
|
||||
pub(crate) struct ConsentPageTemplate<'a> {
|
||||
nonce: &'a str,
|
||||
hostname: &'a str,
|
||||
route: &'a str,
|
||||
client_id: &'a str,
|
||||
redirect_uri: &'a str,
|
||||
scope: &'a str,
|
||||
state: &'a str,
|
||||
code_challenge: &'a str,
|
||||
code_challenge_method: &'a str,
|
||||
response_type: &'a str,
|
||||
response_mode: &'a str,
|
||||
}
|
||||
|
||||
pub(crate) fn encode(text: &str) -> String {
|
||||
utf8_percent_encode(text, NON_ALPHANUMERIC).to_string()
|
||||
}
|
43
src/web/oidc/authorize.rs
Normal file
43
src/web/oidc/authorize.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use url::Url;
|
||||
use super::LoginQuery;
|
||||
|
||||
/// The set of parameters required for an OIDC authorization request.
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct AuthorizationQuery {
|
||||
pub client_id: String,
|
||||
pub redirect_uri: Url,
|
||||
pub scope: String,
|
||||
pub state: String,
|
||||
pub code_challenge: String,
|
||||
pub code_challenge_method: String,
|
||||
pub response_type: String,
|
||||
pub response_mode: String,
|
||||
}
|
||||
|
||||
impl From<LoginQuery> for AuthorizationQuery {
|
||||
fn from(value: LoginQuery) -> Self {
|
||||
let LoginQuery {
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope,
|
||||
state,
|
||||
code_challenge,
|
||||
code_challenge_method,
|
||||
response_type,
|
||||
response_mode,
|
||||
..
|
||||
} = value;
|
||||
|
||||
AuthorizationQuery {
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope,
|
||||
state,
|
||||
code_challenge,
|
||||
code_challenge_method,
|
||||
response_type,
|
||||
response_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
54
src/web/oidc/consent.rs
Normal file
54
src/web/oidc/consent.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use super::{
|
||||
encode,
|
||||
ConsentPageTemplate,
|
||||
AuthorizationQuery,
|
||||
OidcResponse,
|
||||
};
|
||||
use askama::Template;
|
||||
use oxide_auth::frontends::simple::request::{Body, Status};
|
||||
|
||||
/// A web consent solicitor form for the OIDC authentication flow.
|
||||
///
|
||||
/// Asks the resource owner for their consent to let a client access their data
|
||||
/// on this server.
|
||||
pub fn oidc_consent_form(
|
||||
hostname: &str,
|
||||
query: &AuthorizationQuery,
|
||||
) -> OidcResponse {
|
||||
// The target request route.
|
||||
let route = "/_matrix/client/unstable/org.matrix.msc2964/authorize";
|
||||
let nonce = rand::random::<u64>().to_string();
|
||||
let body = Some(Body::Text(consent_page(hostname, query, route, &nonce)));
|
||||
|
||||
OidcResponse {
|
||||
status: Status::Ok,
|
||||
location: None,
|
||||
www_authenticate: None,
|
||||
body,
|
||||
nonce,
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the html contents of the user consent page.
|
||||
fn consent_page(
|
||||
hostname: &str,
|
||||
query: &AuthorizationQuery,
|
||||
route: &str,
|
||||
nonce: &str,
|
||||
) -> String {
|
||||
let template = ConsentPageTemplate {
|
||||
nonce,
|
||||
hostname,
|
||||
route,
|
||||
client_id: &encode(query.client_id.as_str()),
|
||||
redirect_uri: &encode(query.redirect_uri.as_str()),
|
||||
scope: &encode(query.scope.as_str()),
|
||||
state: &encode(query.state.as_str()),
|
||||
code_challenge: &encode(query.code_challenge.as_str()),
|
||||
code_challenge_method: &encode(query.code_challenge_method.as_str()),
|
||||
response_type: &encode(query.response_type.as_str()),
|
||||
response_mode: &encode(query.response_mode.as_str()),
|
||||
};
|
||||
|
||||
template.render().expect("consent page render")
|
||||
}
|
132
src/web/oidc/login.rs
Normal file
132
src/web/oidc/login.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use super::{
|
||||
AuthorizationQuery,
|
||||
LoginPageTemplate,
|
||||
OidcRequest,
|
||||
OidcResponse,
|
||||
};
|
||||
use askama::Template;
|
||||
use oxide_auth::{
|
||||
endpoint::QueryParameter,
|
||||
frontends::simple::request::{Body, Status},
|
||||
};
|
||||
use url::Url;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
||||
/// The set of query parameters a client needs to get authorization.
|
||||
#[derive(serde::Deserialize, Debug, Clone)]
|
||||
pub struct LoginQuery {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub client_id: String,
|
||||
pub redirect_uri: Url,
|
||||
pub scope: String,
|
||||
pub state: String,
|
||||
pub code_challenge: String,
|
||||
pub code_challenge_method: String,
|
||||
pub response_type: String,
|
||||
pub response_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoginError(pub String);
|
||||
|
||||
impl TryFrom<OidcRequest> for LoginQuery {
|
||||
type Error = LoginError;
|
||||
|
||||
fn try_from(value: OidcRequest) -> Result<Self, LoginError> {
|
||||
let body = value.body().expect("body in OidcRequest");
|
||||
|
||||
let Some(username) = body.unique_value("username") else {
|
||||
return Err(LoginError("missing field: username".to_string()));
|
||||
};
|
||||
let Some(password) = body.unique_value("password") else {
|
||||
return Err(LoginError("missing field: password".to_string()));
|
||||
};
|
||||
let Some(client_id) = body.unique_value("client_id") else {
|
||||
return Err(LoginError("missing field: client_id".to_string()));
|
||||
};
|
||||
let Some(redirect_uri) = body.unique_value("redirect_uri") else {
|
||||
return Err(LoginError("missing field: redirect_uri".to_string()));
|
||||
};
|
||||
let Some(scope) = body.unique_value("scope") else {
|
||||
return Err(LoginError("missing field: scope".to_string()));
|
||||
};
|
||||
let Some(state) = body.unique_value("state") else {
|
||||
return Err(LoginError("missing field: state".to_string()));
|
||||
};
|
||||
let Some(code_challenge) = body.unique_value("code_challenge") else {
|
||||
return Err(LoginError("missing field: code_challenge".to_string()));
|
||||
};
|
||||
let Some(code_challenge_method) = body.unique_value("code_challenge_method") else {
|
||||
return Err(LoginError("missing field: code_challenge_method".to_string()));
|
||||
};
|
||||
let Some(response_type) = body.unique_value("response_type") else {
|
||||
return Err(LoginError("missing field: response_type".to_string()));
|
||||
};
|
||||
let Some(response_mode) = body.unique_value("response_mode") else {
|
||||
return Err(LoginError("missing field: response_mode".to_string()));
|
||||
};
|
||||
let Ok(redirect_uri) = Url::from_str(&redirect_uri) else {
|
||||
return Err(LoginError("invalid field: redirect_uri".to_string()));
|
||||
};
|
||||
|
||||
Ok(LoginQuery {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
client_id: client_id.to_string(),
|
||||
redirect_uri,
|
||||
scope: scope.to_string(),
|
||||
state: state.to_string(),
|
||||
code_challenge: code_challenge.to_string(),
|
||||
code_challenge_method: code_challenge_method.to_string(),
|
||||
response_type: response_type.to_string(),
|
||||
response_mode: response_mode.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A web login form for the OIDC authentication flow.
|
||||
///
|
||||
/// The returned `OidcResponse` handles CSP headers to allow that form.
|
||||
pub fn oidc_login_form(
|
||||
hostname: &str,
|
||||
query: &AuthorizationQuery,
|
||||
) -> OidcResponse {
|
||||
// The target request route.
|
||||
let route = "/_matrix/client/unstable/org.matrix.msc2964/login";
|
||||
let nonce = rand::random::<u64>().to_string();
|
||||
let body = Some(Body::Text(login_page(hostname, query, route, &nonce)));
|
||||
|
||||
OidcResponse {
|
||||
status: Status::Ok,
|
||||
location: None,
|
||||
www_authenticate: None,
|
||||
body,
|
||||
nonce,
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the html contents of the login page.
|
||||
fn login_page(
|
||||
hostname: &str,
|
||||
query: &AuthorizationQuery,
|
||||
route: &str,
|
||||
nonce: &str,
|
||||
) -> String {
|
||||
let template = LoginPageTemplate {
|
||||
nonce,
|
||||
hostname,
|
||||
route,
|
||||
client_id: query.client_id.as_str(),
|
||||
redirect_uri: query.redirect_uri.as_str(),
|
||||
scope: query.scope.as_str(),
|
||||
state: query.state.as_str(),
|
||||
code_challenge: query.code_challenge.as_str(),
|
||||
code_challenge_method: query.code_challenge_method.as_str(),
|
||||
response_type: query.response_type.as_str(),
|
||||
response_mode: query.response_mode.as_str(),
|
||||
};
|
||||
|
||||
template.render().expect("login template render")
|
||||
}
|
116
src/web/oidc/request.rs
Normal file
116
src/web/oidc/request.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use super::OidcResponse;
|
||||
use oxide_auth::endpoint::{NormalizedParameter, QueryParameter, WebRequest};
|
||||
use oxide_auth_axum::WebError;
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
extract::{Form, FromRequest, FromRequestParts, Query, Request},
|
||||
http::header,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// An OIDC authentication request.
|
||||
///
|
||||
/// Expected to receive GET and POST requests to the `authorize` endpoint, or
|
||||
/// POST requests to the `login` endpoint.
|
||||
///
|
||||
/// Mostly adapted from the OAuthRequest struct in the [oxide-auth-axum] crate.
|
||||
/// [oxide-auth-axum]: https://docs.rs/oxide-auth-axum
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OidcRequest {
|
||||
pub(crate) auth: Option<String>,
|
||||
pub(crate) query: Option<NormalizedParameter>,
|
||||
pub(crate) body: Option<NormalizedParameter>,
|
||||
}
|
||||
|
||||
impl OidcRequest {
|
||||
/// Fetch the authorization header from the request
|
||||
pub fn authorization_header(&self) -> Option<&str> {
|
||||
self.auth.as_deref()
|
||||
}
|
||||
|
||||
/// Fetch the query for this request
|
||||
pub fn query(&self) -> Option<&NormalizedParameter> {
|
||||
self.query.as_ref()
|
||||
}
|
||||
|
||||
/// Fetch the query mutably
|
||||
pub fn query_mut(&mut self) -> Option<&mut NormalizedParameter> {
|
||||
self.query.as_mut()
|
||||
}
|
||||
|
||||
/// Fetch the body of the request
|
||||
pub fn body(&self) -> Option<&NormalizedParameter> {
|
||||
self.body.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl WebRequest for OidcRequest {
|
||||
type Error = WebError;
|
||||
type Response = OidcResponse;
|
||||
|
||||
fn query(&mut self) -> Result<Cow<'_, dyn QueryParameter + 'static>, Self::Error> {
|
||||
self.query
|
||||
.as_ref()
|
||||
.map(|q| Cow::Borrowed(q as &dyn QueryParameter))
|
||||
.ok_or(WebError::Query)
|
||||
}
|
||||
|
||||
fn urlbody(&mut self) -> Result<Cow<'_, dyn QueryParameter + 'static>, Self::Error> {
|
||||
self.body
|
||||
.as_ref()
|
||||
.map(|b| Cow::Borrowed(b as &dyn QueryParameter))
|
||||
.ok_or(WebError::Body)
|
||||
}
|
||||
|
||||
fn authheader(&mut self) -> Result<Option<Cow<'_, str>>, Self::Error> {
|
||||
Ok(self.auth.as_deref().map(Cow::Borrowed))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequest<S> for OidcRequest
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = WebError;
|
||||
|
||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let mut all_auth = req.headers().get_all(header::AUTHORIZATION).iter();
|
||||
let optional = all_auth.next();
|
||||
|
||||
let auth = if all_auth.next().is_some() {
|
||||
return Err(WebError::Authorization);
|
||||
} else {
|
||||
optional.and_then(|hv| hv.to_str().ok().map(str::to_owned))
|
||||
};
|
||||
|
||||
let (mut parts, body) = req.into_parts();
|
||||
let query = Query::from_request_parts(&mut parts, state)
|
||||
.await
|
||||
.ok()
|
||||
.map(|q: Query<NormalizedParameter>| q.0);
|
||||
|
||||
let req = Request::from_parts(parts, body);
|
||||
let body = Form::from_request(req, state)
|
||||
.await
|
||||
.ok()
|
||||
.map(|b: Form<NormalizedParameter>| b.0);
|
||||
|
||||
// If the query is empty and the body has a request, copy it over
|
||||
// because login forms are POST requests but OAuth flow expects
|
||||
// arguments in query.
|
||||
let query = match query {
|
||||
| None => body.clone(),
|
||||
| Some(params) => {
|
||||
//if params == NormalizedParameter::new() {
|
||||
if params.unique_value("client_id").is_none() {
|
||||
body.clone()
|
||||
} else {
|
||||
Some(params)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Self { auth, query, body })
|
||||
}
|
||||
}
|
127
src/web/oidc/response.rs
Normal file
127
src/web/oidc/response.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
use super::{OidcRequest, oidc_consent_form};
|
||||
|
||||
use crate::oidc::LoginQuery;
|
||||
use oxide_auth::{
|
||||
endpoint::{OwnerConsent, OwnerSolicitor, Solicitation, WebRequest, WebResponse},
|
||||
frontends::simple::request::{Body as OAuthRequestBody, Status},
|
||||
};
|
||||
use oxide_auth_axum::WebError;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Response, header},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
/// A Web response that can be processed by the OIDC authentication flow before
|
||||
/// being sent over.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct OidcResponse {
|
||||
pub(crate) status: Status,
|
||||
pub(crate) location: Option<Url>,
|
||||
pub(crate) www_authenticate: Option<String>,
|
||||
pub(crate) body: Option<OAuthRequestBody>,
|
||||
pub(crate) nonce: String,
|
||||
}
|
||||
|
||||
impl OidcResponse {
|
||||
/// Instanciate from a response body. Used to send login or consent forms.
|
||||
pub fn from_body(body: &str) -> Result<Self, WebError> {
|
||||
let mut result = OidcResponse::default();
|
||||
result.body_text(body)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for OidcResponse {
|
||||
fn into_response(self) -> Response<Body> {
|
||||
let body = self.body.expect("body").as_str().to_string();
|
||||
let response = Response::builder()
|
||||
.header(header::CONTENT_TYPE, "text/html")
|
||||
.header(
|
||||
header::CONTENT_SECURITY_POLICY,
|
||||
format!("default-src 'nonce-{}'; form-action https://eon.presentmatter.one/;", self.nonce)
|
||||
)
|
||||
.body(body.into())
|
||||
.unwrap();
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
impl OwnerSolicitor<OidcRequest> for OidcResponse {
|
||||
fn check_consent(
|
||||
&mut self,
|
||||
request: &mut OidcRequest,
|
||||
_: Solicitation<'_>,
|
||||
) -> OwnerConsent<<OidcRequest as WebRequest>::Response> {
|
||||
//let hostname = self.location.map(|l| l.as_str()).unwrap_or("Continuwuity");
|
||||
let hostname = "Continuwuity";
|
||||
/*
|
||||
let hostname = request
|
||||
.query()
|
||||
.expect("query in OAuth request")
|
||||
.unique_value("hostname")
|
||||
.expect("hostname in OAuth request")
|
||||
.as_str();
|
||||
*/
|
||||
let query: LoginQuery = request
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("login query from OidcRequest");
|
||||
|
||||
OwnerConsent::InProgress(oidc_consent_form(
|
||||
hostname,
|
||||
&query.into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl WebResponse for OidcResponse {
|
||||
type Error = WebError;
|
||||
|
||||
fn ok(&mut self) -> Result<(), Self::Error> {
|
||||
self.status = Status::Ok;
|
||||
self.location = None;
|
||||
self.www_authenticate = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A response which will redirect the user-agent to which the response is issued.
|
||||
fn redirect(&mut self, url: Url) -> Result<(), Self::Error> {
|
||||
self.status = Status::Redirect;
|
||||
self.location = Some(url);
|
||||
self.www_authenticate = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the response status to 400.
|
||||
fn client_error(&mut self) -> Result<(), Self::Error> {
|
||||
self.status = Status::BadRequest;
|
||||
self.location = None;
|
||||
self.www_authenticate = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the response status to 401 and add a `WWW-Authenticate` header.
|
||||
fn unauthorized(&mut self, header_value: &str) -> Result<(), Self::Error> {
|
||||
self.status = Status::Unauthorized;
|
||||
self.location = None;
|
||||
self.www_authenticate = Some(header_value.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A pure text response with no special media type set.
|
||||
fn body_text(&mut self, text: &str) -> Result<(), Self::Error> {
|
||||
self.body = Some(OAuthRequestBody::Text(text.to_owned()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Json repsonse data, with media type `aplication/json.
|
||||
fn body_json(&mut self, data: &str) -> Result<(), Self::Error> {
|
||||
self.body = Some(OAuthRequestBody::Json(data.to_owned()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
14
src/web/templates/consent.html.j2
Normal file
14
src/web/templates/consent.html.j2
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "_layout.html.j2" %}
|
||||
{%- block content -%}
|
||||
<div class="panel">
|
||||
<h1 class-"project-name">{{ hostname }}</h1>
|
||||
<p>
|
||||
'{{ client_id }}' (at {{ redirect_uri }}) is requesting permission for '{{ scope }}'
|
||||
</p>
|
||||
<form method="post">
|
||||
<input type="submit" value="Accept" formaction="{{ route }}?client_id={{ client_id }}&redirect_uri={{ redirect_uri }}&scope={{scope }}&state={{ state }}&code_challenge={{ code_challenge }}&code_challenge_method={{ code_challenge_method }}&response_type={{ response_type }}&response_mode={{ response_mode }}&allow=true">
|
||||
<input type="submit" value="Deny" formaction="{{ route }}?client_id={{ client_id }}&redirect_uri={{ redirect_uri }}&scope={{scope }}&state={{ state }}&code_challenge={{ code_challenge }}&code_challenge_method={{ code_challenge_method }}&response_type={{ response_type }}&response_mode={{ response_mode }}&deny=true">
|
||||
</form>
|
||||
</div>
|
||||
{%- endblock content -%}
|
||||
|
20
src/web/templates/login.html.j2
Normal file
20
src/web/templates/login.html.j2
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "_layout.html.j2" %}
|
||||
{%- block content -%}
|
||||
<div class="panel">
|
||||
<h1 class-"project-name">{{ hostname }}</h1>
|
||||
<form action="{{ route }}" method="post">
|
||||
<input type="text" name="username" placeholder="Username" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<input type="hidden" name="client_id" value="{{ client_id }}">
|
||||
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
|
||||
<input type="hidden" name="scope" value="{{ scope }}">
|
||||
<input type="hidden" name="state" value="{{ state }}">
|
||||
<input type="hidden" name="code_challenge" value="{{ code_challenge }}">
|
||||
<input type="hidden" name="code_challenge_method" value="{{ code_challenge_method }}">
|
||||
<input type="hidden" name="response_type" value="{{ response_type }}">
|
||||
<input type="hidden" name="response_mode" value="{{ response_mode }}">
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
{%- endblock content -%}
|
||||
|
Loading…
Add table
Reference in a new issue