mqtt5_protocol/packet/
auth.rs

1use crate::error::{MqttError, Result};
2use crate::packet::{FixedHeader, MqttPacket, PacketType};
3use crate::prelude::{String, ToString, Vec};
4use crate::protocol::v5::properties::Properties;
5use crate::protocol::v5::reason_codes::ReasonCode;
6use bytes::{Buf, BufMut};
7
8/// AUTH packet for MQTT v5.0 enhanced authentication
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct AuthPacket {
11    /// Reason code indicating the result of the authentication
12    pub reason_code: ReasonCode,
13    /// Properties associated with the authentication
14    pub properties: Properties,
15}
16
17impl AuthPacket {
18    /// Creates a new AUTH packet
19    #[must_use]
20    pub fn new(reason_code: ReasonCode) -> Self {
21        Self {
22            reason_code,
23            properties: Properties::default(),
24        }
25    }
26
27    #[must_use]
28    /// Creates a new AUTH packet with properties
29    pub fn with_properties(reason_code: ReasonCode, properties: Properties) -> Self {
30        Self {
31            reason_code,
32            properties,
33        }
34    }
35
36    /// Creates an AUTH packet for continuing authentication
37    ///
38    /// # Errors
39    ///
40    /// Returns an error if the operation fails
41    pub fn continue_authentication(
42        auth_method: String,
43        auth_data: Option<Vec<u8>>,
44    ) -> Result<Self> {
45        let mut properties = Properties::default();
46
47        // Authentication method is required
48        properties.add(
49            crate::protocol::v5::properties::PropertyId::AuthenticationMethod,
50            crate::protocol::v5::properties::PropertyValue::Utf8String(auth_method),
51        )?;
52
53        // Authentication data is optional
54        if let Some(data) = auth_data {
55            properties.add(
56                crate::protocol::v5::properties::PropertyId::AuthenticationData,
57                crate::protocol::v5::properties::PropertyValue::BinaryData(data.into()),
58            )?;
59        }
60
61        Ok(Self::with_properties(
62            ReasonCode::ContinueAuthentication,
63            properties,
64        ))
65    }
66
67    /// Creates an AUTH packet for re-authentication
68    ///
69    /// # Errors
70    ///
71    /// Returns an error if the operation fails
72    pub fn re_authenticate(auth_method: String, auth_data: Option<Vec<u8>>) -> Result<Self> {
73        let mut properties = Properties::default();
74
75        // Authentication method is required
76        properties.add(
77            crate::protocol::v5::properties::PropertyId::AuthenticationMethod,
78            crate::protocol::v5::properties::PropertyValue::Utf8String(auth_method),
79        )?;
80
81        // Authentication data is optional
82        if let Some(data) = auth_data {
83            properties.add(
84                crate::protocol::v5::properties::PropertyId::AuthenticationData,
85                crate::protocol::v5::properties::PropertyValue::BinaryData(data.into()),
86            )?;
87        }
88
89        Ok(Self::with_properties(
90            ReasonCode::ReAuthenticate,
91            properties,
92        ))
93    }
94
95    /// Creates a successful authentication response
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the operation fails
100    pub fn success(auth_method: String) -> Result<Self> {
101        let mut properties = Properties::default();
102
103        properties.add(
104            crate::protocol::v5::properties::PropertyId::AuthenticationMethod,
105            crate::protocol::v5::properties::PropertyValue::Utf8String(auth_method),
106        )?;
107
108        Ok(Self::with_properties(ReasonCode::Success, properties))
109    }
110
111    /// Creates an authentication failure response
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the operation fails
116    pub fn failure(reason_code: ReasonCode, reason_string: Option<String>) -> Result<Self> {
117        if reason_code.is_success() {
118            return Err(MqttError::ProtocolError(
119                "Cannot create failure AUTH packet with success reason code".to_string(),
120            ));
121        }
122
123        let mut properties = Properties::default();
124
125        if let Some(reason) = reason_string {
126            properties.add(
127                crate::protocol::v5::properties::PropertyId::ReasonString,
128                crate::protocol::v5::properties::PropertyValue::Utf8String(reason),
129            )?;
130        }
131
132        Ok(Self::with_properties(reason_code, properties))
133    }
134
135    #[must_use]
136    /// Gets the authentication method from properties
137    pub fn authentication_method(&self) -> Option<&str> {
138        if let Some(crate::protocol::v5::properties::PropertyValue::Utf8String(method)) = self
139            .properties
140            .get(crate::protocol::v5::properties::PropertyId::AuthenticationMethod)
141        {
142            Some(method)
143        } else {
144            None
145        }
146    }
147
148    #[must_use]
149    /// Gets the authentication data from properties
150    pub fn authentication_data(&self) -> Option<&[u8]> {
151        if let Some(crate::protocol::v5::properties::PropertyValue::BinaryData(data)) = self
152            .properties
153            .get(crate::protocol::v5::properties::PropertyId::AuthenticationData)
154        {
155            Some(data)
156        } else {
157            None
158        }
159    }
160
161    #[must_use]
162    /// Gets the reason string from properties
163    pub fn reason_string(&self) -> Option<&str> {
164        if let Some(crate::protocol::v5::properties::PropertyValue::Utf8String(reason)) = self
165            .properties
166            .get(crate::protocol::v5::properties::PropertyId::ReasonString)
167        {
168            Some(reason)
169        } else {
170            None
171        }
172    }
173
174    /// Validates the AUTH packet
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if the operation fails
179    pub fn validate(&self) -> Result<()> {
180        // For AUTH packets, authentication method is required for Continue and ReAuth
181        if self.authentication_method().is_none()
182            && (self.reason_code == ReasonCode::ContinueAuthentication
183                || self.reason_code == ReasonCode::ReAuthenticate)
184        {
185            return Err(MqttError::ProtocolError(
186                "Authentication method is required for AUTH packets with ContinueAuthentication or ReAuthenticate reason codes".to_string()
187            ));
188        }
189
190        Ok(())
191    }
192}
193
194impl MqttPacket for AuthPacket {
195    fn packet_type(&self) -> PacketType {
196        PacketType::Auth
197    }
198
199    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
200        self.validate()?;
201
202        // For AUTH packets, if reason code is Success and properties are empty,
203        // we can encode as an empty packet (remaining length = 0)
204        if self.reason_code == ReasonCode::Success && self.properties.is_empty() {
205            // Empty packet, nothing to encode
206            return Ok(());
207        }
208
209        // Encode reason code
210        buf.put_u8(u8::from(self.reason_code));
211
212        // Encode properties
213        self.properties.encode(buf)?;
214
215        Ok(())
216    }
217
218    fn decode_body<B: Buf>(buf: &mut B, fixed_header: &FixedHeader) -> Result<Self> {
219        if fixed_header.remaining_length == 0 {
220            // Empty AUTH packet defaults to Success with no properties
221            return Ok(Self::new(ReasonCode::Success));
222        }
223
224        // Decode reason code
225        if !buf.has_remaining() {
226            return Err(MqttError::MalformedPacket(
227                "Missing reason code in AUTH packet".to_string(),
228            ));
229        }
230
231        let reason_code_val = buf.get_u8();
232        let reason_code = ReasonCode::from_u8(reason_code_val)
233            .ok_or(MqttError::InvalidReasonCode(reason_code_val))?;
234
235        // Decode properties (if remaining length > 1)
236        let properties = if buf.has_remaining() {
237            Properties::decode(buf)?
238        } else {
239            Properties::default()
240        };
241
242        let packet = Self {
243            reason_code,
244            properties,
245        };
246
247        packet.validate()?;
248        Ok(packet)
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::protocol::v5::properties::{PropertyId, PropertyValue};
256    use bytes::BytesMut;
257
258    #[test]
259    fn test_auth_packet_new() {
260        let packet = AuthPacket::new(ReasonCode::Success);
261        assert_eq!(packet.reason_code, ReasonCode::Success);
262        assert!(packet.properties.is_empty());
263    }
264
265    #[test]
266    fn test_auth_packet_continue_authentication() {
267        let packet = AuthPacket::continue_authentication(
268            "SCRAM-SHA-256".to_string(),
269            Some(b"challenge_data".to_vec()),
270        )
271        .unwrap();
272
273        assert_eq!(packet.reason_code, ReasonCode::ContinueAuthentication);
274        assert_eq!(packet.authentication_method(), Some("SCRAM-SHA-256"));
275        assert_eq!(
276            packet.authentication_data(),
277            Some(b"challenge_data".as_ref())
278        );
279    }
280
281    #[test]
282    fn test_auth_packet_re_authenticate() {
283        let packet = AuthPacket::re_authenticate("OAUTH2".to_string(), None).unwrap();
284
285        assert_eq!(packet.reason_code, ReasonCode::ReAuthenticate);
286        assert_eq!(packet.authentication_method(), Some("OAUTH2"));
287        assert!(packet.authentication_data().is_none());
288    }
289
290    #[test]
291    fn test_auth_packet_success() {
292        let packet = AuthPacket::success("PLAIN".to_string()).unwrap();
293
294        assert_eq!(packet.reason_code, ReasonCode::Success);
295        assert_eq!(packet.authentication_method(), Some("PLAIN"));
296    }
297
298    #[test]
299    fn test_auth_packet_failure() {
300        let packet = AuthPacket::failure(
301            ReasonCode::BadAuthenticationMethod,
302            Some("Unsupported method".to_string()),
303        )
304        .unwrap();
305
306        assert_eq!(packet.reason_code, ReasonCode::BadAuthenticationMethod);
307        assert_eq!(packet.reason_string(), Some("Unsupported method"));
308    }
309
310    #[test]
311    fn test_auth_packet_failure_with_success_code_fails() {
312        let result = AuthPacket::failure(ReasonCode::Success, None);
313        assert!(result.is_err());
314    }
315
316    #[test]
317    fn test_auth_packet_encode_decode_empty() {
318        let packet = AuthPacket::new(ReasonCode::Success);
319        let mut buf = BytesMut::new();
320
321        packet.encode(&mut buf).unwrap();
322
323        // Decode the complete packet (including fixed header)
324        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
325        let decoded = AuthPacket::decode_body(&mut buf, &fixed_header).unwrap();
326
327        assert_eq!(decoded.reason_code, ReasonCode::Success);
328        assert!(decoded.properties.is_empty());
329    }
330
331    #[test]
332    fn test_auth_packet_encode_decode_with_properties() {
333        let packet = AuthPacket::continue_authentication(
334            "SCRAM-SHA-1".to_string(),
335            Some(b"server_nonce".to_vec()),
336        )
337        .unwrap();
338
339        let mut buf = BytesMut::new();
340        packet.encode(&mut buf).unwrap();
341
342        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
343        let decoded = AuthPacket::decode_body(&mut buf, &fixed_header).unwrap();
344
345        assert_eq!(decoded.reason_code, ReasonCode::ContinueAuthentication);
346        assert_eq!(decoded.authentication_method(), Some("SCRAM-SHA-1"));
347        assert_eq!(
348            decoded.authentication_data(),
349            Some(b"server_nonce".as_ref())
350        );
351    }
352
353    #[test]
354    fn test_auth_packet_encode_decode_failure() {
355        let packet = AuthPacket::failure(
356            ReasonCode::NotAuthorized,
357            Some("Invalid credentials".to_string()),
358        )
359        .unwrap();
360
361        let mut buf = BytesMut::new();
362        packet.encode(&mut buf).unwrap();
363
364        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
365        let decoded = AuthPacket::decode_body(&mut buf, &fixed_header).unwrap();
366
367        assert_eq!(decoded.reason_code, ReasonCode::NotAuthorized);
368        assert_eq!(decoded.reason_string(), Some("Invalid credentials"));
369    }
370
371    #[test]
372    fn test_auth_packet_validation_missing_auth_method() {
373        let packet = AuthPacket::new(ReasonCode::ContinueAuthentication);
374        // Packet without authentication method should be invalid
375        let result = packet.validate();
376        assert!(result.is_err());
377    }
378
379    #[test]
380    fn test_auth_packet_decode_malformed() {
381        let mut buf = BytesMut::new();
382        // Empty buffer should fail for packets with remaining length > 0
383
384        let result = AuthPacket::decode_body(&mut buf, &FixedHeader::new(PacketType::Auth, 0, 1));
385        assert!(result.is_err());
386    }
387
388    #[test]
389    fn test_auth_packet_decode_invalid_reason_code() {
390        let mut buf = BytesMut::new();
391        buf.put_u8(0xFF); // Invalid reason code
392        buf.put_u8(0x00); // Empty properties
393
394        let result = AuthPacket::decode_body(&mut buf, &FixedHeader::new(PacketType::Auth, 0, 2));
395        assert!(result.is_err());
396    }
397
398    #[test]
399    fn test_auth_packet_property_getters() {
400        let mut properties = Properties::default();
401        properties
402            .add(
403                PropertyId::AuthenticationMethod,
404                PropertyValue::Utf8String("TEST".to_string()),
405            )
406            .unwrap();
407        properties
408            .add(
409                PropertyId::AuthenticationData,
410                PropertyValue::BinaryData(b"data".to_vec().into()),
411            )
412            .unwrap();
413        properties
414            .add(
415                PropertyId::ReasonString,
416                PropertyValue::Utf8String("reason".to_string()),
417            )
418            .unwrap();
419
420        let packet = AuthPacket::with_properties(ReasonCode::Success, properties);
421
422        assert_eq!(packet.authentication_method(), Some("TEST"));
423        assert_eq!(packet.authentication_data(), Some(b"data".as_ref()));
424        assert_eq!(packet.reason_string(), Some("reason"));
425    }
426}