Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement /login/get_token #664

Merged
merged 3 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,6 @@ features = ["parse"]
[workspace.dependencies.sanitize-filename]
version = "0.6.0"

[workspace.dependencies.jsonwebtoken]
version = "9.3.0"
default-features = false

[workspace.dependencies.base64]
version = "0.22.1"
default-features = false
Expand Down
20 changes: 16 additions & 4 deletions conduwuit-example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -563,10 +563,6 @@
#
#proxy = "none"

# This item is undocumented. Please contribute documentation for it.
#
#jwt_secret =

# Servers listed here will be used to gather public keys of other servers
# (notary trusted key servers).
#
Expand Down Expand Up @@ -649,6 +645,22 @@
#
#openid_token_ttl = 3600

# Allow an existing session to mint a login token for another client.
# This requires interactive authentication, but has security ramifications
# as a malicious client could use the mechanism to spawn more than one
# session.
# Enabled by default.
#
#login_via_existing_session = true

# Login token expiration/TTL in milliseconds.
#
# These are short-lived tokens for the m.login.token endpoint.
# This is used to allow existing sessions to create new sessions.
# see login_via_existing_session.
#
#login_token_ttl = 120000

# Static TURN username to provide the client if not using a shared secret
# ("turn_secret"), It is recommended to use a shared secret over static
# credentials.
Expand Down
1 change: 0 additions & 1 deletion src/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ http.workspace = true
http-body-util.workspace = true
hyper.workspace = true
ipaddress.workspace = true
jsonwebtoken.workspace = true
log.workspace = true
rand.workspace = true
reqwest.workspace = true
Expand Down
5 changes: 3 additions & 2 deletions src/api/client/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ pub(crate) async fn get_capabilities_route(
// we do not implement 3PID stuff
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability { enabled: false };

// we dont support generating tokens yet
capabilities.get_login_token = GetLoginTokenCapability { enabled: false };
capabilities.get_login_token = GetLoginTokenCapability {
enabled: services.server.config.login_via_existing_session,
};

// MSC4133 capability
capabilities
Expand Down
143 changes: 93 additions & 50 deletions src/api/client/session.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Duration;

use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{debug, err, info, utils::ReadyExt, warn, Err};
Expand All @@ -6,43 +8,42 @@ use ruma::{
api::client::{
error::ErrorKind,
session::{
get_login_token,
get_login_types::{
self,
v3::{ApplicationServiceLoginType, PasswordLoginType},
v3::{ApplicationServiceLoginType, PasswordLoginType, TokenLoginType},
},
login::{
self,
v3::{DiscoveryInfo, HomeserverInfo},
},
logout, logout_all,
},
uiaa::UserIdentifier,
uiaa,
},
OwnedUserId, UserId,
};
use serde::Deserialize;
use service::uiaa::SESSION_ID_LENGTH;

use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{utils, utils::hash, Error, Result, Ruma};

#[derive(Debug, Deserialize)]
struct Claims {
sub: String,
//exp: usize,
}

/// # `GET /_matrix/client/v3/login`
///
/// Get the supported login types of this server. One of these should be used as
/// the `type` field when logging in.
#[tracing::instrument(skip_all, fields(%client), name = "login")]
pub(crate) async fn get_login_types_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
_body: Ruma<get_login_types::v3::Request>,
) -> Result<get_login_types::v3::Response> {
Ok(get_login_types::v3::Response::new(vec![
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
get_login_types::v3::LoginType::Token(TokenLoginType {
get_login_token: services.server.config.login_via_existing_session,
}),
]))
}

Expand Down Expand Up @@ -77,7 +78,9 @@ pub(crate) async fn login_route(
..
}) => {
debug!("Got password login type");
let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
let user_id = if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) =
identifier
{
UserId::parse_with_server_name(
user_id.to_lowercase(),
services.globals.server_name(),
Expand Down Expand Up @@ -108,54 +111,33 @@ pub(crate) async fn login_route(
},
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
debug!("Got token login type");
if let Some(jwt_decoding_key) = services.globals.jwt_decoding_key() {
let token = jsonwebtoken::decode::<Claims>(
token,
jwt_decoding_key,
&jsonwebtoken::Validation::default(),
)
.map_err(|e| {
warn!("Failed to parse JWT token from user logging in: {e}");
Error::BadRequest(ErrorKind::InvalidUsername, "Token is invalid.")
})?;

let username = token.claims.sub.to_lowercase();

UserId::parse_with_server_name(username, services.globals.server_name()).map_err(
|e| {
err!(Request(InvalidUsername(debug_error!(
?e,
"Failed to parse login username"
))))
},
)?
} else {
return Err!(Request(Unknown(
"Token login is not supported (server has no jwt decoding key)."
)));
if !services.server.config.login_via_existing_session {
return Err!(Request(Unknown("Token login is not enabled.")));
}
services.users.find_from_login_token(token).await?
},
#[allow(deprecated)]
| login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService {
identifier,
user,
}) => {
debug!("Got appservice login type");
let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
UserId::parse_with_server_name(
user_id.to_lowercase(),
services.globals.server_name(),
)
} else if let Some(user) = user {
OwnedUserId::parse(user)
} else {
warn!("Bad login type: {:?}", &body.login_info);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
}
.map_err(|e| {
warn!("Failed to parse username from appservice logging in: {e}");
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
})?;
let user_id =
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
UserId::parse_with_server_name(
user_id.to_lowercase(),
services.globals.server_name(),
)
} else if let Some(user) = user {
OwnedUserId::parse(user)
} else {
warn!("Bad login type: {:?}", &body.login_info);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
}
.map_err(|e| {
warn!("Failed to parse username from appservice logging in: {e}");
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
})?;

