mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2025-06-26 21:06:36 +02:00
chore: Fix most clippy issue, format & typos
This commit is contained in:
parent
6c5289192d
commit
4f8afcf3e1
19 changed files with 369 additions and 409 deletions
|
@ -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)
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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,22 +50,23 @@ 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) =>
|
||||||
return Ok(oidc_login_form(hostname, &query));
|
if services.users.find_from_token(token).await.is_err() {
|
||||||
}
|
return Ok(oidc_login_form(hostname, &query));
|
||||||
|
},
|
||||||
}
|
}
|
||||||
// TODO register the device ID ?
|
// TODO register the device ID ?
|
||||||
|
|
||||||
services
|
services
|
||||||
.oidc
|
.oidc
|
||||||
.endpoint()
|
.endpoint()
|
||||||
.with_solicitor(oidc_consent_form(hostname, &query))
|
.with_solicitor(oidc_consent_form(hostname, &query))
|
||||||
.authorization_flow()
|
.authorization_flow()
|
||||||
.execute(oauth)
|
.execute(oauth)
|
||||||
.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:?}"))))
|
||||||
|
|
|
@ -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::{
|
discovery::get_authorization_server_metadata::{
|
||||||
Error as ClientError,
|
self,
|
||||||
ErrorKind as ClientErrorKind,
|
msc2965::{
|
||||||
ErrorBody as ClientErrorBody,
|
AccountManagementAction, AuthorizationServerMetadata, CodeChallengeMethod,
|
||||||
},
|
GrantType, Prompt, Response, ResponseMode, ResponseType,
|
||||||
discovery::get_authorization_server_metadata::{
|
},
|
||||||
self,
|
},
|
||||||
msc2965::{
|
error::{
|
||||||
AccountManagementAction,
|
Error as ClientError, ErrorBody as ClientErrorBody, ErrorKind as ClientErrorKind,
|
||||||
AuthorizationServerMetadata,
|
|
||||||
CodeChallengeMethod,
|
|
||||||
GrantType,
|
|
||||||
Prompt,
|
|
||||||
ResponseMode,
|
|
||||||
ResponseType,
|
|
||||||
Response,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
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_owned(),
|
||||||
message: "This homeserver doesn't do OIDC authentication.".to_string()
|
},
|
||||||
}
|
)));
|
||||||
)
|
|
||||||
));
|
|
||||||
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");
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
|
@ -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
|
||||||
client_uri: Url,
|
/// within.
|
||||||
|
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,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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") => endpoint
|
||||||
| Some("refresh_token") =>
|
.refresh_flow()
|
||||||
endpoint
|
.execute(oauth)
|
||||||
.refresh_flow()
|
.map_err(|err| err!(Request(Unknown("token refresh failed: {err:?}")))),
|
||||||
.execute(oauth)
|
| other => Err(err!(Request(Unknown("unsupported grant type: {other:?}")))),
|
||||||
.map_err(|err| err!(Request(Unknown("token refresh failed: {err:?}")))),
|
|
||||||
| other =>
|
|
||||||
Err(err!(Request(Unknown("unsupported grant type: {other:?}")))),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")),
|
||||||
)
|
))
|
||||||
)
|
}),
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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."
|
));
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -27,8 +28,8 @@ impl From<LoginQuery> for AuthorizationQuery {
|
||||||
response_mode,
|
response_mode,
|
||||||
..
|
..
|
||||||
} = value;
|
} = value;
|
||||||
|
|
||||||
AuthorizationQuery {
|
Self {
|
||||||
client_id,
|
client_id,
|
||||||
redirect_uri,
|
redirect_uri,
|
||||||
scope,
|
scope,
|
||||||
|
@ -40,4 +41,3 @@ impl From<LoginQuery> for AuthorizationQuery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,80 +1,78 @@
|
||||||
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,
|
||||||
/// Request body could not be parsed as a form
|
/// Request body could not be parsed as a form
|
||||||
Form,
|
Form,
|
||||||
/// Request query was absent or could not be parsed
|
/// Request query was absent or could not be parsed
|
||||||
Query,
|
Query,
|
||||||
/// Request query was absent or could not be parsed
|
/// Request query was absent or could not be parsed
|
||||||
Body,
|
Body,
|
||||||
/// The Authorization header was invalid
|
/// The Authorization header was invalid
|
||||||
Authorization,
|
Authorization,
|
||||||
/// General internal server error
|
/// General internal server error
|
||||||
InternalError(Option<String>),
|
InternalError(Option<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
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}"),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for OidcError {
|
impl IntoResponse for OidcError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
|
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
@ -16,84 +18,79 @@ use std::borrow::Cow;
|
||||||
/// [oxide-auth-axum]: https://docs.rs/oxide-auth-axum
|
/// [oxide-auth-axum]: https://docs.rs/oxide-auth-axum
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct OidcRequest {
|
pub struct OidcRequest {
|
||||||
pub(crate) auth: Option<String>,
|
pub(crate) auth: Option<String>,
|
||||||
pub(crate) query: Option<NormalizedParameter>,
|
pub(crate) query: Option<NormalizedParameter>,
|
||||||
pub(crate) body: Option<NormalizedParameter>,
|
pub(crate) body: Option<NormalizedParameter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
type Error = OidcError;
|
type Error = OidcError;
|
||||||
type Response = OidcResponse;
|
type Response = OidcResponse;
|
||||||
|
|
||||||
fn query(&mut self) -> Result<Cow<'_, dyn QueryParameter + 'static>, Self::Error> {
|
fn query(&mut self) -> Result<Cow<'_, dyn QueryParameter + 'static>, Self::Error> {
|
||||||
self.query
|
self.query
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|q| Cow::Borrowed(q as &dyn QueryParameter))
|
.map(|q| Cow::Borrowed(q as &dyn QueryParameter))
|
||||||
.ok_or(OidcError::Query)
|
.ok_or(OidcError::Query)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn urlbody(&mut self) -> Result<Cow<'_, dyn QueryParameter + 'static>, Self::Error> {
|
fn urlbody(&mut self) -> Result<Cow<'_, dyn QueryParameter + 'static>, Self::Error> {
|
||||||
self.body
|
self.body
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|b| Cow::Borrowed(b as &dyn QueryParameter))
|
.map(|b| Cow::Borrowed(b as &dyn QueryParameter))
|
||||||
.ok_or(OidcError::Body)
|
.ok_or(OidcError::Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn authheader(&mut self) -> Result<Option<Cow<'_, str>>, Self::Error> {
|
fn authheader(&mut self) -> Result<Option<Cow<'_, str>>, Self::Error> {
|
||||||
Ok(self.auth.as_deref().map(Cow::Borrowed))
|
Ok(self.auth.as_deref().map(Cow::Borrowed))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<S> FromRequest<S> for OidcRequest
|
impl<S> FromRequest<S> for OidcRequest
|
||||||
where
|
where
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
{
|
{
|
||||||
type Rejection = OidcError;
|
type Rejection = OidcError;
|
||||||
|
|
||||||
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
let mut all_auth = req.headers().get_all(header::AUTHORIZATION).iter();
|
let mut all_auth = req.headers().get_all(header::AUTHORIZATION).iter();
|
||||||
let optional = all_auth.next();
|
let optional = all_auth.next();
|
||||||
|
|
||||||
let auth = if all_auth.next().is_some() {
|
let auth = if all_auth.next().is_some() {
|
||||||
return Err(OidcError::Authorization);
|
return Err(OidcError::Authorization);
|
||||||
} else {
|
} else {
|
||||||
optional.and_then(|hv| hv.to_str().ok().map(str::to_owned))
|
optional.and_then(|hv| hv.to_str().ok().map(str::to_owned))
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mut parts, body) = req.into_parts();
|
let (mut parts, body) = req.into_parts();
|
||||||
let query = Query::from_request_parts(&mut parts, state)
|
let query = Query::from_request_parts(&mut parts, state)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.map(|q: Query<NormalizedParameter>| q.0);
|
.map(|q: Query<NormalizedParameter>| q.0);
|
||||||
|
|
||||||
let req = Request::from_parts(parts, body);
|
let req = Request::from_parts(parts, body);
|
||||||
let body = Form::from_request(req, state)
|
let body = Form::from_request(req, state)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.map(|b: Form<NormalizedParameter>| b.0);
|
.map(|b: Form<NormalizedParameter>| b.0);
|
||||||
|
|
||||||
// If the query is empty and the body has a request, copy it over
|
// If the query is empty and the body has a request, copy it over
|
||||||
// because login forms are POST requests but OAuth flow expects
|
// because login forms are POST requests but OAuth flow expects
|
||||||
|
@ -110,6 +107,6 @@ where
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self { auth, query, body })
|
Ok(Self { auth, query, body })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,39 +80,40 @@ 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
|
||||||
fn redirect(&mut self, url: Url) -> Result<(), Self::Error> {
|
/// issued.
|
||||||
self.status = Status::Redirect;
|
fn redirect(&mut self, url: Url) -> Result<(), Self::Error> {
|
||||||
self.location = Some(url);
|
self.status = Status::Redirect;
|
||||||
self.www_authenticate = None;
|
self.location = Some(url);
|
||||||
Ok(())
|
self.www_authenticate = None;
|
||||||
}
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the response status to 400.
|
/// Set the response status to 400.
|
||||||
fn client_error(&mut self) -> Result<(), Self::Error> {
|
fn client_error(&mut self) -> Result<(), Self::Error> {
|
||||||
self.status = Status::BadRequest;
|
self.status = Status::BadRequest;
|
||||||
self.location = None;
|
self.location = None;
|
||||||
self.www_authenticate = None;
|
self.www_authenticate = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the response status to 401 and add a `WWW-Authenticate` header.
|
/// Set the response status to 401 and add a `WWW-Authenticate` header.
|
||||||
fn unauthorized(&mut self, header_value: &str) -> Result<(), Self::Error> {
|
fn unauthorized(&mut self, header_value: &str) -> Result<(), Self::Error> {
|
||||||
self.status = Status::Unauthorized;
|
self.status = Status::Unauthorized;
|
||||||
self.location = None;
|
self.location = None;
|
||||||
self.www_authenticate = Some(header_value.to_owned());
|
self.www_authenticate = Some(header_value.to_owned());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A pure text response with no special media type set.
|
/// A pure text response with no special media type set.
|
||||||
fn body_text(&mut self, text: &str) -> Result<(), Self::Error> {
|
fn body_text(&mut self, text: &str) -> Result<(), Self::Error> {
|
||||||
self.body = Some(OAuthRequestBody::Text(text.to_owned()));
|
self.body = Some(OAuthRequestBody::Text(text.to_owned()));
|
||||||
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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue