chore: Fix most clippy issue, format & typos

This commit is contained in:
Jade Ellis 2025-05-10 13:29:31 +01:00
parent 6c5289192d
commit 4f8afcf3e1
No known key found for this signature in database
GPG key ID: 8705A2A3EBF77BD2
19 changed files with 369 additions and 409 deletions

View file

@ -998,6 +998,12 @@ let_underscore_future = { level = "allow", priority = 1 }
# rust doesnt understand conduwuit's custom log macros # rust doesnt understand conduwuit's custom log macros
literal_string_with_formatting_args = { level = "allow", priority = 1 } literal_string_with_formatting_args = { level = "allow", priority = 1 }
<<<<<<< HEAD
needless_raw_string_hashes = "allow" needless_raw_string_hashes = "allow"
||||||| parent of fd972f11 (chore: Fix most clippy issue, format & typos)
=======
needless_raw_string_hashes = "allow"
>>>>>>> fd972f11 (chore: Fix most clippy issue, format & typos)

View file

@ -2,7 +2,6 @@ pub(super) mod account;
pub(super) mod account_data; pub(super) mod account_data;
pub(super) mod alias; pub(super) mod alias;
pub(super) mod appservice; pub(super) mod appservice;
pub(super) mod oidc;
pub(super) mod backup; pub(super) mod backup;
pub(super) mod capabilities; pub(super) mod capabilities;
pub(super) mod context; pub(super) mod context;
@ -14,6 +13,7 @@ pub(super) mod media;
pub(super) mod media_legacy; pub(super) mod media_legacy;
pub(super) mod membership; pub(super) mod membership;
pub(super) mod message; pub(super) mod message;
pub(super) mod oidc;
pub(super) mod openid; pub(super) mod openid;
pub(super) mod presence; pub(super) mod presence;
pub(super) mod profile; pub(super) mod profile;
@ -45,7 +45,6 @@ pub(super) use account::*;
pub(super) use account_data::*; pub(super) use account_data::*;
pub(super) use alias::*; pub(super) use alias::*;
pub(super) use appservice::*; pub(super) use appservice::*;
pub(super) use oidc::*;
pub(super) use backup::*; pub(super) use backup::*;
pub(super) use capabilities::*; pub(super) use capabilities::*;
pub(super) use context::*; pub(super) use context::*;
@ -58,6 +57,7 @@ pub(super) use media_legacy::*;
pub(super) use membership::*; pub(super) use membership::*;
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room}; pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room};
pub(super) use message::*; pub(super) use message::*;
pub(super) use oidc::*;
pub(super) use openid::*; pub(super) use openid::*;
pub(super) use presence::*; pub(super) use presence::*;
pub(super) use profile::*; pub(super) use profile::*;

View file

