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