if let Some(ref info) = body.appservice_info {
if !info.is_user_match(&user_id) {
Expand Down Expand Up @@ -247,6 +229,67 @@ pub(crate) async fn login_route(
})
}

/// # `POST /_matrix/client/v1/login/get_token`
///
/// Allows a logged-in user to get a short-lived token which can be used
/// to log in with the m.login.token flow.
///
/// <https://spec.matrix.org/v1.13/client-server-api/#post_matrixclientv1loginget_token>
#[tracing::instrument(skip_all, fields(%client), name = "login_token")]
pub(crate) async fn login_token_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_login_token::v1::Request>,
) -> Result<get_login_token::v1::Response> {
if !services.server.config.login_via_existing_session {
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
}

let sender_user = body.sender_user();
let sender_device = body.sender_device();

// This route SHOULD have UIA
// TODO: How do we make only UIA sessions that have not been used before valid?
girlbossceo marked this conversation as resolved.
Show resolved Hide resolved

let mut uiaainfo = uiaa::UiaaInfo {
flows: vec![uiaa::AuthFlow { stages: vec![uiaa::AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};

if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;

if !worked {
return Err(Error::Uiaa(uiaainfo));
}

// Success!
} else if let Some(json) = body.json_body.as_ref() {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);

return Err(Error::Uiaa(uiaainfo));
} else {
return Err!(Request(NotJson("No JSON body was sent when required.")));
}

let login_token = utils::random_string(TOKEN_LENGTH);
let expires_in = services.users.create_login_token(sender_user, &login_token);

Ok(get_login_token::v1::Response {
expires_in: Duration::from_millis(expires_in),
login_token,
})
}

/// # `POST /_matrix/client/v3/logout`
///
/// Log out the current device.
Expand Down
1 change: 1 addition & 0 deletions src/api/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::register_route)
.ruma_route(&client::get_login_types_route)
.ruma_route(&client::login_route)
.ruma_route(&client::login_token_route)
.ruma_route(&client::whoami_route)
.ruma_route(&client::logout_route)
.ruma_route(&client::logout_all_route)
Expand Down
26 changes: 20 additions & 6 deletions src/core/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,8 +671,6 @@ pub struct Config {
#[serde(default)]
pub proxy: ProxyConfig,

pub jwt_secret: Option<String>,

/// Servers listed here will be used to gather public keys of other servers
/// (notary trusted key servers).
///
Expand Down Expand Up @@ -769,6 +767,24 @@ pub struct Config {
#[serde(default = "default_openid_token_ttl")]
pub openid_token_ttl: u64,

/// Allow an existing session to mint a login token for another client.
/// This requires interactive authentication, but has security ramifications
/// as a malicious client could use the mechanism to spawn more than one
/// session.
/// Enabled by default.
#[serde(default = "true_fn")]
pub login_via_existing_session: bool,

/// Login token expiration/TTL in milliseconds.
///
/// These are short-lived tokens for the m.login.token endpoint.
/// This is used to allow existing sessions to create new sessions.
/// see login_via_existing_session.
///
/// default: 120000
#[serde(default = "default_login_token_ttl")]
pub login_token_ttl: u64,

/// Static TURN username to provide the client if not using a shared secret
/// ("turn_secret"), It is recommended to use a shared secret over static
/// credentials.
Expand Down Expand Up @@ -2005,10 +2021,6 @@ impl fmt::Display for Config {
"Lockdown public room directory (only allow admins to publish)",
&self.lockdown_public_room_directory.to_string(),
);
line("JWT secret", match self.jwt_secret {
| Some(_) => "set",
| None => "not set",
});
line(
"Trusted key servers",
&self
Expand Down Expand Up @@ -2379,6 +2391,8 @@ fn default_notification_push_path() -> String { "/_matrix/push/v1/notify".to_own

fn default_openid_token_ttl() -> u64 { 60 * 60 }

fn default_login_token_ttl() -> u64 { 2 * 60 * 1000 }

fn default_turn_ttl() -> u64 { 60 * 60 * 24 }

fn default_presence_idle_timeout_s() -> u64 { 5 * 60 }
Expand Down
4 changes: 4 additions & 0 deletions src/database/maps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "openidtoken_expiresatuserid",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "logintoken_expiresatuserid",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userroomid_highlightcount",
..descriptor::RANDOM
Expand Down
1 change: 0 additions & 1 deletion src/service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ image.workspace = true
image.optional = true
ipaddress.workspace = true
itertools.workspace = true
jsonwebtoken.workspace = true
log.workspace = true
loole.workspace = true
lru-cache.workspace = true
Expand Down
Loading
Loading