1#![deny(unsafe_code)]
15
16use std::borrow::Cow;
17use std::collections::HashSet;
18use std::sync::Arc;
19use std::time::Duration;
20
21use anyhow::anyhow;
22use async_trait::async_trait;
23use itoa::Buffer;
24use jsonwebtoken::{decode, TokenData, Validation};
25use tokio::sync::RwLock;
26
27use rmqtt::{
28 acl::{
29 AuthInfo, Rule, PLACEHOLDER_CLIENTID, PLACEHOLDER_IPADDR, PLACEHOLDER_PROTOCOL, PLACEHOLDER_USERNAME,
30 },
31 context::ServerContext,
32 hook::{Handler, HookResult, Parameter, Register, ReturnType, Type},
33 macros::Plugin,
34 plugin::{PackageInfo, Plugin},
35 register,
36 types::{AuthResult, ConnectInfo, Disconnect, Message, Reason},
37 Result,
38};
39
40use config::{JWTFrom, PluginConfig, ValidateClaims};
41
42mod config;
43
44type HashMap<K, V> = std::collections::HashMap<K, V, ahash::RandomState>;
45
46register!(AuthJwtPlugin::new);
47
48#[derive(Plugin)]
49struct AuthJwtPlugin {
50 scx: ServerContext,
51 register: Box<dyn Register>,
52 cfg: Arc<RwLock<PluginConfig>>,
53}
54
55impl AuthJwtPlugin {
56 #[inline]
57 async fn new<S: Into<String>>(scx: ServerContext, name: S) -> Result<Self> {
58 let name = name.into();
59 let mut cfg = scx.plugins.read_config::<PluginConfig>(&name)?;
60 cfg.init_decoding_key()?;
61 log::info!("{name} AuthJwtPlugin cfg: {cfg:?}");
62 let cfg = Arc::new(RwLock::new(cfg));
63 let register = scx.extends.hook_mgr().register();
64 Ok(Self { scx, register, cfg })
65 }
66}
67
68#[async_trait]
69impl Plugin for AuthJwtPlugin {
70 #[inline]
71 async fn init(&mut self) -> Result<()> {
72 log::info!("{} init", self.name());
73 let cfg = &self.cfg;
74
75 let priority = cfg.read().await.priority;
76 self.register
77 .add_priority(Type::ClientAuthenticate, priority, Box::new(AuthHandler::new(&self.scx, cfg)))
78 .await;
79 self.register
80 .add_priority(Type::ClientSubscribeCheckAcl, priority, Box::new(AuthHandler::new(&self.scx, cfg)))
81 .await;
82 self.register
83 .add_priority(Type::MessagePublishCheckAcl, priority, Box::new(AuthHandler::new(&self.scx, cfg)))
84 .await;
85 self.register.add(Type::ClientKeepalive, Box::new(AuthHandler::new(&self.scx, cfg))).await;
86 Ok(())
87 }
88
89 #[inline]
90 async fn get_config(&self) -> Result<serde_json::Value> {
91 self.cfg.read().await.to_json()
92 }
93
94 #[inline]
95 async fn load_config(&mut self) -> Result<()> {
96 let new_cfg = self.scx.plugins.read_config::<PluginConfig>(self.name())?;
97 *self.cfg.write().await = new_cfg;
98 log::debug!("load_config ok, {:?}", self.cfg);
99 Ok(())
100 }
101
102 #[inline]
103 async fn start(&mut self) -> Result<()> {
104 log::info!("{} start", self.name());
105 self.register.start().await;
106 Ok(())
107 }
108
109 #[inline]
110 async fn stop(&mut self) -> Result<bool> {
111 log::info!("{} stop", self.name());
112 self.register.stop().await;
113 Ok(true)
114 }
115
116 #[inline]
117 async fn attrs(&self) -> serde_json::Value {
118 serde_json::json!({})
119 }
120}
121
122struct AuthHandler {
123 scx: ServerContext,
124 cfg: Arc<RwLock<PluginConfig>>,
125}
126
127impl AuthHandler {
128 fn new(scx: &ServerContext, cfg: &Arc<RwLock<PluginConfig>>) -> Self {
129 Self { scx: scx.clone(), cfg: cfg.clone() }
130 }
131
132 #[inline]
133 async fn token<'a>(&self, connect_info: &'a ConnectInfo) -> Option<Cow<'a, str>> {
134 let token = match self.cfg.read().await.from {
135 JWTFrom::Username => connect_info.username().map(|u| Cow::Borrowed(u.as_ref())),
136 JWTFrom::Password => connect_info.password().map(|p| String::from_utf8_lossy(p)),
137 };
138 token
139 }
140
141 #[inline]
142 fn replaces(
143 connect_info: &ConnectInfo,
144 item: &str,
145 p_uname: bool,
146 p_cid: bool,
147 p_ipaddr: bool,
148 p_proto: bool,
149 ) -> Result<String> {
150 let mut item = if p_uname {
151 if let Some(username) = connect_info.username() {
152 Cow::Owned(item.replace(PLACEHOLDER_USERNAME, username))
153 } else {
154 return Err(anyhow!("username does not exist"));
155 }
156 } else {
157 Cow::Borrowed(item)
158 };
159 if p_cid {
160 item = Cow::Owned(item.replace(PLACEHOLDER_CLIENTID, connect_info.client_id()));
161 }
162 if p_ipaddr {
163 if let Some(ipaddr) = connect_info.ipaddress() {
164 item = Cow::Owned(item.replace(PLACEHOLDER_IPADDR, ipaddr.ip().to_string().as_str()));
165 } else {
166 return Err(anyhow!("ipaddr does not exist"));
167 }
168 }
169 if p_proto {
170 item = Cow::Owned(
171 item.replace(PLACEHOLDER_PROTOCOL, Buffer::new().format(connect_info.proto_ver())),
172 );
173 }
174 Ok(item.into())
175 }
176
177 #[inline]
178 async fn standard_auth(
179 &self,
180 connect_info: &ConnectInfo,
181 token: &str,
182 validate_claims_cfg: &ValidateClaims,
183 ) -> Result<TokenData<HashMap<String, serde_json::Value>>> {
184 let mut required_spec_claims = HashSet::default();
185
186 let validate_exp = validate_claims_cfg.validate_exp_enable;
187 let validate_nbf = validate_claims_cfg.validate_nbf_enable;
188
189 let mut validate_aud = false;
190 let mut aud = None;
191 let mut iss = None;
192 let mut sub = None;
193
194 if let Some(validate_aud_cfg) = validate_claims_cfg.validate_aud.as_ref() {
195 if !validate_aud_cfg.is_empty() {
196 let items = validate_aud_cfg
197 .iter()
198 .map(|(item, p_uname, p_cid, p_ipaddr, p_proto)| {
199 Self::replaces(connect_info, item, *p_uname, *p_cid, *p_ipaddr, *p_proto)
200 })
201 .collect::<Result<HashSet<String>>>()?;
202 validate_aud = true;
203 aud = Some(items);
204 required_spec_claims.insert("aud".into());
205 }
206 }
207
208 if let Some(validate_iss_cfg) = validate_claims_cfg.validate_iss.as_ref() {
209 if !validate_iss_cfg.is_empty() {
210 let items = validate_iss_cfg
211 .iter()
212 .map(|(item, p_uname, p_cid, p_ipaddr, p_proto)| {
213 Self::replaces(connect_info, item, *p_uname, *p_cid, *p_ipaddr, *p_proto)
214 })
215 .collect::<Result<HashSet<String>>>()?;
216 iss = Some(items);
217 required_spec_claims.insert("iss".into());
218 }
219 }
220
221 if let Some((item, p_uname, p_cid, p_ipaddr, p_proto)) = validate_claims_cfg.validate_sub.as_ref() {
222 sub = Some(Self::replaces(connect_info, item, *p_uname, *p_cid, *p_ipaddr, *p_proto)?);
223 required_spec_claims.insert("sub".into());
224 }
225
226 let header = jsonwebtoken::decode_header(token).map_err(|e| anyhow!(e))?;
227 log::debug!("header: {header:?}");
228 let mut validation = Validation::new(header.alg);
229 validation.validate_exp = validate_exp;
230 validation.validate_nbf = validate_nbf;
231 validation.validate_aud = validate_aud;
232 validation.aud = aud;
233 validation.iss = iss;
234 validation.sub = sub;
235 validation.required_spec_claims = required_spec_claims;
236
237 log::debug!("validation: {validation:?}");
238
239 let token_data = decode::<HashMap<String, serde_json::Value>>(
240 token,
241 &self.cfg.read().await.decoded_key,
242 &validation,
243 )
244 .map_err(|e| anyhow!(e))?;
245
246 Ok(token_data)
247 }
248
249 #[inline]
250 fn extended_auth(
251 &self,
252 connect_info: &ConnectInfo,
253 validate_claims_cfg: &ValidateClaims,
254 token_data: &TokenData<HashMap<String, serde_json::Value>>,
255 ) -> Result<()> {
256 let validates = validate_claims_cfg
257 .validate_customs
258 .iter()
259 .map(|(name, items)| {
260 items
261 .iter()
262 .map(|(item, p_uname, p_cid, p_ipaddr, p_proto)| {
263 Self::replaces(connect_info, item, *p_uname, *p_cid, *p_ipaddr, *p_proto)
264 })
265 .collect::<Result<Vec<String>>>()
266 .map(|items| (name, items))
267 })
268 .collect::<Result<Vec<(_, _)>>>()?;
269
270 let failed = validates.into_iter().find_map(|(name, items)| {
271 let claim_item = token_data.claims.get(name).and_then(|val| val.as_str());
272 let valid_res = claim_item.map(|s| items.iter().any(|item| item == s)).unwrap_or_default();
273 if !valid_res {
274 Some((name, items, claim_item))
275 } else {
276 None
277 }
278 });
279 log::debug!("failed: {failed:?}");
280 if let Some((name, expecteds, actuals)) = failed {
281 Err(anyhow!(format!(
282 "{} verification failed, expected value: {:?}, actual value: {:?}",
283 name, expecteds, actuals
284 )))
285 } else {
286 Ok(())
287 }
288 }
289}
290
291#[async_trait]
292impl Handler for AuthHandler {
293 async fn hook(&self, param: &Parameter, acc: Option<HookResult>) -> ReturnType {
294 match param {
295 Parameter::ClientAuthenticate(connect_info) => {
296 log::debug!("ClientAuthenticate auth-jwt");
297 if matches!(
298 acc,
299 Some(HookResult::AuthResult(AuthResult::BadUsernameOrPassword))
300 | Some(HookResult::AuthResult(AuthResult::NotAuthorized))
301 ) {
302 return (false, acc);
303 }
304
305 let token = match self.token(connect_info).await {
306 Some(token) => token,
307 None => return (false, Some(HookResult::AuthResult(AuthResult::NotAuthorized))),
308 };
309 log::debug!("ClientAuthenticate token: {token}");
310
311 let validate_claims_cfg = &self.cfg.read().await.validate_claims;
312 let token_data =
313 match self.standard_auth(connect_info, token.as_ref(), validate_claims_cfg).await {
314 Ok(token_data) => token_data,
315 Err(e) => {
316 log::warn!("{} token:{}, error: {}", connect_info.id(), token, e);
317 return (false, Some(HookResult::AuthResult(AuthResult::NotAuthorized)));
318 }
319 };
320
321 if let Err(e) = self.extended_auth(connect_info, validate_claims_cfg, &token_data) {
322 log::warn!("{} {}", connect_info.id(), e);
323 return (false, Some(HookResult::AuthResult(AuthResult::NotAuthorized)));
324 }
325
326 log::debug!("token_data header: {:?}", token_data.header);
327 log::debug!("token_data claims: {:?}", token_data.claims);
328
329 let superuser =
330 token_data.claims.get("superuser").and_then(|v| v.as_bool()).unwrap_or_default();
331
332 let rules = if let Some(acls) = token_data.claims.get("acl").and_then(|acl| acl.as_array()) {
333 match acls
334 .iter()
335 .map(|acl| Rule::try_from((acl, *connect_info)))
336 .collect::<Result<Vec<Rule>>>()
337 {
338 Err(e) => {
339 log::warn!("{} {}", connect_info.id(), e);
340 return (false, Some(HookResult::AuthResult(AuthResult::NotAuthorized)));
341 }
342 Ok(rules) => rules,
343 }
344 } else {
345 Vec::new()
346 };
347 log::debug!("rules: {rules:?}");
348 let expire_at =
349 token_data.claims.get("exp").and_then(|exp| exp.as_u64().map(Duration::from_secs));
350 let auth_info = AuthInfo { superuser, expire_at, rules };
351 return (false, Some(HookResult::AuthResult(AuthResult::Allow(superuser, Some(auth_info)))));
352 }
353
354 Parameter::ClientSubscribeCheckAcl(session, subscribe) => {
355 log::debug!("ClientSubscribeCheckAcl auth-jwt");
356 if let Some(HookResult::SubscribeAclResult(acl_result)) = &acc {
357 if acl_result.failure() {
358 return (false, acc);
359 }
360 }
361
362 if let Some(auth_info) = &session.auth_info {
363 if let Some(acl_res) = auth_info.subscribe_acl(subscribe).await {
364 return acl_res;
365 }
366 }
367 }
369
370 Parameter::MessagePublishCheckAcl(session, publish) => {
371 log::debug!("MessagePublishCheckAcl auth-jwt");
372 if let Some(HookResult::PublishAclResult(acl_res)) = &acc {
373 if acl_res.is_rejected() {
374 return (false, acc);
375 }
376 }
377
378 if let Some(auth_info) = &session.auth_info {
379 if let Some(acl_res) =
380 auth_info.publish_acl(publish, self.cfg.read().await.disconnect_if_pub_rejected).await
381 {
382 return acl_res;
383 }
384 }
385 }
387
388 Parameter::ClientKeepalive(s, _) => {
389 if let Some(auth) = &s.auth_info {
390 log::debug!("Keepalive auth-jwt, is_expired: {:?}", auth.is_expired());
391 if auth.is_expired() && self.cfg.read().await.disconnect_if_expiry {
392 if let Some(tx) = self.scx.extends.shared().await.entry(s.id().clone()).tx() {
393 if let Err(e) = tx.unbounded_send(Message::Closed(Reason::ConnectDisconnect(
394 Some(Disconnect::Other("JWT Auth expired".into())),
395 ))) {
396 log::warn!("{} {}", s.id(), e);
397 }
398 }
399 }
400 }
401 }
402
403 _ => {
404 log::error!("unimplemented, {param:?}")
405 }
406 }
407 (true, acc)
408 }
409}