@ -1,10 +1,12 @@
use conduwuit_web::oidc::{oidc_consent_form, oidc_login_form, AuthorizationQuery, OidcRequest, OidcResponse}; use axum::extract::{Query, State};
use conduwuit::{Result, err};
use conduwuit_web::oidc::{
AuthorizationQuery, OidcRequest, OidcResponse, oidc_consent_form, oidc_login_form,
};
use oxide_auth::{ use oxide_auth::{
endpoint::{OwnerConsent, Solicitation}, endpoint::{OwnerConsent, Solicitation},
frontends::simple::endpoint::FnSolicitor, frontends::simple::endpoint::FnSolicitor,
}; };
use axum::extract::{Query, State};
use conduwuit::{Result, err};
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
/// # `GET /_matrix/client/unstable/org.matrix.msc2964/authorize` /// # `GET /_matrix/client/unstable/org.matrix.msc2964/authorize`
@ -24,12 +26,12 @@ pub(crate) async fn authorize(
// Enforce MSC2964's restrictions on OAuth2 flow. // Enforce MSC2964's restrictions on OAuth2 flow.
let Ok(scope) = percent_decode_str(&query.scope).decode_utf8() else { let Ok(scope) = percent_decode_str(&query.scope).decode_utf8() else {
return Err(err!(Request(Unknown("the scope could not be percent-decoded")))); return Err(err!(Request(Unknown("the scope could not be percent-decoded"))));
} ; };
//if ! scope.contains("urn:matrix:api:*") { //if ! scope.contains("urn:matrix:api:*") {
if ! scope.contains("urn:matrix:org.matrix.msc2967.client:api:*") { if !scope.contains("urn:matrix:org.matrix.msc2967.client:api:*") {
return Err(err!(Request(Unknown("the scope does not include the client API")))); return Err(err!(Request(Unknown("the scope does not include the client API"))));
} }
if ! scope.contains("urn:matrix:org.matrix.msc2967.client:device:") { if !scope.contains("urn:matrix:org.matrix.msc2967.client:device:") {
return Err(err!(Request(Unknown("the scope does not include a device ID")))); return Err(err!(Request(Unknown("the scope does not include a device ID"))));
} }
if query.code_challenge_method != "S256" { if query.code_challenge_method != "S256" {
@ -48,9 +50,10 @@ pub(crate) async fn authorize(
| None => { | None => {
return Ok(oidc_login_form(hostname, &query)); return Ok(oidc_login_form(hostname, &query));
}, },
| Some(token) => if services.users.find_from_token(token).await.is_err() { | Some(token) =>
if services.users.find_from_token(token).await.is_err() {
return Ok(oidc_login_form(hostname, &query)); return Ok(oidc_login_form(hostname, &query));
} },
} }
// TODO register the device ID ? // TODO register the device ID ?
@ -63,7 +66,7 @@ pub(crate) async fn authorize(
.map_err(|err| err!("authorization failed: {err:?}")) .map_err(|err| err!("authorization failed: {err:?}"))
} }
/// Wether a user allows their device to access this homeserver's resources. /// Whether a user allows their device to access this homeserver's resources.
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub(crate) struct Allowance { pub(crate) struct Allowance {
allow: Option<bool>, allow: Option<bool>,
@ -85,14 +88,12 @@ pub(crate) async fn authorize_consent(
services services
.oidc .oidc
.endpoint() .endpoint()
.with_solicitor( .with_solicitor(FnSolicitor(
FnSolicitor(move |_: &mut _, solicitation: Solicitation<'_>| move |_: &mut _, solicitation: Solicitation<'_>| match allowed {
match allowed {
| false => OwnerConsent::Denied, | false => OwnerConsent::Denied,
| true => OwnerConsent::Authorized(solicitation.pre_grant().client_id.clone()) | true => OwnerConsent::Authorized(solicitation.pre_grant().client_id.clone()),
} },
) ))
)
.authorization_flow() .authorization_flow()
.execute(oauth) .execute(oauth)
.map_err(|err| err!(Request(Unknown("consent request failed: {err:?}")))) .map_err(|err| err!(Request(Unknown("consent request failed: {err:?}"))))

View file

@ -3,71 +3,76 @@
/// [MSC2965]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965 /// [MSC2965]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965
use axum::extract::State; use axum::extract::State;
use conduwuit::Result; use conduwuit::Result;
use ruma::serde::Raw; use ruma::{
use ruma::api::client::{ api::client::{
error::{
Error as ClientError,
ErrorKind as ClientErrorKind,
ErrorBody as ClientErrorBody,
},
discovery::get_authorization_server_metadata::{ discovery::get_authorization_server_metadata::{
self, self,
msc2965::{ msc2965::{
AccountManagementAction, AccountManagementAction, AuthorizationServerMetadata, CodeChallengeMethod,
AuthorizationServerMetadata, GrantType, Prompt, Response, ResponseMode, ResponseType,
CodeChallengeMethod,
GrantType,
Prompt,
ResponseMode,
ResponseType,
Response,
}, },
}, },
error::{
Error as ClientError, ErrorBody as ClientErrorBody, ErrorKind as ClientErrorKind,
},
},
serde::Raw,
}; };
use crate::{conduwuit::Error, Ruma, RumaResponse};
use crate::{Ruma, RumaResponse, conduwuit::Error};
/// # `GET /_matrix/client/unstable/org.matrix.msc2965/auth_metadata` /// # `GET /_matrix/client/unstable/org.matrix.msc2965/auth_metadata`
/// ///
/// If `globals.auth.enable_oidc_login` is set, advertise this homeserver's OAuth2 endpoints. /// If `globals.auth.enable_oidc_login` is set, advertise this homeserver's
/// Otherwise, MSC2965 requires that the homeserver responds with 404/M_UNRECOGNIZED. /// OAuth2 endpoints. Otherwise, MSC2965 requires that the homeserver responds
/// with 404/M_UNRECOGNIZED.
pub(crate) async fn get_auth_metadata( pub(crate) async fn get_auth_metadata(
State(services): State<crate::State>, State(services): State<crate::State>,
_body: Ruma<get_authorization_server_metadata::msc2965::Request>, _body: Ruma<get_authorization_server_metadata::msc2965::Request>,
) -> Result<RumaResponse<Response>> { ) -> Result<RumaResponse<Response>> {
let unrecognized_error = Err(Error::Ruma( let unrecognized_error = Err(Error::Ruma(ClientError::new(
ClientError::new(
http::StatusCode::NOT_FOUND, http::StatusCode::NOT_FOUND,
ClientErrorBody::Standard { ClientErrorBody::Standard {
kind: ClientErrorKind::Unrecognized, kind: ClientErrorKind::Unrecognized,
message: "This homeserver doesn't do OIDC authentication.".to_string() message: "This homeserver doesn't do OIDC authentication.".to_owned(),
} },
) )));
));
let Some(ref auth) = services.server.config.auth else { let Some(ref auth) = services.server.config.auth else {
return unrecognized_error; return unrecognized_error;
}; };
if ! auth.enable_oidc_login { if !auth.enable_oidc_login {
return unrecognized_error; return unrecognized_error;
}; }
// Advertise this homeserver's access URL as the issuer URL. // Advertise this homeserver's access URL as the issuer URL.
// Unwrap all Url::parse() calls because the issuer URL is validated at startup. // Unwrap all Url::parse() calls because the issuer URL is validated at startup.
let issuer = services.server.config.well_known.client.as_ref().unwrap(); let issuer = services.server.config.well_known.client.as_ref().unwrap();
let account_management_uri = auth let account_management_uri = auth.enable_oidc_account_management.then_some(
.enable_oidc_account_management issuer
.then_some(issuer.join("/_matrix/client/unstable/org.matrix.msc2964/account").unwrap()); .join("/_matrix/client/unstable/org.matrix.msc2964/account")
.unwrap(),
);
let metadata = AuthorizationServerMetadata { let metadata = AuthorizationServerMetadata {
issuer: issuer.clone(), issuer: issuer.clone(),
authorization_endpoint: authorization_endpoint: issuer
issuer.join("/_matrix/client/unstable/org.matrix.msc2964/authorize").unwrap(), .join("/_matrix/client/unstable/org.matrix.msc2964/authorize")
device_authorization_endpoint: .unwrap(),
Some(issuer.join("/_matrix/client/unstable/org.matrix.msc2964/device").unwrap()), device_authorization_endpoint: Some(
token_endpoint: issuer
issuer.join("/_matrix/client/unstable/org.matrix.msc2964/token").unwrap(), .join("/_matrix/client/unstable/org.matrix.msc2964/device")
registration_endpoint: .unwrap(),
Some(issuer.join("/_matrix/client/unstable/org.matrix.msc2964/device/register").unwrap()), ),
revocation_endpoint: token_endpoint: issuer
issuer.join("_matrix/client/unstable/org.matrix.msc2964/revoke").unwrap(), .join("/_matrix/client/unstable/org.matrix.msc2964/token")
.unwrap(),
registration_endpoint: Some(
issuer
.join("/_matrix/client/unstable/org.matrix.msc2964/device/register")
.unwrap(),
),
revocation_endpoint: issuer
.join("_matrix/client/unstable/org.matrix.msc2964/revoke")
.unwrap(),
response_types_supported: [ResponseType::Code].into(), response_types_supported: [ResponseType::Code].into(),
grant_types_supported: [GrantType::AuthorizationCode, GrantType::RefreshToken].into(), grant_types_supported: [GrantType::AuthorizationCode, GrantType::RefreshToken].into(),
response_modes_supported: [ResponseMode::Fragment, ResponseMode::Query].into(), response_modes_supported: [ResponseMode::Fragment, ResponseMode::Query].into(),
@ -77,11 +82,12 @@ pub(crate) async fn get_auth_metadata(
AccountManagementAction::Profile, AccountManagementAction::Profile,
AccountManagementAction::SessionView, AccountManagementAction::SessionView,
AccountManagementAction::SessionEnd, AccountManagementAction::SessionEnd,
].into(), ]
.into(),
prompt_values_supported: match services.server.config.allow_registration { prompt_values_supported: match services.server.config.allow_registration {
| true => vec![Prompt::Create], | true => vec![Prompt::Create],
| false => vec![] | false => vec![],
} },
}; };
let metadata = Raw::new(&metadata).expect("authorization server metadata should serialize"); let metadata = Raw::new(&metadata).expect("authorization server metadata should serialize");

View file

@ -1,16 +1,7 @@
use conduwuit_web::oidc::{
oidc_consent_form,
AuthorizationQuery,
LoginError,
LoginQuery,
OidcRequest,
OidcResponse,
};
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{Result, err, utils::hash::verify_password};
Result, use conduwuit_web::oidc::{
err, AuthorizationQuery, LoginError, LoginQuery, OidcRequest, OidcResponse, oidc_consent_form,
utils::hash::verify_password,
}; };
use ruma::user_id::UserId; use ruma::user_id::UserId;
@ -25,17 +16,15 @@ pub(crate) async fn oidc_login(
State(services): State<crate::State>, State(services): State<crate::State>,
request: OidcRequest, request: OidcRequest,
) -> Result<OidcResponse> { ) -> Result<OidcResponse> {
let query: LoginQuery = request.clone().try_into().map_err(|LoginError(err)| let query: LoginQuery = request.clone().try_into().map_err(|LoginError(err)| {
err!(Request(InvalidParam("Cannot process login form. {err}"))) err!(Request(InvalidParam("Cannot process login form. {err}")))
)?; })?;
// Only accept local usernames. Mostly to simplify things at first. // Only accept local usernames. Mostly to simplify things at first.
let user_id = UserId::parse_with_server_name( let user_id =
query.username.clone(), UserId::parse_with_server_name(query.username.clone(), &services.config.server_name)
&services.config.server_name
)
.map_err(|e| err!(Request(InvalidUsername("Username is invalid: {e}"))))?; .map_err(|e| err!(Request(InvalidUsername("Username is invalid: {e}"))))?;
if ! services.users.exists(&user_id).await { if !services.users.exists(&user_id).await {
return Err(err!(Request(Unknown("unknown username")))); return Err(err!(Request(Unknown("unknown username"))));
} }
tracing::info!("logging in: {user_id:?}"); tracing::info!("logging in: {user_id:?}");
@ -48,7 +37,7 @@ pub(crate) async fn oidc_login(
if valid_hash.is_empty() { if valid_hash.is_empty() {
return Err(err!(Request(UserDeactivated("the user's hash was not found")))); return Err(err!(Request(UserDeactivated("the user's hash was not found"))));
} }
if let Err(_) = verify_password(&query.password, &valid_hash) { if verify_password(&query.password, &valid_hash).is_err() {
return Err(err!(Request(InvalidParam("password does not match")))); return Err(err!(Request(InvalidParam("password does not match"))));
} }
tracing::info!("{user_id:?} passed, forwarding to consent page"); tracing::info!("{user_id:?} passed, forwarding to consent page");

View file

@ -1,25 +1,27 @@
/// OIDC //! OIDC
/// //!
/// Stands for OpenID Connect, and is an authentication scheme relying on OAuth2. //! Stands for OpenID Connect, and is an authentication scheme relying on
/// The [MSC2964] Matrix Spec Proposal describes an authentication process based //! OAuth2. The [MSC2964] Matrix Spec Proposal describes an authentication
/// on the OIDC flow, with restrictions. See the [sample flow] for details on //! process based on the OIDC flow, with restrictions. See the [sample flow] for
/// what's expected. //! details on what's expected.
/// //!
/// This module implements the needed endpoints. It relies on the [oxide-auth] //! This module implements the needed endpoints. It relies on the [oxide-auth]
/// crate, and the [`service::oidc`] and [`web::oidc`] modules. //! crate, and the [`service::oidc`] and [`web::oidc`] modules.
/// //!
/// [MSC2964]: https://github.com/matrix-org/matrix-spec-proposals/pull/2964 //! [MSC2964]: https://github.com/matrix-org/matrix-spec-proposals/pull/2964
/// [oxide-auth]: https://docs.rs/oxide-auth //! [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 //! [sample flow]: https://github.com/sandhose/matrix-spec-proposals/blob/msc/sandhose/oauth2-profile/proposals/2964-oauth2-profile.md#sample-flow
mod authorize;
mod discovery; mod discovery;
mod login; mod login;
mod authorize;
mod token;
mod register; mod register;
mod token;
pub(crate) use self::discovery::get_auth_metadata; pub(crate) use self::{
pub(crate) use self::login::oidc_login; authorize::{authorize, authorize_consent},
pub(crate) use self::authorize::{authorize, authorize_consent}; discovery::get_auth_metadata,
pub(crate) use self::register::register_client; login::oidc_login,
pub(crate) use self::token::token; register::register_client,
token::token,
};

View file

@ -1,22 +1,20 @@
use oxide_auth::primitives::prelude::Client; use axum::{Json, extract::State};
use axum::{
Json,
extract::State,
};
use conduwuit::{Result, err}; use conduwuit::{Result, err};
use ruma::DeviceId; use oxide_auth::primitives::prelude::Client;
use reqwest::Url; use reqwest::Url;
use ruma::DeviceId;
/// The required parameters to register a new client for OAuth2 application. /// The required parameters to register a new client for OAuth2 application.
#[derive(serde::Deserialize, Clone)] #[derive(serde::Deserialize, Clone)]
pub(crate) struct ClientQuery { pub(crate) struct ClientQuery {
/// Human-readable name. /// Human-readable name.
client_name: String, client_name: String,
/// A public page that tells more about the client. All other links must be within. /// A public page that tells more about the client. All other links must be
/// within.
client_uri: Url, client_uri: Url,
/// Redirect URIs declared by the client. At least one. /// Redirect URIs declared by the client. At least one.
redirect_uris: Vec<Url>, redirect_uris: Vec<Url>,
/// Must be ["code"]. /// Must be `["code"]`.
response_types: Vec<String>, response_types: Vec<String>,
/// Must include "authorization_type" and "refresh_token". /// Must include "authorization_type" and "refresh_token".
grant_types: Vec<String>, grant_types: Vec<String>,
@ -51,8 +49,9 @@ pub(crate) struct ClientResponse {
/// # `GET /_matrix/client/unstable/org.matrix.msc2964/device/register` /// # `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
/// will have the right to submit [super::authorize::authorize] requests. /// parlance, will have the right to submit [super::authorize::authorize]
/// requests.
/// ///
/// [MSC2966]: https://github.com/matrix-org/matrix-spec-proposals/pull/2966 /// [MSC2966]: https://github.com/matrix-org/matrix-spec-proposals/pull/2966
pub(crate) async fn register_client( pub(crate) async fn register_client(
@ -66,15 +65,17 @@ pub(crate) async fn register_client(
}; };
let device_id = DeviceId::new(); let device_id = DeviceId::new();
let scope = format!( let scope = format!(
"urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:{}", "urn:matrix:org.matrix.msc2967.client:api:* \
device_id urn:matrix:org.matrix.msc2967.client:device:{device_id}"
); );
// TODO check if the users service needs an update. // TODO check if the users service needs an update.
//services.users.update_device_metadata(); //services.users.update_device_metadata();
services.oidc.register_client(&Client::public( services.oidc.register_client(&Client::public(
&device_id.to_string(), device_id.as_ref(),
redirect_uri.into(), redirect_uri.into(),
scope.parse().expect("device ID should parse in Matrix scope"), scope
.parse()
.expect("device ID should parse in Matrix scope"),
))?; ))?;
Ok(Json(ClientResponse { Ok(Json(ClientResponse {
@ -88,6 +89,6 @@ pub(crate) async fn register_client(
token_endpoint_auth_method: client.token_endpoint_auth_method.clone(), token_endpoint_auth_method: client.token_endpoint_auth_method.clone(),
response_types: client.response_types.clone(), response_types: client.response_types.clone(),
grant_types: client.grant_types.clone(), grant_types: client.grant_types.clone(),
application_type: client.application_type.clone(), application_type: client.application_type,
})) }))
} }

View file

@ -1,7 +1,7 @@
use conduwuit_web::oidc::{OidcRequest, OidcResponse};
use conduwuit::{Result, err};
use oxide_auth::endpoint::QueryParameter;
use axum::extract::State; use axum::extract::State;
use conduwuit::{Result, err};
use conduwuit_web::oidc::{OidcRequest, OidcResponse};
use oxide_auth::endpoint::QueryParameter;
/// # `POST /_matrix/client/unstable/org.matrix.msc2964/token` /// # `POST /_matrix/client/unstable/org.matrix.msc2964/token`
/// ///
@ -20,17 +20,14 @@ pub(crate) async fn token(
let endpoint = services.oidc.endpoint(); let endpoint = services.oidc.endpoint();
match grant_type.as_deref() { match grant_type.as_deref() {
| Some("authorization_code") => | Some("authorization_code") => endpoint
endpoint
.access_token_flow() .access_token_flow()
.execute(oauth) .execute(oauth)
.map_err(|err| err!(Request(Unknown("token grant failed: {err:?}")))), .map_err(|err| err!(Request(Unknown("token grant failed: {err:?}")))),
| Some("refresh_token") => | Some("refresh_token") => endpoint
endpoint
.refresh_flow() .refresh_flow()
.execute(oauth) .execute(oauth)
.map_err(|err| err!(Request(Unknown("token refresh failed: {err:?}")))), .map_err(|err| err!(Request(Unknown("token refresh failed: {err:?}")))),
| other => | other => Err(err!(Request(Unknown("unsupported grant type: {other:?}")))),
Err(err!(Request(Unknown("unsupported grant type: {other:?}")))),
} }
} }

View file

@ -3,10 +3,7 @@ use conduwuit::{Error, Result};
use ruma::api::client::{ use ruma::api::client::{
discovery::{ discovery::{
discover_homeserver::{ discover_homeserver::{
self, self, AuthenticationServerInfo, HomeserverInfo, SlidingSyncProxyInfo,
HomeserverInfo,
SlidingSyncProxyInfo,
AuthenticationServerInfo,
}, },
discover_support::{self, Contact}, discover_support::{self, Contact},
}, },
@ -32,16 +29,14 @@ pub(crate) async fn well_known_client(
identity_server: None, identity_server: None,
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url.clone() }), sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url.clone() }),
tile_server: None, tile_server: None,
authentication: services.config.auth.as_ref().and_then(|auth| authentication: services.config.auth.as_ref().and_then(|auth| {
auth.enable_oidc_login.then_some( auth.enable_oidc_login
AuthenticationServerInfo::new( .then_some(AuthenticationServerInfo::new(
client_url.clone(), client_url.clone(),
auth.enable_oidc_account_management.then_some( auth.enable_oidc_account_management
format!("{client_url}/account") .then_some(format!("{client_url}/account")),
) ))
) }),
)
)
}) })
} }

View file

@ -272,13 +272,11 @@ pub fn check(config: &Config) -> Result {
} }
if let Some(auth) = &config.auth { if let Some(auth) = &config.auth {
if auth.enable_oidc_login { if auth.enable_oidc_login && config.well_known.client.is_none() {
if config.well_known.client.is_none() {
return Err!(Config( return Err!(Config(
"auth.enable_oidc_login", "auth.enable_oidc_login",
"Oidc authentication is enabled but the well-known client is not set." "Oidc authentication is enabled but the well-known client is not set."
)) ));
}
} }
} }

