Skip to main content

mq_rest_admin/
auth.rs

1//! Authentication credential types and LTPA login support.
2
3use std::collections::HashMap;
4
5use serde_json::Value;
6
7use crate::error::MqRestError;
8use crate::transport::{MqRestTransport, TransportResponse};
9
10/// LTPA cookie name used by IBM MQ.
11pub const LTPA_COOKIE_NAME: &str = "LtpaToken2";
12const LTPA_LOGIN_PATH: &str = "/login";
13const ERROR_LTPA_LOGIN_FAILED: &str = "LTPA login failed.";
14const ERROR_LTPA_TOKEN_MISSING: &str =
15    "LTPA login succeeded but no LtpaToken2 cookie was returned.";
16
17/// Supported credential types for MQ REST authentication.
18#[derive(Debug, Clone)]
19pub enum Credentials {
20    /// HTTP Basic authentication.
21    Basic {
22        /// Username for HTTP Basic authentication.
23        username: String,
24        /// Password for HTTP Basic authentication.
25        password: String,
26    },
27    /// LTPA token-based authentication.
28    Ltpa {
29        /// Username for the LTPA login request.
30        username: String,
31        /// Password for the LTPA login request.
32        password: String,
33    },
34    /// Mutual TLS (mTLS) client certificate authentication.
35    Certificate {
36        /// Path to the client certificate PEM file.
37        cert_path: String,
38        /// Path to the private key PEM file.
39        key_path: Option<String>,
40    },
41}
42
43/// Perform an LTPA login and return the cookie name and token value.
44///
45/// The cookie name may be `"LtpaToken2"` or a suffixed variant like
46/// `"LtpaToken2_xyz"`.
47pub(crate) fn perform_ltpa_login(
48    transport: &dyn MqRestTransport,
49    rest_base_url: &str,
50    username: &str,
51    password: &str,
52    csrf_token: Option<&str>,
53    timeout_seconds: Option<f64>,
54    verify_tls: bool,
55) -> crate::error::Result<(String, String)> {
56    let login_url = format!("{rest_base_url}{LTPA_LOGIN_PATH}");
57    let mut headers = HashMap::new();
58    headers.insert("Accept".into(), "application/json".into());
59    if let Some(token) = csrf_token {
60        headers.insert("ibm-mq-rest-csrf-token".into(), token.into());
61    }
62    let mut payload = HashMap::new();
63    payload.insert("username".into(), Value::String(username.into()));
64    payload.insert("password".into(), Value::String(password.into()));
65    let response =
66        transport.post_json(&login_url, &payload, &headers, timeout_seconds, verify_tls)?;
67    if response.status_code >= 400 {
68        return Err(MqRestError::Auth {
69            url: login_url,
70            status_code: Some(response.status_code),
71            message: ERROR_LTPA_LOGIN_FAILED.into(),
72        });
73    }
74    match extract_ltpa_token(&response) {
75        Some(result) => Ok(result),
76        None => Err(MqRestError::Auth {
77            url: login_url,
78            status_code: Some(response.status_code),
79            message: ERROR_LTPA_TOKEN_MISSING.into(),
80        }),
81    }
82}
83
84/// Extract an `LtpaToken2` cookie from response `Set-Cookie` headers.
85///
86/// Matches any cookie whose name equals `"LtpaToken2"` or starts with
87/// `"LtpaToken2"` (e.g. `"LtpaToken2_abcdef"`), using prefix matching
88/// to support Liberty's suffixed cookie names.
89///
90/// Returns a `(cookie_name, token_value)` tuple, or `None` if not found.
91fn extract_ltpa_token(response: &TransportResponse) -> Option<(String, String)> {
92    let set_cookie = response
93        .headers
94        .get("Set-Cookie")
95        .or_else(|| response.headers.get("set-cookie"))?;
96    // Parse cookie header to find LtpaToken2 (exact or prefixed name)
97    for cookie_part in set_cookie.split(';') {
98        let cookie_part = cookie_part.trim();
99        if cookie_part.starts_with(LTPA_COOKIE_NAME)
100            && let Some(eq_index) = cookie_part.find('=')
101        {
102            let name = &cookie_part[..eq_index];
103            let value = &cookie_part[eq_index + 1..];
104            return Some((name.to_owned(), value.to_owned()));
105        }
106    }
107    // Also try comma-separated cookies
108    for cookie_entry in set_cookie.split(',') {
109        for cookie_part in cookie_entry.split(';') {
110            let cookie_part = cookie_part.trim();
111            if cookie_part.starts_with(LTPA_COOKIE_NAME)
112                && let Some(eq_index) = cookie_part.find('=')
113            {
114                let name = &cookie_part[..eq_index];
115                let value = &cookie_part[eq_index + 1..];
116                return Some((name.to_owned(), value.to_owned()));
117            }
118        }
119    }
120    None
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::test_helpers::MockTransport;
127
128    fn login_response_with_cookie(cookie_header: &str, cookie_value: &str) -> TransportResponse {
129        let mut headers = HashMap::new();
130        headers.insert(cookie_header.into(), cookie_value.into());
131        TransportResponse {
132            status_code: 200,
133            text: "{}".into(),
134            headers,
135        }
136    }
137
138    #[test]
139    fn ltpa_login_success() {
140        let transport = MockTransport::new(vec![login_response_with_cookie(
141            "Set-Cookie",
142            "LtpaToken2=abc123; Path=/",
143        )]);
144        let result = perform_ltpa_login(
145            &transport,
146            "https://host/ibmmq/rest/v2",
147            "user",
148            "pass",
149            Some("csrf"),
150            Some(10.0),
151            true,
152        );
153        let (name, value) = result.unwrap();
154        assert_eq!(name, "LtpaToken2");
155        assert_eq!(value, "abc123");
156    }
157
158    #[test]
159    fn ltpa_login_success_with_suffixed_cookie() {
160        let transport = MockTransport::new(vec![login_response_with_cookie(
161            "Set-Cookie",
162            "LtpaToken2_abcdef=suffixed_tok; Path=/",
163        )]);
164        let result = perform_ltpa_login(
165            &transport,
166            "https://host/ibmmq/rest/v2",
167            "user",
168            "pass",
169            None,
170            None,
171            false,
172        );
173        let (name, value) = result.unwrap();
174        assert_eq!(name, "LtpaToken2_abcdef");
175        assert_eq!(value, "suffixed_tok");
176    }
177
178    #[test]
179    fn ltpa_login_case_insensitive_header() {
180        let transport = MockTransport::new(vec![login_response_with_cookie(
181            "set-cookie",
182            "LtpaToken2=token456; Path=/",
183        )]);
184        let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
185        let (name, value) = result.unwrap();
186        assert_eq!(name, "LtpaToken2");
187        assert_eq!(value, "token456");
188    }
189
190    #[test]
191    fn ltpa_login_comma_separated_cookies() {
192        let transport = MockTransport::new(vec![login_response_with_cookie(
193            "Set-Cookie",
194            "other=x, LtpaToken2=fromcomma; Path=/",
195        )]);
196        let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
197        let (name, value) = result.unwrap();
198        assert_eq!(name, "LtpaToken2");
199        assert_eq!(value, "fromcomma");
200    }
201
202    #[test]
203    fn ltpa_login_http_401() {
204        let transport = MockTransport::new(vec![TransportResponse {
205            status_code: 401,
206            text: "Unauthorized".into(),
207            headers: HashMap::new(),
208        }]);
209        let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
210        assert!(format!("{:?}", result.unwrap_err()).starts_with("Auth"));
211    }
212
213    #[test]
214    fn ltpa_login_missing_token() {
215        let transport = MockTransport::new(vec![TransportResponse {
216            status_code: 200,
217            text: "{}".into(),
218            headers: HashMap::new(),
219        }]);
220        let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
221        assert!(format!("{:?}", result.unwrap_err()).starts_with("Auth"));
222    }
223
224    #[test]
225    fn ltpa_login_csrf_token_present_in_request() {
226        let transport = MockTransport::new(vec![login_response_with_cookie(
227            "Set-Cookie",
228            "LtpaToken2=tok; Path=/",
229        )]);
230        perform_ltpa_login(
231            &transport,
232            "https://h",
233            "u",
234            "p",
235            Some("mytoken"),
236            None,
237            false,
238        )
239        .unwrap();
240        let requests = transport.requests();
241        assert_eq!(
242            requests[0].headers.get("ibm-mq-rest-csrf-token").unwrap(),
243            "mytoken"
244        );
245    }
246
247    #[test]
248    fn ltpa_login_csrf_token_absent() {
249        let transport = MockTransport::new(vec![login_response_with_cookie(
250            "Set-Cookie",
251            "LtpaToken2=tok; Path=/",
252        )]);
253        perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false).unwrap();
254        let requests = transport.requests();
255        assert!(!requests[0].headers.contains_key("ibm-mq-rest-csrf-token"));
256    }
257
258    #[test]
259    fn ltpa_login_cookie_present_but_no_ltpa_token() {
260        let transport = MockTransport::new(vec![login_response_with_cookie(
261            "Set-Cookie",
262            "SomeOtherCookie=value; Path=/",
263        )]);
264        let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
265        assert!(format!("{:?}", result.unwrap_err()).starts_with("Auth"));
266    }
267
268    #[test]
269    fn ltpa_login_transport_error() {
270        let transport = MockTransport::new(vec![]);
271        let result = perform_ltpa_login(&transport, "https://h", "u", "p", None, None, false);
272        assert!(result.is_err());
273    }
274}