Skip to main content

tuwunel_api/client/session/
jwt.rs

1use jwt::{TokenData, Validation, dangerous::insecure_decode, decode};
2use ruma::{
3	OwnedUserId, UserId,
4	api::client::session::login::v3::{Request, Token},
5};
6use serde::Deserialize;
7use tuwunel_core::{Err, Result, at, config::JwtConfig, debug, err, jwt, utils::BoolExt, warn};
8use tuwunel_service::Services;
9
10use crate::Ruma;
11
12#[derive(Debug, Deserialize)]
13struct Claim {
14	/// Subject is the localpart of the User MXID
15	sub: String,
16}
17
18pub(super) async fn handle_login(
19	services: &Services,
20	_body: &Ruma<Request>,
21	info: &Token,
22) -> Result<OwnedUserId> {
23	let user_id = validate_user(services, &info.token)?;
24
25	if !services.users.exists(&user_id).await {
26		let config = &services.config.jwt;
27
28		if !config.register_user {
29			return Err!(Request(NotFound("User {user_id} is not registered on this server.")));
30		}
31
32		services
33			.users
34			.create(&user_id, Some("*"), Some("jwt"))
35			.await?;
36	}
37
38	Ok(user_id)
39}
40
41pub(crate) fn validate_user(services: &Services, token: &str) -> Result<OwnedUserId> {
42	let config = &services.config.jwt;
43
44	if !config.enable {
45		return Err!(Request(Unauthorized("JWT login is not enabled.")));
46	}
47
48	let claim = validate(config, token)?;
49	let local = claim.sub.to_lowercase();
50	let server = &services.server.name;
51	let user_id = UserId::parse_with_server_name(local, server).map_err(|e| {
52		err!(Request(InvalidUsername("JWT subject is not a valid user MXID: {e}")))
53	})?;
54
55	Ok(user_id)
56}
57
58fn validate(config: &JwtConfig, token: &str) -> Result<Claim> {
59	let token_data = if cfg!(debug_assertions) && !config.validate_signature {
60		warn!("JWT signature validation is disabled!");
61		insecure_decode(token)
62	} else {
63		let verifier = config.decoding_key()?;
64		let validator = init_validator(config)?;
65
66		decode(token, &verifier, &validator)
67	};
68
69	token_data
70		.map(|decoded: TokenData<Claim>| (decoded.header, decoded.claims))
71		.inspect(|(head, claim)| debug!(?head, ?claim, "JWT token decoded"))
72		.map_err(|e| err!(Request(Forbidden("Invalid JWT token: {e}"))))
73		.map(at!(1))
74}
75
76fn init_validator(config: &JwtConfig) -> Result<Validation> {
77	let has_audience = config.audience.is_empty().is_false();
78	let has_issuer = config.issuer.is_empty().is_false();
79	let required = [
80		Some("sub"),
81		config.require_exp.then_some("exp"),
82		config.require_nbf.then_some("nbf"),
83		has_audience.then_some("aud"),
84		has_issuer.then_some("iss"),
85	];
86
87	let validator = Validation {
88		required_spec_claims: required
89			.into_iter()
90			.flatten()
91			.map(ToOwned::to_owned)
92			.collect(),
93		validate_exp: config.validate_exp,
94		validate_nbf: config.validate_nbf,
95		aud: has_audience.then(|| config.audience.iter().cloned().collect()),
96		iss: has_issuer.then(|| config.issuer.iter().cloned().collect()),
97		..Validation::new(config.algorithm()?)
98	};
99
100	debug!(?validator, "JWT configured");
101
102	Ok(validator)
103}