Skip to main content

tuwunel_api/router/auth/
jwt.rs

1use jwt::{TokenData, Validation, dangerous::insecure_decode, decode};
2use ruma::{OwnedUserId, UserId};
3use serde::Deserialize;
4use tuwunel_core::{Err, Result, at, config::JwtConfig, debug, err, jwt, utils::BoolExt, warn};
5use tuwunel_service::Services;
6
7#[derive(Debug, Deserialize)]
8struct Claim {
9	/// Subject is the localpart of the User MXID
10	sub: String,
11}
12
13/// Validates a login token and resolves its local user identity.
14///
15/// The configured JWT rules validate the token before its subject becomes a user ID.
16pub fn validate_user(services: &Services, token: &str) -> Result<OwnedUserId> {
17	let config = &services.config.jwt;
18
19	if !config.enable {
20		return Err!(Request(Unauthorized("JWT login is not enabled.")));
21	}
22
23	let claim = validate(config, token)?;
24	let local = claim.sub.to_lowercase();
25	let server = &services.server.name;
26	let user_id = UserId::parse_with_server_name(local, server).map_err(|e| {
27		err!(Request(InvalidUsername("JWT subject is not a valid user MXID: {e}")))
28	})?;
29
30	Ok(user_id)
31}
32
33fn validate(config: &JwtConfig, token: &str) -> Result<Claim> {
34	let token_data = if cfg!(debug_assertions) && !config.validate_signature {
35		warn!("JWT signature validation is disabled!");
36		insecure_decode(token)
37	} else {
38		let verifier = config.decoding_key()?;
39		let validator = init_validator(config)?;
40
41		decode(token, &verifier, &validator)
42	};
43
44	token_data
45		.map(|decoded: TokenData<Claim>| (decoded.header, decoded.claims))
46		.inspect(|(head, claim)| debug!(?head, ?claim, "JWT token decoded"))
47		.map_err(|e| err!(Request(Forbidden("Invalid JWT token: {e}"))))
48		.map(at!(1))
49}
50
51fn init_validator(config: &JwtConfig) -> Result<Validation> {
52	let has_audience = config.audience.is_empty().is_false();
53	let has_issuer = config.issuer.is_empty().is_false();
54	let required = [
55		Some("sub"),
56		config.require_exp.then_some("exp"),
57		config.require_nbf.then_some("nbf"),
58		has_audience.then_some("aud"),
59		has_issuer.then_some("iss"),
60	];
61
62	let validator = Validation {
63		required_spec_claims: required
64			.into_iter()
65			.flatten()
66			.map(ToOwned::to_owned)
67			.collect(),
68		validate_exp: config.validate_exp,
69		validate_nbf: config.validate_nbf,
70		aud: has_audience.then(|| config.audience.iter().cloned().collect()),
71		iss: has_issuer.then(|| config.issuer.iter().cloned().collect()),
72		..Validation::new(config.algorithm()?)
73	};
74
75	debug!(?validator, "JWT configured");
76
77	Ok(validator)
78}