Add org.matrix.login.jwt support.
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
61
Cargo.lock
generated
61
Cargo.lock
generated
@@ -2096,6 +2096,21 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonwebtoken"
|
||||||
|
version = "9.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"js-sys",
|
||||||
|
"pem",
|
||||||
|
"ring",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"simple_asn1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "konst"
|
name = "konst"
|
||||||
version = "0.3.16"
|
version = "0.3.16"
|
||||||
@@ -2796,6 +2811,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "3.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@@ -3384,7 +3409,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma"
|
name = "ruma"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assign",
|
"assign",
|
||||||
"js_int",
|
"js_int",
|
||||||
@@ -3404,7 +3429,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-appservice-api"
|
name = "ruma-appservice-api"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
@@ -3416,7 +3441,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-client-api"
|
name = "ruma-client-api"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"assign",
|
"assign",
|
||||||
@@ -3439,7 +3464,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-common"
|
name = "ruma-common"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3471,7 +3496,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-events"
|
name = "ruma-events"
|
||||||
version = "0.28.1"
|
version = "0.28.1"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"as_variant",
|
"as_variant",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -3496,7 +3521,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-federation-api"
|
name = "ruma-federation-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers",
|
"headers",
|
||||||
@@ -3518,7 +3543,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-identifiers-validation"
|
name = "ruma-identifiers-validation"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
@@ -3527,7 +3552,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-identity-service-api"
|
name = "ruma-identity-service-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
@@ -3537,7 +3562,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-macros"
|
name = "ruma-macros"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
@@ -3552,7 +3577,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-push-gateway-api"
|
name = "ruma-push-gateway-api"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js_int",
|
"js_int",
|
||||||
"ruma-common",
|
"ruma-common",
|
||||||
@@ -3564,7 +3589,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ruma-signatures"
|
name = "ruma-signatures"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
source = "git+https://github.com/matrix-construct/ruma?rev=3d082885f6532cc84ba65ebd2c3ff31b25a7022d#3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
source = "git+https://github.com/matrix-construct/ruma?rev=0155c2b33233bec9dece79d5134a9574b347f4c1#0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
@@ -4140,6 +4165,18 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple_asn1"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@@ -4793,6 +4830,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"log",
|
"log",
|
||||||
"ruma",
|
"ruma",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -4864,6 +4902,7 @@ dependencies = [
|
|||||||
"http-body-util",
|
"http-body-util",
|
||||||
"ipaddress",
|
"ipaddress",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
"jsonwebtoken",
|
||||||
"ldap3",
|
"ldap3",
|
||||||
"libc",
|
"libc",
|
||||||
"libloading",
|
"libloading",
|
||||||
|
|||||||
@@ -225,6 +225,11 @@ version = "0.1.3"
|
|||||||
[workspace.dependencies.itertools]
|
[workspace.dependencies.itertools]
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
||||||
|
[workspace.dependencies.jsonwebtoken]
|
||||||
|
version = "9.3.1"
|
||||||
|
default-features = false
|
||||||
|
features = ["use_pem"]
|
||||||
|
|
||||||
[workspace.dependencies.ldap3]
|
[workspace.dependencies.ldap3]
|
||||||
git = "https://github.com/matrix-construct/ldap3"
|
git = "https://github.com/matrix-construct/ldap3"
|
||||||
rev = "7d423314b9dbc66347284e38fc2b78c3d8f3d494"
|
rev = "7d423314b9dbc66347284e38fc2b78c3d8f3d494"
|
||||||
@@ -306,7 +311,7 @@ default-features = false
|
|||||||
|
|
||||||
[workspace.dependencies.ruma]
|
[workspace.dependencies.ruma]
|
||||||
git = "https://github.com/matrix-construct/ruma"
|
git = "https://github.com/matrix-construct/ruma"
|
||||||
rev = "3d082885f6532cc84ba65ebd2c3ff31b25a7022d"
|
rev = "0155c2b33233bec9dece79d5134a9574b347f4c1"
|
||||||
features = [
|
features = [
|
||||||
"compat",
|
"compat",
|
||||||
"rand",
|
"rand",
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ ctor.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
ruma.workspace = true
|
ruma.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde_yaml.workspace = true
|
serde_yaml.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
iter::once,
|
iter::once,
|
||||||
|
str::FromStr,
|
||||||
time::{Instant, SystemTime},
|
time::{Instant, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -11,9 +12,10 @@ use ruma::{
|
|||||||
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
|
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
|
||||||
api::federation::event::get_room_state, events::AnyStateEvent, serde::Raw,
|
api::federation::event::get_room_state, events::AnyStateEvent, serde::Raw,
|
||||||
};
|
};
|
||||||
|
use serde::Serialize;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use tuwunel_core::{
|
use tuwunel_core::{
|
||||||
Err, Result, debug_error, err, info,
|
Err, Result, debug_error, err, info, jwt,
|
||||||
matrix::{
|
matrix::{
|
||||||
Event,
|
Event,
|
||||||
pdu::{PduEvent, PduId, RawPduId},
|
pdu::{PduEvent, PduId, RawPduId},
|
||||||
@@ -22,6 +24,7 @@ use tuwunel_core::{
|
|||||||
utils::{
|
utils::{
|
||||||
stream::{IterStream, ReadyExt},
|
stream::{IterStream, ReadyExt},
|
||||||
string::EMPTY,
|
string::EMPTY,
|
||||||
|
time::now_secs,
|
||||||
},
|
},
|
||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
@@ -975,3 +978,60 @@ pub(super) async fn trim_memory(&self) -> Result {
|
|||||||
|
|
||||||
writeln!(self, "done").await
|
writeln!(self, "done").await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn create_jwt(
|
||||||
|
&self,
|
||||||
|
user: String,
|
||||||
|
exp_from_now: Option<u64>,
|
||||||
|
nbf_from_now: Option<u64>,
|
||||||
|
issuer: Option<String>,
|
||||||
|
audience: Option<String>,
|
||||||
|
) -> Result {
|
||||||
|
use jwt::{Algorithm, EncodingKey, Header, encode};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Claim {
|
||||||
|
sub: String,
|
||||||
|
iss: String,
|
||||||
|
aud: String,
|
||||||
|
exp: usize,
|
||||||
|
nbf: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = &self.services.config.jwt;
|
||||||
|
if config.format.as_str() != "HMAC" {
|
||||||
|
return Err!("This command only supports HMAC key format, not {}.", config.format);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = EncodingKey::from_secret(config.key.as_ref());
|
||||||
|
let alg = Algorithm::from_str(config.algorithm.as_str()).map_err(|e| {
|
||||||
|
err!(Config("jwt.algorithm", "JWT algorithm is not recognized or configured {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let header = Header { alg, ..Default::default() };
|
||||||
|
let claim = Claim {
|
||||||
|
sub: user,
|
||||||
|
|
||||||
|
iss: issuer.unwrap_or_default(),
|
||||||
|
|
||||||
|
aud: audience.unwrap_or_default(),
|
||||||
|
|
||||||
|
exp: exp_from_now
|
||||||
|
.and_then(|val| now_secs().checked_add(val))
|
||||||
|
.map(TryInto::try_into)
|
||||||
|
.and_then(Result::ok)
|
||||||
|
.unwrap_or(usize::MAX),
|
||||||
|
|
||||||
|
nbf: nbf_from_now
|
||||||
|
.and_then(|val| now_secs().checked_add(val))
|
||||||
|
.map(TryInto::try_into)
|
||||||
|
.and_then(Result::ok)
|
||||||
|
.unwrap_or(0),
|
||||||
|
};
|
||||||
|
|
||||||
|
encode(&header, &claim, &key)
|
||||||
|
.map_err(|e| err!("Failed to encode JWT: {e}"))
|
||||||
|
.map(async |token| self.write_str(&token).await)?
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@@ -234,6 +234,28 @@ pub(super) enum DebugCommand {
|
|||||||
level: Option<i32>,
|
level: Option<i32>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// - Create a JWT token for login
|
||||||
|
CreateJwt {
|
||||||
|
/// Localpart of the user's MXID
|
||||||
|
user: String,
|
||||||
|
|
||||||
|
/// Set expiration time in seconds from now.
|
||||||
|
#[arg(long)]
|
||||||
|
exp_from_now: Option<u64>,
|
||||||
|
|
||||||
|
/// Set not-before time in seconds from now.
|
||||||
|
#[arg(long)]
|
||||||
|
nbf_from_now: Option<u64>,
|
||||||
|
|
||||||
|
/// Claim an issuer.
|
||||||
|
#[arg(long)]
|
||||||
|
issuer: Option<String>,
|
||||||
|
|
||||||
|
/// Claim an audience.
|
||||||
|
#[arg(long)]
|
||||||
|
audience: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// - Developer test stubs
|
/// - Developer test stubs
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
|||||||
117
src/api/client/session/jwt.rs
Normal file
117
src/api/client/session/jwt.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use jwt::{Algorithm, DecodingKey, Validation, decode};
|
||||||
|
use ruma::{
|
||||||
|
OwnedUserId, UserId,
|
||||||
|
api::client::session::login::v3::{Request, Token},
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tuwunel_core::{Err, Result, at, config::JwtConfig, debug, err, jwt, warn};
|
||||||
|
use tuwunel_service::Services;
|
||||||
|
|
||||||
|
use crate::Ruma;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Claim {
|
||||||
|
/// Subject is the localpart of the User MXID
|
||||||
|
sub: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn handle_login(
|
||||||
|
services: &Services,
|
||||||
|
_body: &Ruma<Request>,
|
||||||
|
info: &Token,
|
||||||
|
) -> Result<OwnedUserId> {
|
||||||
|
let config = &services.config.jwt;
|
||||||
|
|
||||||
|
if !config.enable {
|
||||||
|
return Err!(Request(Unknown("JWT login is not enabled.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let claim = validate(config, &info.token)?;
|
||||||
|
let local = claim.sub.to_lowercase();
|
||||||
|
let server = &services.server.name;
|
||||||
|
let user_id = UserId::parse_with_server_name(local, server).map_err(|e| {
|
||||||
|
err!(Request(InvalidUsername("JWT subject is not a valid user MXID: {e}")))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !services.users.exists(&user_id).await {
|
||||||
|
if !config.register_user {
|
||||||
|
return Err!(Request(NotFound("User {user_id} is not registered on this server.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.create(&user_id, Some("*"), Some("jwt"))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(user_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(config: &JwtConfig, token: &str) -> Result<Claim> {
|
||||||
|
let verifier = init_verifier(config)?;
|
||||||
|
let validator = init_validator(config)?;
|
||||||
|
decode::<Claim>(token, &verifier, &validator)
|
||||||
|
.map(|decoded| (decoded.header, decoded.claims))
|
||||||
|
.inspect(|(head, claim)| debug!(?head, ?claim, "JWT token decoded"))
|
||||||
|
.map_err(|e| err!(Request(Forbidden("Invalid JWT token: {e}"))))
|
||||||
|
.map(at!(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_verifier(config: &JwtConfig) -> Result<DecodingKey> {
|
||||||
|
let key = &config.key;
|
||||||
|
let format = config.format.as_str();
|
||||||
|
|
||||||
|
Ok(match format {
|
||||||
|
| "HMAC" => DecodingKey::from_secret(key.as_bytes()),
|
||||||
|
|
||||||
|
| "HMACB64" => DecodingKey::from_base64_secret(key.as_str())
|
||||||
|
.map_err(|e| err!(Config("jwt.key", "JWT key is not valid base64: {e}")))?,
|
||||||
|
|
||||||
|
| "ECDSA" => DecodingKey::from_ec_pem(key.as_bytes())
|
||||||
|
.map_err(|e| err!(Config("jwt.key", "JWT key is not valid PEM: {e}")))?,
|
||||||
|
|
||||||
|
| _ => return Err!(Config("jwt.format", "Key format {format:?} is not supported.")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_validator(config: &JwtConfig) -> Result<Validation> {
|
||||||
|
let alg = config.algorithm.as_str();
|
||||||
|
let alg = Algorithm::from_str(alg).map_err(|e| {
|
||||||
|
err!(Config("jwt.algorithm", "JWT algorithm is not recognized or configured {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut validator = Validation::new(alg);
|
||||||
|
let mut required_spec_claims: Vec<_> = ["sub"].into();
|
||||||
|
|
||||||
|
validator.validate_exp = config.validate_exp;
|
||||||
|
if config.require_exp {
|
||||||
|
required_spec_claims.push("exp");
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.validate_nbf = config.validate_nbf;
|
||||||
|
if config.require_nbf {
|
||||||
|
required_spec_claims.push("nbf");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.audience.is_empty() {
|
||||||
|
required_spec_claims.push("aud");
|
||||||
|
validator.set_audience(&config.audience);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.issuer.is_empty() {
|
||||||
|
required_spec_claims.push("iss");
|
||||||
|
validator.set_issuer(&config.issuer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(debug_assertions) && !config.validate_signature {
|
||||||
|
warn!("JWT signature validation is disabled!");
|
||||||
|
validator.insecure_disable_signature_validation();
|
||||||
|
}
|
||||||
|
|
||||||
|
validator.set_required_spec_claims(&required_spec_claims);
|
||||||
|
debug!(?validator, "JWT configured");
|
||||||
|
|
||||||
|
Ok(validator)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod appservice;
|
mod appservice;
|
||||||
|
mod jwt;
|
||||||
mod ldap;
|
mod ldap;
|
||||||
mod logout;
|
mod logout;
|
||||||
mod password;
|
mod password;
|
||||||
@@ -9,7 +10,10 @@ use axum_client_ip::InsecureClientIp;
|
|||||||
use ruma::api::client::session::{
|
use ruma::api::client::session::{
|
||||||
get_login_types::{
|
get_login_types::{
|
||||||
self,
|
self,
|
||||||
v3::{ApplicationServiceLoginType, LoginType, PasswordLoginType, TokenLoginType},
|
v3::{
|
||||||
|
ApplicationServiceLoginType, JwtLoginType, LoginType, PasswordLoginType,
|
||||||
|
TokenLoginType,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
login::{
|
login::{
|
||||||
self,
|
self,
|
||||||
@@ -39,6 +43,7 @@ pub(crate) async fn get_login_types_route(
|
|||||||
Ok(get_login_types::v3::Response::new(vec![
|
Ok(get_login_types::v3::Response::new(vec![
|
||||||
LoginType::Password(PasswordLoginType::default()),
|
LoginType::Password(PasswordLoginType::default()),
|
||||||
LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
||||||
|
LoginType::Jwt(JwtLoginType::default()),
|
||||||
LoginType::Token(TokenLoginType {
|
LoginType::Token(TokenLoginType {
|
||||||
get_login_token: services.config.login_via_existing_session,
|
get_login_token: services.config.login_via_existing_session,
|
||||||
}),
|
}),
|
||||||
@@ -69,6 +74,7 @@ pub(crate) async fn login_route(
|
|||||||
let user_id = match &body.login_info {
|
let user_id = match &body.login_info {
|
||||||
| LoginInfo::Password(info) => password::handle_login(&services, &body, info).await?,
|
| LoginInfo::Password(info) => password::handle_login(&services, &body, info).await?,
|
||||||
| LoginInfo::Token(info) => token::handle_login(&services, &body, info).await?,
|
| LoginInfo::Token(info) => token::handle_login(&services, &body, info).await?,
|
||||||
|
| LoginInfo::Jwt(info) => jwt::handle_login(&services, &body, info).await?,
|
||||||
| LoginInfo::ApplicationService(info) =>
|
| LoginInfo::ApplicationService(info) =>
|
||||||
appservice::handle_login(&services, &body, info).await?,
|
appservice::handle_login(&services, &body, info).await?,
|
||||||
| _ => {
|
| _ => {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ http-body-util.workspace = true
|
|||||||
http.workspace = true
|
http.workspace = true
|
||||||
ipaddress.workspace = true
|
ipaddress.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
jsonwebtoken.workspace = true
|
||||||
ldap3.workspace = true
|
ldap3.workspace = true
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
libloading.workspace = true
|
libloading.workspace = true
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ use crate::{Result, err, error::Error, utils::sys};
|
|||||||
### For more information, see:
|
### For more information, see:
|
||||||
### https://tuwunel.chat/configuration.html
|
### https://tuwunel.chat/configuration.html
|
||||||
"#,
|
"#,
|
||||||
ignore = "catchall well_known tls blurhashing allow_invalid_tls_certificates ldap"
|
ignore = "catchall well_known tls blurhashing allow_invalid_tls_certificates ldap jwt"
|
||||||
)]
|
)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// The server_name is the pretty name of this server. It is used as a
|
/// The server_name is the pretty name of this server. It is used as a
|
||||||
@@ -1814,6 +1814,10 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ldap: LdapConfig,
|
pub ldap: LdapConfig,
|
||||||
|
|
||||||
|
// external structure; separate section
|
||||||
|
#[serde(default)]
|
||||||
|
pub jwt: JwtConfig,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
#[allow(clippy::zero_sized_map_values)]
|
#[allow(clippy::zero_sized_map_values)]
|
||||||
// this is a catchall, the map shouldn't be zero at runtime
|
// this is a catchall, the map shouldn't be zero at runtime
|
||||||
@@ -1991,6 +1995,99 @@ pub struct LdapConfig {
|
|||||||
pub admin_filter: String,
|
pub admin_filter: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
#[config_example_generator(filename = "tuwunel-example.toml", section = "global.jwt")]
|
||||||
|
pub struct JwtConfig {
|
||||||
|
/// Enable JWT logins
|
||||||
|
///
|
||||||
|
/// default: false
|
||||||
|
#[serde(default)]
|
||||||
|
pub enable: bool,
|
||||||
|
|
||||||
|
/// Validation key, also called 'secret' in Synapse config. The type of key
|
||||||
|
/// can be configured in 'format', but defaults to the common HMAC which
|
||||||
|
/// is a plaintext shared-secret, so you should keep this value private.
|
||||||
|
///
|
||||||
|
/// display: sensitive
|
||||||
|
/// default:
|
||||||
|
#[serde(default, alias = "secret")]
|
||||||
|
pub key: String,
|
||||||
|
|
||||||
|
/// Format of the 'key'. Only HMAC, ECDSA, and B64HMAC are supported
|
||||||
|
/// Binary keys cannot be pasted into this config, so B64HMAC is an
|
||||||
|
/// alternative to HMAC for properly random secret strings.
|
||||||
|
/// - HMAC is a plaintext shared-secret private-key.
|
||||||
|
/// - B64HMAC is a base64-encoded version of HMAC.
|
||||||
|
/// - ECDSA is a PEM-encoded public-key.
|
||||||
|
///
|
||||||
|
/// default: "HMAC"
|
||||||
|
#[serde(default = "default_jwt_format")]
|
||||||
|
pub format: String,
|
||||||
|
|
||||||
|
/// Automatically create new user from a valid claim, otherwise access is
|
||||||
|
/// denied for an unknown even with an authentic token.
|
||||||
|
///
|
||||||
|
/// default: true
|
||||||
|
#[serde(default = "true_fn")]
|
||||||
|
pub register_user: bool,
|
||||||
|
|
||||||
|
/// JWT algorithm
|
||||||
|
///
|
||||||
|
/// default: "HS256"
|
||||||
|
#[serde(default = "default_jwt_algorithm")]
|
||||||
|
pub algorithm: String,
|
||||||
|
|
||||||
|
/// Optional audience claim list. The token must claim one or more values
|
||||||
|
/// from this list when set.
|
||||||
|
///
|
||||||
|
/// default: []
|
||||||
|
#[serde(default)]
|
||||||
|
pub audience: Vec<String>,
|
||||||
|
|
||||||
|
/// Optional issuer claim list. The token must claim one or more values
|
||||||
|
/// from this list when set.
|
||||||
|
///
|
||||||
|
/// default: []
|
||||||
|
#[serde(default)]
|
||||||
|
pub issuer: Vec<String>,
|
||||||
|
|
||||||
|
/// Require expiration claim in the token. This defaults to false for
|
||||||
|
/// synapse migration compatibility.
|
||||||
|
///
|
||||||
|
/// default: false
|
||||||
|
#[serde(default)]
|
||||||
|
pub require_exp: bool,
|
||||||
|
|
||||||
|
/// Require not-before claim in the token. This defaults to false for
|
||||||
|
/// synapse migration compatibility.
|
||||||
|
///
|
||||||
|
/// default: false
|
||||||
|
#[serde(default)]
|
||||||
|
pub require_nbf: bool,
|
||||||
|
|
||||||
|
/// Validate expiration time of the token when present. Whether or not it is
|
||||||
|
/// required depends on require_exp, but when present this ensures the token
|
||||||
|
/// is not used after a time.
|
||||||
|
///
|
||||||
|
/// default: true
|
||||||
|
#[serde(default = "true_fn")]
|
||||||
|
pub validate_exp: bool,
|
||||||
|
|
||||||
|
/// Validate not-before time of the token when present. Whether or not it is
|
||||||
|
/// required depends on require_nbf, but when present this ensures the token
|
||||||
|
/// is not used before a time.
|
||||||
|
///
|
||||||
|
/// default: true
|
||||||
|
#[serde(default = "true_fn")]
|
||||||
|
pub validate_nbf: bool,
|
||||||
|
|
||||||
|
/// Bypass validation for diagnostic/debug use only.
|
||||||
|
///
|
||||||
|
/// default: true
|
||||||
|
#[serde(default = "true_fn")]
|
||||||
|
pub validate_signature: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
struct ListeningPort {
|
struct ListeningPort {
|
||||||
@@ -2392,3 +2489,7 @@ fn default_ldap_uid_attribute() -> String { String::from("uid") }
|
|||||||
fn default_ldap_mail_attribute() -> String { String::from("mail") }
|
fn default_ldap_mail_attribute() -> String { String::from("mail") }
|
||||||
|
|
||||||
fn default_ldap_name_attribute() -> String { String::from("givenName") }
|
fn default_ldap_name_attribute() -> String { String::from("givenName") }
|
||||||
|
|
||||||
|
fn default_jwt_algorithm() -> String { "HS256".to_owned() }
|
||||||
|
|
||||||
|
fn default_jwt_format() -> String { "HMAC".to_owned() }
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod utils;
|
|||||||
|
|
||||||
pub use ::arrayvec;
|
pub use ::arrayvec;
|
||||||
pub use ::http;
|
pub use ::http;
|
||||||
|
pub use ::jsonwebtoken as jwt;
|
||||||
pub use ::ruma;
|
pub use ::ruma;
|
||||||
pub use ::smallstr;
|
pub use ::smallstr;
|
||||||
pub use ::smallvec;
|
pub use ::smallvec;
|
||||||
|
|||||||
@@ -1712,3 +1712,69 @@
|
|||||||
# example: "(objectClass=tuwunelAdmin)" or "(uid={username})"
|
# example: "(objectClass=tuwunelAdmin)" or "(uid={username})"
|
||||||
#
|
#
|
||||||
#admin_filter = false
|
#admin_filter = false
|
||||||
|
|
||||||
|
[global.jwt]
|
||||||
|
|
||||||
|
# Enable JWT logins
|
||||||
|
#
|
||||||
|
#enable = false
|
||||||
|
|
||||||
|
# Validation key, also called 'secret' in Synapse config. The type of key
|
||||||
|
# can be configured in 'format', but defaults to the common HMAC which
|
||||||
|
# is a plaintext shared-secret, so you should keep this value private.
|
||||||
|
#
|
||||||
|
#key =
|
||||||
|
|
||||||
|
# Format of the 'key'. Only HMAC, ECDSA, and B64HMAC are supported
|
||||||
|
# Binary keys cannot be pasted into this config, so B64HMAC is an
|
||||||
|
# alternative to HMAC for properly random secret strings.
|
||||||
|
# - HMAC is a plaintext shared-secret private-key.
|
||||||
|
# - B64HMAC is a base64-encoded version of HMAC.
|
||||||
|
# - ECDSA is a PEM-encoded public-key.
|
||||||
|
#
|
||||||
|
#format = "HMAC"
|
||||||
|
|
||||||
|
# Automatically create new user from a valid claim, otherwise access is
|
||||||
|
# denied for an unknown even with an authentic token.
|
||||||
|
#
|
||||||
|
#register_user = true
|
||||||
|
|
||||||
|
# JWT algorithm
|
||||||
|
#
|
||||||
|
#algorithm = "HS256"
|
||||||
|
|
||||||
|
# Optional audience claim list. The token must claim one or more values
|
||||||
|
# from this list when set.
|
||||||
|
#
|
||||||
|
#audience = []
|
||||||
|
|
||||||
|
# Optional issuer claim list. The token must claim one or more values
|
||||||
|
# from this list when set.
|
||||||
|
#
|
||||||
|
#issuer = []
|
||||||
|
|
||||||
|
# Require expiration claim in the token. This defaults to false for
|
||||||
|
# synapse migration compatibility.
|
||||||
|
#
|
||||||
|
#require_exp = false
|
||||||
|
|
||||||
|
# Require not-before claim in the token. This defaults to false for
|
||||||
|
# synapse migration compatibility.
|
||||||
|
#
|
||||||
|
#require_nbf = false
|
||||||
|
|
||||||
|
# Validate expiration time of the token when present. Whether or not it is
|
||||||
|
# required depends on require_exp, but when present this ensures the token
|
||||||
|
# is not used after a time.
|
||||||
|
#
|
||||||
|
#validate_exp = true
|
||||||
|
|
||||||
|
# Validate not-before time of the token when present. Whether or not it is
|
||||||
|
# required depends on require_nbf, but when present this ensures the token
|
||||||
|
# is not used before a time.
|
||||||
|
#
|
||||||
|
#validate_nbf = true
|
||||||
|
|
||||||
|
# Bypass validation for diagnostic/debug use only.
|
||||||
|
#
|
||||||
|
#validate_signature = true
|
||||||
|
|||||||
Reference in New Issue
Block a user