Skip to main content

reddb_wire/redwire/
handshake.rs

1//! RedWire handshake payload contracts.
2//!
3//! Authentication policy and credential validation belong in the
4//! server. This module owns only the wire-visible JSON shapes used by
5//! Hello, HelloAck, AuthResponse, AuthOk, and AuthFail.
6
7use serde_json::Value as JsonValue;
8use std::fmt;
9
10pub use reddb_types::encoding::base64_encode as base64_std;
11
12use super::{BuildError, Frame, FrameBuilder, MessageKind, MAX_KNOWN_MINOR_VERSION};
13
14/// Methods RedWire v2.1 knows how to negotiate.
15pub const SUPPORTED_METHODS: &[&str] = &["bearer", "anonymous", "scram-sha-256", "oauth-jwt"];
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Hello {
19    pub versions: Vec<u8>,
20    pub auth_methods: Vec<String>,
21    pub features: u32,
22    pub client_name: Option<String>,
23}
24
25impl Hello {
26    pub fn to_payload(&self) -> Vec<u8> {
27        build_hello_payload(
28            &self.versions,
29            self.auth_methods.iter().map(String::as_str),
30            self.features,
31            self.client_name.as_deref(),
32        )
33    }
34
35    pub fn from_payload(bytes: &[u8]) -> Result<Self, String> {
36        let v: JsonValue =
37            serde_json::from_slice(bytes).map_err(|e| format!("Hello: invalid JSON: {e}"))?;
38        let obj = match v {
39            JsonValue::Object(o) => o,
40            _ => return Err("Hello: payload must be a JSON object".into()),
41        };
42        let versions: Vec<u8> = obj
43            .get("versions")
44            .and_then(|v| v.as_array())
45            .map(|arr| {
46                arr.iter()
47                    .filter_map(|n| n.as_u64().map(|u| u as u8))
48                    .collect()
49            })
50            .unwrap_or_default();
51        let auth_methods: Vec<String> = obj
52            .get("auth_methods")
53            .and_then(|v| v.as_array())
54            .map(|arr| {
55                arr.iter()
56                    .filter_map(|s| s.as_str().map(String::from))
57                    .collect()
58            })
59            .unwrap_or_default();
60        let features = obj
61            .get("features")
62            .and_then(|v| v.as_u64())
63            .map(|u| u as u32)
64            .unwrap_or(0);
65        let client_name = obj
66            .get("client_name")
67            .and_then(|v| v.as_str())
68            .map(String::from);
69        if versions.is_empty() {
70            return Err("Hello: versions[] is empty".into());
71        }
72        if auth_methods.is_empty() {
73            return Err("Hello: auth_methods[] is empty".into());
74        }
75        Ok(Self {
76            versions,
77            auth_methods,
78            features,
79            client_name,
80        })
81    }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct HelloAck {
86    pub version: u8,
87    pub auth: String,
88    pub features: u32,
89    pub server: Option<String>,
90    pub topology: Option<String>,
91}
92
93impl HelloAck {
94    pub fn from_payload(bytes: &[u8]) -> Result<Self, String> {
95        let obj = object_from_payload("HelloAck", bytes)?;
96        let version = required_u8(&obj, "HelloAck", "version")?;
97        let auth = required_string(&obj, "HelloAck", "auth")?;
98        let features = optional_u32(&obj, "features").unwrap_or(0);
99        let server = optional_string(&obj, "server");
100        let topology = optional_string(&obj, "topology");
101        Ok(Self {
102            version,
103            auth,
104            features,
105            server,
106            topology,
107        })
108    }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct AuthOk {
113    pub session_id: String,
114    pub username: Option<String>,
115    pub role: Option<String>,
116    pub features: u32,
117    pub server_signature: Option<String>,
118}
119
120impl AuthOk {
121    pub fn from_payload(bytes: &[u8]) -> Result<Self, String> {
122        let obj = object_from_payload("AuthOk", bytes)?;
123        let session_id = required_string(&obj, "AuthOk", "session_id")?;
124        let username = optional_string(&obj, "username");
125        let role = optional_string(&obj, "role");
126        let features = optional_u32(&obj, "features").unwrap_or(0);
127        let server_signature = optional_string(&obj, "v");
128        Ok(Self {
129            session_id,
130            username,
131            role,
132            features,
133            server_signature,
134        })
135    }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct AuthFail {
140    pub reason: String,
141}
142
143impl AuthFail {
144    pub fn from_payload(bytes: &[u8]) -> Result<Self, String> {
145        let obj = object_from_payload("AuthFail", bytes)?;
146        Ok(Self {
147            reason: required_string(&obj, "AuthFail", "reason")?,
148        })
149    }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct AuthResponseKindError {
154    pub expected: &'static str,
155    pub actual: MessageKind,
156}
157
158impl fmt::Display for AuthResponseKindError {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        write!(f, "expected {}", self.expected)
161    }
162}
163
164impl std::error::Error for AuthResponseKindError {}
165
166pub fn build_hello_payload<'a, I>(
167    versions: &[u8],
168    auth_methods: I,
169    features: u32,
170    client_name: Option<&str>,
171) -> Vec<u8>
172where
173    I: IntoIterator<Item = &'a str>,
174{
175    let mut obj = serde_json::Map::new();
176    obj.insert(
177        "versions".to_string(),
178        JsonValue::Array(
179            versions
180                .iter()
181                .map(|version| JsonValue::Number((*version).into()))
182                .collect(),
183        ),
184    );
185    obj.insert(
186        "auth_methods".to_string(),
187        JsonValue::Array(
188            auth_methods
189                .into_iter()
190                .map(|method| JsonValue::String(method.to_string()))
191                .collect(),
192        ),
193    );
194    obj.insert("features".to_string(), JsonValue::Number(features.into()));
195    if let Some(name) = client_name {
196        obj.insert(
197            "client_name".to_string(),
198            JsonValue::String(name.to_string()),
199        );
200    }
201    serde_json::to_vec(&JsonValue::Object(obj)).unwrap_or_default()
202}
203
204pub fn build_client_hello_payload<'a, I>(
205    auth_methods: I,
206    features: u32,
207    client_name: Option<&str>,
208) -> Vec<u8>
209where
210    I: IntoIterator<Item = &'a str>,
211{
212    build_hello_payload(
213        &[MAX_KNOWN_MINOR_VERSION],
214        auth_methods,
215        features,
216        client_name,
217    )
218}
219
220pub fn build_client_hello_frame<'a, I>(
221    correlation_id: u64,
222    auth_methods: I,
223    features: u32,
224    client_name: Option<&str>,
225) -> Result<Frame, BuildError>
226where
227    I: IntoIterator<Item = &'a str>,
228{
229    FrameBuilder::request(correlation_id)
230        .kind(MessageKind::Hello)
231        .payload(build_client_hello_payload(
232            auth_methods,
233            features,
234            client_name,
235        ))
236        .build()
237}
238
239pub fn choose_hello_minor_version(client_versions: &[u8]) -> Option<u8> {
240    client_versions
241        .iter()
242        .copied()
243        .filter(|version| *version > 0 && *version <= MAX_KNOWN_MINOR_VERSION)
244        .max()
245}
246
247pub fn build_hello_ack(
248    chosen_version: u8,
249    chosen_auth: &str,
250    server_features: u32,
251    topology: Option<&crate::topology::Topology>,
252) -> Vec<u8> {
253    let mut obj = serde_json::Map::new();
254    obj.insert(
255        "version".to_string(),
256        JsonValue::Number(chosen_version.into()),
257    );
258    obj.insert(
259        "auth".to_string(),
260        JsonValue::String(chosen_auth.to_string()),
261    );
262    obj.insert(
263        "features".to_string(),
264        JsonValue::Number(server_features.into()),
265    );
266    obj.insert(
267        "server".to_string(),
268        JsonValue::String(format!("reddb/{}", env!("CARGO_PKG_VERSION"))),
269    );
270    if let Some(topo) = topology {
271        obj.insert(
272            "topology".to_string(),
273            JsonValue::String(crate::topology::encode_topology_for_hello_ack(topo)),
274        );
275    }
276    serde_json::to_vec(&JsonValue::Object(obj)).unwrap_or_default()
277}
278
279pub fn build_hello_ack_frame(
280    correlation_id: u64,
281    chosen_version: u8,
282    chosen_auth: &str,
283    server_features: u32,
284    topology: Option<&crate::topology::Topology>,
285) -> Result<Frame, BuildError> {
286    FrameBuilder::reply_to(correlation_id)
287        .kind(MessageKind::HelloAck)
288        .payload(build_hello_ack(
289            chosen_version,
290            chosen_auth,
291            server_features,
292            topology,
293        ))
294        .build()
295}
296
297pub fn build_auth_response_anonymous_payload() -> Vec<u8> {
298    Vec::new()
299}
300
301pub fn build_auth_response_bearer_payload(token: &str) -> Vec<u8> {
302    let mut obj = serde_json::Map::new();
303    obj.insert("token".to_string(), JsonValue::String(token.to_string()));
304    serde_json::to_vec(&JsonValue::Object(obj)).unwrap_or_default()
305}
306
307pub fn parse_auth_response_bearer_token(payload: &[u8]) -> Result<String, String> {
308    let obj = object_from_payload("AuthResponse", payload)?;
309    required_string(&obj, "AuthResponse", "token")
310}
311
312pub fn build_auth_response_oauth_jwt_payload(jwt: &str) -> Vec<u8> {
313    let mut obj = serde_json::Map::new();
314    obj.insert("jwt".to_string(), JsonValue::String(jwt.to_string()));
315    serde_json::to_vec(&JsonValue::Object(obj)).unwrap_or_default()
316}
317
318pub fn build_auth_response_frame(
319    correlation_id: u64,
320    payload: Vec<u8>,
321) -> Result<Frame, BuildError> {
322    FrameBuilder::request(correlation_id)
323        .kind(MessageKind::AuthResponse)
324        .payload(payload)
325        .build()
326}
327
328pub fn parse_auth_response_oauth_jwt(payload: &[u8]) -> Result<String, String> {
329    let obj = object_from_payload("AuthResponse", payload)?;
330    required_string(&obj, "AuthResponse", "jwt")
331}
332
333pub fn expect_auth_response_payload<'a>(
334    kind: MessageKind,
335    payload: &'a [u8],
336    expected: &'static str,
337) -> Result<&'a [u8], AuthResponseKindError> {
338    if kind == MessageKind::AuthResponse {
339        Ok(payload)
340    } else {
341        Err(AuthResponseKindError {
342            expected,
343            actual: kind,
344        })
345    }
346}
347
348pub fn build_auth_ok_payload(
349    session_id: &str,
350    username: &str,
351    role: &str,
352    server_features: u32,
353) -> Vec<u8> {
354    let mut obj = serde_json::Map::new();
355    obj.insert(
356        "session_id".to_string(),
357        JsonValue::String(session_id.to_string()),
358    );
359    obj.insert(
360        "username".to_string(),
361        JsonValue::String(username.to_string()),
362    );
363    obj.insert("role".to_string(), JsonValue::String(role.to_string()));
364    obj.insert(
365        "features".to_string(),
366        JsonValue::Number(server_features.into()),
367    );
368    serde_json::to_vec(&JsonValue::Object(obj)).unwrap_or_default()
369}
370
371pub fn build_auth_ok_frame_from_payload(
372    correlation_id: u64,
373    payload: Vec<u8>,
374) -> Result<Frame, BuildError> {
375    FrameBuilder::reply_to(correlation_id)
376        .kind(MessageKind::AuthOk)
377        .payload(payload)
378        .build()
379}
380
381pub fn build_auth_fail_frame(correlation_id: u64, reason: &str) -> Result<Frame, BuildError> {
382    FrameBuilder::reply_to(correlation_id)
383        .kind(MessageKind::AuthFail)
384        .payload(build_auth_fail_payload(reason))
385        .build()
386}
387
388pub fn build_scram_auth_ok_payload(
389    session_id: &str,
390    username: &str,
391    role: &str,
392    server_features: u32,
393    server_signature: &[u8],
394) -> Vec<u8> {
395    let mut obj = serde_json::Map::new();
396    obj.insert(
397        "session_id".to_string(),
398        JsonValue::String(session_id.to_string()),
399    );
400    obj.insert(
401        "username".to_string(),
402        JsonValue::String(username.to_string()),
403    );
404    obj.insert("role".to_string(), JsonValue::String(role.to_string()));
405    obj.insert(
406        "features".to_string(),
407        JsonValue::Number(server_features.into()),
408    );
409    obj.insert(
410        "v".to_string(),
411        JsonValue::String(base64_std(server_signature)),
412    );
413    serde_json::to_vec(&JsonValue::Object(obj)).unwrap_or_default()
414}
415
416pub fn build_auth_fail_payload(reason: &str) -> Vec<u8> {
417    let mut obj = serde_json::Map::new();
418    obj.insert("reason".to_string(), JsonValue::String(reason.to_string()));
419    serde_json::to_vec(&JsonValue::Object(obj)).unwrap_or_default()
420}
421
422/// Parse a SCRAM client-first-message.
423///
424/// Format: `n,,n=<user>,r=<client_nonce>` (no channel binding, no authzid).
425/// Returns `(username, client_nonce, bare_message)`.
426pub fn parse_scram_client_first(payload: &[u8]) -> Result<(String, String, String), String> {
427    let s = std::str::from_utf8(payload).map_err(|_| "client-first not UTF-8".to_string())?;
428    let bare = s
429        .strip_prefix("n,,")
430        .ok_or_else(|| "client-first must start with 'n,,' (no channel binding)".to_string())?;
431    let mut user = None;
432    let mut nonce = None;
433    for part in bare.split(',') {
434        if let Some(v) = part.strip_prefix("n=") {
435            user = Some(v.to_string());
436        } else if let Some(v) = part.strip_prefix("r=") {
437            nonce = Some(v.to_string());
438        }
439    }
440    let user = user.ok_or_else(|| "missing n=<user>".to_string())?;
441    let nonce = nonce.ok_or_else(|| "missing r=<nonce>".to_string())?;
442    Ok((user, nonce, bare.to_string()))
443}
444
445/// Build the SCRAM server-first-message.
446///
447/// Format: `r=<client_nonce><server_nonce>,s=<salt_b64>,i=<iter>`.
448pub fn build_scram_server_first(
449    client_nonce: &str,
450    server_nonce: &str,
451    salt: &[u8],
452    iter: u32,
453) -> String {
454    format!(
455        "r={client_nonce}{server_nonce},s={},i={iter}",
456        base64_std(salt)
457    )
458}
459
460/// Parse SCRAM client-final-message.
461///
462/// Format: `c=<channel_binding_b64>,r=<combined_nonce>,p=<proof_b64>`.
463pub fn parse_scram_client_final(payload: &[u8]) -> Result<(String, Vec<u8>, String), String> {
464    let s = std::str::from_utf8(payload).map_err(|_| "client-final not UTF-8".to_string())?;
465    let mut channel_binding = None;
466    let mut nonce = None;
467    let mut proof_b64 = None;
468    for part in s.split(',') {
469        if let Some(v) = part.strip_prefix("c=") {
470            channel_binding = Some(v.to_string());
471        } else if let Some(v) = part.strip_prefix("r=") {
472            nonce = Some(v.to_string());
473        } else if let Some(v) = part.strip_prefix("p=") {
474            proof_b64 = Some(v.to_string());
475        }
476    }
477    let channel_binding =
478        channel_binding.ok_or_else(|| "missing c=<channel-binding>".to_string())?;
479    let nonce = nonce.ok_or_else(|| "missing r=<nonce>".to_string())?;
480    let proof_b64 = proof_b64.ok_or_else(|| "missing p=<proof>".to_string())?;
481    let proof = base64_std_decode(&proof_b64)
482        .ok_or_else(|| "client proof is not valid base64".to_string())?;
483    if channel_binding != "biws" {
484        return Err(format!(
485            "channel binding must be 'biws' (n,,), got '{channel_binding}'"
486        ));
487    }
488    let no_proof = format!("c={channel_binding},r={nonce}");
489    Ok((nonce, proof, no_proof))
490}
491
492pub fn base64_std_decode(input: &str) -> Option<Vec<u8>> {
493    let trimmed = input.trim_end_matches('=');
494    let mut out = Vec::with_capacity(trimmed.len() * 3 / 4);
495    let mut buf = 0u32;
496    let mut bits = 0u8;
497    for ch in trimmed.bytes() {
498        let v: u32 = match ch {
499            b'A'..=b'Z' => (ch - b'A') as u32,
500            b'a'..=b'z' => (ch - b'a' + 26) as u32,
501            b'0'..=b'9' => (ch - b'0' + 52) as u32,
502            b'+' => 62,
503            b'/' => 63,
504            _ => return None,
505        };
506        buf = (buf << 6) | v;
507        bits += 6;
508        if bits >= 8 {
509            bits -= 8;
510            out.push(((buf >> bits) & 0xFF) as u8);
511        }
512    }
513    Some(out)
514}
515
516fn object_from_payload(
517    name: &str,
518    bytes: &[u8],
519) -> Result<serde_json::Map<String, JsonValue>, String> {
520    let v: JsonValue =
521        serde_json::from_slice(bytes).map_err(|e| format!("{name}: invalid JSON: {e}"))?;
522    match v {
523        JsonValue::Object(o) => Ok(o),
524        _ => Err(format!("{name}: payload must be a JSON object")),
525    }
526}
527
528fn required_string(
529    obj: &serde_json::Map<String, JsonValue>,
530    name: &str,
531    field: &str,
532) -> Result<String, String> {
533    obj.get(field)
534        .and_then(JsonValue::as_str)
535        .map(String::from)
536        .ok_or_else(|| format!("{name}: missing {field} string"))
537}
538
539fn optional_string(obj: &serde_json::Map<String, JsonValue>, field: &str) -> Option<String> {
540    obj.get(field).and_then(JsonValue::as_str).map(String::from)
541}
542
543fn required_u8(
544    obj: &serde_json::Map<String, JsonValue>,
545    name: &str,
546    field: &str,
547) -> Result<u8, String> {
548    let n = obj
549        .get(field)
550        .and_then(JsonValue::as_u64)
551        .ok_or_else(|| format!("{name}: missing {field} number"))?;
552    u8::try_from(n).map_err(|_| format!("{name}: {field} out of range for u8"))
553}
554
555fn optional_u32(obj: &serde_json::Map<String, JsonValue>, field: &str) -> Option<u32> {
556    obj.get(field)
557        .and_then(JsonValue::as_u64)
558        .and_then(|n| u32::try_from(n).ok())
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564    use crate::topology::{Endpoint, ReplicaInfo, Topology};
565
566    #[test]
567    fn hello_parses_client_payload() {
568        let payload =
569            br#"{"versions":[1],"auth_methods":["bearer"],"features":1,"client_name":"x"}"#;
570        let hello = Hello::from_payload(payload).unwrap();
571        assert_eq!(hello.versions, vec![1]);
572        assert_eq!(hello.auth_methods, vec!["bearer"]);
573        assert_eq!(hello.features, 1);
574        assert_eq!(hello.client_name.as_deref(), Some("x"));
575    }
576
577    #[test]
578    fn hello_builds_client_payload() {
579        let bytes = build_hello_payload(&[1], ["anonymous", "bearer"], 7, Some("client"));
580        let hello = Hello::from_payload(&bytes).unwrap();
581        assert_eq!(hello.versions, vec![1]);
582        assert_eq!(hello.auth_methods, vec!["anonymous", "bearer"]);
583        assert_eq!(hello.features, 7);
584        assert_eq!(hello.client_name.as_deref(), Some("client"));
585    }
586
587    #[test]
588    fn client_hello_payload_uses_current_minor_version() {
589        let bytes = build_client_hello_payload(["anonymous"], 0, Some("client"));
590        let hello = Hello::from_payload(&bytes).unwrap();
591        assert_eq!(hello.versions, vec![MAX_KNOWN_MINOR_VERSION]);
592        assert_eq!(hello.auth_methods, vec!["anonymous"]);
593        assert_eq!(hello.client_name.as_deref(), Some("client"));
594    }
595
596    #[test]
597    fn hello_minor_version_negotiation_picks_highest_supported_nonzero_version() {
598        assert_eq!(
599            choose_hello_minor_version(&[0, MAX_KNOWN_MINOR_VERSION]),
600            Some(MAX_KNOWN_MINOR_VERSION)
601        );
602        assert_eq!(
603            choose_hello_minor_version(&[
604                MAX_KNOWN_MINOR_VERSION.saturating_add(1),
605                MAX_KNOWN_MINOR_VERSION,
606                1,
607            ]),
608            Some(MAX_KNOWN_MINOR_VERSION)
609        );
610        assert_eq!(choose_hello_minor_version(&[]), None);
611        assert_eq!(choose_hello_minor_version(&[0]), None);
612        assert_eq!(
613            choose_hello_minor_version(&[MAX_KNOWN_MINOR_VERSION.saturating_add(1)]),
614            None
615        );
616    }
617
618    #[test]
619    fn hello_requires_versions_and_auth_methods() {
620        assert!(Hello::from_payload(br#"{"auth_methods":["bearer"]}"#).is_err());
621        assert!(Hello::from_payload(br#"{"versions":[1]}"#).is_err());
622    }
623
624    #[test]
625    fn hello_ack_can_embed_topology() {
626        let topology = Topology {
627            epoch: 7,
628            primary: Endpoint {
629                addr: "127.0.0.1:5050".to_string(),
630                region: "local".to_string(),
631            },
632            replicas: vec![ReplicaInfo {
633                addr: "127.0.0.1:5051".to_string(),
634                region: "local".to_string(),
635                healthy: true,
636                lag_ms: 3,
637                last_applied_lsn: 9,
638                rebootstrapping: false,
639            }],
640        };
641        let bytes = build_hello_ack(1, "bearer", 0, Some(&topology));
642        let json: JsonValue = serde_json::from_slice(&bytes).unwrap();
643        assert_eq!(json["version"], 1);
644        assert!(json["topology"].as_str().is_some());
645        let ack = HelloAck::from_payload(&bytes).unwrap();
646        assert_eq!(ack.version, 1);
647        assert_eq!(ack.auth, "bearer");
648        assert_eq!(ack.features, 0);
649        assert!(ack.topology.is_some());
650    }
651
652    #[test]
653    fn auth_response_builders_are_pinned() {
654        assert!(build_auth_response_anonymous_payload().is_empty());
655
656        let bearer: JsonValue =
657            serde_json::from_slice(&build_auth_response_bearer_payload("token")).unwrap();
658        assert_eq!(bearer["token"], "token");
659
660        let oauth: JsonValue =
661            serde_json::from_slice(&build_auth_response_oauth_jwt_payload("jwt")).unwrap();
662        assert_eq!(oauth["jwt"], "jwt");
663    }
664
665    #[test]
666    fn auth_response_kind_expectation_is_pinned() {
667        assert_eq!(
668            expect_auth_response_payload(MessageKind::AuthResponse, b"proof", "AuthResponse")
669                .unwrap(),
670            b"proof"
671        );
672
673        let err =
674            expect_auth_response_payload(MessageKind::Hello, b"{}", "AuthResponse").unwrap_err();
675        assert_eq!(err.actual, MessageKind::Hello);
676        assert_eq!(err.to_string(), "expected AuthResponse");
677    }
678
679    #[test]
680    fn auth_ok_and_fail_parse_payloads() {
681        let ok = AuthOk::from_payload(&build_auth_ok_payload("s1", "alice", "admin", 3)).unwrap();
682        assert_eq!(ok.session_id, "s1");
683        assert_eq!(ok.username.as_deref(), Some("alice"));
684        assert_eq!(ok.role.as_deref(), Some("admin"));
685        assert_eq!(ok.features, 3);
686        assert_eq!(ok.server_signature.as_deref(), None);
687
688        let scram_ok = AuthOk::from_payload(&build_scram_auth_ok_payload(
689            "s1", "alice", "admin", 3, b"sig",
690        ))
691        .unwrap();
692        assert_eq!(scram_ok.server_signature.as_deref(), Some("c2ln"));
693
694        let fail = AuthFail::from_payload(&build_auth_fail_payload("nope")).unwrap();
695        assert_eq!(fail.reason, "nope");
696    }
697
698    #[test]
699    fn handshake_frame_builders_pin_message_kinds() {
700        let hello_ack = build_hello_ack_frame(7, 1, "anonymous", 3, None).unwrap();
701        assert_eq!(hello_ack.kind, MessageKind::HelloAck);
702        assert_eq!(hello_ack.correlation_id, 7);
703        assert_eq!(
704            HelloAck::from_payload(&hello_ack.payload).unwrap().auth,
705            "anonymous"
706        );
707
708        let auth_ok =
709            build_auth_ok_frame_from_payload(8, build_auth_ok_payload("s1", "alice", "admin", 3))
710                .unwrap();
711        assert_eq!(auth_ok.kind, MessageKind::AuthOk);
712        assert_eq!(auth_ok.correlation_id, 8);
713        assert_eq!(
714            AuthOk::from_payload(&auth_ok.payload)
715                .unwrap()
716                .username
717                .as_deref(),
718            Some("alice")
719        );
720
721        let auth_fail = build_auth_fail_frame(9, "nope").unwrap();
722        assert_eq!(auth_fail.kind, MessageKind::AuthFail);
723        assert_eq!(auth_fail.correlation_id, 9);
724        assert_eq!(
725            AuthFail::from_payload(&auth_fail.payload).unwrap().reason,
726            "nope"
727        );
728    }
729
730    #[test]
731    fn auth_response_parsers_are_pinned() {
732        assert_eq!(
733            parse_auth_response_bearer_token(&build_auth_response_bearer_payload("token")).unwrap(),
734            "token"
735        );
736        assert_eq!(
737            parse_auth_response_oauth_jwt(&build_auth_response_oauth_jwt_payload("jwt")).unwrap(),
738            "jwt"
739        );
740        assert!(parse_auth_response_bearer_token(br#"{"jwt":"x"}"#).is_err());
741    }
742
743    #[test]
744    fn scram_wire_messages_round_trip() {
745        let (user, nonce, bare) = parse_scram_client_first(b"n,,n=alice,r=client").unwrap();
746        assert_eq!(user, "alice");
747        assert_eq!(nonce, "client");
748        assert_eq!(bare, "n=alice,r=client");
749
750        let server_first = build_scram_server_first("client", "server", b"salt", 4096);
751        assert_eq!(server_first, "r=clientserver,s=c2FsdA==,i=4096");
752
753        let proof = base64_std(b"proof");
754        let final_msg = format!("c=biws,r=clientserver,p={proof}");
755        let (combined, decoded_proof, without_proof) =
756            parse_scram_client_final(final_msg.as_bytes()).unwrap();
757        assert_eq!(combined, "clientserver");
758        assert_eq!(decoded_proof, b"proof");
759        assert_eq!(without_proof, "c=biws,r=clientserver");
760    }
761}