mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-05-14 09:15:06 +00:00
feat: Allow configuring the OAuth compatibility mode
This commit is contained in:
@@ -1989,3 +1989,16 @@
|
||||
# provide an email address.
|
||||
#
|
||||
#require_email_for_token_registration = false
|
||||
|
||||
#[global.oauth]
|
||||
|
||||
# The compatibility mode to use for OAuth.
|
||||
#
|
||||
# - "disabled": OAuth will be unavailable. Users will only be able to log
|
||||
# in using legacy authentication.
|
||||
# - "hybrid": OAuth and legacy authentication will both be available. Some
|
||||
# clients may only use one or the other.
|
||||
# - "exclusive": Only OAuth will be available. Clients which require
|
||||
# legacy authentication will be unable to log in.
|
||||
#
|
||||
#compatibility_mode = "hybrid"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::State,
|
||||
extract::{Request, State},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
routing::method_routing::{get, post},
|
||||
};
|
||||
use const_str::concat;
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
pub(crate) use server_metadata::*;
|
||||
|
||||
@@ -19,14 +22,28 @@
|
||||
const TOKEN_PATH: &str = "grant/token";
|
||||
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
|
||||
|
||||
pub(crate) fn router() -> Router<crate::State> {
|
||||
Router::new().nest(BASE_PATH, oauth_router())
|
||||
// TODO(unspecced): used by old versions of the matrix-js-sdk
|
||||
.route("/.well-known/openid-configuration", get(
|
||||
async |State(services): State<crate::State>| {
|
||||
Json(authorization_server_metadata(&services).await)
|
||||
}
|
||||
))
|
||||
pub(crate) fn router(state: crate::State) -> Router<crate::State> {
|
||||
Router::new()
|
||||
.nest(BASE_PATH, oauth_router())
|
||||
.route(
|
||||
"/.well-known/openid-configuration",
|
||||
get(
|
||||
// TODO(unspecced): used by old versions of the matrix-js-sdk
|
||||
async |State(services): State<crate::State>| {
|
||||
Json(authorization_server_metadata(&services).await)
|
||||
},
|
||||
),
|
||||
)
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state,
|
||||
async |State(state): State<crate::State>, request: Request, next: Next| -> Response {
|
||||
if state.config.oauth.compatibility_mode.oauth_available() {
|
||||
next.run(request).await
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response()
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn oauth_router() -> Router<crate::State> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::Result;
|
||||
use conduwuit::{Err, Result};
|
||||
use ruma::{
|
||||
api::client::discovery::get_authorization_server_metadata::{
|
||||
self, v1::AccountManagementAction,
|
||||
@@ -21,6 +21,10 @@ pub(crate) async fn get_authorization_server_metadata_route(
|
||||
State(services): State<crate::State>,
|
||||
_body: Ruma<get_authorization_server_metadata::v1::Request>,
|
||||
) -> Result<get_authorization_server_metadata::v1::Response> {
|
||||
if !services.config.oauth.compatibility_mode.oauth_available() {
|
||||
return Err!(Request(Unrecognized("OAuth is unavailable on this server")));
|
||||
}
|
||||
|
||||
let metadata = Raw::new(&authorization_server_metadata(&services).await).unwrap();
|
||||
|
||||
Ok(get_authorization_server_metadata::v1::Response::new(metadata.cast_unchecked()))
|
||||
|
||||
@@ -43,6 +43,12 @@ pub(crate) async fn get_login_types_route(
|
||||
ClientIp(client): ClientIp,
|
||||
_body: Ruma<get_login_types::v3::Request>,
|
||||
) -> Result<get_login_types::v3::Response> {
|
||||
if !services.config.oauth.compatibility_mode.uiaa_available() {
|
||||
return Err!(Request(Unrecognized(
|
||||
"User-interactive authentication is not available on this server."
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(get_login_types::v3::Response::new(vec![
|
||||
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
|
||||
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
||||
@@ -118,10 +124,15 @@ pub(crate) async fn login_route(
|
||||
ClientIp(client): ClientIp,
|
||||
body: Ruma<login::v3::Request>,
|
||||
) -> Result<login::v3::Response> {
|
||||
if !services.config.oauth.compatibility_mode.uiaa_available() {
|
||||
return Err!(Request(Unrecognized(
|
||||
"User-interactive authentication is not available on this server."
|
||||
)));
|
||||
}
|
||||
|
||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||
|
||||
// Validate login method
|
||||
// TODO: Other login methods
|
||||
let user_id = match &body.login_info {
|
||||
#[allow(deprecated)]
|
||||
| login::v3::LoginInfo::Password(login::v3::Password {
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{any, get, post},
|
||||
};
|
||||
use conduwuit::{Server, err};
|
||||
use conduwuit::err;
|
||||
pub(super) use conduwuit_service::state::State;
|
||||
use http::{Uri, uri};
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
pub(super) use self::{args::Args as Ruma, response::RumaResponse};
|
||||
use crate::{admin, client, server};
|
||||
|
||||
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
let config = &server.config;
|
||||
pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
let config = &state.server.config;
|
||||
let mut router = router
|
||||
.ruma_route(&client::appservice_ping)
|
||||
.ruma_route(&client::get_supported_versions_route)
|
||||
@@ -186,7 +186,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&client::get_rtc_transports)
|
||||
.ruma_route(&client::room_initial_sync_route)
|
||||
.ruma_route(&client::get_authorization_server_metadata_route)
|
||||
.merge(client::oauth::router())
|
||||
.merge(client::oauth::router(state))
|
||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
|
||||
@@ -670,6 +670,10 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub registration_terms: HashMap<String, HashMap<String, TermsDocument>>,
|
||||
|
||||
/// display: nested
|
||||
#[serde(default)]
|
||||
pub oauth: OauthConfig,
|
||||
|
||||
/// Controls whether encrypted rooms and events are allowed.
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_encryption: bool,
|
||||
@@ -2372,6 +2376,43 @@ pub struct TermsDocument {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.oauth",
|
||||
optional = "true"
|
||||
)]
|
||||
pub struct OauthConfig {
|
||||
/// The compatibility mode to use for OAuth.
|
||||
///
|
||||
/// - "disabled": OAuth will be unavailable. Users will only be able to log
|
||||
/// in using legacy authentication.
|
||||
/// - "hybrid": OAuth and legacy authentication will both be available. Some
|
||||
/// clients may only use one or the other.
|
||||
/// - "exclusive": Only OAuth will be available. Clients which require
|
||||
/// legacy authentication will be unable to log in.
|
||||
///
|
||||
/// default: "hybrid"
|
||||
pub compatibility_mode: OAuthMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OAuthMode {
|
||||
Disabled,
|
||||
#[default]
|
||||
Hybrid,
|
||||
Exclusive,
|
||||
}
|
||||
|
||||
impl OAuthMode {
|
||||
#[must_use]
|
||||
pub fn uiaa_available(&self) -> bool { matches!(self, Self::Disabled | Self::Hybrid) }
|
||||
|
||||
#[must_use]
|
||||
pub fn oauth_available(&self) -> bool { matches!(self, Self::Hybrid | Self::Exclusive) }
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str] = &[
|
||||
"cache_capacity",
|
||||
"conduit_cache_capacity_modifier",
|
||||
|
||||
@@ -73,11 +73,8 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
||||
// 413
|
||||
| TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
|
||||
|
||||
// 405
|
||||
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
|
||||
|
||||
// 404
|
||||
| NotFound => StatusCode::NOT_FOUND,
|
||||
| Unrecognized | NotFound => StatusCode::NOT_FOUND,
|
||||
|
||||
// 403
|
||||
| GuestAccessForbidden
|
||||
|
||||
@@ -112,7 +112,9 @@ fn handle_result(method: &Method, uri: &Uri, result: Response) -> Result<Respons
|
||||
}
|
||||
|
||||
if status == StatusCode::METHOD_NOT_ALLOWED {
|
||||
return Ok(err!(Request(Unrecognized("Method Not Allowed"))).into_response());
|
||||
return Ok(
|
||||
err!(Request(Unrecognized("Method not allowed"), METHOD_NOT_ALLOWED)).into_response()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
|
||||
let router = Router::<state::State>::new();
|
||||
let (state, guard) = state::create(services.clone());
|
||||
let router = conduwuit_api::router::build(router, &services.server)
|
||||
let router = conduwuit_api::router::build(router, state)
|
||||
.merge(conduwuit_web::build(services))
|
||||
.fallback(not_found)
|
||||
.with_state(state);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
Dep, config,
|
||||
Dep,
|
||||
oauth::{
|
||||
client_metadata::{ApplicationType, ClientMetadata, ResponseType},
|
||||
grant::{
|
||||
@@ -44,7 +44,6 @@ struct Data {
|
||||
}
|
||||
|
||||
struct Services {
|
||||
config: Dep<config::Service>,
|
||||
users: Dep<users::Service>,
|
||||
}
|
||||
|
||||
@@ -111,7 +110,6 @@ impl crate::Service for Service {
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
services: Services {
|
||||
config: args.depend::<config::Service>("config"),
|
||||
users: args.depend::<users::Service>("users"),
|
||||
},
|
||||
db: Data {
|
||||
|
||||
@@ -304,6 +304,20 @@ async fn create_session(
|
||||
UiaaSessionMetadata::Legacy { identity: Identity::default() }
|
||||
};
|
||||
|
||||
// Legacy sessions aren't available if OAuth is required
|
||||
if matches!(&session_metadata, UiaaSessionMetadata::Legacy { .. })
|
||||
&& !self
|
||||
.services
|
||||
.config
|
||||
.oauth
|
||||
.compatibility_mode
|
||||
.uiaa_available()
|
||||
{
|
||||
return Err!(Request(Unrecognized(
|
||||
"User-interactive authentication is unavailable on this server"
|
||||
)));
|
||||
}
|
||||
|
||||
uiaa_sessions.insert(session_id, UiaaSession { session_metadata, info: info.clone() });
|
||||
|
||||
Ok(info)
|
||||
|
||||
@@ -94,7 +94,7 @@ async fn route_remove_device(
|
||||
Expect(Path(query)): Expect<Path<DevicePath>>,
|
||||
PostForm(form): PostForm<()>,
|
||||
) -> Result {
|
||||
let user_id = user.expect(LoginTarget::RemoveDevice(query.clone()))?;
|
||||
let user_id = user.expect_recent(LoginTarget::RemoveDevice(query.clone()))?;
|
||||
|
||||
let Ok(device) = services
|
||||
.users
|
||||
|
||||
Reference in New Issue
Block a user