View file

@ -1,33 +1,26 @@
/// OIDC service. //! OIDC service.
/// //!
/// Provides the registrar, authorizer and issuer needed by [api::client::oidc]. //! Provides the registrar, authorizer and issuer needed by [api::client::oidc].
/// The whole OAuth2 flow is taken care of by [oxide-auth]. //! The whole OAuth2 flow is taken care of by [oxide-auth].
/// //!
/// TODO this service 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 //! [oxide-auth]: https://docs.rs/oxide-auth
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use conduwuit::Result; use conduwuit::Result;
use oxide_auth::{ use oxide_auth::{
frontends::simple::endpoint::{Generic, Vacant}, frontends::simple::endpoint::{Generic, Vacant},
primitives::{ primitives::{
prelude::{ prelude::{
AuthMap, AuthMap, Authorizer, Client, ClientMap, Issuer, RandomGenerator, Registrar, TokenMap,
Authorizer,
Client,
ClientMap,
Issuer,
RandomGenerator,
Registrar,
TokenMap,
}, },
registrar::RegisteredUrl, registrar::RegisteredUrl,
}, },
}; };
use async_trait::async_trait;
use std::sync::{Arc, Mutex};
pub struct Service { pub struct Service {
registrar: Mutex<ClientMap>, registrar: Mutex<ClientMap>,
authorizer: Mutex<AuthMap<RandomGenerator>>, authorizer: Mutex<AuthMap<RandomGenerator>>,
@ -36,17 +29,14 @@ pub struct Service {
#[async_trait] #[async_trait]
impl crate::Service for Service { impl crate::Service for Service {
fn build(_args: crate::Args<'_>) -> Result<Arc<Self>> { fn build(_args: crate::Args<'_>) -> Result<Arc<Self>> { Ok(Arc::new(Self::preconfigured())) }
Ok(Arc::new(Self::preconfigured()))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
} }
impl Service { impl Service {
pub fn register_client(&self, client: &Client) -> Result<()> { pub fn register_client(&self, client: &Client) -> Result<()> {
self self.registrar
.registrar
.lock() .lock()
.expect("lockable registrar") .expect("lockable registrar")
.register_client(client.clone()); .register_client(client.clone());
@ -54,8 +44,9 @@ impl Service {
Ok(()) Ok(())
} }
#[must_use]
pub fn preconfigured() -> Self { pub fn preconfigured() -> Self {
Service { Self {
registrar: Mutex::new( registrar: Mutex::new(
vec![Client::public( vec![Client::public(
"LocalClient", "LocalClient",
@ -80,7 +71,9 @@ impl Service {
} }
/// The oxide-auth carry-all endpoint. /// The oxide-auth carry-all endpoint.
pub fn endpoint(&self) -> Generic<impl Registrar + '_, impl Authorizer + '_, impl Issuer + '_> { pub fn endpoint(
&self,
) -> Generic<impl Registrar + '_, impl Authorizer + '_, impl Issuer + '_> {
Generic { Generic {
registrar: self.registrar.lock().unwrap(), registrar: self.registrar.lock().unwrap(),
authorizer: self.authorizer.lock().unwrap(), authorizer: self.authorizer.lock().unwrap(),

View file

@ -13,9 +13,9 @@ use crate::{
account_data, admin, announcements, appservice, client, config, emergency, federation, account_data, admin, announcements, appservice, client, config, emergency, federation,
globals, key_backups, globals, key_backups,
manager::Manager, manager::Manager,
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service, media, moderation, oidc, presence, pusher, resolver, rooms, sending, server_keys, service,
service::{Args, Map, Service}, service::{Args, Map, Service},
sync, transaction_ids, uiaa, users, oidc, sync, transaction_ids, uiaa, users,
}; };
pub struct Services { pub struct Services {

View file

@ -1,20 +1,19 @@
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use askama::Template; use askama::Template;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
// Imports needed by askama templates. // Imports needed by askama templates.
use crate::{ use crate::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL, VERSION_EXTRA};
VERSION_EXTRA, GIT_REMOTE_WEB_URL, GIT_REMOTE_COMMIT_URL,
};
mod authorize; mod authorize;
mod consent; mod consent;
mod error; mod error;
mod login; mod login;
mod response;
mod request; mod request;
mod response;
pub use authorize::AuthorizationQuery; pub use authorize::AuthorizationQuery;
pub use consent::oidc_consent_form; pub use consent::oidc_consent_form;
pub use error::OidcError; pub use error::OidcError;
pub use login::{LoginQuery, LoginError, oidc_login_form}; pub use login::{LoginError, LoginQuery, oidc_login_form};
pub use request::OidcRequest; pub use request::OidcRequest;
pub use response::OidcResponse; pub use response::OidcResponse;
@ -35,7 +34,6 @@ pub(crate) struct LoginPageTemplate<'a> {
response_mode: &'a str, response_mode: &'a str,
} }
/// The parameters for the OIDC consent page template. /// The parameters for the OIDC consent page template.
#[derive(Template)] #[derive(Template)]
#[template(path = "consent.html.j2")] #[template(path = "consent.html.j2")]

View file

@ -1,4 +1,5 @@
use url::Url; use url::Url;
use super::LoginQuery; use super::LoginQuery;
/// The set of parameters required for an OIDC authorization request. /// The set of parameters required for an OIDC authorization request.
@ -28,7 +29,7 @@ impl From<LoginQuery> for AuthorizationQuery {
.. ..
} = value; } = value;
AuthorizationQuery { Self {
client_id, client_id,
redirect_uri, redirect_uri,
scope, scope,
@ -40,4 +41,3 @@ impl From<LoginQuery> for AuthorizationQuery {
} }
} }
} }

View file

@ -1,20 +1,14 @@
use super::{
encode,
ConsentPageTemplate,
AuthorizationQuery,
OidcResponse,
};
use askama::Template; use askama::Template;
use oxide_auth::frontends::simple::request::{Body, Status}; use oxide_auth::frontends::simple::request::{Body, Status};
use super::{AuthorizationQuery, ConsentPageTemplate, OidcResponse, encode};
/// A web consent solicitor form for the OIDC authentication flow. /// A web consent solicitor form for the OIDC authentication flow.
/// ///
/// Asks the resource owner for their consent to let a client access their data /// Asks the resource owner for their consent to let a client access their data
/// on this server. /// on this server.
pub fn oidc_consent_form( #[must_use]
hostname: &str, pub fn oidc_consent_form(hostname: &str, query: &AuthorizationQuery) -> OidcResponse {
query: &AuthorizationQuery,
) -> OidcResponse {
// The target request route. // The target request route.
let route = "/_matrix/client/unstable/org.matrix.msc2964/authorize"; let route = "/_matrix/client/unstable/org.matrix.msc2964/authorize";
let nonce = rand::random::<u64>().to_string(); let nonce = rand::random::<u64>().to_string();
@ -30,12 +24,7 @@ pub fn oidc_consent_form(
} }
/// Render the html contents of the user consent page. /// Render the html contents of the user consent page.
fn consent_page( fn consent_page(hostname: &str, query: &AuthorizationQuery, route: &str, nonce: &str) -> String {
hostname: &str,
query: &AuthorizationQuery,
route: &str,
nonce: &str,
) -> String {
let template = ConsentPageTemplate { let template = ConsentPageTemplate {
nonce, nonce,
hostname, hostname,

View file

@ -1,16 +1,17 @@
use super::OidcRequest;
use axum::{ use axum::{
http::{header::InvalidHeaderValue, StatusCode}, http::{StatusCode, header::InvalidHeaderValue},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use oxide_auth::frontends::{dev::OAuthError, simple::endpoint::Error}; use oxide_auth::frontends::{dev::OAuthError, simple::endpoint::Error};
use super::OidcRequest;
#[derive(Debug)] #[derive(Debug)]
/// The error type for Oxide Auth operations /// The error type for Oxide Auth operations
pub enum OidcError { pub enum OidcError {
/// Errors occuring in Endpoint operations /// Errors occurring in Endpoint operations
Endpoint(OAuthError), Endpoint(OAuthError),
/// Errors occuring in Endpoint operations /// Errors occurring in Endpoint operations
Header(InvalidHeaderValue), Header(InvalidHeaderValue),
/// Errors with the request encoding /// Errors with the request encoding
Encoding, Encoding,
@ -29,15 +30,16 @@ pub enum OidcError {
impl std::fmt::Display for OidcError { impl std::fmt::Display for OidcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self { match *self {
OidcError::Endpoint(ref e) => write!(f, "Endpoint, {}", e), | Self::Endpoint(ref e) => write!(f, "Endpoint, {e}"),
OidcError::Header(ref e) => write!(f, "Couldn't set header, {}", e), | Self::Header(ref e) => write!(f, "Couldn't set header, {e}"),
OidcError::Encoding => write!(f, "Error decoding request"), | Self::Encoding => write!(f, "Error decoding request"),
OidcError::Form => write!(f, "Request is not a form"), | Self::Form => write!(f, "Request is not a form"),
OidcError::Query => write!(f, "No query present"), | Self::Query => write!(f, "No query present"),
OidcError::Body => write!(f, "No body present"), | Self::Body => write!(f, "No body present"),
OidcError::Authorization => write!(f, "Request has invalid Authorization headers"), | Self::Authorization => write!(f, "Request has invalid Authorization headers"),
OidcError::InternalError(None) => write!(f, "An internal server error occured"), | Self::InternalError(None) => write!(f, "An internal server error occurred"),
OidcError::InternalError(Some(ref e)) => write!(f, "An internal server error occured: {}", e), | Self::InternalError(Some(ref e)) =>
write!(f, "An internal server error occurred: {e}"),
} }
} }
} }
@ -45,9 +47,9 @@ impl std::fmt::Display for OidcError {
impl std::error::Error for OidcError { impl std::error::Error for OidcError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self { match *self {
OidcError::Endpoint(ref e) => e.source(), | Self::Endpoint(ref e) => e.source(),
OidcError::Header(ref e) => e.source(), | Self::Header(ref e) => e.source(),
_ => None, | _ => None,
} }
} }
} }
@ -61,20 +63,16 @@ impl IntoResponse for OidcError {
impl From<Error<OidcRequest>> for OidcError { impl From<Error<OidcRequest>> for OidcError {
fn from(e: Error<OidcRequest>) -> Self { fn from(e: Error<OidcRequest>) -> Self {
match e { match e {
Error::Web(e) => e, | Error::Web(e) => e,
Error::OAuth(e) => e.into(), | Error::OAuth(e) => e.into(),
} }
} }
} }
impl From<OAuthError> for OidcError { impl From<OAuthError> for OidcError {
fn from(e: OAuthError) -> Self { fn from(e: OAuthError) -> Self { Self::Endpoint(e) }
OidcError::Endpoint(e)
}
} }
impl From<InvalidHeaderValue> for OidcError { impl From<InvalidHeaderValue> for OidcError {
fn from(e: InvalidHeaderValue) -> Self { fn from(e: InvalidHeaderValue) -> Self { Self::Header(e) }
Self::Header(e)
}
} }

View file

@ -1,17 +1,13 @@
use super::{ use std::str::FromStr;
AuthorizationQuery,
LoginPageTemplate,
OidcRequest,
OidcResponse,
};
use askama::Template; use askama::Template;
use oxide_auth::{ use oxide_auth::{
endpoint::QueryParameter, endpoint::QueryParameter,
frontends::simple::request::{Body, Status}, frontends::simple::request::{Body, Status},
}; };
use url::Url; use url::Url;
use std::str::FromStr;
use super::{AuthorizationQuery, LoginPageTemplate, OidcRequest, OidcResponse};
/// The set of query parameters a client needs to get authorization. /// The set of query parameters a client needs to get authorization.
#[derive(serde::Deserialize, Debug, Clone)] #[derive(serde::Deserialize, Debug, Clone)]
@ -38,40 +34,40 @@ impl TryFrom<OidcRequest> for LoginQuery {
let body = value.body().expect("body in OidcRequest"); let body = value.body().expect("body in OidcRequest");
let Some(username) = body.unique_value("username") else { let Some(username) = body.unique_value("username") else {
return Err(LoginError("missing field: username".to_string())); return Err(LoginError("missing field: username".to_owned()));
}; };
let Some(password) = body.unique_value("password") else { let Some(password) = body.unique_value("password") else {
return Err(LoginError("missing field: password".to_string())); return Err(LoginError("missing field: password".to_owned()));
}; };
let Some(client_id) = body.unique_value("client_id") else { let Some(client_id) = body.unique_value("client_id") else {
return Err(LoginError("missing field: client_id".to_string())); return Err(LoginError("missing field: client_id".to_owned()));
}; };
let Some(redirect_uri) = body.unique_value("redirect_uri") else { let Some(redirect_uri) = body.unique_value("redirect_uri") else {
return Err(LoginError("missing field: redirect_uri".to_string())); return Err(LoginError("missing field: redirect_uri".to_owned()));
}; };
let Some(scope) = body.unique_value("scope") else { let Some(scope) = body.unique_value("scope") else {
return Err(LoginError("missing field: scope".to_string())); return Err(LoginError("missing field: scope".to_owned()));
}; };
let Some(state) = body.unique_value("state") else { let Some(state) = body.unique_value("state") else {
return Err(LoginError("missing field: state".to_string())); return Err(LoginError("missing field: state".to_owned()));
}; };
let Some(code_challenge) = body.unique_value("code_challenge") else { let Some(code_challenge) = body.unique_value("code_challenge") else {
return Err(LoginError("missing field: code_challenge".to_string())); return Err(LoginError("missing field: code_challenge".to_owned()));
}; };
let Some(code_challenge_method) = body.unique_value("code_challenge_method") else { let Some(code_challenge_method) = body.unique_value("code_challenge_method") else {
return Err(LoginError("missing field: code_challenge_method".to_string())); return Err(LoginError("missing field: code_challenge_method".to_owned()));
}; };
let Some(response_type) = body.unique_value("response_type") else { let Some(response_type) = body.unique_value("response_type") else {
return Err(LoginError("missing field: response_type".to_string())); return Err(LoginError("missing field: response_type".to_owned()));
}; };
let Some(response_mode) = body.unique_value("response_mode") else { let Some(response_mode) = body.unique_value("response_mode") else {
return Err(LoginError("missing field: response_mode".to_string())); return Err(LoginError("missing field: response_mode".to_owned()));
}; };
let Ok(redirect_uri) = Url::from_str(&redirect_uri) else { let Ok(redirect_uri) = Url::from_str(&redirect_uri) else {
return Err(LoginError("invalid field: redirect_uri".to_string())); return Err(LoginError("invalid field: redirect_uri".to_owned()));
}; };
Ok(LoginQuery { Ok(Self {
username: username.to_string(), username: username.to_string(),
password: password.to_string(), password: password.to_string(),
client_id: client_id.to_string(), client_id: client_id.to_string(),
@ -89,10 +85,8 @@ impl TryFrom<OidcRequest> for LoginQuery {
/// A web login form for the OIDC authentication flow. /// A web login form for the OIDC authentication flow.
/// ///
/// The returned `OidcResponse` handles CSP headers to allow that form. /// The returned `OidcResponse` handles CSP headers to allow that form.
pub fn oidc_login_form( #[must_use]
hostname: &str, pub fn oidc_login_form(hostname: &str, query: &AuthorizationQuery) -> OidcResponse {
query: &AuthorizationQuery,
) -> OidcResponse {
// The target request route. // The target request route.
let route = "/_matrix/client/unstable/org.matrix.msc2964/login"; let route = "/_matrix/client/unstable/org.matrix.msc2964/login";
let nonce = rand::random::<u64>().to_string(); let nonce = rand::random::<u64>().to_string();
@ -108,12 +102,7 @@ pub fn oidc_login_form(
} }
/// Render the html contents of the login page. /// Render the html contents of the login page.
fn login_page( fn login_page(hostname: &str, query: &AuthorizationQuery, route: &str, nonce: &str) -> String {
hostname: &str,
query: &AuthorizationQuery,
route: &str,
nonce: &str,
) -> String {
let template = LoginPageTemplate { let template = LoginPageTemplate {
nonce, nonce,
hostname, hostname,

View file

@ -1,11 +1,13 @@
use super::{OidcError, OidcResponse}; use std::borrow::Cow;
use oxide_auth::endpoint::{NormalizedParameter, QueryParameter, WebRequest};
use async_trait::async_trait; use async_trait::async_trait;
use axum::{ use axum::{
extract::{Form, FromRequest, FromRequestParts, Query, Request}, extract::{Form, FromRequest, FromRequestParts, Query, Request},
http::header, http::header,
}; };
use std::borrow::Cow; use oxide_auth::endpoint::{NormalizedParameter, QueryParameter, WebRequest};
use super::{OidcError, OidcResponse};
/// An OIDC authentication request. /// An OIDC authentication request.
/// ///
@ -23,24 +25,19 @@ pub struct OidcRequest {
impl OidcRequest { impl OidcRequest {
/// Fetch the authorization header from the request /// Fetch the authorization header from the request
pub fn authorization_header(&self) -> Option<&str> { #[must_use]
self.auth.as_deref() pub fn authorization_header(&self) -> Option<&str> { self.auth.as_deref() }
}
/// Fetch the query for this request /// Fetch the query for this request
pub fn query(&self) -> Option<&NormalizedParameter> { #[must_use]
self.query.as_ref() pub fn query(&self) -> Option<&NormalizedParameter> { self.query.as_ref() }
}
/// Fetch the query mutably /// Fetch the query mutably
pub fn query_mut(&mut self) -> Option<&mut NormalizedParameter> { pub fn query_mut(&mut self) -> Option<&mut NormalizedParameter> { self.query.as_mut() }
self.query.as_mut()
}
/// Fetch the body of the request /// Fetch the body of the request
pub fn body(&self) -> Option<&NormalizedParameter> { #[must_use]
self.body.as_ref() pub fn body(&self) -> Option<&NormalizedParameter> { self.body.as_ref() }
}
} }
impl WebRequest for OidcRequest { impl WebRequest for OidcRequest {

View file

@ -1,15 +1,16 @@
use super::{oidc_consent_form, LoginQuery, OidcError, OidcRequest}; use axum::{
body::Body,
http::{Response, header},
response::IntoResponse,
};
use oxide_auth::{ use oxide_auth::{
endpoint::{OwnerConsent, OwnerSolicitor, Solicitation, WebRequest, WebResponse}, endpoint::{OwnerConsent, OwnerSolicitor, Solicitation, WebRequest, WebResponse},
frontends::simple::request::{Body as OAuthRequestBody, Status}, frontends::simple::request::{Body as OAuthRequestBody, Status},
}; };
use axum::{
body::Body,
http::{header, Response},
response::IntoResponse,
};
use url::Url; use url::Url;
use super::{LoginQuery, OidcError, OidcRequest, oidc_consent_form};
/// A Web response that can be processed by the OIDC authentication flow before /// A Web response that can be processed by the OIDC authentication flow before
/// being sent over. /// being sent over.
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
@ -22,9 +23,9 @@ pub struct OidcResponse {
} }
impl OidcResponse { impl OidcResponse {
/// Instanciate from a response body. Used to send login or consent forms. /// Instantiate from a response body. Used to send login or consent forms.
pub fn from_body(body: &str) -> Result<Self, OidcError> { pub fn from_body(body: &str) -> Result<Self, OidcError> {
let mut result = OidcResponse::default(); let mut result = Self::default();
result.body_text(body)?; result.body_text(body)?;
Ok(result) Ok(result)
@ -33,17 +34,19 @@ impl OidcResponse {
impl IntoResponse for OidcResponse { impl IntoResponse for OidcResponse {
fn into_response(self) -> Response<Body> { fn into_response(self) -> Response<Body> {
let body = self.body.expect("body").as_str().to_string(); let body = self.body.expect("body").as_str().to_owned();
let response = Response::builder()
Response::builder()
.header(header::CONTENT_TYPE, "text/html") .header(header::CONTENT_TYPE, "text/html")
.header( .header(
header::CONTENT_SECURITY_POLICY, header::CONTENT_SECURITY_POLICY,
format!("default-src 'nonce-{}'; form-action https://eon.presentmatter.one/;", self.nonce) format!(
"default-src 'nonce-{}'; form-action https://eon.presentmatter.one/;",
self.nonce
),
) )
.body(body.into()) .body(body.into())
.unwrap(); .unwrap()
response
} }
} }
@ -62,10 +65,7 @@ impl OwnerSolicitor<OidcRequest> for OidcResponse {
.try_into() .try_into()
.expect("login query from OidcRequest"); .expect("login query from OidcRequest");
OwnerConsent::InProgress(oidc_consent_form( OwnerConsent::InProgress(oidc_consent_form(hostname, &query.into()))
hostname,
&query.into(),
))
} }
} }
@ -80,7 +80,8 @@ impl WebResponse for OidcResponse {
Ok(()) Ok(())
} }
/// A response which will redirect the user-agent to which the response is issued. /// A response which will redirect the user-agent to which the response is
/// issued.
fn redirect(&mut self, url: Url) -> Result<(), Self::Error> { fn redirect(&mut self, url: Url) -> Result<(), Self::Error> {
self.status = Status::Redirect; self.status = Status::Redirect;
self.location = Some(url); self.location = Some(url);
@ -110,7 +111,7 @@ impl WebResponse for OidcResponse {
Ok(()) Ok(())
} }
/// Json repsonse data, with media type `aplication/json. /// Json response data, with media type `aplication/json.
fn body_json(&mut self, data: &str) -> Result<(), Self::Error> { fn body_json(&mut self, data: &str) -> Result<(), Self::Error> {
self.body = Some(OAuthRequestBody::Json(data.to_owned())); self.body = Some(OAuthRequestBody::Json(data.to_owned()));
Ok(()) Ok(())