Implement SSO/OIDC support. (closes #7)

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk
2025-12-23 14:55:29 +00:00
parent d665a34f30
commit 11309062a2
23 changed files with 1959 additions and 27 deletions

View File

@@ -1,6 +1,7 @@
mod account_data;
mod appservice;
mod globals;
mod oauth;
mod presence;
mod pusher;
mod raw;
@@ -18,10 +19,10 @@ use tuwunel_core::Result;
use self::{
account_data::AccountDataCommand, appservice::AppserviceCommand, globals::GlobalsCommand,
presence::PresenceCommand, pusher::PusherCommand, raw::RawCommand, resolver::ResolverCommand,
room_alias::RoomAliasCommand, room_state_cache::RoomStateCacheCommand,
room_timeline::RoomTimelineCommand, sending::SendingCommand, short::ShortCommand,
sync::SyncCommand, users::UsersCommand,
oauth::OauthCommand, presence::PresenceCommand, pusher::PusherCommand, raw::RawCommand,
resolver::ResolverCommand, room_alias::RoomAliasCommand,
room_state_cache::RoomStateCacheCommand, room_timeline::RoomTimelineCommand,
sending::SendingCommand, short::ShortCommand, sync::SyncCommand, users::UsersCommand,
};
use crate::admin_command_dispatch;
@@ -81,6 +82,10 @@ pub(super) enum QueryCommand {
#[command(subcommand)]
Sync(SyncCommand),
/// - oauth service
#[command(subcommand)]
Oauth(OauthCommand),
/// - raw service
#[command(subcommand)]
Raw(RawCommand),

172
src/admin/query/oauth.rs Normal file
View File

@@ -0,0 +1,172 @@
use clap::Subcommand;
use futures::{StreamExt, TryStreamExt};
use ruma::OwnedUserId;
use tuwunel_core::{Err, Result, utils::stream::IterStream};
use tuwunel_service::{
Services,
oauth::{Provider, Session},
};
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch(handler_prefix = "oauth")]
#[derive(Debug, Subcommand)]
/// Query OAuth service state
pub(crate) enum OauthCommand {
/// List configured OAuth providers.
ListProviders,
/// List users associated with an OAuth provider
ListUsers,
/// Show active configuration of a provider.
ShowProvider {
id: String,
#[arg(long)]
config: bool,
},
/// Show session state
ShowSession {
id: String,
},
/// Token introspection request to provider.
TokenInfo {
id: String,
},
/// Revoke token for user_id or sess_id.
Revoke {
id: String,
},
/// Remove oauth state (DANGER!)
Remove {
id: String,
#[arg(long)]
force: bool,
},
}
#[admin_command]
pub(super) async fn oauth_list_providers(&self) -> Result {
self.services
.config
.identity_provider
.iter()
.try_stream()
.map_ok(Provider::id)
.map_ok(|id| format!("{id}\n"))
.try_for_each(async |id| self.write_str(&id).await)
.await
}
#[admin_command]
pub(super) async fn oauth_list_users(&self) -> Result {
self.services
.oauth
.sessions
.users()
.map(|id| format!("{id}\n"))
.map(Ok)
.try_for_each(async |id: String| self.write_str(&id).await)
.await
}
#[admin_command]
pub(super) async fn oauth_show_provider(&self, id: String, config: bool) -> Result {
if config {
let config = self.services.oauth.providers.get_config(&id)?;
self.write_str(&format!("{config:#?}\n")).await?;
return Ok(());
}
let provider = self.services.oauth.providers.get(&id).await?;
self.write_str(&format!("{provider:#?}\n")).await
}
#[admin_command]
pub(super) async fn oauth_show_session(&self, id: String) -> Result {
let session = find_session(self.services, &id).await?;
self.write_str(&format!("{session:#?}\n")).await
}
#[admin_command]
pub(super) async fn oauth_token_info(&self, id: String) -> Result {
let session = find_session(self.services, &id).await?;
let provider = self
.services
.oauth
.sessions
.provider(&session)
.await?;
let tokeninfo = self
.services
.oauth
.request_tokeninfo((&provider, &session))
.await;
self.write_str(&format!("{tokeninfo:#?}\n")).await
}
#[admin_command]
pub(super) async fn oauth_revoke(&self, id: String) -> Result {
let session = find_session(self.services, &id).await?;
let provider = self
.services
.oauth
.sessions
.provider(&session)
.await?;
self.services
.oauth
.revoke_token((&provider, &session))
.await?;
self.write_str("done").await
}
#[admin_command]
pub(super) async fn oauth_remove(&self, id: String, force: bool) -> Result {
let session = find_session(self.services, &id).await?;
let Some(sess_id) = session.sess_id else {
return Err!("Missing sess_id in oauth Session state");
};
if !force {
return Err!(
"Deleting these records can cause registration conflicts. Use --force to be sure."
);
}
self.services
.oauth
.sessions
.delete(&sess_id)
.await;
self.write_str("done").await
}
async fn find_session(services: &Services, id: &str) -> Result<Session> {
if let Ok(user_id) = OwnedUserId::parse(id) {
services
.oauth
.sessions
.get_by_user(&user_id)
.await
} else {
services.oauth.sessions.get(id).await
}
}