Skip to main content

oracledb_protocol/thin/
auth.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4use crate::wire::ProtocolLimits;
5
6pub fn append_auth_phase_one(
7    out: &mut Vec<u8>,
8    user: &str,
9    program: &str,
10    machine: &str,
11    osuser: &str,
12    terminal: &str,
13    pid: u32,
14) -> Result<()> {
15    let mut writer = TtcWriter::new();
16    writer.write_function_code(TNS_FUNC_AUTH_PHASE_ONE);
17    write_auth_header(&mut writer, user, TNS_AUTH_MODE_LOGON, 5)?;
18    write_key_value(&mut writer, "AUTH_TERMINAL", terminal, 0)?;
19    write_key_value(&mut writer, "AUTH_PROGRAM_NM", program, 0)?;
20    write_key_value(&mut writer, "AUTH_MACHINE", machine, 0)?;
21    write_key_value(&mut writer, "AUTH_PID", &pid.to_string(), 0)?;
22    write_key_value(&mut writer, "AUTH_SID", osuser, 0)?;
23    out.extend_from_slice(&writer.into_bytes());
24    Ok(())
25}
26
27/// OCI IAM database-token **proof-of-possession** material for the token
28/// phase-two message. `header` is the HTTP-Signatures signing string
29/// (`date: …\n(request-target): …\nhost: …`) and `signature` is its base64
30/// RSA-PKCS1v15-SHA256 signature under the token's bound private key. The two
31/// are written as `AUTH_HEADER` / `AUTH_SIGNATURE` and flip the auth mode to
32/// carry `TNS_AUTH_MODE_IAM_TOKEN` so the server verifies possession against the
33/// token's embedded public key (reference `messages/auth.pyx` `_write_message`,
34/// `impl/thin/crypto.pyx` `get_signature`). The crypto (key parse + signing)
35/// lives in the `oracledb` crate; this sans-I/O codec only frames the result.
36#[derive(Clone, Copy, Debug)]
37pub struct TokenPop<'a> {
38    pub header: &'a str,
39    pub signature: &'a str,
40}
41
42/// Appends the auth message for **token authentication** (OCI IAM database
43/// token / OAuth2) to the fast-auth bundle. Unlike password auth there is no
44/// verifier challenge: the reference sends auth phase TWO directly, carrying the
45/// token in `AUTH_TOKEN` with no `AUTH_SESSKEY`/`AUTH_PASSWORD` and auth mode
46/// `LOGON` (no `WITH_PASSWORD`); it never resends (messages/auth.pyx
47/// `_set_params`/`_write_message`, messages/fast_auth.pyx). Because this message
48/// lives inside the fast-auth bundle (ttc field version 19.1), the function code
49/// carries no `ub8` token-num — exactly like [`append_auth_phase_one`].
50///
51pub fn append_auth_phase_two_token(
52    out: &mut Vec<u8>,
53    user: &str,
54    token: &str,
55    driver_name: &str,
56    version_num: u32,
57    connect_string: &str,
58    edition: Option<&str>,
59) -> Result<()> {
60    append_auth_phase_two_token_with_pop(
61        out,
62        user,
63        token,
64        driver_name,
65        version_num,
66        connect_string,
67        edition,
68        None,
69    )
70}
71
72/// [`append_auth_phase_two_token`] with an optional OCI IAM database-token
73/// proof-of-possession. When `pop` is `Some`, the token is bound to a private
74/// key: the auth mode gains `TNS_AUTH_MODE_IAM_TOKEN` and two extra pairs
75/// (`AUTH_HEADER`, `AUTH_SIGNATURE`) are written between `AUTH_ALTER_SESSION` and
76/// `AUTH_ORA_EDITION`, exactly matching the reference key/value order. `None` is
77/// identical to [`append_auth_phase_two_token`] (a plain OAuth2 bearer token).
78#[allow(clippy::too_many_arguments)] // mirrors the reference message attribute set
79pub fn append_auth_phase_two_token_with_pop(
80    out: &mut Vec<u8>,
81    user: &str,
82    token: &str,
83    driver_name: &str,
84    version_num: u32,
85    connect_string: &str,
86    edition: Option<&str>,
87    pop: Option<TokenPop<'_>>,
88) -> Result<()> {
89    let mut writer = TtcWriter::new();
90    writer.write_function_code(TNS_FUNC_AUTH_PHASE_TWO);
91    // AUTH_TOKEN + the four mandatory session pairs, plus the optional
92    // AUTH_HEADER/AUTH_SIGNATURE (IAM proof-of-possession), AUTH_ORA_EDITION and
93    // AUTH_CONNECT_STRING.
94    let mut num_pairs = 5u32;
95    let auth_mode = if pop.is_some() {
96        // AUTH_HEADER + AUTH_SIGNATURE.
97        num_pairs += 2;
98        TNS_AUTH_MODE_LOGON | TNS_AUTH_MODE_IAM_TOKEN
99    } else {
100        TNS_AUTH_MODE_LOGON
101    };
102    if edition.is_some() {
103        num_pairs += 1;
104    }
105    if !connect_string.is_empty() {
106        num_pairs += 1;
107    }
108    write_auth_header(&mut writer, user, auth_mode, num_pairs)?;
109    write_key_value(&mut writer, "AUTH_TOKEN", token, 0)?;
110    write_key_value(&mut writer, "SESSION_CLIENT_CHARSET", "873", 0)?;
111    write_key_value(&mut writer, "SESSION_CLIENT_DRIVER_NAME", driver_name, 0)?;
112    write_key_value(
113        &mut writer,
114        "SESSION_CLIENT_VERSION",
115        &version_num.to_string(),
116        0,
117    )?;
118    write_key_value(
119        &mut writer,
120        "AUTH_ALTER_SESSION",
121        "ALTER SESSION SET TIME_ZONE='+00:00'\0",
122        1,
123    )?;
124    // IAM database-token proof-of-possession, in the reference's key/value order
125    // (after AUTH_ALTER_SESSION, before AUTH_ORA_EDITION): the server verifies
126    // AUTH_SIGNATURE over AUTH_HEADER using the public key embedded in the token.
127    if let Some(pop) = pop {
128        write_key_value(&mut writer, "AUTH_HEADER", pop.header, 0)?;
129        write_key_value(&mut writer, "AUTH_SIGNATURE", pop.signature, 0)?;
130    }
131    // Edition-Based Redefinition applies to token auth too — the reference writes
132    // AUTH_ORA_EDITION after AUTH_ALTER_SESSION on both auth paths (messages/auth.pyx
133    // `_write_message`); omitting it here silently ran token sessions under the
134    // default edition.
135    if let Some(edition) = edition {
136        write_key_value(&mut writer, "AUTH_ORA_EDITION", edition, 0)?;
137    }
138    if !connect_string.is_empty() {
139        write_key_value(&mut writer, "AUTH_CONNECT_STRING", connect_string, 0)?;
140    }
141    out.extend_from_slice(&writer.into_bytes());
142    Ok(())
143}
144
145pub fn build_auth_phase_two_payload(
146    user: &str,
147    encrypted: &crate::crypto::EncryptedPassword,
148    driver_name: &str,
149    version_num: u32,
150    connect_string: &str,
151) -> Result<Vec<u8>> {
152    build_auth_phase_two_payload_with_seq(
153        user,
154        encrypted,
155        driver_name,
156        version_num,
157        connect_string,
158        1,
159    )
160}
161
162pub fn build_auth_phase_two_payload_with_seq(
163    user: &str,
164    encrypted: &crate::crypto::EncryptedPassword,
165    driver_name: &str,
166    version_num: u32,
167    connect_string: &str,
168    seq_num: u8,
169) -> Result<Vec<u8>> {
170    build_auth_phase_two_payload_with_context_with_seq(
171        user,
172        encrypted,
173        driver_name,
174        version_num,
175        connect_string,
176        seq_num,
177        &[],
178    )
179}
180
181pub fn build_auth_phase_two_payload_with_context_with_seq(
182    user: &str,
183    encrypted: &crate::crypto::EncryptedPassword,
184    driver_name: &str,
185    version_num: u32,
186    connect_string: &str,
187    seq_num: u8,
188    app_context: &[(String, String, String)],
189) -> Result<Vec<u8>> {
190    build_auth_phase_two_payload_with_proxy_with_seq(
191        user,
192        encrypted,
193        driver_name,
194        version_num,
195        connect_string,
196        seq_num,
197        app_context,
198        None,
199        None,
200        ClientCapabilities::default().ttc_field_version,
201    )
202}
203
204/// Phase-two auth payload with optional proxy authentication: the reference
205/// writes `PROXY_CLIENT_NAME` as the first key/value pair when the connect
206/// user is of the form `user[proxy_user]` (messages/auth.pyx).
207///
208/// `ttc_field_version` is the field version negotiated with THIS server: the
209/// ub8 pipeline-token in the function header is a 23.1+ field (reference
210/// messages/base.pyx `_write_function_code`); a pre-23ai server parses the
211/// stray byte as part of the auth header and breaks the connection with a
212/// MARKER (observed live against Oracle XE 18c).
213#[allow(clippy::too_many_arguments)]
214pub fn build_auth_phase_two_payload_with_proxy_with_seq(
215    user: &str,
216    encrypted: &crate::crypto::EncryptedPassword,
217    driver_name: &str,
218    version_num: u32,
219    connect_string: &str,
220    seq_num: u8,
221    app_context: &[(String, String, String)],
222    proxy_user: Option<&str>,
223    edition: Option<&str>,
224    ttc_field_version: u8,
225) -> Result<Vec<u8>> {
226    let mut writer = TtcWriter::new();
227    writer.write_function_code_with_seq(TNS_FUNC_AUTH_PHASE_TWO, seq_num);
228    if version_gates::writes_pipeline_token(ttc_field_version) {
229        writer.write_ub8(0);
230    }
231    let mut num_pairs = 6u32;
232    if encrypted.speedy_key.is_some() {
233        num_pairs += 1;
234    }
235    if proxy_user.is_some() {
236        num_pairs += 1;
237    }
238    if !connect_string.is_empty() {
239        num_pairs += 1;
240    }
241    if edition.is_some() {
242        num_pairs += 1;
243    }
244    let app_context_pairs =
245        app_context
246            .len()
247            .checked_mul(3)
248            .ok_or(ProtocolError::InvalidPacketLength {
249                length: app_context.len(),
250                minimum: 0,
251            })?;
252    num_pairs +=
253        u32::try_from(app_context_pairs).map_err(|_| ProtocolError::InvalidPacketLength {
254            length: app_context.len(),
255            minimum: 0,
256        })?;
257    write_auth_header(
258        &mut writer,
259        user,
260        TNS_AUTH_MODE_LOGON | TNS_AUTH_MODE_WITH_PASSWORD,
261        num_pairs,
262    )?;
263    if let Some(proxy_user) = proxy_user {
264        write_key_value(&mut writer, "PROXY_CLIENT_NAME", proxy_user, 0)?;
265    }
266    write_key_value(&mut writer, "AUTH_SESSKEY", &encrypted.session_key, 1)?;
267    if let Some(speedy_key) = &encrypted.speedy_key {
268        write_key_value(&mut writer, "AUTH_PBKDF2_SPEEDY_KEY", speedy_key, 0)?;
269    }
270    write_key_value(&mut writer, "AUTH_PASSWORD", &encrypted.password, 0)?;
271    write_key_value(&mut writer, "SESSION_CLIENT_CHARSET", "873", 0)?;
272    write_key_value(&mut writer, "SESSION_CLIENT_DRIVER_NAME", driver_name, 0)?;
273    write_key_value(
274        &mut writer,
275        "SESSION_CLIENT_VERSION",
276        &version_num.to_string(),
277        0,
278    )?;
279    write_key_value(
280        &mut writer,
281        "AUTH_ALTER_SESSION",
282        "ALTER SESSION SET TIME_ZONE='+00:00'\0",
283        1,
284    )?;
285    // Edition-Based Redefinition: select the session edition during auth, exactly
286    // as the reference does (messages/auth.pyx writes `AUTH_ORA_EDITION` when
287    // `params.edition is not None`). Applied before any user SQL.
288    if let Some(edition) = edition {
289        write_key_value(&mut writer, "AUTH_ORA_EDITION", edition, 0)?;
290    }
291    for (namespace, name, value) in app_context {
292        write_key_value(&mut writer, "AUTH_APPCTX_NSPACE\0", namespace, 0)?;
293        write_key_value(&mut writer, "AUTH_APPCTX_ATTR\0", name, 0)?;
294        write_key_value(&mut writer, "AUTH_APPCTX_VALUE\0", value, 0)?;
295    }
296    if !connect_string.is_empty() {
297        write_key_value(&mut writer, "AUTH_CONNECT_STRING", connect_string, 0)?;
298    }
299    Ok(writer.into_bytes())
300}
301
302/// Change-password payload: an AUTH_PHASE_TWO message carrying only the
303/// combo-key-encrypted old/new passwords (reference
304/// connection.pyx `_create_change_password_message` + messages/auth.pyx
305/// `_write_message`: auth mode WITH_PASSWORD|CHANGE_PASSWORD, two pairs).
306pub fn build_change_password_payload_with_seq(
307    user: &str,
308    encoded_password: &str,
309    encoded_newpassword: &str,
310    seq_num: u8,
311    ttc_field_version: u8,
312) -> Result<Vec<u8>> {
313    let mut writer = TtcWriter::new();
314    writer.write_function_code_with_seq(TNS_FUNC_AUTH_PHASE_TWO, seq_num);
315    if version_gates::writes_pipeline_token(ttc_field_version) {
316        writer.write_ub8(0);
317    }
318    write_auth_header(
319        &mut writer,
320        user,
321        TNS_AUTH_MODE_WITH_PASSWORD | TNS_AUTH_MODE_CHANGE_PASSWORD,
322        2,
323    )?;
324    write_key_value(&mut writer, "AUTH_PASSWORD", encoded_password, 0)?;
325    write_key_value(&mut writer, "AUTH_NEWPASSWORD", encoded_newpassword, 0)?;
326    Ok(writer.into_bytes())
327}
328
329pub fn parse_auth_response(payload: &[u8]) -> Result<AuthResponse> {
330    parse_auth_response_with_limits(payload, ProtocolLimits::DEFAULT)
331}
332
333/// Whether an accumulated classic (pre-END_OF_RESPONSE) connect-phase response
334/// is complete.
335///
336/// Servers that did not negotiate END_OF_RESPONSE framing (protocol version
337/// below 319, i.e. everything before 23ai) never set the end-of-response DATA
338/// flag; the response instead ends when its *terminal message* has been read
339/// (reference messages/base.pyx `Message.process`: loop until
340/// `end_of_response`). The terminal messages for the connect-phase round trips
341/// are:
342///
343/// - protocol negotiation (msg 1): ends the response once processed
344///   (messages/protocol.pyx),
345/// - data types (msg 2): same (messages/data_types.pyx),
346/// - STATUS (msg 9): ends any response when END_OF_RESPONSE framing is off
347///   (messages/base.pyx),
348/// - ERROR (msg 4): carries the failure that the real parse will surface.
349///
350/// Returns `Ok(false)` when the payload runs out mid-message — the caller must
351/// read the next DATA packet and try again, exactly like the reference's
352/// `ReadBuffer` blocking for more packets mid-parse. Unknown message types
353/// propagate as errors.
354pub fn classic_connect_response_is_complete(
355    payload: &[u8],
356    limits: ProtocolLimits,
357) -> Result<bool> {
358    let Ok(mut reader) = TtcReader::with_limits(payload, limits) else {
359        return Ok(false);
360    };
361    while reader.remaining() > 0 {
362        let message_type = reader.read_u8()?;
363        let terminal = match message_type {
364            TNS_MSG_TYPE_PROTOCOL => match skip_protocol_message(&mut reader) {
365                Ok(_) => true,
366                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
367                Err(err) => return Err(err),
368            },
369            TNS_MSG_TYPE_DATA_TYPES => match skip_data_types_response(&mut reader) {
370                Ok(()) => true,
371                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
372                Err(err) => return Err(err),
373            },
374            TNS_MSG_TYPE_PARAMETER => match parse_return_parameters(&mut reader) {
375                Ok(_) => false,
376                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
377                Err(err) => return Err(err),
378            },
379            TNS_MSG_TYPE_STATUS => {
380                let complete = reader.read_ub4().and_then(|_| reader.read_ub2());
381                match complete {
382                    Ok(_) => true,
383                    Err(ProtocolError::TtcDecode(_)) => return Ok(false),
384                    Err(err) => return Err(err),
385                }
386            }
387            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => match skip_server_side_piggyback(&mut reader) {
388                Ok(_) => false,
389                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
390                Err(err) => return Err(err),
391            },
392            TNS_MSG_TYPE_END_OF_RESPONSE => true,
393            TNS_MSG_TYPE_ERROR => match parse_server_error(&mut reader, 13) {
394                Ok(_) | Err(ProtocolError::ServerError(_)) => true,
395                Err(ProtocolError::TtcDecode(_)) => return Ok(false),
396                Err(err) => return Err(err),
397            },
398            _ => {
399                return Err(ProtocolError::UnknownMessageType {
400                    message_type,
401                    position: reader.position().saturating_sub(1),
402                })
403            }
404        };
405        if terminal {
406            return Ok(true);
407        }
408    }
409    Ok(false)
410}
411
412pub fn parse_auth_response_with_limits(
413    payload: &[u8],
414    limits: ProtocolLimits,
415) -> Result<AuthResponse> {
416    let mut reader = TtcReader::with_limits(payload, limits)?;
417    let mut response = AuthResponse::default();
418    while reader.remaining() > 0 {
419        let message_type = reader.read_u8()?;
420        match message_type {
421            TNS_MSG_TYPE_PROTOCOL => {
422                if let Some(capabilities) = skip_protocol_message(&mut reader)? {
423                    response.capabilities = Some(capabilities);
424                }
425            }
426            TNS_MSG_TYPE_DATA_TYPES => skip_data_types_response(&mut reader)?,
427            TNS_MSG_TYPE_PARAMETER => {
428                let mut parsed = parse_return_parameters(&mut reader)?;
429                response.session_data.append(&mut parsed.session_data);
430                if parsed.verifier_type.is_some() {
431                    response.verifier_type = parsed.verifier_type;
432                }
433            }
434            TNS_MSG_TYPE_STATUS => {
435                let _call_status = reader.read_ub4()?;
436                let _seq = reader.read_ub2()?;
437            }
438            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
439                let _ = skip_server_side_piggyback(&mut reader)?;
440            }
441            TNS_MSG_TYPE_END_OF_RESPONSE => break,
442            TNS_MSG_TYPE_ERROR => {
443                if let Some(message) = parse_server_error(&mut reader, 13)? {
444                    return Err(ProtocolError::ServerError(message));
445                }
446            }
447            _ => {
448                return Err(ProtocolError::UnknownMessageType {
449                    message_type,
450                    position: reader.position().saturating_sub(1),
451                })
452            }
453        }
454    }
455    Ok(response)
456}
457
458pub(crate) fn write_auth_header(
459    writer: &mut TtcWriter,
460    user: &str,
461    auth_mode: u32,
462    num_pairs: u32,
463) -> Result<()> {
464    let user_bytes = user.as_bytes();
465    writer.write_u8(u8::from(!user_bytes.is_empty()));
466    writer.write_ub4(u32::try_from(user_bytes.len()).map_err(|_| {
467        ProtocolError::InvalidPacketLength {
468            length: user_bytes.len(),
469            minimum: 0,
470        }
471    })?);
472    writer.write_ub4(auth_mode);
473    writer.write_u8(1);
474    writer.write_ub4(num_pairs);
475    writer.write_u8(1);
476    writer.write_u8(1);
477    if !user_bytes.is_empty() {
478        writer.write_bytes_with_length(user_bytes)?;
479    }
480    Ok(())
481}
482
483pub(crate) fn write_key_value(
484    writer: &mut TtcWriter,
485    key: &str,
486    value: &str,
487    flags: u32,
488) -> Result<()> {
489    writer.write_str_two_lengths(key)?;
490    writer.write_str_two_lengths(value)?;
491    writer.write_ub4(flags);
492    Ok(())
493}
494
495pub(crate) fn parse_return_parameters(reader: &mut TtcReader<'_>) -> Result<AuthResponse> {
496    let num_params = reader.read_ub2()?;
497    reader
498        .limits()
499        .check_length_prefixed_elements(usize::from(num_params))?;
500    let mut response = AuthResponse::default();
501    for _ in 0..num_params {
502        let key = reader
503            .read_string_with_length()?
504            .ok_or(ProtocolError::TtcDecode("missing auth response key"))?;
505        let value = reader.read_string_with_length()?.unwrap_or_default();
506        if key == "AUTH_VFR_DATA" {
507            response.verifier_type = Some(reader.read_ub4()?);
508        } else {
509            let _flags = reader.read_ub4()?;
510        }
511        response.session_data.insert(key, value);
512    }
513    Ok(response)
514}
515
516#[cfg(test)]
517mod token_auth_tests {
518    use super::*;
519
520    /// Decode an Oracle `ub4` at `*pos`, advancing it (see `WriteBuffer::write_ub4`).
521    fn read_ub4(bytes: &[u8], pos: &mut usize) -> u32 {
522        let len = bytes[*pos] as usize;
523        *pos += 1;
524        let mut value = 0u32;
525        for _ in 0..len {
526            value = (value << 8) | u32::from(bytes[*pos]);
527            *pos += 1;
528        }
529        value
530    }
531
532    fn contains(haystack: &[u8], needle: &[u8]) -> bool {
533        haystack.windows(needle.len()).any(|w| w == needle)
534    }
535
536    /// The token auth message must encode the token as `AUTH_TOKEN`, in auth mode
537    /// `LOGON` (never `WITH_PASSWORD`), with no `AUTH_SESSKEY`/`AUTH_PASSWORD` and
538    /// the correct key/value-pair count. This is the deterministic "cassette" that
539    /// pins the wire format against the reference (messages/auth.pyx).
540    #[test]
541    fn token_message_carries_auth_token_not_password() {
542        let mut out = Vec::new();
543        append_auth_phase_two_token(
544            &mut out,
545            "scott",
546            "HEADER.PAYLOAD.SIG",
547            "drv",
548            300_000_000,
549            "cs",
550            None,
551        )
552        .unwrap();
553
554        // Function header: TTC function message, phase two, then the auth header.
555        assert_eq!(out[0], TNS_MSG_TYPE_FUNCTION);
556        assert_eq!(out[1], TNS_FUNC_AUTH_PHASE_TWO);
557        // out[2] is the sequence byte; out[3] is the has_user flag.
558        assert_eq!(out[3], 1, "user is present");
559        let mut pos = 4;
560        assert_eq!(read_ub4(&out, &mut pos), 5, "user length = len(\"scott\")");
561        assert_eq!(
562            read_ub4(&out, &mut pos),
563            TNS_AUTH_MODE_LOGON,
564            "token auth uses LOGON only — never the WITH_PASSWORD bit"
565        );
566        assert_eq!(out[pos], 1); // authivl pointer
567        pos += 1;
568        assert_eq!(
569            read_ub4(&out, &mut pos),
570            6,
571            "AUTH_TOKEN + 4 session pairs + AUTH_CONNECT_STRING"
572        );
573
574        assert!(contains(&out, b"AUTH_TOKEN"));
575        assert!(
576            contains(&out, b"HEADER.PAYLOAD.SIG"),
577            "the token value is sent"
578        );
579        assert!(contains(&out, b"AUTH_CONNECT_STRING"));
580        assert!(
581            !contains(&out, b"AUTH_PASSWORD") && !contains(&out, b"AUTH_SESSKEY"),
582            "token auth must not send any password material"
583        );
584    }
585
586    /// Without a connect string the pair count drops to exactly the token + the
587    /// four mandatory session pairs.
588    #[test]
589    fn token_message_pair_count_without_connect_string() {
590        let mut out = Vec::new();
591        append_auth_phase_two_token(&mut out, "u", "tok", "drv", 1, "", None).unwrap();
592        let mut pos = 4;
593        let _user_len = read_ub4(&out, &mut pos);
594        let _auth_mode = read_ub4(&out, &mut pos);
595        pos += 1; // authivl pointer
596        assert_eq!(read_ub4(&out, &mut pos), 5, "AUTH_TOKEN + 4 session pairs");
597        assert!(!contains(&out, b"AUTH_CONNECT_STRING"));
598    }
599
600    /// An OCI IAM database-token proof-of-possession adds exactly two pairs
601    /// (`AUTH_HEADER`, `AUTH_SIGNATURE`), flips the auth mode to carry
602    /// `TNS_AUTH_MODE_IAM_TOKEN` alongside `LOGON`, and keeps `AUTH_TOKEN`. This
603    /// pins the wire framing against the reference `_write_message` order.
604    #[test]
605    fn token_message_carries_iam_proof_of_possession() {
606        let mut out = Vec::new();
607        append_auth_phase_two_token_with_pop(
608            &mut out,
609            "OMCP_IAM",
610            "HEADER.PAYLOAD.SIG",
611            "drv",
612            1,
613            "",
614            None,
615            Some(TokenPop {
616                header: "date: Fri, 17 Jul 2026 07:00:00 GMT\n(request-target): svc\nhost: h:1522",
617                signature: "c2lnbmF0dXJl",
618            }),
619        )
620        .unwrap();
621        let mut pos = 4;
622        let _user_len = read_ub4(&out, &mut pos);
623        assert_eq!(
624            read_ub4(&out, &mut pos),
625            TNS_AUTH_MODE_LOGON | TNS_AUTH_MODE_IAM_TOKEN,
626            "IAM proof-of-possession sets the IAM_TOKEN mode bit alongside LOGON"
627        );
628        pos += 1; // authivl pointer
629        assert_eq!(
630            read_ub4(&out, &mut pos),
631            7,
632            "AUTH_TOKEN + 4 session pairs + AUTH_HEADER + AUTH_SIGNATURE"
633        );
634        assert!(contains(&out, b"AUTH_TOKEN"));
635        assert!(contains(&out, b"AUTH_HEADER"));
636        assert!(contains(&out, b"AUTH_SIGNATURE"));
637        assert!(
638            contains(&out, b"(request-target): svc"),
639            "header is sent verbatim"
640        );
641        assert!(contains(&out, b"c2lnbmF0dXJl"), "base64 signature is sent");
642    }
643
644    /// Edition-Based Redefinition must reach the server on the token path too:
645    /// `AUTH_ORA_EDITION` is written and counted, exactly as on the password path
646    /// (regression guard for the 0.2.0 bug where token auth dropped the edition).
647    #[test]
648    fn token_message_carries_edition() {
649        let mut out = Vec::new();
650        append_auth_phase_two_token(&mut out, "u", "tok", "drv", 1, "", Some("E_TEST")).unwrap();
651        let mut pos = 4;
652        let _user_len = read_ub4(&out, &mut pos);
653        let _auth_mode = read_ub4(&out, &mut pos);
654        pos += 1; // authivl pointer
655        assert_eq!(
656            read_ub4(&out, &mut pos),
657            6,
658            "AUTH_TOKEN + 4 session pairs + AUTH_ORA_EDITION"
659        );
660        assert!(contains(&out, b"AUTH_ORA_EDITION"));
661        assert!(contains(&out, b"E_TEST"), "the edition value is sent");
662
663        // With both an edition and a connect string the count rises to 7.
664        let mut out2 = Vec::new();
665        append_auth_phase_two_token(&mut out2, "u", "tok", "drv", 1, "cs", Some("E_TEST")).unwrap();
666        let mut p = 4;
667        let _ = read_ub4(&out2, &mut p);
668        let _ = read_ub4(&out2, &mut p);
669        p += 1;
670        assert_eq!(read_ub4(&out2, &mut p), 7, "+ AUTH_CONNECT_STRING");
671    }
672}