mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
feat: Allow configuring the OAuth compatibility mode
This commit is contained in:
@@ -1987,3 +1987,16 @@
|
|||||||
# `require_email_for_registration`.
|
# `require_email_for_registration`.
|
||||||
#
|
#
|
||||||
#require_email_for_token_registration = false
|
#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::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
extract::State,
|
extract::{Request, State},
|
||||||
|
middleware::{self, Next},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
routing::method_routing::{get, post},
|
routing::method_routing::{get, post},
|
||||||
};
|
};
|
||||||
use const_str::concat;
|
use const_str::concat;
|
||||||
|
use http::StatusCode;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
pub(crate) use server_metadata::*;
|
pub(crate) use server_metadata::*;
|
||||||
|
|
||||||
@@ -19,13 +22,27 @@ const TOKEN_REVOKE_PATH: &str = "client/revoke";
|
|||||||
const TOKEN_PATH: &str = "grant/token";
|
const TOKEN_PATH: &str = "grant/token";
|
||||||
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
|
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
|
||||||
|
|
||||||
pub(crate) fn router() -> Router<crate::State> {
|
pub(crate) fn router(state: crate::State) -> Router<crate::State> {
|
||||||
Router::new().nest(BASE_PATH, oauth_router())
|
Router::new()
|
||||||
|
.nest(BASE_PATH, oauth_router())
|
||||||
|
.route(
|
||||||
|
"/.well-known/openid-configuration",
|
||||||
|
get(
|
||||||
// TODO(unspecced): used by old versions of the matrix-js-sdk
|
// TODO(unspecced): used by old versions of the matrix-js-sdk
|
||||||
.route("/.well-known/openid-configuration", get(
|
|
||||||
async |State(services): State<crate::State>| {
|
async |State(services): State<crate::State>| {
|
||||||
Json(authorization_server_metadata(&services).await)
|
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()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use conduwuit::Result;
|
use conduwuit::{Err, Result};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::client::discovery::get_authorization_server_metadata::{
|
api::client::discovery::get_authorization_server_metadata::{
|
||||||
self, v1::AccountManagementAction,
|
self, v1::AccountManagementAction,
|
||||||
@@ -21,6 +21,10 @@ pub(crate) async fn get_authorization_server_metadata_route(
|
|||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
_body: Ruma<get_authorization_server_metadata::v1::Request>,
|
_body: Ruma<get_authorization_server_metadata::v1::Request>,
|
||||||
) -> Result<get_authorization_server_metadata::v1::Response> {
|
) -> 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();
|
let metadata = Raw::new(&authorization_server_metadata(&services).await).unwrap();
|
||||||
|
|
||||||
Ok(get_authorization_server_metadata::v1::Response::new(metadata.cast_unchecked()))
|
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,
|
ClientIp(client): ClientIp,
|
||||||
_body: Ruma<get_login_types::v3::Request>,
|
_body: Ruma<get_login_types::v3::Request>,
|
||||||
) -> Result<get_login_types::v3::Response> {
|
) -> 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![
|
Ok(get_login_types::v3::Response::new(vec![
|
||||||
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
|
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
|
||||||
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
||||||
@@ -118,10 +124,15 @@ pub(crate) async fn login_route(
|
|||||||
ClientIp(client): ClientIp,
|
ClientIp(client): ClientIp,
|
||||||
body: Ruma<login::v3::Request>,
|
body: Ruma<login::v3::Request>,
|
||||||
) -> Result<login::v3::Response> {
|
) -> 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();
|
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||||
|
|
||||||
// Validate login method
|
// Validate login method
|
||||||
// TODO: Other login methods
|
|
||||||
let user_id = match &body.login_info {
|
let user_id = match &body.login_info {
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
| login::v3::LoginInfo::Password(login::v3::Password {
|
| login::v3::LoginInfo::Password(login::v3::Password {
|
||||||
|
|||||||
+4
-4
@@ -10,7 +10,7 @@ use axum::{
|
|||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
routing::{any, get, post},
|
routing::{any, get, post},
|
||||||
};
|
};
|
||||||
use conduwuit::{Server, err};
|
use conduwuit::err;
|
||||||
pub(super) use conduwuit_service::state::State;
|
pub(super) use conduwuit_service::state::State;
|
||||||
use http::{Uri, uri};
|
use http::{Uri, uri};
|
||||||
|
|
||||||
@@ -18,8 +18,8 @@ use self::handler::RouterExt;
|
|||||||
pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
|
pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
|
||||||
use crate::{admin, client, server};
|
use crate::{admin, client, server};
|
||||||
|
|
||||||
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||||
let config = &server.config;
|
let config = &state.server.config;
|
||||||
let mut router = router
|
let mut router = router
|
||||||
.ruma_route(&client::appservice_ping)
|
.ruma_route(&client::appservice_ping)
|
||||||
.ruma_route(&client::get_supported_versions_route)
|
.ruma_route(&client::get_supported_versions_route)
|
||||||
@@ -187,7 +187,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
|||||||
.ruma_route(&client::get_rtc_transports)
|
.ruma_route(&client::get_rtc_transports)
|
||||||
.ruma_route(&client::room_initial_sync_route)
|
.ruma_route(&client::room_initial_sync_route)
|
||||||
.ruma_route(&client::get_authorization_server_metadata_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("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||||
.ruma_route(&admin::rooms::ban::ban_room)
|
.ruma_route(&admin::rooms::ban::ban_room)
|
||||||
|
|||||||
@@ -669,6 +669,10 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub registration_terms: HashMap<String, HashMap<String, TermsDocument>>,
|
pub registration_terms: HashMap<String, HashMap<String, TermsDocument>>,
|
||||||
|
|
||||||
|
/// display: nested
|
||||||
|
#[serde(default)]
|
||||||
|
pub oauth: OauthConfig,
|
||||||
|
|
||||||
/// Controls whether encrypted rooms and events are allowed.
|
/// Controls whether encrypted rooms and events are allowed.
|
||||||
#[serde(default = "true_fn")]
|
#[serde(default = "true_fn")]
|
||||||
pub allow_encryption: bool,
|
pub allow_encryption: bool,
|
||||||
@@ -2358,6 +2362,43 @@ pub struct TermsDocument {
|
|||||||
pub url: String,
|
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] = &[
|
const DEPRECATED_KEYS: &[&str] = &[
|
||||||
"cache_capacity",
|
"cache_capacity",
|
||||||
"conduit_cache_capacity_modifier",
|
"conduit_cache_capacity_modifier",
|
||||||
|
|||||||
@@ -73,11 +73,8 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
|||||||
// 413
|
// 413
|
||||||
| TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
|
| TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
|
||||||
|
|
||||||
// 405
|
|
||||||
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
|
|
||||||
|
|
||||||
// 404
|
// 404
|
||||||
| NotFound => StatusCode::NOT_FOUND,
|
| Unrecognized | NotFound => StatusCode::NOT_FOUND,
|
||||||
|
|
||||||
// 403
|
// 403
|
||||||
| GuestAccessForbidden
|
| GuestAccessForbidden
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ fn handle_result(method: &Method, uri: &Uri, result: Response) -> Result<Respons
|
|||||||
}
|
}
|
||||||
|
|
||||||
if status == StatusCode::METHOD_NOT_ALLOWED {
|
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)
|
Ok(result)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use ruma::api::error::ErrorKind;
|
|||||||
pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
|
pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
|
||||||
let router = Router::<state::State>::new();
|
let router = Router::<state::State>::new();
|
||||||
let (state, guard) = state::create(services.clone());
|
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))
|
.merge(conduwuit_web::build(services))
|
||||||
.fallback(not_found)
|
.fallback(not_found)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Dep, config,
|
Dep,
|
||||||
oauth::{
|
oauth::{
|
||||||
client_metadata::{ApplicationType, ClientMetadata, ResponseType},
|
client_metadata::{ApplicationType, ClientMetadata, ResponseType},
|
||||||
grant::{
|
grant::{
|
||||||
@@ -44,7 +44,6 @@ struct Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Services {
|
struct Services {
|
||||||
config: Dep<config::Service>,
|
|
||||||
users: Dep<users::Service>,
|
users: Dep<users::Service>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +110,6 @@ 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 {
|
Ok(Arc::new(Self {
|
||||||
services: Services {
|
services: Services {
|
||||||
config: args.depend::<config::Service>("config"),
|
|
||||||
users: args.depend::<users::Service>("users"),
|
users: args.depend::<users::Service>("users"),
|
||||||
},
|
},
|
||||||
db: Data {
|
db: Data {
|
||||||
|
|||||||
@@ -310,6 +310,20 @@ impl Service {
|
|||||||
UiaaSessionMetadata::Legacy { identity: Identity::default() }
|
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() });
|
uiaa_sessions.insert(session_id, UiaaSession { session_metadata, info: info.clone() });
|
||||||
|
|
||||||
Ok(info)
|
Ok(info)
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ async fn route_remove_device(
|
|||||||
Expect(Path(query)): Expect<Path<DevicePath>>,
|
Expect(Path(query)): Expect<Path<DevicePath>>,
|
||||||
PostForm(form): PostForm<()>,
|
PostForm(form): PostForm<()>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
let user_id = user.expect(LoginTarget::RemoveDevice(query.clone()))?;
|
let user_id = user.expect_recent(LoginTarget::RemoveDevice(query.clone()))?;
|
||||||
|
|
||||||
let Ok(device) = services
|
let Ok(device) = services
|
||||||
.users
|
.users
|
||||||
|
|||||||
Reference in New Issue
Block a user