Skip to main content

himmelblau/
auth.rs

1/*
2   Unix Azure Entra ID implementation
3   Copyright (C) David Mulder <dmulder@samba.org> 2024
4
5   This program is free software: you can redistribute it and/or modify
6   it under the terms of the GNU Lesser General Public License as published by
7   the Free Software Foundation, either version 3 of the License, or
8   (at your option) any later version.
9
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13   GNU Lesser General Public License for more details.
14
15   You should have received a copy of the GNU Lesser General Public License
16   along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use crate::aadsts_err_gen::AADSTSError;
20use crate::error::{ErrorResponse, MsalError, AUTH_PENDING};
21#[cfg(feature = "broker")]
22use crate::ssh::{
23    build_ssh_certificate_request_form, parse_ssh_certificate_response, ssh_rsa_public_key_to_jwk,
24    EntraSshCertificate, SshCertificateTokenResponse, SshRsaJwk, AZURE_CLI_APP_ID,
25};
26use base64::engine::general_purpose::URL_SAFE_NO_PAD;
27use base64::Engine;
28#[cfg(feature = "broker")]
29use crypto_glue::traits::SpkiEncodePublicKey;
30#[cfg(feature = "broker")]
31use crypto_glue::x509::oiddb::rfc5912;
32use crypto_glue::x509::Certificate;
33#[cfg(feature = "broker")]
34use der::asn1::{BitString, OctetString, SetOfVec};
35#[cfg(feature = "broker")]
36use der::{Decode, Encode, Sequence};
37use kanidm_hsm_crypto::structures::RS256Key;
38use percent_encoding::percent_decode_str;
39use reqwest::redirect::Policy;
40#[cfg(feature = "proxyable")]
41use reqwest::Proxy;
42use reqwest::{header, Body, Client, Response, Url};
43use scraper::{Html, Selector};
44use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
45use serde::ser::{SerializeMap, Serializer};
46use serde::{Deserialize, Serialize};
47use serde_json::{from_str as json_from_str, json, Value};
48#[cfg(feature = "set_timeout")]
49use std::cmp::min;
50use std::collections::HashMap;
51use std::fmt;
52use std::marker::PhantomData;
53use std::str::FromStr;
54use std::sync::RwLock;
55use std::thread::sleep;
56use std::time::Duration;
57use tracing::{error, info, warn};
58use urlencoding::encode as url_encode;
59use uuid::Uuid;
60#[cfg(feature = "broker")]
61use x509_cert::attr::Attribute;
62#[cfg(feature = "broker")]
63use x509_cert::name::Name;
64#[cfg(feature = "broker")]
65use x509_cert::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned};
66use zeroize::{Zeroize, ZeroizeOnDrop};
67
68#[cfg(feature = "broker")]
69use compact_jwt::{
70    compact::JweCompact,
71    crypto::{JwsTpmRs256Signer, MsOapxbcSessionKey},
72    jwe::Jwe,
73    jws::{Jws, JwsBuilder},
74    traits::{JwsMutSigner, JwsSignable},
75};
76
77#[cfg(feature = "broker")]
78use kanidm_hsm_crypto::{
79    provider::{BoxedDynTpm, Tpm, TpmMsExtensions, TpmRS256},
80    structures::{
81        LoadableMsDeviceEnrolmentKey, LoadableMsHelloKey, LoadableMsOapxbcRsaKey, LoadableRS256Key,
82        MsOapxbcRsaKey, SealedData, StorageKey,
83    },
84    PinValue,
85};
86
87#[cfg(feature = "broker")]
88use openssl::asn1::{Asn1Time, Asn1TimeRef};
89#[cfg(feature = "broker")]
90use openssl::hash::{hash, MessageDigest};
91#[cfg(feature = "broker")]
92use openssl::pkey::{PKey, Public};
93use openssl::rand::rand_bytes;
94#[cfg(feature = "broker")]
95use openssl::rsa::Rsa;
96use openssl::sha::sha256;
97#[cfg(feature = "broker")]
98use openssl::sign::Signer;
99#[cfg(feature = "broker")]
100use openssl::x509::X509;
101#[cfg(feature = "broker")]
102use os_release::OsRelease;
103#[cfg(feature = "broker")]
104use regex::Regex;
105#[cfg(feature = "broker")]
106use serde_json::{from_slice as json_from_slice, to_vec as json_to_vec};
107#[cfg(feature = "broker")]
108use std::convert::TryInto;
109#[cfg(feature = "broker")]
110use std::time::{SystemTime, UNIX_EPOCH};
111#[cfg(feature = "broker")]
112use tracing::debug;
113
114#[cfg(feature = "broker")]
115use crate::discovery::Services;
116#[cfg(feature = "broker")]
117use crate::discovery::{BcryptRsaKeyBlob, EnrollAttrs};
118
119#[cfg(feature = "broker")]
120use libkrimes::ccache::resolve as ccache_resolve;
121#[cfg(feature = "broker")]
122use libkrimes::proto::{AuthenticationReply, DerivedKey, KerberosCredentials, KerberosReply};
123#[cfg(feature = "broker")]
124use std::fs;
125#[cfg(feature = "broker")]
126use std::io::Read;
127
128#[cfg(feature = "broker")]
129use base64::engine::general_purpose::STANDARD;
130#[cfg(feature = "broker")]
131use compact_jwt::JwtError;
132#[cfg(feature = "broker")]
133use serde_json::to_string_pretty;
134#[cfg(feature = "broker")]
135use zeroize::Zeroizing;
136
137use reqwest_cookie_store::{CookieStore, CookieStoreMutex};
138use std::sync::Arc;
139
140#[cfg(feature = "broker")]
141const BROKER_CLIENT_IDENT: &str = "38aa3b87-a06d-4817-b275-7a316988d93b";
142#[cfg(feature = "broker")]
143pub const BROKER_APP_ID: &str = "29d9ed98-a469-4536-ade2-f981bc1d605e";
144#[cfg(feature = "broker")]
145pub const LINUX_BROKER_APP_ID: &str = "b743a22d-6705-4147-8670-d92fa515ee2b";
146#[cfg(feature = "broker")]
147const DRS_APP_ID: &str = "01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9";
148#[cfg(feature = "broker")]
149const AZURE_PORTAL_APP_ID: &str = "c44b4083-3bb0-49c1-b47d-974e53cbdf3c";
150const HIMMELBLAU_REDIRECT_URI: &str = "himmelblau://Himmelblau.EntraId.BrokerPlugin";
151
152#[derive(Debug, Deserialize)]
153pub struct SidToName {
154    pub upn: String,
155    pub email: Option<String>,
156    pub name: String,
157    pub family_name: Option<String>,
158    pub sid: String,
159    pub onprem_sam_account_name: Option<String>,
160    pub domain_netbios_name: Option<String>,
161    pub domain_dns_name: Option<String>,
162}
163
164/* FIDO Authentication requires specifying a user agent which
165 * MS endorses as appropriate for FIDO */
166#[cfg(feature = "broker")]
167const FIDO_USER_AGENT: &str =
168    "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0";
169
170/* RFC8628: 3.2. Device Authorization Response */
171#[derive(Default, Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
172pub struct DeviceAuthorizationResponse {
173    pub device_code: String,
174    pub user_code: String,
175    pub verification_uri: String,
176    // MS doesn't implement verification_uri_complete yet
177    pub verification_uri_complete: Option<String>,
178    pub expires_in: u32,
179    pub interval: Option<u32>,
180    pub message: Option<String>,
181}
182
183#[derive(Clone, Debug, Deserialize)]
184struct ArrUserProofs {
185    #[serde(rename = "authMethodId")]
186    auth_method_id: String,
187    #[serde(rename = "isDefault")]
188    is_default: bool,
189    display: String,
190}
191
192/// Detailed information about an MFA method
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct MfaMethodInfo {
195    /// The authentication method identifier (e.g., "PhoneAppNotification", "PhoneAppOTP")
196    pub auth_method_id: String,
197    /// Human-readable display name for the method (e.g., phone number for SMS)
198    pub display: String,
199    /// Whether this is the default/preferred method
200    pub is_default: bool,
201}
202
203impl From<&ArrUserProofs> for MfaMethodInfo {
204    fn from(proof: &ArrUserProofs) -> Self {
205        MfaMethodInfo {
206            auth_method_id: proof.auth_method_id.clone(),
207            display: proof.display.clone(),
208            is_default: proof.is_default,
209        }
210    }
211}
212
213#[derive(Clone, Deserialize)]
214struct AuthConfig {
215    #[serde(rename = "sessionId")]
216    session_id: String,
217    #[serde(rename = "sFT")]
218    sft: Option<String>,
219    #[serde(rename = "sCtx")]
220    sctx: Option<String>,
221    #[serde(rename = "urlPost")]
222    url_post: Option<String>,
223    canary: String,
224    #[serde(rename = "iAllowedIdentities")]
225    allowed_identities: Option<u32>,
226    #[serde(rename = "strServiceExceptionMessage")]
227    service_exception_msg: Option<String>,
228    pgid: Option<String>,
229    #[serde(rename = "urlSkipMfaRegistration")]
230    url_skip_mfa_registration: Option<String>,
231    #[serde(rename = "iRemainingDaysToSkipMfaRegistration")]
232    remaining_days_to_skip_mfa_reg: Option<u32>,
233    #[serde(rename = "arrUserProofs")]
234    arr_user_proofs: Option<Vec<ArrUserProofs>>,
235    #[serde(rename = "arrFidoAllowList")]
236    fido_allow_list: Option<Vec<String>>,
237    #[serde(rename = "urlEndAuth")]
238    url_end_auth: Option<String>,
239    #[serde(rename = "urlBeginAuth")]
240    url_begin_auth: Option<String>,
241    #[serde(rename = "urlFidoLogin")]
242    url_fido_login: Option<String>,
243    #[serde(rename = "urlResume")]
244    url_resume: Option<String>,
245    #[serde(rename = "iMaxPollAttempts")]
246    max_poll_attempts: Option<u32>,
247    #[serde(rename = "iPollingInterval")]
248    polling_interval: Option<u32>,
249    #[serde(rename = "sErrorCode")]
250    error_code: Option<String>,
251    #[serde(rename = "iErrorCode")]
252    error_code2: Option<u32>,
253    #[serde(rename = "sErrTxt")]
254    err_txt: Option<String>,
255    #[serde(rename = "sFidoChallenge")]
256    fido_challenge: Option<String>,
257    #[serde(rename = "sCrossDomainCanary")]
258    cross_domain_canary: Option<String>,
259    #[serde(rename = "urlGetOneTimeCode")]
260    url_get_one_time_code: Option<String>,
261    #[serde(rename = "urlGetCredentialType")]
262    url_get_credential_type: Option<String>,
263    #[serde(rename = "urlSessionState")]
264    url_session_state: Option<String>,
265    #[cfg(feature = "changepassword")]
266    #[serde(rename = "urlAsyncSsprBegin")]
267    url_async_sspr_begin: Option<String>,
268    #[cfg(feature = "changepassword")]
269    #[serde(rename = "urlAsyncSsprPoll")]
270    url_async_sspr_poll: Option<String>,
271    #[serde(rename = "fIsPasskeySupportEnabled")]
272    is_passkey_support_enabled: Option<bool>,
273}
274
275#[derive(Deserialize, Serialize, Default)]
276pub struct MFAAuthContinue {
277    pub msg: String,
278    pub entropy: Option<u8>,
279    pub max_poll_attempts: Option<u32>,
280    pub polling_interval: Option<u32>,
281    pub session_id: String,
282    pub flow_token: String,
283    pub ctx: String,
284    pub canary: String,
285    pub url_end_auth: Option<String>,
286    pub url_post: String,
287    pub url_session_state: Option<String>,
288    pub resource: Option<String>,
289    pub dag: Option<DeviceAuthorizationResponse>,
290    pub fido_challenge: Option<String>,
291    pub fido_allow_list: Option<Vec<String>>,
292    pub cross_domain_canary: Option<String>,
293    pub mfa_methods: Vec<String>,
294    pub mfa_method_details: Vec<MfaMethodInfo>,
295    pub selected_mfa_method_id: Option<String>,
296    /// Authorization code received directly from login (when MFA not required).
297    /// When set, acquire_token_by_mfa_flow should skip polling and exchange
298    /// this code directly for an access token.
299    pub auth_code: Option<String>,
300    #[deprecated(note = "Use `skip_fido_for_mfa` instead")]
301    pub fido_is_passkey: bool,
302    /// Whether to skip FidoKey in MFA method selection.
303    pub skip_fido_for_mfa: bool,
304    /// Whether the user has a physical USB security key.
305    pub has_physical_security_key: bool,
306    /// Whether the user has a cross-device capable passkey (e.g. MS Authenticator).
307    pub has_cross_device_passkey: bool,
308}
309
310impl From<DeviceAuthorizationResponse> for MFAAuthContinue {
311    fn from(item: DeviceAuthorizationResponse) -> Self {
312        let msg = match &item.message {
313            Some(msg) => msg.to_string(),
314            None => format!(
315                "Using a browser on another device, visit:\n{}\n \
316                    And enter the code:\n{}",
317                item.verification_uri, item.user_code
318            ),
319        };
320        // Interval is in seconds, but polling_interval expects milliseconds
321        let polling_interval = item.interval.unwrap_or(5);
322        // Convert `expires_in` (a lifetime in seconds) to max_poll_attempts
323        let max_poll_attempts = item.expires_in / polling_interval;
324        MFAAuthContinue {
325            msg,
326            max_poll_attempts: Some(max_poll_attempts),
327            polling_interval: Some(polling_interval * 1000),
328            dag: Some(item),
329            ..Default::default()
330        }
331    }
332}
333
334impl MFAAuthContinue {
335    /// Get the default MFA method (for backwards compatibility)
336    pub fn mfa_method(&self) -> String {
337        if let Some(method) = self.get_default_mfa_method_details() {
338            method.auth_method_id
339        } else if !self.mfa_methods.is_empty() {
340            for method in self.get_mfa_method_details() {
341                if !self.should_skip_fido_method(&method) {
342                    return method.auth_method_id.clone();
343                }
344            }
345            "".to_string()
346        } else {
347            // This happens with a DAG fallback
348            "".to_string()
349        }
350    }
351
352    /// Get all available MFA methods. Returns a vector containing all method IDs.
353    pub fn get_available_mfa_methods(&self) -> Vec<String> {
354        self.mfa_methods.clone()
355    }
356
357    /// Get detailed information about all available MFA methods.
358    /// Returns detailed info including display names and default status.
359    pub fn get_mfa_method_details(&self) -> Vec<MfaMethodInfo> {
360        self.mfa_method_details.clone()
361    }
362
363    /// Check if a specific MFA method is available
364    pub fn has_mfa_method(&self, method_id: &str) -> bool {
365        self.get_available_mfa_methods()
366            .contains(&method_id.to_string())
367    }
368
369    /// Get the count of available MFA methods
370    pub fn mfa_method_count(&self) -> usize {
371        self.get_available_mfa_methods().len()
372    }
373
374    fn should_skip_fido_method(&self, method: &MfaMethodInfo) -> bool {
375        if method.auth_method_id == "FidoKey" {
376            self.skip_fido_for_mfa
377        } else {
378            false
379        }
380    }
381
382    /// Get details of the first default MFA method - if no default is specified, return the first MFA method, or None
383    pub fn get_default_mfa_method_details(&self) -> Option<MfaMethodInfo> {
384        if let Some(details) = self
385            .get_mfa_method_details()
386            .into_iter()
387            .find(|method| method.is_default && !self.should_skip_fido_method(method))
388        {
389            Some(details)
390        } else if !self.mfa_methods.is_empty() {
391            for method in self.get_mfa_method_details() {
392                if !self.should_skip_fido_method(&method) {
393                    return Some(method);
394                }
395            }
396            None
397        } else {
398            None
399        }
400    }
401
402    /// Get detailed information about a specific MFA method by ID
403    pub fn get_mfa_method_by_id(&self, method_id: &str) -> Option<MfaMethodInfo> {
404        self.get_mfa_method_details()
405            .into_iter()
406            .find(|method| method.auth_method_id == method_id)
407    }
408}
409
410#[derive(Deserialize)]
411struct AuthResponse {
412    #[serde(rename = "Success")]
413    success: bool,
414    #[serde(rename = "Retry")]
415    retry: Option<bool>,
416    #[serde(rename = "Message")]
417    message: Option<String>,
418    #[serde(rename = "ErrCode")] // AADSTS error code
419    error_code: Option<u32>,
420    #[serde(rename = "Ctx")]
421    ctx: String,
422    #[serde(rename = "FlowToken")]
423    flow_token: String,
424    #[serde(rename = "Entropy")]
425    entropy: u8,
426}
427
428#[derive(Deserialize)]
429struct DeviceCodeStatus {
430    #[serde(rename = "AuthorizationState")]
431    authorization_state: u8,
432}
433
434#[derive(Clone, Deserialize)]
435struct RemoteNgcParams {
436    #[serde(rename = "SessionIdentifier")]
437    session_identifier: String,
438    #[serde(rename = "Entropy")]
439    entropy: u8,
440}
441
442#[derive(Deserialize)]
443struct OTCError {
444    message: String,
445}
446
447#[derive(Deserialize)]
448struct OneTimeCode {
449    #[serde(rename = "RemoteNgcParams")]
450    remote_ngc_params: Option<RemoteNgcParams>,
451    error: Option<OTCError>,
452}
453
454#[derive(Clone, Deserialize)]
455struct FidoParams {
456    #[serde(rename = "AllowList")]
457    fido_allow_list: Vec<String>,
458    #[serde(rename = "HasCrossDeviceCapablePasskey")]
459    has_cross_device_capable_passkey: Option<bool>,
460}
461
462#[allow(dead_code)]
463#[derive(Clone, Deserialize)]
464struct Credentials {
465    #[serde(rename = "FederationRedirectUrl")]
466    federation_redirect_url: Option<String>,
467    #[serde(rename = "HasPassword")]
468    has_password: bool,
469    #[serde(rename = "RemoteNgcParams")]
470    remote_ngc_params: Option<RemoteNgcParams>,
471    #[serde(rename = "FidoParams")]
472    fido_params: Option<FidoParams>,
473    #[serde(rename = "PrefCredential")]
474    pref_credential: u8,
475    #[serde(rename = "HasAccessPass")]
476    has_access_pass: Option<bool>,
477    #[serde(rename = "HasFido")]
478    has_fido: Option<bool>,
479    #[serde(rename = "HasRemoteNGC")]
480    has_remote_ngc: Option<bool>,
481}
482
483#[derive(Clone, Deserialize)]
484struct CredType {
485    #[serde(rename = "Credentials")]
486    credentials: Credentials,
487    #[serde(rename = "ThrottleStatus")]
488    throttle_status: u8,
489    #[serde(rename = "IfExistsResult")]
490    if_exists_result: i32,
491}
492
493impl CredType {
494    /// Record backend throttling telemetry returned by GetCredentialType.
495    ///
496    /// Microsoft's converged-login client defines these values as
497    /// NotThrottled (0), AadThrottled (1), and MsaThrottled (2). This field is
498    /// not the lookup outcome: IfExistsResult=2 is the terminal "Throttled"
499    /// result. A nonzero ThrottleStatus can accompany an otherwise usable
500    /// response, so it must not control authentication.
501    fn log_throttle_status(&self) {
502        match self.throttle_status {
503            0 => {}
504            1 => debug!("GetCredentialType reports AAD backend throttling"),
505            2 => debug!("GetCredentialType reports MSA backend throttling"),
506            other => warn!("GetCredentialType returned unknown ThrottleStatus={other}"),
507        }
508    }
509
510    /// Whether the user exists in the directory
511    pub fn account_exists(&self) -> Result<bool, MsalError> {
512        /* `ifExistsResult` meanings:
513           — 0: the account exists and uses that domain for authentication (i.e., “normal” cloud account)
514           — 1: the account does not exist
515           — 2: response is being throttled (rate-limiting)
516           — 4: some server error
517           — 5: the account exists, but is set up to authenticate with a different identity provider (e.g. personal Microsoft account / consumer account / “other IdP”)
518           — 6: the account exists, and is set up to support both the domain and a different ID provider (i.e. dual mode)
519        */
520        match self.if_exists_result {
521            0 | 5 | 6 => Ok(true),
522            1 => Ok(false),
523            // TenantThrottlingError
524            2 => Err(MsalError::AADSTSError(AADSTSError::new(90055, None))),
525            // ExternalServerRetryableError
526            -1 | 4 => Err(MsalError::AADSTSError(AADSTSError::new(90006, None))),
527            other => {
528                warn!(
529                    "GetCredentialType returned unknown IfExistsResult={other}, ThrottleStatus={}",
530                    self.throttle_status
531                );
532                Ok(true)
533            }
534        }
535    }
536
537    pub fn is_personal_account(&self) -> bool {
538        self.if_exists_result == 5
539    }
540}
541
542const MAX_ADFS_RESPONSE_BYTES: usize = 2 * 1024 * 1024;
543const MAX_ADFS_REDIRECTS: usize = 5;
544
545#[derive(Debug, PartialEq, Eq)]
546struct WsFedForm {
547    wa: String,
548    wresult: String,
549    wctx: String,
550}
551
552fn parse_adfs_federation_url(value: &str) -> Result<Option<Url>, MsalError> {
553    let url = Url::parse(value)
554        .map_err(|e| MsalError::URLFormatFailed(format!("Invalid federation URL: {e}")))?;
555    let is_adfs = url
556        .path_segments()
557        .map(|segments| {
558            segments
559                .into_iter()
560                .any(|segment| segment.eq_ignore_ascii_case("adfs"))
561        })
562        .unwrap_or(false);
563    if url.scheme() != "https"
564        || url.host_str().is_none()
565        || !url.username().is_empty()
566        || url.password().is_some()
567        || url.fragment().is_some()
568        || !is_adfs
569    {
570        return Ok(None);
571    }
572    Ok(Some(url))
573}
574
575fn same_origin(left: &Url, right: &Url) -> bool {
576    left.scheme() == right.scheme()
577        && left.host_str() == right.host_str()
578        && left.port_or_known_default() == right.port_or_known_default()
579}
580
581#[derive(Clone, Copy, Debug, PartialEq, Eq)]
582enum AdfsRequestMethod {
583    Get,
584    PostCredentials,
585}
586
587fn resolve_adfs_redirect(
588    origin: &Url,
589    current: &Url,
590    location: &str,
591    status: u16,
592    method: AdfsRequestMethod,
593) -> Result<(Url, AdfsRequestMethod), MsalError> {
594    let next = current
595        .join(location)
596        .map_err(|e| MsalError::URLFormatFailed(format!("Invalid AD FS redirect URL: {e}")))?;
597    if next.scheme() != "https"
598        || !same_origin(origin, &next)
599        || !next.username().is_empty()
600        || next.password().is_some()
601        || next.fragment().is_some()
602    {
603        return Err(MsalError::GeneralFailure(
604            "AD FS attempted an unsafe redirect".to_string(),
605        ));
606    }
607    let next_method = if matches!(status, 307 | 308) {
608        method
609    } else {
610        AdfsRequestMethod::Get
611    };
612    Ok((next, next_method))
613}
614
615fn entra_login_srf_url(authority: &str) -> Result<Url, MsalError> {
616    let mut url = Url::parse(authority)
617        .map_err(|e| MsalError::URLFormatFailed(format!("Invalid authority URL: {e}")))?;
618    if url.scheme() != "https" || url.host_str().is_none() {
619        return Err(MsalError::URLFormatFailed(
620            "Entra authority must be an absolute HTTPS URL".to_string(),
621        ));
622    }
623    url.set_path("/login.srf");
624    url.set_query(None);
625    url.set_fragment(None);
626    Ok(url)
627}
628
629fn parse_ws_fed_form(text: &str) -> Result<WsFedForm, MsalError> {
630    let document = Html::parse_document(text);
631    let form_selector = Selector::parse("form")
632        .map_err(|e| MsalError::InvalidParse(format!("Failed parsing AD FS form: {e:?}")))?;
633    let input_selector = Selector::parse("input")
634        .map_err(|e| MsalError::InvalidParse(format!("Failed parsing AD FS inputs: {e:?}")))?;
635
636    for form in document.select(&form_selector) {
637        let mut wa = None;
638        let mut wresult = None;
639        let mut wctx = None;
640        for input in form.select(&input_selector) {
641            let Some(name) = input.value().attr("name") else {
642                continue;
643            };
644            let Some(value) = input.value().attr("value") else {
645                continue;
646            };
647            if name.eq_ignore_ascii_case("wa") {
648                wa = Some(value.to_string());
649            } else if name.eq_ignore_ascii_case("wresult") {
650                wresult = Some(value.to_string());
651            } else if name.eq_ignore_ascii_case("wctx") {
652                wctx = Some(value.to_string());
653            }
654        }
655        if let (Some(wa), Some(wresult), Some(wctx)) = (wa, wresult, wctx) {
656            if wa == "wsignin1.0" && !wresult.is_empty() && !wctx.is_empty() {
657                return Ok(WsFedForm { wa, wresult, wctx });
658            }
659        }
660    }
661
662    Err(MsalError::GeneralFailure(
663        "AD FS did not return a supported WS-Federation sign-in response".to_string(),
664    ))
665}
666
667#[derive(Default, Clone, Deserialize, Serialize)]
668#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
669pub struct IdToken {
670    pub name: String,
671    pub oid: String,
672    pub preferred_username: Option<String>,
673    pub puid: Option<String>,
674    pub tenant_region_scope: Option<String>,
675    pub tid: String,
676    #[serde(skip_serializing)]
677    pub raw: Option<String>,
678}
679
680fn decode_string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
681where
682    T: Deserialize<'de> + FromStr<Err = MsalError>,
683    D: Deserializer<'de>,
684{
685    struct StringOrStruct<T>(PhantomData<fn() -> T>);
686
687    impl<'de, T> Visitor<'de> for StringOrStruct<T>
688    where
689        T: Deserialize<'de> + FromStr<Err = MsalError>,
690    {
691        type Value = T;
692
693        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
694            formatter.write_str("string or map")
695        }
696
697        fn visit_str<E>(self, value: &str) -> Result<T, E>
698        where
699            E: de::Error,
700        {
701            FromStr::from_str(value)
702                .map_err(|e| serde::de::Error::custom(format!("Failed to parse string: {:?}", e)))
703        }
704
705        fn visit_map<M>(self, map: M) -> Result<T, M::Error>
706        where
707            M: MapAccess<'de>,
708        {
709            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
710        }
711    }
712
713    deserializer.deserialize_any(StringOrStruct(PhantomData))
714}
715
716impl FromStr for IdToken {
717    type Err = MsalError;
718    fn from_str(s: &str) -> Result<Self, Self::Err> {
719        let mut siter = s.splitn(3, '.');
720        if siter.next().is_none() {
721            return Err(MsalError::InvalidParse(
722                "Failed parsing id_token header".to_string(),
723            ));
724        }
725        let payload_str = match siter.next() {
726            Some(payload_str) => URL_SAFE_NO_PAD
727                .decode(payload_str)
728                .map_err(|e| MsalError::InvalidParse(format!("Failed parsing id_token: {}", e)))
729                .and_then(|bytes| {
730                    String::from_utf8(bytes).map_err(|e| {
731                        MsalError::InvalidParse(format!("Failed parsing id_token: {}", e))
732                    })
733                })?,
734            None => {
735                return Err(MsalError::InvalidParse(
736                    "Failed parsing id_token payload".to_string(),
737                ));
738            }
739        };
740        let mut payload: IdToken = json_from_str(&payload_str).map_err(|e| {
741            MsalError::InvalidParse(format!("Failed parsing id_token from json: {}", e))
742        })?;
743        payload.raw = Some(s.to_string());
744        Ok(payload)
745    }
746}
747
748#[derive(Clone, Default, Deserialize, Serialize)]
749#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
750pub struct ClientInfo {
751    pub uid: Option<Uuid>,
752    pub utid: Option<Uuid>,
753}
754
755impl FromStr for ClientInfo {
756    type Err = MsalError;
757    fn from_str(s: &str) -> Result<Self, Self::Err> {
758        let client_info: Value = URL_SAFE_NO_PAD
759            .decode(s)
760            .map_err(|e| MsalError::InvalidParse(format!("Failed parsing client_info: {}", e)))
761            .and_then(|bytes| {
762                String::from_utf8(bytes).map_err(|e| {
763                    MsalError::InvalidParse(format!("Failed parsing client_info: {}", e))
764                })
765            })
766            .and_then(|client_info_str| {
767                json_from_str(&client_info_str).map_err(|e| {
768                    MsalError::InvalidParse(format!("Failed parsing client_info: {}", e))
769                })
770            })?;
771
772        let uid_str = client_info["uid"].to_string();
773        let uid = Uuid::parse_str(uid_str.trim_matches('"'))
774            .map_err(|e| MsalError::InvalidParse(format!("Failed parsing client_info: {}", e)))?;
775
776        let utid_str = client_info["utid"].to_string();
777        let utid = Uuid::parse_str(utid_str.trim_matches('"'))
778            .map_err(|e| MsalError::InvalidParse(format!("Failed parsing client_info: {}", e)))?;
779
780        Ok(ClientInfo {
781            uid: Some(uid),
782            utid: Some(utid),
783        })
784    }
785}
786
787/// Convert a v2-style API scope to a v1 resource identifier by stripping the
788/// permission suffix. The v1 `/oauth2/authorize` endpoint uses the `resource`
789/// parameter (not `scope`) to determine the token audience.
790///
791/// Examples:
792///   `api://<guid>/access_as_user`           → `api://<guid>`
793///   `https://graph.microsoft.com/User.Read` → `https://graph.microsoft.com`
794///   `https://graph.microsoft.com`           → `https://graph.microsoft.com`
795fn v2_scope_to_v1_resource(scope: &str) -> String {
796    if let Some(scheme_end) = scope.find("://") {
797        let authority_start = scheme_end + 3;
798        let after_authority = &scope[authority_start..];
799        if let Some(slash_pos) = after_authority.find('/') {
800            let permission = &after_authority[slash_pos + 1..];
801            if !permission.is_empty() {
802                return scope[..authority_start + slash_pos].to_string();
803            }
804        }
805    }
806    scope.trim_end_matches('/').to_string()
807}
808
809fn decode_number_from_string<'de, D>(d: D) -> Result<u32, D::Error>
810where
811    D: Deserializer<'de>,
812{
813    let v: Value = Deserialize::deserialize(d)?;
814    match v {
815        Value::Number(n) => Ok(n
816            .as_u64()
817            .ok_or(serde::de::Error::custom("Expected number or string"))?
818            as u32),
819        Value::String(s) => s
820            .parse::<u32>()
821            .map_err(|e| serde::de::Error::custom(format!("{}", e))),
822        _ => Err(serde::de::Error::custom("Expected number or string")),
823    }
824}
825
826#[derive(Deserialize, Zeroize, ZeroizeOnDrop)]
827pub struct AccessTokenPayload {
828    amr: Vec<String>,
829    tid: String,
830    // Some Entra token types (e.g. Authenticator number-match MFA flow) use
831    // `unique_name` instead of `upn` in the JWT payload. Accept both.
832    unique_name: Option<String>,
833    upn: Option<String>,
834}
835
836#[derive(Clone, Deserialize, Zeroize, ZeroizeOnDrop)]
837pub struct UserToken {
838    pub token_type: String,
839    pub scope: Option<String>,
840    #[serde(deserialize_with = "decode_number_from_string")]
841    pub expires_in: u32,
842    #[serde(deserialize_with = "decode_number_from_string")]
843    pub ext_expires_in: u32,
844    pub access_token: Option<String>,
845    pub refresh_token: String,
846    #[serde(deserialize_with = "decode_string_or_struct", default)]
847    #[zeroize(skip)]
848    pub id_token: IdToken,
849    #[serde(deserialize_with = "decode_string_or_struct", default)]
850    #[zeroize(skip)]
851    pub client_info: ClientInfo,
852    #[cfg(feature = "broker")]
853    #[zeroize(skip)]
854    pub prt: Option<SealedData>,
855}
856
857#[derive(Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
858pub struct AuthorizationCodePkceFlow {
859    pub auth_url: String,
860    pub redirect_uri: String,
861    pub state: String,
862    scope: String,
863    code_verifier: String,
864}
865
866impl AuthorizationCodePkceFlow {
867    pub fn auth_url(&self) -> &str {
868        &self.auth_url
869    }
870
871    pub fn redirect_uri(&self) -> &str {
872        &self.redirect_uri
873    }
874
875    pub fn state(&self) -> &str {
876        &self.state
877    }
878}
879
880fn generate_base64url_random(bytes_len: usize) -> Result<String, MsalError> {
881    let mut bytes = vec![0u8; bytes_len];
882    rand_bytes(&mut bytes)
883        .map_err(|e| MsalError::CryptoFail(format!("Failed generating random bytes: {}", e)))?;
884    Ok(URL_SAFE_NO_PAD.encode(bytes))
885}
886
887fn pkce_code_challenge(code_verifier: &str) -> String {
888    URL_SAFE_NO_PAD.encode(sha256(code_verifier.as_bytes()))
889}
890
891impl UserToken {
892    /// Fetch the tenant id from the user token
893    ///
894    /// # Returns
895    ///
896    /// * Success: The user's tenant id
897    /// * Failure: An MsalError, indicating the failure.
898    pub fn tenant_id(&self) -> Result<String, MsalError> {
899        if !self.id_token.tid.is_empty() {
900            Ok(self.id_token.tid.clone())
901        } else if let Some(utid) = self.client_info.utid {
902            Ok(utid.to_string())
903        } else if let Some(access_token) = &self.access_token {
904            let mut siter = access_token.splitn(3, '.');
905            siter.next(); // Ignore the header
906            let payload: AccessTokenPayload = json_from_str(
907                &String::from_utf8(
908                    URL_SAFE_NO_PAD
909                        .decode(siter.next().ok_or_else(|| {
910                            MsalError::InvalidParse("Payload not present".to_string())
911                        })?)
912                        .map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?,
913                )
914                .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
915            )
916            .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
917            Ok(payload.tid.clone())
918        } else {
919            Err(MsalError::GeneralFailure(
920                "No tid available for UserToken".to_string(),
921            ))
922        }
923    }
924
925    /// Fetch the UUID from the user token
926    ///
927    /// # Returns
928    ///
929    /// * Success: The user Azure UUID
930    /// * Failure: An MsalError, indicating the failure.
931    pub fn uuid(&self) -> Result<Uuid, MsalError> {
932        Uuid::parse_str(&self.id_token.oid).map_err(|e| MsalError::InvalidParse(format!("{}", e)))
933    }
934
935    /// Fetch the spn from the user token
936    ///
937    /// # Returns
938    ///
939    /// * Success: The user Azure spn
940    /// * Failure: An MsalError, indicating the failure.
941    pub fn spn(&self) -> Result<String, MsalError> {
942        match &self.id_token.preferred_username {
943            Some(spn) => Ok(spn.to_string()),
944            // If all else fails, extract the upn from the access_token
945            None => match &self.access_token {
946                Some(access_token) => {
947                    let mut siter = access_token.splitn(3, '.');
948                    siter.next(); // Ignore the header
949                    let payload: AccessTokenPayload = json_from_str(
950                        &String::from_utf8(
951                            URL_SAFE_NO_PAD
952                                .decode(siter.next().ok_or_else(|| {
953                                    MsalError::InvalidParse("Payload not present".to_string())
954                                })?)
955                                .map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?,
956                        )
957                        .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
958                    )
959                    .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
960                    if let Some(upn) = &payload.upn {
961                        Ok(upn.clone())
962                    } else if let Some(unique_name) = &payload.unique_name {
963                        Ok(unique_name.clone())
964                    } else {
965                        Err(MsalError::GeneralFailure(
966                            "No spn available for UserToken".to_string(),
967                        ))
968                    }
969                }
970                None => Err(MsalError::GeneralFailure(
971                    "No spn available for UserToken".to_string(),
972                )),
973            },
974        }
975    }
976
977    /// Check if the access token amr contains an MFA authorization
978    ///
979    /// # Returns
980    ///
981    /// * Success: Whether or not the token has MFA authorization.
982    /// * Failure: An MsalError, indicating the failure.
983    pub fn amr_mfa(&self) -> Result<bool, MsalError> {
984        match &self.access_token {
985            Some(access_token) => {
986                let mut siter = access_token.splitn(3, '.');
987                siter.next(); // Ignore the header
988                let payload: AccessTokenPayload = json_from_str(
989                    &String::from_utf8(
990                        URL_SAFE_NO_PAD
991                            .decode(siter.next().ok_or_else(|| {
992                                MsalError::InvalidParse("Payload not present".to_string())
993                            })?)
994                            .map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?,
995                    )
996                    .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
997                )
998                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
999                Ok(payload.amr.iter().any(|s| s == "ngcmfa" || s == "mfa"))
1000            }
1001            None => Err(MsalError::GeneralFailure(
1002                "No access token available for UserToken".to_string(),
1003            )),
1004        }
1005    }
1006
1007    /// Check if the access token amr contains an NGC MFA authorization
1008    ///
1009    /// Sometimes it isn't sufficient simply to know if MFA has been
1010    /// performed. Key enrollment (such as a Hello Key), for example,
1011    /// explicitly requires an NGC MFA authorization.
1012    ///
1013    /// # Returns
1014    ///
1015    /// * Success: Whether or not the token has an NGC MFA authorization.
1016    /// * Failure: An MsalError, indicating the failure.
1017    pub fn amr_ngcmfa(&self) -> Result<bool, MsalError> {
1018        match &self.access_token {
1019            Some(access_token) => {
1020                let mut siter = access_token.splitn(3, '.');
1021                siter.next(); // Ignore the header
1022                let payload: AccessTokenPayload = json_from_str(
1023                    &String::from_utf8(
1024                        URL_SAFE_NO_PAD
1025                            .decode(siter.next().ok_or_else(|| {
1026                                MsalError::InvalidParse("Payload not present".to_string())
1027                            })?)
1028                            .map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?,
1029                    )
1030                    .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
1031                )
1032                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
1033                Ok(payload.amr.iter().any(|s| s == "ngcmfa"))
1034            }
1035            None => Err(MsalError::GeneralFailure(
1036                "No access token available for UserToken".to_string(),
1037            )),
1038        }
1039    }
1040}
1041
1042#[cfg(feature = "broker")]
1043#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
1044struct UsernamePasswordAuthenticationPayload {
1045    client_id: String,
1046    request_nonce: String,
1047    scope: String,
1048    win_ver: Option<String>,
1049    grant_type: String,
1050    username: String,
1051    password: String,
1052}
1053
1054#[cfg(feature = "broker")]
1055impl UsernamePasswordAuthenticationPayload {
1056    fn new(username: &str, password: &str, request_nonce: &str) -> Self {
1057        let os_release = match OsRelease::new() {
1058            Ok(os_release) => Some(format!(
1059                "{} {}",
1060                os_release.pretty_name, os_release.version_id
1061            )),
1062            Err(_) => None,
1063        };
1064        UsernamePasswordAuthenticationPayload {
1065            client_id: BROKER_CLIENT_IDENT.to_string(),
1066            request_nonce: request_nonce.to_string(),
1067            scope: "openid aza ugs".to_string(),
1068            win_ver: os_release,
1069            grant_type: "password".to_string(),
1070            username: username.to_string(),
1071            password: password.to_string(),
1072        }
1073    }
1074}
1075
1076#[cfg(feature = "broker")]
1077#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
1078struct RefreshTokenAuthenticationPayload {
1079    client_id: String,
1080    request_nonce: String,
1081    scope: String,
1082    win_ver: Option<String>,
1083    grant_type: String,
1084    refresh_token: String,
1085}
1086
1087#[cfg(feature = "broker")]
1088impl RefreshTokenAuthenticationPayload {
1089    fn new(refresh_token: &str, request_nonce: &str) -> Self {
1090        let os_release = match OsRelease::new() {
1091            Ok(os_release) => Some(format!(
1092                "{} {}",
1093                os_release.pretty_name, os_release.version_id
1094            )),
1095            Err(_) => None,
1096        };
1097        RefreshTokenAuthenticationPayload {
1098            client_id: BROKER_APP_ID.to_string(),
1099            request_nonce: request_nonce.to_string(),
1100            scope: "openid aza ugs".to_string(),
1101            win_ver: os_release,
1102            grant_type: "refresh_token".to_string(),
1103            refresh_token: refresh_token.to_string(),
1104        }
1105    }
1106}
1107
1108#[cfg(feature = "broker")]
1109#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
1110struct HelloForBusinessAssertion {
1111    iss: String,
1112    aud: String,
1113    iat: u64,
1114    exp: u64,
1115    scope: String,
1116    request_nonce: String,
1117}
1118
1119#[cfg(feature = "broker")]
1120impl HelloForBusinessAssertion {
1121    fn new(username: &str, request_nonce: &str) -> Result<Self, MsalError> {
1122        let iat: u64 = SystemTime::now()
1123            .duration_since(UNIX_EPOCH)
1124            .map_err(|e| MsalError::GeneralFailure(format!("Failed choosing iat: {}", e)))?
1125            .as_secs();
1126        Ok(HelloForBusinessAssertion {
1127            iss: username.to_string(),
1128            aud: "common".to_string(),
1129            iat: iat - 300,
1130            exp: iat + 300,
1131            scope: "openid aza ugs".to_string(),
1132            request_nonce: request_nonce.to_string(),
1133        })
1134    }
1135}
1136
1137#[cfg(feature = "broker")]
1138#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
1139struct HelloForBusinessPayload {
1140    client_id: String,
1141    request_nonce: String,
1142    scope: String,
1143    win_ver: Option<String>,
1144    grant_type: String,
1145    username: String,
1146    assertion: String,
1147}
1148
1149#[cfg(feature = "broker")]
1150impl HelloForBusinessPayload {
1151    fn new(username: &str, assertion: &str, request_nonce: &str) -> Self {
1152        let os_release = match OsRelease::new() {
1153            Ok(os_release) => Some(format!(
1154                "{} {}",
1155                os_release.pretty_name, os_release.version_id
1156            )),
1157            Err(_) => None,
1158        };
1159        HelloForBusinessPayload {
1160            client_id: BROKER_APP_ID.to_string(),
1161            request_nonce: request_nonce.to_string(),
1162            scope: "openid aza ugs".to_string(),
1163            win_ver: os_release,
1164            grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer".to_string(),
1165            username: username.to_string(),
1166            assertion: assertion.to_string(),
1167        }
1168    }
1169}
1170
1171#[cfg(feature = "broker")]
1172#[derive(Serialize, Clone, Default)]
1173struct ExchangePRTPayload {
1174    win_ver: Option<String>,
1175    scope: String,
1176    resource: Option<String>,
1177    request_nonce: String,
1178    refresh_token: String,
1179    iss: String,
1180    grant_type: String,
1181    client_id: String,
1182    aud: String,
1183}
1184
1185#[cfg(feature = "broker")]
1186impl ExchangePRTPayload {
1187    fn new(
1188        prt: &PrimaryRefreshToken,
1189        nonce: &str,
1190        resource: Option<String>,
1191        request_prt: bool,
1192    ) -> Result<Self, MsalError> {
1193        let mut scopes = "openid ugs".to_string();
1194        if request_prt {
1195            scopes = format!("{} aza", scopes);
1196        }
1197        let os_release = match OsRelease::new() {
1198            Ok(os_release) => Some(format!(
1199                "{} {}",
1200                os_release.pretty_name, os_release.version_id
1201            )),
1202            Err(_) => None,
1203        };
1204        Ok(ExchangePRTPayload {
1205            win_ver: os_release,
1206            scope: scopes,
1207            resource,
1208            request_nonce: nonce.to_string(),
1209            refresh_token: prt.refresh_token.clone(),
1210            iss: "aad:brokerplugin".to_string(),
1211            grant_type: "refresh_token".to_string(),
1212            client_id: BROKER_CLIENT_IDENT.to_string(),
1213            aud: "login.microsoftonline.com".to_string(),
1214        })
1215    }
1216}
1217
1218/// JWT payload for exchanging a PRT directly for an access token via the
1219/// JWT bearer grant. Used for PoP (Proof of Possession) token requests
1220/// where the authorize -> auth_code -> token flow is not supported.
1221#[cfg(feature = "broker")]
1222#[derive(Serialize, Clone, Default)]
1223struct ExchangePRTForATPayload {
1224    win_ver: Option<String>,
1225    scope: String,
1226    #[serde(skip_serializing_if = "Option::is_none")]
1227    resource: Option<String>,
1228    request_nonce: String,
1229    refresh_token: String,
1230    iss: String,
1231    grant_type: String,
1232    client_id: String,
1233    redirect_uri: String,
1234    aud: String,
1235}
1236
1237#[cfg(feature = "broker")]
1238impl ExchangePRTForATPayload {
1239    fn new(
1240        prt: &PrimaryRefreshToken,
1241        nonce: &str,
1242        scopes: &str,
1243        client_id: &str,
1244        redirect_uri: &str,
1245        resource: Option<&str>,
1246    ) -> Result<Self, MsalError> {
1247        let os_release = match OsRelease::new() {
1248            Ok(os_release) => Some(format!(
1249                "{} {}",
1250                os_release.pretty_name, os_release.version_id
1251            )),
1252            Err(_) => None,
1253        };
1254        Ok(ExchangePRTForATPayload {
1255            win_ver: os_release,
1256            scope: scopes.to_string(),
1257            resource: resource.map(|r| r.to_string()),
1258            request_nonce: nonce.to_string(),
1259            refresh_token: prt.refresh_token.clone(),
1260            iss: "aad:brokerplugin".to_string(),
1261            grant_type: "refresh_token".to_string(),
1262            client_id: client_id.to_string(),
1263            redirect_uri: redirect_uri.to_string(),
1264            aud: "login.microsoftonline.com".to_string(),
1265        })
1266    }
1267}
1268
1269#[cfg(feature = "broker")]
1270#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
1271struct RefreshTokenCredentialPayload {
1272    #[serde(skip_serializing_if = "Option::is_none")]
1273    iat: Option<i64>,
1274    refresh_token: String,
1275    #[serde(skip_serializing_if = "Option::is_none")]
1276    request_nonce: Option<String>,
1277    ua_client_id: Option<String>,
1278    ua_redirect_uri: Option<String>,
1279    x_client_platform: Option<String>,
1280    win_ver: Option<String>,
1281    windows_api_version: Option<String>,
1282}
1283
1284#[cfg(feature = "broker")]
1285impl RefreshTokenCredentialPayload {
1286    fn new(prt: &PrimaryRefreshToken, nonce: Option<&str>) -> Result<Self, MsalError> {
1287        let os_release = match OsRelease::new() {
1288            Ok(os_release) => Some(format!(
1289                "{} {}",
1290                os_release.pretty_name, os_release.version_id
1291            )),
1292            Err(_) => None,
1293        };
1294        // When a nonce is provided (from the sso_nonce URL parameter),
1295        // use it as request_nonce and omit iat.
1296        // When no nonce is provided (proactive SSO flow), use iat only.
1297        let (iat, request_nonce) = match nonce {
1298            Some(n) => (None, Some(n.to_string())),
1299            None => {
1300                let iat: i64 = SystemTime::now()
1301                    .duration_since(UNIX_EPOCH)
1302                    .map_err(|e| MsalError::GeneralFailure(format!("Failed choosing iat: {}", e)))?
1303                    .as_secs()
1304                    .try_into()
1305                    .map_err(|e| {
1306                        MsalError::GeneralFailure(format!("Failed choosing iat: {}", e))
1307                    })?;
1308                (Some(iat), None)
1309            }
1310        };
1311        Ok(RefreshTokenCredentialPayload {
1312            iat,
1313            refresh_token: prt.refresh_token.clone(),
1314            request_nonce,
1315            ua_client_id: None,
1316            ua_redirect_uri: None,
1317            x_client_platform: None,
1318            win_ver: os_release,
1319            windows_api_version: Some("2.0.1".to_string()),
1320        })
1321    }
1322}
1323
1324#[cfg(feature = "broker")]
1325#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
1326struct DeviceCredentialPayload {
1327    grant_type: String,
1328    iss: String,
1329    request_nonce: String,
1330    ua_client_id: Option<String>,
1331    ua_redirect_uri: Option<String>,
1332    x_client_platform: Option<String>,
1333    win_ver: Option<String>,
1334    windows_api_version: Option<String>,
1335}
1336
1337#[cfg(feature = "broker")]
1338impl DeviceCredentialPayload {
1339    fn new(nonce: &str) -> Result<Self, MsalError> {
1340        let os_release = match OsRelease::new() {
1341            Ok(os_release) => Some(format!(
1342                "{} {}",
1343                os_release.pretty_name, os_release.version_id
1344            )),
1345            Err(_) => None,
1346        };
1347        Ok(DeviceCredentialPayload {
1348            grant_type: "device_auth".to_string(),
1349            iss: "aad:brokerplugin".to_string(),
1350            request_nonce: nonce.to_string(),
1351            ua_client_id: None,
1352            ua_redirect_uri: None,
1353            x_client_platform: None,
1354            win_ver: os_release,
1355            windows_api_version: Some("2.0.1".to_string()),
1356        })
1357    }
1358}
1359
1360#[cfg(feature = "broker")]
1361#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
1362struct P2PDeviceCertificatePayload {
1363    client_id: String,
1364    request_nonce: String,
1365    win_ver: Option<String>,
1366    grant_type: String,
1367    cert_token_use: String,
1368    csr_type: String,
1369    csr: String,
1370    netbios_name: String,
1371    dns_names: Vec<String>,
1372}
1373
1374#[cfg(feature = "broker")]
1375impl P2PDeviceCertificatePayload {
1376    fn new(nonce: &str, csr: &str, device_name: &str, dns_names: &[String]) -> Self {
1377        let os_release = match OsRelease::new() {
1378            Ok(os_release) => Some(format!(
1379                "{} {}",
1380                os_release.pretty_name, os_release.version_id
1381            )),
1382            Err(_) => None,
1383        };
1384        P2PDeviceCertificatePayload {
1385            client_id: BROKER_CLIENT_IDENT.to_string(),
1386            request_nonce: nonce.to_string(),
1387            win_ver: os_release,
1388            grant_type: "device_auth".to_string(),
1389            cert_token_use: "device_cert".to_string(),
1390            csr_type: "http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10"
1391                .to_string(),
1392            csr: csr.to_string(),
1393            netbios_name: device_name.to_string(),
1394            dns_names: dns_names.to_vec(),
1395        }
1396    }
1397}
1398
1399#[cfg(feature = "broker")]
1400#[derive(Serialize)]
1401struct P2PDeviceCertificateHeader<'a> {
1402    alg: &'static str,
1403    typ: &'static str,
1404    /// The broker sends the device certificate as a single base64 string here,
1405    /// not as the array of certificates specified by RFC 7515. Entra rejects
1406    /// the request when the standard array form is used, so this must stay a
1407    /// string.
1408    x5c: &'a str,
1409}
1410
1411#[cfg(feature = "broker")]
1412#[derive(Serialize)]
1413struct P2PUserCertificateHeader<'a> {
1414    alg: &'static str,
1415    typ: &'static str,
1416    ctx: &'a str,
1417}
1418
1419#[cfg(feature = "broker")]
1420#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
1421struct P2PUserCertificatePayload {
1422    iss: String,
1423    grant_type: String,
1424    aud: String,
1425    request_nonce: String,
1426    scope: String,
1427    refresh_token: String,
1428    client_id: String,
1429    cert_token_use: String,
1430    csr_type: String,
1431    csr: String,
1432}
1433
1434#[cfg(feature = "broker")]
1435impl P2PUserCertificatePayload {
1436    fn new(prt: &PrimaryRefreshToken, nonce: &str, csr: &str) -> Self {
1437        P2PUserCertificatePayload {
1438            iss: "aad:brokerplugin".to_string(),
1439            grant_type: "refresh_token".to_string(),
1440            aud: "login.microsoftonline.com".to_string(),
1441            request_nonce: nonce.to_string(),
1442            scope: "openid aza ugs".to_string(),
1443            refresh_token: prt.refresh_token.clone(),
1444            client_id: BROKER_CLIENT_IDENT.to_string(),
1445            cert_token_use: "user_cert".to_string(),
1446            csr_type: "http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10"
1447                .to_string(),
1448            csr: csr.to_string(),
1449        }
1450    }
1451
1452    /// Serialize the payload with the refresh token replaced, for logging.
1453    fn redacted(&self) -> Result<Value, MsalError> {
1454        let mut value = serde_json::to_value(self).map_err(|e| {
1455            MsalError::InvalidJson(format!("Failed serializing P2P user payload: {}", e))
1456        })?;
1457        value["refresh_token"] = "**********".into();
1458        Ok(value)
1459    }
1460}
1461
1462#[cfg(feature = "broker")]
1463#[derive(Deserialize)]
1464struct P2PCertificateResponse {
1465    x5c: String,
1466    x5c_ca: String,
1467}
1468
1469#[cfg(feature = "broker")]
1470#[derive(Debug, Deserialize)]
1471struct Nonce {
1472    #[serde(rename = "Nonce")]
1473    nonce: String,
1474}
1475
1476#[cfg(feature = "broker")]
1477trait Tgt {
1478    /// Returs the long-term key to decrypt the enc-part field in the AS-REP.
1479    fn derived_key(
1480        &self,
1481        tpm: &mut BoxedDynTpm,
1482        transport_key: &MsOapxbcRsaKey,
1483        storage_key: &StorageKey,
1484        session_key: &SessionKey,
1485    ) -> Result<DerivedKey, MsalError>;
1486
1487    /// Returns the AS-REP
1488    fn as_rep(&self) -> Result<AuthenticationReply, MsalError>;
1489}
1490
1491#[cfg(feature = "broker")]
1492impl FromStr for StructuredTgt {
1493    type Err = MsalError;
1494    fn from_str(s: &str) -> Result<Self, Self::Err> {
1495        json_from_str(s).map_err(|e| MsalError::InvalidParse(format!("Failed parsing tgt: {}", e)))
1496    }
1497}
1498
1499#[cfg(feature = "broker")]
1500#[derive(Default, Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
1501#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
1502pub struct StructuredTgt {
1503    #[serde(rename = "clientKey")]
1504    client_key: Option<String>,
1505    #[serde(rename = "keyType")]
1506    key_type: u32,
1507    error: Option<String>,
1508    #[serde(rename = "messageBuffer")]
1509    message_buffer: Option<String>,
1510    pub realm: Option<String>,
1511    pub sn: Option<String>,
1512    pub cn: Option<String>,
1513    #[serde(rename = "sessionKeyType")]
1514    pub session_key_type: u32,
1515    #[serde(rename = "accountType")]
1516    pub account_type: u32,
1517}
1518
1519#[cfg(feature = "broker")]
1520impl Tgt for StructuredTgt {
1521    fn derived_key(
1522        &self,
1523        tpm: &mut BoxedDynTpm,
1524        transport_key: &MsOapxbcRsaKey,
1525        storage_key: &StorageKey,
1526        session_key: &SessionKey,
1527    ) -> Result<DerivedKey, MsalError> {
1528        let client_key = match self.client_key.as_deref() {
1529            Some(k) => {
1530                // Check if the client_key is a JWE compact serialization (5 parts separated by dots)
1531                // or a raw base64-encoded key (no dots)
1532                if k.contains('.') {
1533                    // JWE format - decrypt it
1534                    let jwe = JweCompact::from_str(k)
1535                        .map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))?;
1536                    session_key
1537                        .decipher_tgt_client_key(tpm, transport_key, storage_key, &jwe)
1538                        .map_err(|e| {
1539                            MsalError::CryptoFail(format!(
1540                                "Failed to unwrap the TGT session key: {:?}",
1541                                e
1542                            ))
1543                        })?
1544                } else {
1545                    // Raw base64-encoded key - just decode it
1546                    Zeroizing::new(STANDARD.decode(k).map_err(|e| {
1547                        MsalError::CryptoFail(format!(
1548                            "Failed to decode base64 client key: {:?}",
1549                            e
1550                        ))
1551                    })?)
1552                }
1553            }
1554            None => {
1555                return Err(MsalError::CryptoFail(
1556                    "TGT client key is missing".to_string(),
1557                ))
1558            }
1559        };
1560
1561        match self.key_type {
1562            18 => {
1563                let k: [u8; 32] = client_key.as_slice().try_into().map_err(|_| {
1564                    MsalError::CryptoFail("Unexpected TGT session key length".to_string())
1565                })?;
1566                let dk = DerivedKey::Aes256CtsHmacSha196 {
1567                    k,
1568                    i: 0,
1569                    s: String::new(),
1570                    kvno: 1,
1571                };
1572                Ok(dk)
1573            }
1574            _ => Err(MsalError::CryptoFail(format!(
1575                "Unexpected TGT session key type {}",
1576                self.key_type
1577            ))),
1578        }
1579    }
1580
1581    fn as_rep(&self) -> Result<AuthenticationReply, MsalError> {
1582        let buf = match self.message_buffer.as_deref() {
1583            Some(buf) => STANDARD
1584                .decode(buf)
1585                .map_err(|e| MsalError::CryptoFail(format!("{:?}", e)))?,
1586            None => {
1587                return Err(MsalError::CryptoFail(
1588                    "TGT message buffer is missing".to_string(),
1589                ))
1590            }
1591        };
1592
1593        let reply = match KerberosReply::try_from(buf.as_slice()) {
1594            Ok(r) => r,
1595            Err(e) => {
1596                return Err(MsalError::GeneralFailure(format!(
1597                    "Failed to decode the cloud kerberos reply: {:?}",
1598                    e
1599                )));
1600            }
1601        };
1602
1603        match reply {
1604            KerberosReply::AS(as_rep) => Ok(as_rep),
1605            _ => Err(MsalError::GeneralFailure(
1606                "Unexpected kerberos reply message".to_string(),
1607            )),
1608        }
1609    }
1610}
1611
1612#[derive(Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
1613#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
1614struct RawTgt {
1615    /// Base64 encoded KERB_MESSAGE_BUFFER.
1616    tgt_message_buffer: String,
1617
1618    /// Base64 encoded client key (secret). This key is the client secret used
1619    /// to protect the TGT. In this passwordless scenario, the client secret is
1620    /// generated by the server as part of each TGT request and then returned to
1621    /// the client in the response.
1622    tgt_client_key: String,
1623
1624    /// The on-premises AD DS key type used for both the client key and the
1625    /// Kerberos session key included in the KERB_MESSAGE_BUFFER.
1626    tgt_key_type: u32,
1627}
1628
1629#[cfg(feature = "broker")]
1630impl Tgt for RawTgt {
1631    fn derived_key(
1632        &self,
1633        tpm: &mut BoxedDynTpm,
1634        transport_key: &MsOapxbcRsaKey,
1635        storage_key: &StorageKey,
1636        session_key: &SessionKey,
1637    ) -> Result<DerivedKey, MsalError> {
1638        let jwe = JweCompact::from_str(&self.tgt_client_key)
1639            .map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))?;
1640        let long_term_krb_session_key = session_key
1641            .decipher_tgt_client_key(tpm, transport_key, storage_key, &jwe)
1642            .map_err(|e| {
1643                MsalError::CryptoFail(format!("Failed to unwrap the TGT session key: {:?}", e))
1644            })?;
1645
1646        let dk = match self.tgt_key_type {
1647            18 => {
1648                let k: [u8; 32] =
1649                    long_term_krb_session_key
1650                        .as_slice()
1651                        .try_into()
1652                        .map_err(|_| {
1653                            MsalError::CryptoFail("Unexpected TGT session key length".to_string())
1654                        })?;
1655                DerivedKey::Aes256CtsHmacSha196 {
1656                    k,
1657                    i: 0,
1658                    s: String::new(),
1659                    kvno: 1,
1660                }
1661            }
1662            _ => {
1663                return Err(MsalError::CryptoFail(format!(
1664                    "Unexpected TGT session key type {}",
1665                    self.tgt_key_type
1666                )));
1667            }
1668        };
1669        Ok(dk)
1670    }
1671
1672    fn as_rep(&self) -> Result<AuthenticationReply, MsalError> {
1673        let buf = STANDARD
1674            .decode(&self.tgt_message_buffer)
1675            .map_err(|e| MsalError::CryptoFail(format!("{:?}", e)))?;
1676
1677        let reply = match KerberosReply::try_from(buf.as_slice()) {
1678            Ok(r) => r,
1679            Err(e) => {
1680                return Err(MsalError::GeneralFailure(format!(
1681                    "Failed to decode the cloud kerberos reply: {:?}",
1682                    e
1683                )));
1684            }
1685        };
1686
1687        match reply {
1688            KerberosReply::AS(as_rep) => Ok(as_rep),
1689            _ => Err(MsalError::GeneralFailure(
1690                "Unexpected kerberos reply message".to_string(),
1691            )),
1692        }
1693    }
1694}
1695
1696#[derive(Clone, Zeroize, ZeroizeOnDrop)]
1697#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
1698enum OnPremTgt {
1699    /// `{ "tgt_message_buffer": "...", "tgt_client_key": "...", "tgt_key_type": 18 }`
1700    Raw(RawTgt),
1701    /// `{ "tgt_ad": { ... } }`
1702    Structured {
1703        tgt_ad: StructuredTgt,
1704    },
1705    Absent,
1706}
1707
1708impl Serialize for OnPremTgt {
1709    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1710        match self {
1711            // Raw is itself a struct with 3 fields -> emit them inline
1712            OnPremTgt::Raw(raw) => {
1713                let mut m = serializer.serialize_map(Some(3))?;
1714                m.serialize_entry("tgt_message_buffer", &raw.tgt_message_buffer)?;
1715                m.serialize_entry("tgt_client_key", &raw.tgt_client_key)?;
1716                m.serialize_entry("tgt_key_type", &raw.tgt_key_type)?;
1717                m.end()
1718            }
1719            OnPremTgt::Structured { tgt_ad } => {
1720                let mut m = serializer.serialize_map(Some(1))?;
1721                m.serialize_entry("tgt_ad", tgt_ad)?;
1722                m.end()
1723            }
1724            // emit nothing at all
1725            OnPremTgt::Absent => serializer.serialize_map(Some(0))?.end(),
1726        }
1727    }
1728}
1729
1730impl<'de> Deserialize<'de> for OnPremTgt {
1731    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1732        struct V;
1733
1734        impl<'de> Visitor<'de> for V {
1735            type Value = OnPremTgt;
1736
1737            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
1738                f.write_str("on-prem TGT fields (raw or structured)")
1739            }
1740
1741            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
1742                let mut tgt_message_buffer = None;
1743                let mut tgt_client_key = None;
1744                let mut tgt_key_type = None;
1745                let mut tgt_ad: Option<StructuredTgt> = None;
1746
1747                while let Some(key) = map.next_key::<String>()? {
1748                    match key.as_str() {
1749                        "tgt_message_buffer" => tgt_message_buffer = Some(map.next_value()?),
1750                        "tgt_client_key" => tgt_client_key = Some(map.next_value()?),
1751                        "tgt_key_type" => tgt_key_type = Some(map.next_value()?),
1752                        "tgt_ad" => tgt_ad = Some(map.next_value()?),
1753                        // critical: other flattened/sibling fields land here
1754                        _ => {
1755                            map.next_value::<IgnoredAny>()?;
1756                        }
1757                    }
1758                }
1759
1760                match (tgt_ad, tgt_message_buffer, tgt_client_key, tgt_key_type) {
1761                    (Some(tgt_ad), None, None, None) => Ok(OnPremTgt::Structured { tgt_ad }),
1762                    (None, Some(buf), Some(key), Some(kt)) => Ok(OnPremTgt::Raw(RawTgt {
1763                        tgt_message_buffer: buf,
1764                        tgt_client_key: key,
1765                        tgt_key_type: kt,
1766                    })),
1767                    (None, None, None, None) => Ok(OnPremTgt::Absent),
1768                    _ => Err(de::Error::custom(
1769                        "on-prem TGT is a mix of raw and structured, or is incomplete",
1770                    )),
1771                }
1772            }
1773        }
1774
1775        deserializer.deserialize_map(V)
1776    }
1777}
1778
1779#[cfg(feature = "broker")]
1780#[derive(Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
1781#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
1782struct PrimaryRefreshToken {
1783    token_type: String,
1784    expires_in: String,
1785    ext_expires_in: String,
1786    expires_on: String,
1787    refresh_token: String,
1788    refresh_token_expires_in: u64,
1789    session_key_jwe: Option<String>,
1790    #[serde(deserialize_with = "decode_string_or_struct")]
1791    #[zeroize(skip)]
1792    id_token: IdToken,
1793    #[serde(deserialize_with = "decode_string_or_struct", default)]
1794    #[zeroize(skip)]
1795    client_info: ClientInfo,
1796    device_tenant_id: Option<String>,
1797    #[serde(flatten)]
1798    tgt_on_prem: OnPremTgt,
1799    #[serde(deserialize_with = "decode_string_or_struct", default)]
1800    tgt_cloud: StructuredTgt,
1801    kerberos_top_level_names: Option<String>,
1802}
1803
1804#[cfg(feature = "broker")]
1805impl PrimaryRefreshToken {
1806    fn name(&self) -> String {
1807        self.id_token.name.clone()
1808    }
1809
1810    fn spn(&self) -> Result<String, MsalError> {
1811        match &self.id_token.preferred_username {
1812            Some(spn) => Ok(spn.to_string()),
1813            None => Err(MsalError::GeneralFailure(
1814                "No spn available for PRT".to_string(),
1815            )),
1816        }
1817    }
1818
1819    fn uuid(&self) -> Result<Uuid, MsalError> {
1820        Uuid::parse_str(&self.id_token.oid).map_err(|e| MsalError::InvalidParse(format!("{}", e)))
1821    }
1822
1823    fn session_key(&self) -> Result<SessionKey, MsalError> {
1824        match &self.session_key_jwe {
1825            Some(session_key_jwe) => SessionKey::new(session_key_jwe),
1826            None => Err(MsalError::CryptoFail("session_key_jwe missing".to_string())),
1827        }
1828    }
1829
1830    fn clone_session_key(&self, new_prt: &mut PrimaryRefreshToken) {
1831        new_prt.session_key_jwe.clone_from(&self.session_key_jwe);
1832    }
1833
1834    fn is_expired(&self) -> bool {
1835        match self.expires_on.parse::<u64>() {
1836            Ok(expiry_ts) => match SystemTime::now().duration_since(UNIX_EPOCH) {
1837                Ok(now) => {
1838                    let now = now.as_secs();
1839                    now >= expiry_ts
1840                }
1841                Err(_) => true,
1842            },
1843            Err(e) => {
1844                error!(?e, "Failed parsing PRT expires_on '{}'", self.expires_on);
1845                true
1846            }
1847        }
1848    }
1849}
1850
1851#[cfg(feature = "broker")]
1852struct SessionKey {
1853    session_key_jwe: JweCompact,
1854}
1855
1856#[cfg(feature = "broker")]
1857impl SessionKey {
1858    fn new(session_key_jwe: &str) -> Result<Self, MsalError> {
1859        Ok(SessionKey {
1860            session_key_jwe: JweCompact::from_str(session_key_jwe)
1861                .map_err(|e| MsalError::InvalidParse(format!("Failed parsing jwe: {}", e)))?,
1862        })
1863    }
1864
1865    fn decipher_prt_v2(
1866        &self,
1867        tpm: &mut BoxedDynTpm,
1868        transport_key: &MsOapxbcRsaKey,
1869        storage_key: &StorageKey,
1870        jwe: &JweCompact,
1871    ) -> Result<Jwe, MsalError> {
1872        // This allows a transition, where existing msoapxbc keys will provide
1873        // their sealing content encryption keys, but newer keys will not, falling back
1874        // to the provided key.
1875        let maybe_transport_storage_key = tpm.rs256_yield_cek(transport_key);
1876        let storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
1877
1878        let session_key = MsOapxbcSessionKey::complete_tpm_rsa_oaep_key_agreement(
1879            tpm,
1880            storage_key,
1881            transport_key,
1882            &self.session_key_jwe,
1883        )
1884        .map_err(|e| MsalError::CryptoFail(format!("Unable to decipher session_key_jwe: {}", e)))?;
1885        session_key
1886            .decipher_prt_v2(tpm, storage_key, jwe)
1887            .map_err(|e| MsalError::CryptoFail(format!("Failed to decipher Jwe: {}", e)))
1888    }
1889
1890    #[allow(dead_code)]
1891    fn decipher_tgt_client_key(
1892        &self,
1893        tpm: &mut BoxedDynTpm,
1894        transport_key: &MsOapxbcRsaKey,
1895        storage_key: &StorageKey,
1896        jwe: &JweCompact,
1897    ) -> Result<Zeroizing<Vec<u8>>, MsalError> {
1898        // This allows a transition, where existing msoapxbc keys will provide
1899        // their sealing content encryption keys, but newer keys will not, falling back
1900        // to the provided key.
1901        let maybe_transport_storage_key = tpm.rs256_yield_cek(transport_key);
1902        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
1903
1904        let session_key = MsOapxbcSessionKey::complete_tpm_rsa_oaep_key_agreement(
1905            tpm,
1906            prt_storage_key,
1907            transport_key,
1908            &self.session_key_jwe,
1909        )
1910        .map_err(|e| MsalError::CryptoFail(format!("Unable to decipher session_key_jwe: {}", e)))?;
1911        match session_key.decipher_prt_v2(tpm, prt_storage_key, jwe) {
1912            Ok(decrypted) => Ok(Zeroizing::new(decrypted.payload().to_vec())),
1913            Err(JwtError::OpenSSLError) => match session_key.decipher(tpm, prt_storage_key, jwe) {
1914                Ok(decrypted) => Ok(Zeroizing::new(decrypted.payload().to_vec())),
1915                Err(e) => Err(MsalError::CryptoFail(format!(
1916                    "Failed to decipher Jwe: {}",
1917                    e
1918                ))),
1919            },
1920            Err(e) => Err(MsalError::CryptoFail(format!(
1921                "Failed to decipher Jwe: {}",
1922                e
1923            ))),
1924        }
1925    }
1926
1927    fn sign<V: JwsSignable>(
1928        &self,
1929        tpm: &mut BoxedDynTpm,
1930        transport_key: &MsOapxbcRsaKey,
1931        storage_key: &StorageKey,
1932        jws: &V,
1933    ) -> Result<V::Signed, MsalError> {
1934        // This allows a transition, where existing msoapxbc keys will provide
1935        // their sealing content encryption keys, but newer keys will not, falling back
1936        // to the provided key.
1937        let maybe_transport_storage_key = tpm.rs256_yield_cek(transport_key);
1938        let storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
1939
1940        let session_key = MsOapxbcSessionKey::complete_tpm_rsa_oaep_key_agreement(
1941            tpm,
1942            storage_key,
1943            transport_key,
1944            &self.session_key_jwe,
1945        )
1946        .map_err(|e| MsalError::CryptoFail(format!("Unable to decipher session_key_jwe: {}", e)))?;
1947
1948        session_key
1949            .sign(tpm, storage_key, jws)
1950            .map_err(|e| MsalError::CryptoFail(format!("Failed signing jwk: {}", e)))
1951    }
1952}
1953
1954/// Determines whether passwordless security key authentication should be
1955/// attempted (physical USB FIDO2 key).
1956///
1957/// Attempts whenever any FIDO method is registered for the user
1958/// (FidoParams present in GetCredentialType). The actual key matching
1959/// happens via the allow list at the WebAuthn level.
1960pub(crate) fn should_attempt_passwordless_security_key(
1961    options: &[AuthOption],
1962    has_fido_params: bool,
1963) -> bool {
1964    let enabled = options.contains(&AuthOption::PasswordlessSecurityKey)
1965        || options.contains(&AuthOption::PasswordlessFido);
1966    if !enabled {
1967        debug!("passwordless_security_key: skipped (not enabled in config)");
1968        return false;
1969    }
1970    if !has_fido_params {
1971        debug!("passwordless_security_key: skipped (no FIDO params from GetCredentialType)");
1972        return false;
1973    }
1974    debug!("passwordless_security_key: user has FIDO params, attempting");
1975    true
1976}
1977
1978/// Determines whether passwordless QR/Bluetooth (caBLE hybrid transport)
1979/// authentication should be attempted.
1980///
1981/// Uses `has_cross_device_capable_passkey` from GetCredentialType to
1982/// determine if the user has a cross-device passkey. No Graph permission
1983/// required. Bluetooth hardware availability is checked at runtime in the
1984/// PAM layer, not here.
1985pub(crate) fn should_attempt_passwordless_qr_bluetooth(
1986    options: &[AuthOption],
1987    has_fido_params: bool,
1988    user_has_any_cross_device_fido: bool,
1989) -> bool {
1990    if !options.contains(&AuthOption::PasswordlessQrBluetooth) {
1991        debug!("passwordless_qr_bluetooth: skipped (not enabled in config)");
1992        return false;
1993    }
1994    if !has_fido_params {
1995        debug!("passwordless_qr_bluetooth: skipped (no FIDO params from GetCredentialType)");
1996        return false;
1997    }
1998    if !user_has_any_cross_device_fido {
1999        debug!("passwordless_qr_bluetooth: skipped (user has no cross-device passkey)");
2000        return false;
2001    }
2002    debug!("passwordless_qr_bluetooth: user has cross-device passkey, attempting");
2003    true
2004}
2005
2006#[repr(C)]
2007#[derive(PartialEq)]
2008pub enum AuthOption {
2009    Fido,
2010    Passwordless,
2011    PasswordlessFido,
2012    PasswordlessSecurityKey,
2013    PasswordlessQrBluetooth,
2014    NoDAGFallback,
2015    /// Demand ngcmfa in the amr_values, which forces MFA authentication.
2016    /// This should be used for remote connections (SSH) where we want to ensure
2017    /// MFA is enforced. For local terminal authentication (GDM, etc.), this
2018    /// should NOT be set - let the natural passwordless auth flow happen without
2019    /// prematurely triggering MFA notifications.
2020    #[cfg(feature = "optional_mfa")]
2021    ForceMFA,
2022    /// Indicates this is a remote session (e.g., SSH). When set, if the user
2023    /// has not enrolled in MFA, authentication will be denied rather than
2024    /// falling back to DAG. Users must enroll in MFA before remote
2025    /// authentication is permitted.
2026    #[cfg(feature = "optional_mfa")]
2027    RemoteSession,
2028}
2029
2030#[cfg(feature = "ipvers")]
2031#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2032pub enum IpVersion {
2033    V4,
2034    V6,
2035}
2036
2037pub(crate) struct ClientApplication {
2038    pub(crate) client: Client,
2039    pub(crate) client_id: String,
2040    authority: RwLock<String>,
2041    jar: Arc<CookieStoreMutex>,
2042    #[cfg(feature = "ipvers")]
2043    pub(crate) ip_version: Vec<IpVersion>,
2044    #[cfg(feature = "set_timeout")]
2045    pub(crate) timeout: Duration,
2046}
2047
2048impl ClientApplication {
2049    pub(crate) fn new(
2050        client_id: &str,
2051        authority: Option<&str>,
2052        #[cfg(feature = "set_timeout")] timeout: Duration,
2053        #[cfg(feature = "ipvers")] ip_version: &[IpVersion],
2054    ) -> Result<Self, MsalError> {
2055        let jar = Arc::new(CookieStoreMutex::new(CookieStore::default()));
2056
2057        #[cfg(feature = "set_timeout")]
2058        let (timeout, connect_timeout) = { (timeout, min(timeout / 2, Duration::from_secs(3))) };
2059        #[cfg(not(feature = "set_timeout"))]
2060        let (timeout, connect_timeout) = (Duration::from_secs(3), Duration::from_secs(1));
2061
2062        #[allow(unused_mut)]
2063        let mut builder = reqwest::Client::builder()
2064            .connect_timeout(connect_timeout)
2065            .timeout(timeout)
2066            .redirect(Policy::none())
2067            .cookie_provider(jar.clone());
2068
2069        #[cfg(feature = "proxyable")]
2070        {
2071            if let Some(proxy_var) = std::env::var("HTTPS_PROXY")
2072                .ok()
2073                .or_else(|| std::env::var("ALL_PROXY").ok())
2074            {
2075                let proxy = Proxy::https(proxy_var)
2076                    .map_err(|e| MsalError::GeneralFailure(format!("{:?}", e)))?;
2077                builder = builder.proxy(proxy).danger_accept_invalid_certs(true);
2078            }
2079        }
2080
2081        #[cfg(feature = "ipvers")]
2082        {
2083            let has_v4 = ip_version.contains(&IpVersion::V4);
2084            let has_v6 = ip_version.contains(&IpVersion::V6);
2085            if has_v4 && !has_v6 {
2086                builder =
2087                    builder.local_address(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED))
2088            } else if !has_v4 && has_v6 {
2089                builder =
2090                    builder.local_address(std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED))
2091            }
2092        }
2093
2094        let client = builder
2095            .build()
2096            .map_err(|e| MsalError::RequestFailed(format!("{}", e)))?;
2097
2098        Ok(ClientApplication {
2099            client,
2100            client_id: client_id.to_string(),
2101            authority: RwLock::new(match authority {
2102                Some(authority) => authority.to_string(),
2103                None => "https://login.microsoftonline.com/common".to_string(),
2104            }),
2105            jar,
2106            #[cfg(feature = "ipvers")]
2107            ip_version: ip_version.to_vec(),
2108            #[cfg(feature = "set_timeout")]
2109            timeout,
2110        })
2111    }
2112
2113    pub(crate) fn clear_cookies(&self) {
2114        match self.jar.lock() {
2115            Ok(mut jar) => jar.clear(),
2116            Err(e) => error!("Failed to clear cookies: {:?}", e),
2117        }
2118    }
2119
2120    pub(crate) fn authority(&self) -> Result<String, MsalError> {
2121        self.authority
2122            .read()
2123            .map_err(|e| {
2124                MsalError::GeneralFailure(format!(
2125                    "Failed to lock authority URL for reading: {:?}",
2126                    e
2127                ))
2128            })
2129            .map(|authority| authority.clone())
2130    }
2131
2132    pub(crate) fn set_authority(&self, new_authority: &str) -> Result<(), MsalError> {
2133        self.authority
2134            .write()
2135            .map_err(|e| {
2136                MsalError::GeneralFailure(format!(
2137                    "Failed to acquire authority write lock: {:?}",
2138                    e
2139                ))
2140            })
2141            .map(|mut auth| *auth = new_authority.to_string())
2142    }
2143
2144    async fn acquire_token_by_username_password(
2145        &self,
2146        username: &str,
2147        password: &str,
2148        scopes: Vec<&str>,
2149    ) -> Result<UserToken, MsalError> {
2150        let mut all_scopes = vec!["openid", "profile", "offline_access"];
2151        all_scopes.extend(scopes);
2152        let scopes_str = all_scopes.join(" ");
2153
2154        let params = [
2155            ("client_id", self.client_id.as_str()),
2156            ("scope", &scopes_str),
2157            ("username", username),
2158            ("password", password),
2159            ("grant_type", "password"),
2160            ("client_info", "1"),
2161        ];
2162        let payload = params
2163            .iter()
2164            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
2165            .collect::<Vec<String>>()
2166            .join("&");
2167
2168        let resp = self
2169            .client
2170            .post(format!("{}/oauth2/v2.0/token", self.authority()?))
2171            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
2172            .header(header::ACCEPT, "application/json")
2173            .body(payload)
2174            .send()
2175            .await
2176            .map_err(|e| MsalError::request_failed(&e))?;
2177        if resp.status().is_success() {
2178            let token: UserToken = resp
2179                .json()
2180                .await
2181                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2182
2183            Ok(token)
2184        } else {
2185            let json_resp: ErrorResponse = resp
2186                .json()
2187                .await
2188                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2189            Err(MsalError::AcquireTokenFailed(json_resp))
2190        }
2191    }
2192
2193    async fn acquire_token_by_refresh_token(
2194        &self,
2195        refresh_token: &str,
2196        scopes: Vec<&str>,
2197    ) -> Result<UserToken, MsalError> {
2198        let mut all_scopes = vec!["openid", "profile", "offline_access"];
2199        all_scopes.extend(scopes);
2200        let scopes_str = all_scopes.join(" ");
2201
2202        let params = [
2203            ("client_id", self.client_id.as_str()),
2204            ("scope", &scopes_str),
2205            ("grant_type", "refresh_token"),
2206            ("refresh_token", refresh_token),
2207            ("client_info", "1"),
2208        ];
2209        let payload = params
2210            .iter()
2211            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
2212            .collect::<Vec<String>>()
2213            .join("&");
2214
2215        let resp = self
2216            .client
2217            .post(format!("{}/oauth2/v2.0/token", self.authority()?))
2218            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
2219            .header(header::ACCEPT, "application/json")
2220            .body(payload)
2221            .send()
2222            .await
2223            .map_err(|e| MsalError::request_failed(&e))?;
2224        if resp.status().is_success() {
2225            let token: UserToken = resp
2226                .json()
2227                .await
2228                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2229
2230            Ok(token)
2231        } else {
2232            let json_resp: ErrorResponse = resp
2233                .json()
2234                .await
2235                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2236            Err(MsalError::AcquireTokenFailed(json_resp))
2237        }
2238    }
2239
2240    fn get_auth_redirect_uri(&self, client_id: Option<&str>, resource: Option<&str>) -> String {
2241        let client_id = client_id.unwrap_or(self.client_id.as_str());
2242        let resource = resource.unwrap_or("");
2243
2244        match client_id {
2245            "1fec8e78-bce4-4aaf-ab1b-5451cc387264" => {
2246                "https://login.microsoftonline.com/common/oauth2/nativeclient".to_string()
2247            },
2248            "9bc3ab49-b65d-410a-85ad-de819febfddc" => {
2249                "https://oauth.spops.microsoft.com/".to_string()
2250            },
2251            "c44b4083-3bb0-49c1-b47d-974e53cbdf3c" => {
2252                "https://portal.azure.com/signin/index/?feature.prefetchtokens=true&feature.showservicehealthalerts=true&feature.usemsallogin=true".to_string()
2253            },
2254            "0000000c-0000-0000-c000-000000000000" => {
2255                "https://account.activedirectory.windowsazure.com/".to_string()
2256            },
2257            "19db86c3-b2b9-44cc-b339-36da233a3be2" => {
2258                "https://mysignins.microsoft.com".to_string()
2259            },
2260            "29d9ed98-a469-4536-ade2-f981bc1d605e" => {
2261                if resource.contains("enrollment.manage.microsoft.com") {
2262                    "ms-aadj-redir://auth/drs".to_string()
2263                } else {
2264                    "msauth://Microsoft.AAD.BrokerPlugin".to_string()
2265                }
2266            },
2267            "b743a22d-6705-4147-8670-d92fa515ee2b" => {
2268                "companyportal://com.microsoft.CompanyPortal".to_string()
2269            }
2270            "d3590ed6-52b3-4102-aeff-aad2292ab01c" => {
2271                "ms-appx-web://Microsoft.AAD.BrokerPlugin/d3590ed6-52b3-4102-aeff-aad2292ab01c".to_string()
2272            },
2273            "0c1307d4-29d6-4389-a11c-5cbe7f65d7fa" => {
2274                "https://azureapp".to_string()
2275            },
2276            "33be1cef-03fb-444b-8fd3-08ca1b4d803f" => {
2277                "https://admin.onedrive.com/".to_string()
2278            },
2279            "ab9b8c07-8f02-4f72-87fa-80105867a763" => {
2280                "https://login.windows.net/common/oauth2/nativeclient".to_string()
2281            },
2282            "3d5cffa9-04da-4657-8cab-c7f074657cad" => {
2283                "http://localhost/m365/commerce".to_string()
2284            },
2285            "4990cffe-04e8-4e8b-808a-1175604b879f" => {
2286                "https://partner.microsoft.com/aad/authPostGateway".to_string()
2287            },
2288            "fb78d390-0c51-40cd-8e17-fdbfab77341b" |
2289            "fdd7719f-d61e-4592-b501-793734eb8a0e" |
2290            "a0c73c16-a7e3-4564-9a95-2bdf47383716" => {
2291                "https://login.microsoftonline.com/common/oauth2/nativeclient".to_string()
2292            },
2293            "3b511579-5e00-46e1-a89e-a6f0870e2f5a" => {
2294                "https://windows365.microsoft.com/signin-oidc".to_string()
2295            },
2296            "08e18876-6177-487e-b8b5-cf950c1e598c" => {
2297                "https://*-admin.sharepoint.com/_forms/spfxsinglesignon.aspx".to_string()
2298            },
2299            "dd762716-544d-4aeb-a526-687b73838a22" => {
2300                "ms-appx-web://microsoft.aad.brokerplugin/dd762716-544d-4aeb-a526-687b73838a22".to_string()
2301            },
2302            "4765445b-32c6-49b0-83e6-1d93765276ca" => {
2303                "https://www.office.com/landingv2".to_string()
2304            },
2305            _ => {
2306                "https://login.microsoftonline.com/common/oauth2/nativeclient".to_string()
2307            },
2308        }
2309    }
2310}
2311
2312#[derive(Clone)]
2313pub struct AuthInit {
2314    auth_config: AuthConfig,
2315    cred_type: CredType,
2316}
2317
2318impl AuthInit {
2319    /// Whether the user exists in the directory
2320    ///
2321    /// Legacy/best-effort API. If the GetCredentialType response indicates a
2322    /// transient lookup failure, log it and preserve the historical 'error
2323    /// indicates non-existence' behavior.
2324    #[deprecated(
2325        since = "0.8.25",
2326        note = "use AuthInit::try_exists() to distinguish nonexistent accounts from transient Entra ID lookup failures"
2327    )]
2328    pub fn exists(&self) -> bool {
2329        match self.cred_type.account_exists() {
2330            Ok(exists) => exists,
2331            Err(err) => {
2332                warn!("Unable to determine account existence from GetCredentialType: {err:?}");
2333                false
2334            }
2335        }
2336    }
2337
2338    /// Checked account-existence result for callers that need to distinguish
2339    /// nonexistent users from transient Entra ID lookup failures.
2340    pub fn try_exists(&self) -> Result<bool, MsalError> {
2341        self.cred_type.account_exists()
2342    }
2343
2344    pub fn is_personal_account(&self) -> bool {
2345        self.cred_type.is_personal_account()
2346    }
2347
2348    /// Whether passwordless authentication was negotiated
2349    pub fn passwordless(&self) -> bool {
2350        // Just list whether any passwordless type is possible to negotiate. If
2351        // we attempt to pick the preferred method, that method might not even
2352        // be enabled or possible (facepalm MS).
2353        // If it's a personal account, we skip the password prompt since we have
2354        // to perform a DAG anyway.
2355        self.cred_type.credentials.has_access_pass.unwrap_or(false)
2356            || (self.cred_type.credentials.has_remote_ngc.unwrap_or(false)
2357                && self.cred_type.credentials.remote_ngc_params.is_some())
2358            || (self.cred_type.credentials.has_fido.unwrap_or(false)
2359                && self.cred_type.credentials.fido_params.is_some())
2360            || self.cred_type.is_personal_account()
2361    }
2362}
2363
2364#[cfg(feature = "changepassword")]
2365#[derive(Deserialize)]
2366struct SsprResponse {
2367    #[serde(rename = "IsJobPending")]
2368    is_job_pending: bool,
2369    #[serde(rename = "Ctx")]
2370    ctx: String,
2371    #[serde(rename = "FlowToken")]
2372    flow_token: String,
2373    #[serde(rename = "CoupledDataCenter")]
2374    coupled_data_center: String,
2375    #[serde(rename = "CoupledScaleUnit")]
2376    coupled_scale_unit: String,
2377    #[serde(rename = "ErrorMessage")]
2378    error_message: Option<String>,
2379}
2380
2381pub struct PublicClientApplication {
2382    app: ClientApplication,
2383}
2384
2385impl PublicClientApplication {
2386    /// Create an instance of an application.
2387    ///
2388    /// # Arguments
2389    ///
2390    /// * `client_id` - Your app has a client_id after you register it on
2391    ///   AAD.
2392    ///
2393    /// * `authority` - A URL that identifies a token authority. It should
2394    ///   be of the format <https://login.microsoftonline.com/your_tenant> By
2395    ///   default, we will use <https://login.microsoftonline.com/common>.
2396    pub fn new(
2397        client_id: &str,
2398        authority: Option<&str>,
2399        #[cfg(feature = "set_timeout")] timeout: Duration,
2400        #[cfg(feature = "ipvers")] ip_version: &[IpVersion],
2401    ) -> Result<Self, MsalError> {
2402        Ok(PublicClientApplication {
2403            app: ClientApplication::new(
2404                client_id,
2405                authority,
2406                #[cfg(feature = "set_timeout")]
2407                timeout,
2408                #[cfg(feature = "ipvers")]
2409                ip_version,
2410            )?,
2411        })
2412    }
2413
2414    fn client(&self) -> &Client {
2415        &self.app.client
2416    }
2417
2418    fn client_id(&self) -> &str {
2419        &self.app.client_id
2420    }
2421
2422    /// Clears all cookies stored in the internal cookie jar
2423    ///
2424    /// This forcibly removes all session and persistent cookies
2425    /// currently held by the HTTP client. Useful for resetting
2426    /// authentication state or periodically cycling cookies to
2427    /// avoid issues with long-lived sessions.
2428    ///
2429    /// # Remarks
2430    ///
2431    /// This function does not return an error on failure, but will
2432    /// log an error message if the internal cookie store cannot be
2433    /// locked.
2434    pub fn clear_cookies(&self) {
2435        self.app.clear_cookies()
2436    }
2437
2438    fn authority(&self) -> Result<String, MsalError> {
2439        self.app.authority()
2440    }
2441
2442    /// Changes the authority url set at initialization time
2443    ///
2444    /// # Arguments
2445    ///
2446    /// * `new_authority` - The new authority url to be used when communicating
2447    ///   with Entra Id.
2448    ///
2449    /// # Returns
2450    /// * Failure: An MsalError, indicating the failure.
2451    pub fn set_authority(&self, new_authority: &str) -> Result<(), MsalError> {
2452        self.app.set_authority(new_authority)
2453    }
2454
2455    /// Gets a token for a given resource via user credentials.
2456    ///
2457    /// # Arguments
2458    ///
2459    /// * `username` - Typically a UPN in the form of an email address.
2460    ///
2461    /// * `password` - The password.
2462    ///
2463    /// * `scopes` - Scopes requested to access a protected API (a resource).
2464    ///
2465    /// # Returns
2466    /// * Success: A UserToken containing an access_token.
2467    /// * Failure: An MsalError, indicating the failure.
2468    pub async fn acquire_token_by_username_password(
2469        &self,
2470        username: &str,
2471        password: &str,
2472        scopes: Vec<&str>,
2473    ) -> Result<UserToken, MsalError> {
2474        self.app
2475            .acquire_token_by_username_password(username, password, scopes)
2476            .await
2477    }
2478
2479    /// Acquire token(s) based on a refresh token (RT) obtained from elsewhere.
2480    ///
2481    /// # Arguments
2482    ///
2483    /// * `refresh_token` - The old refresh token, as a string.
2484    ///
2485    /// * `scopes` - The scopes associated with this old RT.
2486    ///
2487    /// # Returns
2488    ///
2489    /// * Success: A UserToken, which means migration was successful.
2490    /// * Failure: An MsalError, indicating the failure.
2491    pub async fn acquire_token_by_refresh_token(
2492        &self,
2493        refresh_token: &str,
2494        scopes: Vec<&str>,
2495    ) -> Result<UserToken, MsalError> {
2496        self.app
2497            .acquire_token_by_refresh_token(refresh_token, scopes)
2498            .await
2499    }
2500
2501    /// Initiate an OAuth 2.0 authorization code flow with PKCE.
2502    ///
2503    /// This returns the authentication URL and flow state. The caller is
2504    /// responsible for opening or presenting the URL and capturing the final
2505    /// redirect URI.
2506    ///
2507    /// # Arguments
2508    ///
2509    /// * `scopes` - Scopes requested to access a protected API.
2510    /// * `redirect_uri` - A redirect URI registered for the application.
2511    ///
2512    /// # Returns
2513    ///
2514    /// * Success: An AuthorizationCodePkceFlow containing the authentication URL.
2515    /// * Failure: An MsalError, indicating the failure.
2516    pub fn initiate_authorization_code_pkce_flow(
2517        &self,
2518        scopes: Vec<&str>,
2519        redirect_uri: &str,
2520    ) -> Result<AuthorizationCodePkceFlow, MsalError> {
2521        if redirect_uri.trim().is_empty() {
2522            return Err(MsalError::ConfigError(
2523                "redirect_uri must not be empty".to_string(),
2524            ));
2525        }
2526
2527        let mut all_scopes = vec!["openid", "profile", "offline_access"];
2528        all_scopes.extend(scopes);
2529        let scope = all_scopes.join(" ");
2530        let code_verifier = generate_base64url_random(32)?;
2531        let code_challenge = pkce_code_challenge(&code_verifier);
2532        let state = generate_base64url_random(32)?;
2533
2534        let url = Url::parse_with_params(
2535            &format!("{}/oauth2/v2.0/authorize", self.authority()?),
2536            [
2537                ("client_id", self.client_id()),
2538                ("response_type", "code"),
2539                ("redirect_uri", redirect_uri),
2540                ("response_mode", "query"),
2541                ("scope", &scope),
2542                ("state", &state),
2543                ("code_challenge", &code_challenge),
2544                ("code_challenge_method", "S256"),
2545            ],
2546        )
2547        .map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
2548
2549        Ok(AuthorizationCodePkceFlow {
2550            auth_url: url.to_string(),
2551            redirect_uri: redirect_uri.to_string(),
2552            state,
2553            scope,
2554            code_verifier,
2555        })
2556    }
2557
2558    /// Exchange a captured authorization-code redirect for a token.
2559    ///
2560    /// The supplied `redirect_url` should be the full URI received by the
2561    /// caller after the user-agent redirects back to the registered redirect
2562    /// URI. This function parses the authorization code, validates `state`,
2563    /// and submits the PKCE verifier.
2564    pub async fn acquire_token_by_authorization_code_pkce_flow(
2565        &self,
2566        flow: &AuthorizationCodePkceFlow,
2567        redirect_url: &str,
2568    ) -> Result<UserToken, MsalError> {
2569        let url =
2570            Url::parse(redirect_url).map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
2571        let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
2572
2573        if let Some(error) = params.get("error") {
2574            return Err(MsalError::AcquireTokenFailed(ErrorResponse {
2575                error: error.clone(),
2576                error_description: params.get("error_description").cloned().unwrap_or_default(),
2577                suberror: params.get("suberror").cloned(),
2578                error_codes: Vec::new(),
2579            }));
2580        }
2581
2582        let returned_state = params
2583            .get("state")
2584            .ok_or_else(|| MsalError::InvalidParse("state missing from redirect".to_string()))?;
2585        if returned_state != &flow.state {
2586            return Err(MsalError::InvalidParse(
2587                "state returned by redirect does not match the PKCE flow".to_string(),
2588            ));
2589        }
2590
2591        let code = params
2592            .get("code")
2593            .ok_or_else(|| MsalError::InvalidParse("code missing from redirect".to_string()))?;
2594
2595        let form = self.authorization_code_pkce_token_form(flow, code);
2596
2597        let resp = self
2598            .client()
2599            .post(format!("{}/oauth2/v2.0/token", self.authority()?))
2600            .header(header::ACCEPT, "application/json")
2601            .form(&form)
2602            .send()
2603            .await
2604            .map_err(|e| MsalError::request_failed(&e))?;
2605        if resp.status().is_success() {
2606            let token: UserToken = resp
2607                .json()
2608                .await
2609                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2610            Ok(token)
2611        } else {
2612            let json_resp: ErrorResponse = resp
2613                .json()
2614                .await
2615                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2616            Err(MsalError::AcquireTokenFailed(json_resp))
2617        }
2618    }
2619
2620    fn authorization_code_pkce_token_form<'a>(
2621        &'a self,
2622        flow: &'a AuthorizationCodePkceFlow,
2623        code: &'a str,
2624    ) -> [(&'static str, &'a str); 7] {
2625        [
2626            ("client_id", self.client_id()),
2627            ("grant_type", "authorization_code"),
2628            ("code", code),
2629            ("redirect_uri", flow.redirect_uri.as_str()),
2630            ("scope", flow.scope.as_str()),
2631            ("code_verifier", flow.code_verifier.as_str()),
2632            ("client_info", "1"),
2633        ]
2634    }
2635
2636    /// Initiate a Device Flow instance, which will be used in
2637    /// acquire_token_by_device_flow.
2638    ///
2639    /// # Arguments
2640    ///
2641    /// * `scopes` - Scopes requested to access a protected API (a resource).
2642    ///
2643    /// # Returns
2644    ///
2645    /// * Success: A DeviceAuthorizationResponse containing a user_code key,
2646    ///   among others
2647    /// * Failure: An MsalError, indicating the failure.
2648    pub async fn initiate_device_flow(
2649        &self,
2650        scopes: Vec<&str>,
2651    ) -> Result<DeviceAuthorizationResponse, MsalError> {
2652        let mut all_scopes = vec!["openid", "profile", "offline_access"];
2653        all_scopes.extend(scopes);
2654        let scopes_str = all_scopes.join(" ");
2655
2656        let params = [("client_id", self.client_id()), ("scope", &scopes_str)];
2657        let payload = params
2658            .iter()
2659            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
2660            .collect::<Vec<String>>()
2661            .join("&");
2662
2663        let resp = self
2664            .client()
2665            .post(format!("{}/oauth2/v2.0/devicecode", self.authority()?))
2666            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
2667            .header(header::ACCEPT, "application/json")
2668            .body(payload)
2669            .send()
2670            .await
2671            .map_err(|e| MsalError::request_failed(&e))?;
2672        if resp.status().is_success() {
2673            let json_resp: DeviceAuthorizationResponse = resp
2674                .json()
2675                .await
2676                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2677            Ok(json_resp)
2678        } else {
2679            let json_resp: ErrorResponse = resp
2680                .json()
2681                .await
2682                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2683            Err(MsalError::AcquireTokenFailed(json_resp))
2684        }
2685    }
2686
2687    async fn initiate_personal_device_flow(
2688        &self,
2689        scopes: Vec<&str>,
2690    ) -> Result<DeviceAuthorizationResponse, MsalError> {
2691        let mut all_scopes = vec!["openid", "profile", "offline_access"];
2692        all_scopes.extend(scopes);
2693        let scopes_str = all_scopes.join(" ");
2694
2695        let params = [("client_id", self.client_id()), ("scope", &scopes_str)];
2696        let payload = params
2697            .iter()
2698            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
2699            .collect::<Vec<String>>()
2700            .join("&");
2701
2702        let resp = self
2703            .client()
2704            .post("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
2705            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
2706            .header(header::ACCEPT, "application/json")
2707            .body(payload)
2708            .send()
2709            .await
2710            .map_err(|e| MsalError::request_failed(&e))?;
2711        if resp.status().is_success() {
2712            let json_resp: DeviceAuthorizationResponse = resp
2713                .json()
2714                .await
2715                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2716            Ok(json_resp)
2717        } else {
2718            let json_resp: ErrorResponse = resp
2719                .json()
2720                .await
2721                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2722            Err(MsalError::AcquireTokenFailed(json_resp))
2723        }
2724    }
2725
2726    /// Obtain token by a device flow object, with customizable polling effect.
2727    ///
2728    /// # Arguments
2729    ///
2730    /// * `flow` - A DeviceAuthorizationResponse previously generated by
2731    ///   initiate_device_flow.
2732    ///
2733    /// # Returns
2734    ///
2735    /// * Success: A UserToken containing an access_token.
2736    /// * Failure: An MsalError, indicating the failure.
2737    pub async fn acquire_token_by_device_flow(
2738        &self,
2739        flow: DeviceAuthorizationResponse,
2740    ) -> Result<UserToken, MsalError> {
2741        let params = [
2742            ("client_id", self.client_id()),
2743            ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
2744            ("device_code", &flow.device_code),
2745        ];
2746        let payload = params
2747            .iter()
2748            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
2749            .collect::<Vec<String>>()
2750            .join("&");
2751
2752        let resp = self
2753            .client()
2754            .post(format!("{}/oauth2/v2.0/token", self.authority()?))
2755            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
2756            .header(header::ACCEPT, "application/json")
2757            .body(payload)
2758            .send()
2759            .await
2760            .map_err(|e| MsalError::request_failed(&e))?;
2761        if resp.status().is_success() {
2762            let token: UserToken = resp
2763                .json()
2764                .await
2765                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2766
2767            Ok(token)
2768        } else {
2769            let json_resp: ErrorResponse = resp
2770                .json()
2771                .await
2772                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2773            Err(MsalError::AcquireTokenFailed(json_resp))
2774        }
2775    }
2776
2777    async fn acquire_token_by_personal_device_flow(
2778        &self,
2779        flow: DeviceAuthorizationResponse,
2780    ) -> Result<UserToken, MsalError> {
2781        let params = [
2782            ("client_id", self.client_id()),
2783            ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
2784            ("device_code", &flow.device_code),
2785        ];
2786        let payload = params
2787            .iter()
2788            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
2789            .collect::<Vec<String>>()
2790            .join("&");
2791
2792        let resp = self
2793            .client()
2794            .post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
2795            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
2796            .header(header::ACCEPT, "application/json")
2797            .body(payload)
2798            .send()
2799            .await
2800            .map_err(|e| MsalError::request_failed(&e))?;
2801        if resp.status().is_success() {
2802            let token: UserToken = resp
2803                .json()
2804                .await
2805                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2806
2807            Ok(token)
2808        } else {
2809            let json_resp: ErrorResponse = resp
2810                .json()
2811                .await
2812                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
2813            Err(MsalError::AcquireTokenFailed(json_resp))
2814        }
2815    }
2816
2817    #[allow(unused_variables)]
2818    fn parse_auth_config(
2819        &self,
2820        text: &str,
2821        initial: bool,
2822        password_change: bool,
2823    ) -> Result<AuthConfig, MsalError> {
2824        let document = Html::parse_document(text);
2825        for script in
2826            document.select(&Selector::parse("script").map_err(|e| {
2827                MsalError::GeneralFailure(format!("Failed parsing auth config: {}", e))
2828            })?)
2829        {
2830            let text = script.inner_html();
2831            if let Some(config_index) = text.find(r"$Config=") {
2832                let sconfig = &text[config_index + 8..];
2833                if let Some(end_index) = sconfig.rfind(r"//]]&gt;") {
2834                    let config = &sconfig[..end_index - 2];
2835                    let auth_config: AuthConfig = json_from_str(config).map_err(|e| {
2836                        MsalError::InvalidJson(format!("Failed parsing auth config: {}", e))
2837                    })?;
2838                    // MS always throws errors when we're just setting up the
2839                    // *initial* authorize. MS then ignores them, so shall we.
2840                    if !initial {
2841                        let error_code = match auth_config.error_code {
2842                            Some(ref error_code) => {
2843                                Some(error_code.parse::<u32>().map_err(|e| {
2844                                    MsalError::InvalidParse(format!(
2845                                        "error_code {}: {:?}",
2846                                        error_code, e
2847                                    ))
2848                                })?)
2849                            }
2850                            None => auth_config.error_code2,
2851                        };
2852                        if let Some(error_code) = error_code {
2853                            let description =
2854                                auth_config.err_txt.or(auth_config.service_exception_msg);
2855
2856                            // Check to see if we can get the failure message
2857                            if let Some(err_txt) = description.clone() {
2858                                if !err_txt.is_empty() {
2859                                    error!("{}", err_txt);
2860                                }
2861                            }
2862
2863                            // AADSTS50203: User has not registered the authenticator app
2864                            if error_code == 50203 {
2865                                if let Some(url_skip_mfa_registration) =
2866                                    auth_config.url_skip_mfa_registration
2867                                {
2868                                    return Err(MsalError::SkipMfaRegistration(
2869                                        url_skip_mfa_registration,
2870                                        auth_config.sft,
2871                                        auth_config.canary,
2872                                    ));
2873                                }
2874                            }
2875                            return Err(MsalError::AADSTSError(AADSTSError::new(
2876                                error_code,
2877                                description,
2878                            )));
2879                        }
2880                    }
2881                    #[cfg(feature = "changepassword")]
2882                    if !password_change {
2883                        if let Some(ref pgid) = auth_config.pgid {
2884                            if pgid == "ConvergedChangePassword" {
2885                                return Err(MsalError::ChangePassword);
2886                            }
2887                        }
2888                    }
2889                    if let Some(ref pgid) = auth_config.pgid {
2890                        if pgid == "ConvergedConsent" {
2891                            return Err(MsalError::ConsentRequested(
2892                                "The client application requires additional consent to proceed."
2893                                    .to_string(),
2894                            ));
2895                        }
2896                    }
2897                    return Ok(auth_config);
2898                }
2899            }
2900        }
2901        Err(MsalError::GeneralFailure(
2902            "Auth config was not found".to_string(),
2903        ))
2904    }
2905
2906    /// Change the password for an Entra Id user.
2907    ///
2908    /// This function allows changing the password for an Entra Id user. It
2909    /// requires the current username and password to validate the user's
2910    /// identity before updating the password.
2911    ///
2912    /// # Arguments
2913    ///
2914    /// * `username` - The username associated with the account for which the
2915    ///   password is being changed.
2916    /// * `password` - The current password for the account.
2917    /// * `new_password` - The new password that will replace the current
2918    ///   password.
2919    ///
2920    /// # Returns
2921    ///
2922    /// * Success: An empty Ok result indicating the password has been changed
2923    ///   successfully.
2924    /// * Failure: An MsalError, indicating problems such as authentication
2925    ///   failures or password complexity requirements not met.
2926    #[cfg(feature = "changepassword")]
2927    pub async fn handle_password_change(
2928        &self,
2929        username: &str,
2930        password: &str,
2931        new_password: &str,
2932    ) -> Result<(), MsalError> {
2933        let request_id = Uuid::new_v4().to_string();
2934        let auth_config = self
2935            .request_auth_config_internal(vec![], &request_id, None, false)
2936            .await?;
2937        let ctx = auth_config
2938            .sctx
2939            .clone()
2940            .ok_or(MsalError::GeneralFailure("ctx is missing".to_string()))?;
2941        let flow_token = auth_config
2942            .sft
2943            .clone()
2944            .ok_or(MsalError::GeneralFailure("sft is missing".to_string()))?;
2945
2946        let params = vec![
2947            ("login", username),
2948            ("passwd", password),
2949            ("ctx", &ctx),
2950            ("flowToken", &flow_token),
2951            ("canary", &auth_config.canary),
2952            ("client_id", self.client_id()),
2953            ("client-request-id", &request_id),
2954        ];
2955        let auth_config = self
2956            .handle_auth_config_req_internal(&params, &auth_config, &[], true)
2957            .await?;
2958
2959        let payload = json!({
2960            "Ctx": &auth_config
2961                    .sctx
2962                    .ok_or(MsalError::GeneralFailure("ctx is missing".to_string()))?,
2963            "FlowToken": &auth_config
2964                    .sft
2965                    .ok_or(MsalError::GeneralFailure("sft is missing".to_string()))?,
2966            "OldPassword": password,
2967            "NewPassword": new_password,
2968        });
2969
2970        let url_async_sspr_begin = match &auth_config.url_async_sspr_begin {
2971            Some(url_async_sspr_begin) => url_async_sspr_begin.clone(),
2972            None => {
2973                return Err(MsalError::GeneralFailure(
2974                    "url_async_sspr_begin missing from auth config".to_string(),
2975                ))
2976            }
2977        };
2978        let url = match url_async_sspr_begin.starts_with('/') {
2979            true => {
2980                let authority = self.authority()?.to_string();
2981                let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
2982                    "Failed to splice auth config url".to_string(),
2983                ))?;
2984                format!("{}/{}", &authority[..index], url_async_sspr_begin)
2985            }
2986            false => url_async_sspr_begin.clone(),
2987        };
2988
2989        let resp = self
2990            .client()
2991            .post(&url)
2992            .json(&payload)
2993            .send()
2994            .await
2995            .map_err(|e| MsalError::request_failed(&e))?;
2996        if resp.status().is_success() {
2997            let mut sspr_response: SsprResponse = resp
2998                .json()
2999                .await
3000                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
3001
3002            let url_async_sspr_poll = match &auth_config.url_async_sspr_poll {
3003                Some(url_async_sspr_poll) => url_async_sspr_poll.clone(),
3004                None => {
3005                    return Err(MsalError::GeneralFailure(
3006                        "url_async_sspr_poll missing from auth config".to_string(),
3007                    ))
3008                }
3009            };
3010            let url = match url_async_sspr_poll.starts_with('/') {
3011                true => {
3012                    let authority = self.authority()?.to_string();
3013                    let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
3014                        "Failed to splice auth config url".to_string(),
3015                    ))?;
3016                    format!("{}/{}", &authority[..index], url_async_sspr_poll)
3017                }
3018                false => url_async_sspr_poll.clone(),
3019            };
3020
3021            while sspr_response.is_job_pending {
3022                sleep(Duration::from_secs(1));
3023                let poll_body = json!({
3024                    "Ctx": sspr_response.ctx,
3025                    "FlowToken": sspr_response.flow_token,
3026                    "CoupledDataCenter": sspr_response.coupled_data_center,
3027                    "CoupledScaleUnit": sspr_response.coupled_scale_unit,
3028                });
3029                sspr_response = self
3030                    .client()
3031                    .post(&url)
3032                    .json(&poll_body)
3033                    .send()
3034                    .await
3035                    .map_err(|e| MsalError::request_failed(&e))?
3036                    .json()
3037                    .await
3038                    .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
3039
3040                if let Some(e) = sspr_response.error_message {
3041                    return Err(MsalError::GeneralFailure(format!(
3042                        "Failed changing password: {}",
3043                        e
3044                    )));
3045                }
3046            }
3047
3048            let url_post = match &auth_config.url_post {
3049                Some(url_post) => url_post.clone(),
3050                None => {
3051                    return Err(MsalError::GeneralFailure(
3052                        "urlPost missing from auth config".to_string(),
3053                    ))
3054                }
3055            };
3056            let url = match url_post.starts_with('/') {
3057                true => {
3058                    let authority = self.authority()?.to_string();
3059                    let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
3060                        "Failed to splice auth config url".to_string(),
3061                    ))?;
3062                    format!("{}/{}", &authority[..index], url_post)
3063                }
3064                false => url_post.clone(),
3065            };
3066
3067            let final_body = json!({
3068                "Ctx": sspr_response.ctx,
3069                "FlowToken": sspr_response.flow_token,
3070                "currentpasswd": password,
3071                "confirmnewpasswd": new_password,
3072                "canary": auth_config.canary,
3073            });
3074            let resp = self
3075                .client()
3076                .post(&url)
3077                .json(&final_body)
3078                .send()
3079                .await
3080                .map_err(|e| MsalError::request_failed(&e))?;
3081            if resp.status().is_success() {
3082                Ok(())
3083            } else {
3084                let text = resp.text().await.map_err(|e| {
3085                    MsalError::GeneralFailure(format!("Failed changing password: {}", e))
3086                })?;
3087                Err(MsalError::GeneralFailure(format!(
3088                    "Failed changing password: {}",
3089                    text
3090                )))
3091            }
3092        } else {
3093            let text = resp.text().await.map_err(|e| {
3094                MsalError::GeneralFailure(format!("Failed changing password: {}", e))
3095            })?;
3096            Err(MsalError::GeneralFailure(format!(
3097                "Failed changing password: {}",
3098                text
3099            )))
3100        }
3101    }
3102
3103    async fn handle_auth_config_fido_get(
3104        &self,
3105        username: &str,
3106        auth_config: &AuthConfig,
3107        request_id: &str,
3108    ) -> Result<AuthConfig, MsalError> {
3109        let url_post = match &auth_config.url_post {
3110            Some(url_post) => url_post.clone(),
3111            None => {
3112                return Err(MsalError::GeneralFailure(
3113                    "urlPost missing from auth config".to_string(),
3114                ))
3115            }
3116        };
3117
3118        let url_resume = match &auth_config.url_resume {
3119            Some(url_resume) => url_resume.clone(),
3120            None => {
3121                return Err(MsalError::GeneralFailure(
3122                    "urlResume missing from auth config".to_string(),
3123                ))
3124            }
3125        };
3126
3127        let credentials_json = match auth_config.fido_allow_list.as_deref() {
3128            Some([credentials_json, ..]) => credentials_json,
3129            _ => {
3130                return Err(MsalError::GeneralFailure(
3131                    "arrFidoAllowList missing from auth config".to_string(),
3132                ))
3133            }
3134        };
3135
3136        let sctx = match &auth_config.sctx {
3137            Some(sctx) => sctx.clone(),
3138            None => {
3139                return Err(MsalError::GeneralFailure(
3140                    "sCtx missing from auth config".to_string(),
3141                ));
3142            }
3143        };
3144
3145        let sft = match &auth_config.sft {
3146            Some(sft) => sft.clone(),
3147            None => {
3148                return Err(MsalError::GeneralFailure(
3149                    "sFt missing from auth config".to_string(),
3150                ));
3151            }
3152        };
3153
3154        let allowed_identities = match &auth_config.allowed_identities {
3155            Some(allowed_identities) => format!("{}", allowed_identities),
3156            None => {
3157                return Err(MsalError::GeneralFailure(
3158                    "iAllowedIdentities missing from auth config".to_string(),
3159                ));
3160            }
3161        };
3162
3163        let params = [
3164            ("flow", "mfa"),
3165            ("allowedIdentities", &allowed_identities),
3166            ("canary", &sft),
3167            ("serverChallenge", &sft),
3168            ("postBackUrl", &url_post),
3169            ("postBackUrlAad", &url_post),
3170            ("cancelUrl", &url_resume),
3171            ("resumeUrl", &url_resume),
3172            ("correlationId", request_id),
3173            ("credentialsJson", credentials_json),
3174            ("ctx", &sctx),
3175            ("username", username),
3176            ("loginCanary", &auth_config.canary),
3177        ];
3178        let payload = params
3179            .iter()
3180            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
3181            .collect::<Vec<String>>()
3182            .join("&");
3183
3184        let url_fido_login = match &auth_config.url_fido_login {
3185            Some(url_fido_login) => url_fido_login.clone(),
3186            None => {
3187                return Err(MsalError::GeneralFailure(
3188                    "urlFidoLogin missing from auth config".to_string(),
3189                ))
3190            }
3191        };
3192
3193        let mut resp = self
3194            .client()
3195            .post(url_fido_login)
3196            .header(header::USER_AGENT, FIDO_USER_AGENT)
3197            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
3198            .body(payload)
3199            .send()
3200            .await
3201            .map_err(|e| MsalError::request_failed(&e))?;
3202        let text;
3203        (text, resp) = self.await_working(resp).await?;
3204        if resp.status().is_success() {
3205            self.parse_auth_config(&text, false, false)
3206        } else {
3207            Err(MsalError::GeneralFailure(resp.text().await.map_err(
3208                |e| MsalError::GeneralFailure(format!("Request to FIDO login URL failed: {}", e)),
3209            )?))
3210        }
3211    }
3212
3213    async fn handle_auth_config_req_internal(
3214        &self,
3215        req_params: &[(&str, &str)],
3216        auth_config: &AuthConfig,
3217        options: &[AuthOption],
3218        password_change: bool,
3219    ) -> Result<AuthConfig, MsalError> {
3220        let payload = req_params
3221            .iter()
3222            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
3223            .collect::<Vec<String>>()
3224            .join("&");
3225
3226        let url_post = match &auth_config.url_post {
3227            Some(url_post) => url_post.clone(),
3228            None => {
3229                return Err(MsalError::GeneralFailure(
3230                    "urlPost missing from auth config".to_string(),
3231                ))
3232            }
3233        };
3234        let url = match url_post.starts_with('/') {
3235            true => {
3236                let authority = self.authority()?.to_string();
3237                let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
3238                    "Failed to splice auth config url".to_string(),
3239                ))?;
3240                format!("{}/{}", &authority[..index], url_post)
3241            }
3242            false => url_post.clone(),
3243        };
3244
3245        let user_agent = if options.contains(&AuthOption::Fido) {
3246            FIDO_USER_AGENT
3247        } else {
3248            env!("CARGO_PKG_NAME")
3249        };
3250        let resp = self
3251            .client()
3252            .post(url)
3253            .header(header::USER_AGENT, user_agent)
3254            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
3255            .body(payload)
3256            .send()
3257            .await
3258            .map_err(|e| MsalError::request_failed(&e))?;
3259        self.handle_auth_config_response_internal(resp, password_change)
3260            .await
3261    }
3262
3263    async fn handle_auth_config_response_internal(
3264        &self,
3265        mut resp: Response,
3266        password_change: bool,
3267    ) -> Result<AuthConfig, MsalError> {
3268        let text;
3269        (text, resp) = self.await_working(resp).await?;
3270        if resp.status().is_success() {
3271            self.parse_auth_config(&text, false, password_change)
3272        } else if resp.status().is_redirection() {
3273            let redirect = resp
3274                .headers()
3275                .get(header::LOCATION)
3276                .ok_or_else(|| MsalError::InvalidParse("Redirect location is missing".to_string()))?
3277                .to_str()
3278                .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
3279            let url =
3280                Url::parse(redirect).map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
3281            let params = url.query_pairs().collect::<Vec<_>>();
3282
3283            // Check for errors returned in the redirect URL parameters.
3284            if let Some((_, error_description)) =
3285                params.iter().find(|(k, _)| k == "error_description")
3286            {
3287                let error_code_regex = Regex::new(r"AADSTS(\d+):");
3288
3289                if let Ok(regex) = error_code_regex {
3290                    if let Some(captures) = regex.captures(error_description) {
3291                        if let Some(code_match) = captures.get(1) {
3292                            if let Ok(code) = code_match.as_str().parse::<u32>() {
3293                                return Err(MsalError::AADSTSError(AADSTSError::new(
3294                                    code,
3295                                    Some(error_description.to_string()),
3296                                )));
3297                            }
3298                        }
3299                    }
3300                }
3301
3302                return Err(MsalError::GeneralFailure(format!(
3303                    "Unknown error in request to {}: {}",
3304                    url, error_description
3305                )));
3306            }
3307
3308            let (_, code) =
3309                params
3310                    .iter()
3311                    .find(|(k, _)| k == "code")
3312                    .ok_or(MsalError::InvalidParse(
3313                        "Authorization code missing from redirect".to_string(),
3314                    ))?;
3315            debug!("Received auth code directly from login redirect");
3316            Err(MsalError::AuthCodeReceived(code.to_string()))
3317        } else {
3318            Err(MsalError::GeneralFailure(resp.text().await.map_err(
3319                |e| {
3320                    MsalError::GeneralFailure(format!(
3321                        "Request for handle_auth_config_req_internal() failed: {}",
3322                        e
3323                    ))
3324                },
3325            )?))
3326        }
3327    }
3328
3329    async fn read_adfs_response_body(&self, mut resp: Response) -> Result<String, MsalError> {
3330        let mut body = Vec::new();
3331        while let Some(chunk) = resp
3332            .chunk()
3333            .await
3334            .map_err(|e| MsalError::RequestFailed(format!("Failed reading AD FS response: {e}")))?
3335        {
3336            if body.len().saturating_add(chunk.len()) > MAX_ADFS_RESPONSE_BYTES {
3337                return Err(MsalError::GeneralFailure(
3338                    "AD FS response exceeded the 2 MiB limit".to_string(),
3339                ));
3340            }
3341            body.extend_from_slice(&chunk);
3342        }
3343        String::from_utf8(body)
3344            .map_err(|e| MsalError::InvalidParse(format!("AD FS response was not UTF-8: {e}")))
3345    }
3346
3347    async fn request_adfs_form_internal(
3348        &self,
3349        federation_url: Url,
3350        username: &str,
3351        password: &str,
3352    ) -> Result<String, MsalError> {
3353        let origin = federation_url.clone();
3354        let mut url = federation_url;
3355        let mut method = AdfsRequestMethod::PostCredentials;
3356
3357        for redirect_count in 0..=MAX_ADFS_REDIRECTS {
3358            let response = if method == AdfsRequestMethod::PostCredentials {
3359                self.client()
3360                    .post(url.clone())
3361                    .header(header::USER_AGENT, FIDO_USER_AGENT)
3362                    .form(&[
3363                        ("UserName", username),
3364                        ("Password", password),
3365                        ("Kmsi", ""),
3366                        ("AuthMethod", "FormsAuthentication"),
3367                    ])
3368                    .send()
3369                    .await
3370            } else {
3371                self.client()
3372                    .get(url.clone())
3373                    .header(header::USER_AGENT, FIDO_USER_AGENT)
3374                    .send()
3375                    .await
3376            }
3377            .map_err(|e| MsalError::request_failed(&e))?;
3378
3379            if response.status().is_redirection() {
3380                if redirect_count == MAX_ADFS_REDIRECTS {
3381                    return Err(MsalError::GeneralFailure(
3382                        "AD FS redirect limit exceeded".to_string(),
3383                    ));
3384                }
3385                let location = response
3386                    .headers()
3387                    .get(header::LOCATION)
3388                    .ok_or_else(|| {
3389                        MsalError::InvalidParse(
3390                            "AD FS redirect did not include a location".to_string(),
3391                        )
3392                    })?
3393                    .to_str()
3394                    .map_err(|e| {
3395                        MsalError::InvalidParse(format!("Invalid AD FS redirect location: {e}"))
3396                    })?;
3397                (url, method) = resolve_adfs_redirect(
3398                    &origin,
3399                    &url,
3400                    location,
3401                    response.status().as_u16(),
3402                    method,
3403                )?;
3404                continue;
3405            }
3406
3407            if !response.status().is_success() {
3408                return Err(MsalError::GeneralFailure(format!(
3409                    "AD FS authentication failed with HTTP status {}",
3410                    response.status()
3411                )));
3412            }
3413            let content_type = response
3414                .headers()
3415                .get(header::CONTENT_TYPE)
3416                .and_then(|value| value.to_str().ok())
3417                .map(|value| value.to_ascii_lowercase())
3418                .unwrap_or_default();
3419            if !content_type.starts_with("text/html")
3420                && !content_type.starts_with("application/xhtml+xml")
3421            {
3422                return Err(MsalError::GeneralFailure(
3423                    "AD FS returned an unsupported content type".to_string(),
3424                ));
3425            }
3426            return self.read_adfs_response_body(response).await;
3427        }
3428
3429        Err(MsalError::GeneralFailure(
3430            "AD FS redirect limit exceeded".to_string(),
3431        ))
3432    }
3433
3434    async fn submit_ws_fed_form_internal(&self, form: &WsFedForm) -> Result<AuthConfig, MsalError> {
3435        let login_srf = entra_login_srf_url(&self.authority()?)?;
3436        let resp = self
3437            .client()
3438            .post(login_srf)
3439            .header(header::USER_AGENT, FIDO_USER_AGENT)
3440            .form(&[
3441                ("wa", form.wa.as_str()),
3442                ("wresult", form.wresult.as_str()),
3443                ("wctx", form.wctx.as_str()),
3444            ])
3445            .send()
3446            .await
3447            .map_err(|e| MsalError::request_failed(&e))?;
3448        self.handle_auth_config_response_internal(resp, false).await
3449    }
3450
3451    /// Check if a user exists in Azure Entra ID
3452    ///
3453    /// # Arguments
3454    ///
3455    /// * `username` - Typically a UPN in the form of an email address.
3456    ///
3457    /// * `resource` - A resource for obtaining an access token.
3458    ///   Default is the MS Graph API (00000002-0000-0000-c000-000000000000).
3459    ///
3460    /// * `options` - Authentication options to enable, such as Fido and
3461    ///   Passwordless auth.
3462    ///
3463    /// # Returns
3464    /// * Success: An AuthInit object. Call `exists` to get the result.
3465    /// * Failure: An MsalError, indicating the failure.
3466    pub async fn check_user_exists(
3467        &self,
3468        username: &str,
3469        resource: Option<&str>,
3470        options: &[AuthOption],
3471    ) -> Result<AuthInit, MsalError> {
3472        let request_id = Uuid::new_v4().to_string();
3473        // Only demand ngcmfa if ForceMFA option is set. This allows callers to
3474        // decide based on context (local terminal vs remote connection) whether
3475        // to trigger MFA notifications during the user existence check.
3476        #[cfg(feature = "optional_mfa")]
3477        let force_mfa = options.contains(&AuthOption::ForceMFA);
3478        #[cfg(not(feature = "optional_mfa"))]
3479        let force_mfa = true;
3480        let auth_config = self
3481            .request_auth_config_internal(vec![], &request_id, resource, force_mfa)
3482            .await?;
3483        let cred_type = self
3484            .get_cred_type(username, &auth_config, &request_id, options)
3485            .await?;
3486        Ok(AuthInit {
3487            auth_config,
3488            cred_type,
3489        })
3490    }
3491
3492    /// Initiate an MFA flow via user credentials.
3493    ///
3494    /// For accounts whose GetCredentialType response identifies a standard
3495    /// HTTPS AD FS endpoint, this automatically performs AD FS forms
3496    /// authentication and submits the resulting WS-Federation assertion to
3497    /// Entra before continuing with the normal MFA or authorization-code flow.
3498    ///
3499    /// # Arguments
3500    ///
3501    /// * `username` - Typically a UPN in the form of an email address.
3502    ///
3503    /// * `password` - The password.
3504    ///
3505    /// * `scopes` - Scopes requested to access a protected API (a resource).
3506    ///
3507    /// * `resource` - A resource for obtaining an access token.
3508    ///   Default is the MS Graph API (00000002-0000-0000-c000-000000000000).
3509    ///
3510    /// * `options` - Authentication options to enable, such as Fido.
3511    ///
3512    /// * `auth_init` - The result of `check_user_exists`, required if called
3513    ///   prior to `initiate_acquire_token_by_mfa_flow`.
3514    ///
3515    /// # Returns
3516    /// * Success: A MFAAuthContinue containing the information needed to continue the
3517    ///   authentication flow.
3518    /// * Failure: An MsalError, indicating the failure.
3519    pub async fn initiate_acquire_token_by_mfa_flow(
3520        &self,
3521        username: &str,
3522        password: Option<&str>,
3523        scopes: Vec<&str>,
3524        resource: Option<&str>,
3525        options: &[AuthOption],
3526        auth_init: Option<AuthInit>,
3527        #[cfg(feature = "mfa_method_selection")] mfa_method: Option<&str>,
3528    ) -> Result<MFAAuthContinue, MsalError> {
3529        #[cfg(not(feature = "mfa_method_selection"))]
3530        let mfa_method: Option<&str> = None;
3531
3532        #[cfg(feature = "optional_mfa")]
3533        let force_mfa = options.contains(&AuthOption::ForceMFA);
3534        #[cfg(not(feature = "optional_mfa"))]
3535        let force_mfa = true;
3536
3537        macro_rules! dag_fallback {
3538            () => {
3539                if !options.contains(&AuthOption::NoDAGFallback) {
3540                    let mut dag_scopes: Vec<String> =
3541                        scopes.into_iter().map(|s| s.to_string()).collect();
3542                    // Enforce MFA via the azure portal, but only when the caller
3543                    // has not requested scopes for a specific non-Portal resource.
3544                    // The token endpoint rejects .default combined with explicit
3545                    // permission scopes from a different audience (AADSTS70011).
3546                    let has_resource_scope =
3547                        dag_scopes.iter().any(|s| s.contains("://"));
3548                    if force_mfa && !has_resource_scope {
3549                        dag_scopes.push(format!("{}/.default", AZURE_PORTAL_APP_ID));
3550                    }
3551                    info!("MFA auth failed, falling back to Device Authorization Grant.");
3552                    let flow = self
3553                        .initiate_device_flow(dag_scopes.iter().map(|i| i.as_str()).collect())
3554                        .await?;
3555                    let mut flow: MFAAuthContinue = flow.into();
3556                    flow.resource = resource.map(|s| s.to_string());
3557                    return Ok(flow);
3558                } else {
3559                    return Err(MsalError::MFADAGFallbackDisabled);
3560                }
3561            };
3562            ($err:expr) => {
3563                if !options.contains(&AuthOption::NoDAGFallback) {
3564                    // If we got a change password request, return it.
3565                    #[cfg(feature = "changepassword")]
3566                    if let MsalError::ChangePassword = $err {
3567                        return Err($err);
3568                    }
3569
3570                    // If we got an AADSTSError, then we don't want to perform a
3571                    // fallback, since the authentication legitimately failed.
3572                    if let MsalError::AADSTSError(ref e) = $err {
3573                        // For remote sessions, if the user hasn't enrolled in MFA,
3574                        // deny authentication. DAG cannot reliably enforce MFA for
3575                        // unenrolled users, so we require MFA enrollment before
3576                        // permitting remote access.
3577                        // AADSTS50072: UserStrongAuthEnrollmentRequiredInterrupt
3578                        // AADSTS50203: User has not registered the authenticator app
3579                        #[cfg(feature = "optional_mfa")]
3580                        if options.contains(&AuthOption::RemoteSession)
3581                            && [50072, 50203].contains(&e.code)
3582                        {
3583                            error!(
3584                                "Remote session with unenrolled MFA user denied. \
3585                                 User must enroll in MFA before remote authentication is permitted."
3586                            );
3587                            return Err($err);
3588                        }
3589                        // There are a couple of exceptions to the rule. If
3590                        // interaction is required, or MFA enrollment is required,
3591                        // continue with the fallback (for local sessions or when
3592                        // optional_mfa feature is not enabled).
3593                        // AADSTS16000: InteractionRequired
3594                        // AADSTS50072: UserStrongAuthEnrollmentRequiredInterrupt
3595                        // AADSTS50203: User has not registered the authenticator app
3596                        if ![16000, 50072, 50203].contains(&e.code) {
3597                            return Err($err);
3598                        }
3599                    }
3600
3601                    let mut dag_scopes: Vec<String> =
3602                        scopes.into_iter().map(|s| s.to_string()).collect();
3603                    // Enforce MFA via the azure portal, but only when the caller
3604                    // has not requested scopes for a specific non-Portal resource.
3605                    // The token endpoint rejects .default combined with explicit
3606                    // permission scopes from a different audience (AADSTS70011).
3607                    let has_resource_scope =
3608                        dag_scopes.iter().any(|s| s.contains("://"));
3609                    if force_mfa && !has_resource_scope {
3610                        dag_scopes.push(format!("{}/.default", AZURE_PORTAL_APP_ID));
3611                    }
3612                    info!("MFA auth failed, falling back to Device Authorization Grant.");
3613                    let flow = self
3614                        .initiate_device_flow(dag_scopes.iter().map(|i| i.as_str()).collect())
3615                        .await?;
3616                    let mut flow: MFAAuthContinue = flow.into();
3617                    flow.resource = resource.map(|s| s.to_string());
3618                    return Ok(flow);
3619                } else {
3620                    return Err($err);
3621                }
3622            };
3623        }
3624        macro_rules! dag_personal_fallback {
3625            () => {
3626                let flow = self.initiate_personal_device_flow(scopes).await?;
3627                let mut flow: MFAAuthContinue = flow.into();
3628                flow.resource = resource.map(|s| s.to_string());
3629                return Ok(flow);
3630            };
3631        }
3632
3633        // The auth config page uses the v1 /oauth2/authorize endpoint, where the
3634        // `resource` parameter (not `scope`) drives the token audience. If the caller
3635        // passed v2-style API scopes (e.g. `api://<guid>/access_as_user`) without an
3636        // explicit resource, derive the v1 resource from the first non-OIDC scope so
3637        // that the login page sees the correct audience and returns a valid sCtx.
3638        const OIDC_SCOPES: &[&str] = &["openid", "profile", "email", "offline_access"];
3639        let _derived_resource: Option<String>;
3640        let resource: Option<&str> = match resource {
3641            Some(r) => Some(r),
3642            None => {
3643                _derived_resource = scopes
3644                    .iter()
3645                    .find(|&&s| !OIDC_SCOPES.contains(&s))
3646                    .map(|&s| v2_scope_to_v1_resource(s));
3647                _derived_resource.as_deref()
3648            }
3649        };
3650
3651        let request_id = Uuid::new_v4().to_string();
3652        let (mut auth_config, cred_type) = if let Some(auth_init) = auth_init {
3653            (auth_init.auth_config, auth_init.cred_type)
3654        } else {
3655            let auth_config = match self
3656                .request_auth_config_internal(scopes.clone(), &request_id, resource, force_mfa)
3657                .await
3658            {
3659                Ok(auth_config) => auth_config,
3660                Err(e) => {
3661                    error!("{:?}", e);
3662                    dag_fallback!();
3663                }
3664            };
3665            let cred_type = match self
3666                .get_cred_type(username, &auth_config, &request_id, options)
3667                .await
3668            {
3669                Ok(cred_type) => cred_type,
3670                Err(e) => {
3671                    error!("{:?}", e);
3672                    dag_fallback!(e);
3673                }
3674            };
3675            (auth_config, cred_type)
3676        };
3677        let sctx = match &auth_config.sctx {
3678            Some(sctx) => sctx.clone(),
3679            None => {
3680                info!("sCtx is missing");
3681                dag_fallback!();
3682            }
3683        };
3684        let sft = match &auth_config.sft {
3685            Some(sft) => sft.clone(),
3686            None => {
3687                info!("sFt is missing");
3688                dag_fallback!();
3689            }
3690        };
3691
3692        macro_rules! passwordless_tap {
3693            () => {
3694                if cred_type.credentials.has_access_pass.unwrap_or(false) {
3695                    debug!("passwordless_tap: attempting (has_access_pass=true)");
3696                    let msg = "Enter Temporary Access Pass: ".to_string();
3697                    let url_post = match &auth_config.url_post {
3698                        Some(url_post) => url_post.clone(),
3699                        None => {
3700                            return Err(MsalError::GeneralFailure(
3701                                "urlBeginAuth is missing".to_string(),
3702                            ))
3703                        }
3704                    };
3705                    return Ok(MFAAuthContinue {
3706                        msg,
3707                        entropy: None,
3708                        max_poll_attempts: None,
3709                        polling_interval: None,
3710                        session_id: auth_config.session_id,
3711                        flow_token: sft,
3712                        ctx: sctx,
3713                        canary: auth_config.canary,
3714                        url_end_auth: None,
3715                        url_post,
3716                        resource: resource.map(|s| s.to_string()),
3717                        dag: None,
3718                        fido_challenge: None,
3719                        fido_allow_list: None,
3720                        cross_domain_canary: None,
3721                        url_session_state: auth_config.url_session_state,
3722                        mfa_methods: vec!["AccessPass".to_string()].into(),
3723                        mfa_method_details: vec![MfaMethodInfo {
3724                            auth_method_id: "AccessPass".to_string(),
3725                            display: "AccessPass".to_string(),
3726                            is_default: true,
3727                        }],
3728                        selected_mfa_method_id: Some("AccessPass".to_string()),
3729                        auth_code: None,
3730                        #[allow(deprecated)]
3731                        fido_is_passkey: false,
3732                        skip_fido_for_mfa: false,
3733                        has_physical_security_key: false,
3734                        has_cross_device_passkey: false,
3735                    });
3736                } else {
3737                    debug!("passwordless_tap: skipped (has_access_pass=false)");
3738                }
3739            };
3740        }
3741
3742        // `get_one_time_code` might fail, and we don't want this tried twice
3743        // if it's failing. Just move on to other mechanisms.
3744        // Note: This is a per-request flag, NOT a static, so each authentication
3745        // attempt gets a fresh chance to try passwordless auth.
3746        let mut passwordless_remote_ngc_called = false;
3747        // Track if a push notification was potentially sent via remote NGC, so we
3748        // don't send a duplicate push via mfa_begin_auth_internal later.
3749        let mut remote_ngc_push_attempted = false;
3750        macro_rules! passwordless_remote_ngc {
3751            () => {
3752                if !passwordless_remote_ngc_called {
3753                    passwordless_remote_ngc_called = true;
3754                    if let Some(ref remote_ngc_params) = cred_type.credentials.remote_ngc_params {
3755                        debug!("passwordless_remote_ngc: attempting (remote_ngc_params present)");
3756                        // Mark that we're about to attempt a push via remote NGC
3757                        remote_ngc_push_attempted = true;
3758                        // Ensure the one time code didn't fail, if it did, we need to continue
3759                        if let Ok(remote_ngc_params) = self
3760                            .get_one_time_code(&auth_config, &remote_ngc_params, &request_id)
3761                            .await
3762                        {
3763                            // Passwordless MS Authenticator is enabled, we can drop out here
3764                            let msg = format!(
3765                                "Open your Authenticator app, and enter the number '{}' to sign in.",
3766                                remote_ngc_params.entropy
3767                            );
3768                            let url_post = match &auth_config.url_post {
3769                                Some(url_post) => url_post.clone(),
3770                                None => {
3771                                    return Err(MsalError::GeneralFailure(
3772                                        "urlBeginAuth is missing".to_string(),
3773                                    ))
3774                                }
3775                            };
3776                            return Ok(MFAAuthContinue {
3777                                msg,
3778                                entropy: Some(remote_ngc_params.entropy),
3779                                max_poll_attempts: auth_config.max_poll_attempts,
3780                                polling_interval: Some(5000),
3781                                session_id: remote_ngc_params.session_identifier,
3782                                flow_token: sft,
3783                                ctx: sctx,
3784                                canary: auth_config.canary,
3785                                url_end_auth: None,
3786                                url_post,
3787                                resource: resource.map(|s| s.to_string()),
3788                                dag: None,
3789                                fido_challenge: None,
3790                                fido_allow_list: None,
3791                                cross_domain_canary: None,
3792                                url_session_state: auth_config.url_session_state,
3793                                mfa_methods: vec!["PhoneAppNotification".to_string()].into(),
3794                                mfa_method_details: vec![MfaMethodInfo {
3795                                    auth_method_id: "PhoneAppNotification".to_string(),
3796                                    display: "PhoneAppNotification".to_string(),
3797                                    is_default: true
3798                                }],
3799                                selected_mfa_method_id: Some("PhoneAppNotification".to_string()),
3800                                auth_code: None,
3801                                #[allow(deprecated)]
3802                                fido_is_passkey: false,
3803                                skip_fido_for_mfa: false,
3804                                has_physical_security_key: false,
3805                                has_cross_device_passkey: false,
3806                            });
3807                        }
3808                    } else {
3809                        debug!("passwordless_remote_ngc: skipped (remote_ngc_params absent)");
3810                    }
3811                } else {
3812                    debug!("passwordless_remote_ngc: skipped (already called)");
3813                }
3814            };
3815        }
3816
3817        debug!("Credential type: pref_credential={}, has_password={}, has_fido={:?}, has_remote_ngc={:?}, has_access_pass={:?}, is_passkey_support_enabled={:?}",
3818            cred_type.credentials.pref_credential,
3819            cred_type.credentials.has_password,
3820            cred_type.credentials.has_fido,
3821            cred_type.credentials.has_remote_ngc,
3822            cred_type.credentials.has_access_pass,
3823            auth_config.is_passkey_support_enabled,
3824        );
3825        if let Some(ref fido_params) = cred_type.credentials.fido_params {
3826            debug!(
3827                "FIDO params: has_cross_device_capable_passkey={:?}, allow_list_count={}",
3828                fido_params.has_cross_device_capable_passkey,
3829                fido_params.fido_allow_list.len(),
3830            );
3831        }
3832
3833        let user_has_any_cross_device_fido = cred_type
3834            .credentials
3835            .fido_params
3836            .as_ref()
3837            .map(|fido_params| {
3838                fido_params
3839                    .has_cross_device_capable_passkey
3840                    .unwrap_or(false)
3841            })
3842            .unwrap_or(false);
3843        let attempt_security_key = should_attempt_passwordless_security_key(
3844            options,
3845            cred_type.credentials.fido_params.is_some(),
3846        );
3847        let attempt_qr_bluetooth = should_attempt_passwordless_qr_bluetooth(
3848            options,
3849            cred_type.credentials.fido_params.is_some(),
3850            user_has_any_cross_device_fido,
3851        );
3852
3853        macro_rules! passwordless_fido {
3854            () => {
3855                if attempt_security_key || attempt_qr_bluetooth {
3856                    let fido_params = cred_type.credentials.fido_params.as_ref().unwrap();
3857                    let url_post = match &auth_config.url_post {
3858                        Some(url_post) => url_post.clone(),
3859                        None => {
3860                            return Err(MsalError::GeneralFailure(
3861                                "urlBeginAuth is missing".to_string(),
3862                            ))
3863                        }
3864                    };
3865                    auth_config.fido_allow_list = Some(fido_params.fido_allow_list.clone());
3866                    let fido_auth_config = self
3867                        .handle_auth_config_fido_get(username, &auth_config, &request_id)
3868                        .await?;
3869                    return Ok(MFAAuthContinue {
3870                        msg: "".to_string(),
3871                        entropy: None,
3872                        max_poll_attempts: auth_config.max_poll_attempts,
3873                        polling_interval: Some(5000),
3874                        session_id: fido_auth_config.session_id,
3875                        flow_token: sft,
3876                        ctx: sctx,
3877                        canary: auth_config.canary,
3878                        url_end_auth: auth_config.url_end_auth,
3879                        url_post,
3880                        resource: resource.map(|s| s.to_string()),
3881                        dag: None,
3882                        fido_challenge: fido_auth_config.fido_challenge,
3883                        fido_allow_list: Some(fido_params.fido_allow_list.clone()),
3884                        cross_domain_canary: fido_auth_config.cross_domain_canary,
3885                        url_session_state: auth_config.url_session_state,
3886                        mfa_methods: vec!["FidoKey".to_string()].into(),
3887                        mfa_method_details: vec![MfaMethodInfo {
3888                            auth_method_id: "FidoKey".to_string(),
3889                            display: "FidoKey".to_string(),
3890                            is_default: true,
3891                        }],
3892                        selected_mfa_method_id: Some("FidoKey".to_string()),
3893                        auth_code: None,
3894                        #[allow(deprecated)]
3895                        fido_is_passkey: false,
3896                        skip_fido_for_mfa: false,
3897                        has_physical_security_key: attempt_security_key,
3898                        has_cross_device_passkey: attempt_qr_bluetooth,
3899                    });
3900                }
3901            };
3902        }
3903
3904        // Methods are attempted most-secure-first: FIDO (phishing-resistant)
3905        // always before remote_ngc (number matching), regardless of the
3906        // pref_credential value returned by Microsoft. This ensures users
3907        // under a phishing-resistant CAP are offered a compliant method.
3908        match cred_type.credentials.pref_credential {
3909            13 => passwordless_tap!(),
3910            2 | 7 => {
3911                debug!(
3912                    "passwordless_fido triggered via pref_credential={}",
3913                    cred_type.credentials.pref_credential
3914                );
3915                passwordless_fido!();
3916                passwordless_remote_ngc!();
3917            }
3918            _ => {}
3919        }
3920
3921        // Now attempt to emulate the old behavior, attempting passwordless
3922        // auth even if it isn't the preferred method.
3923        debug!("passwordless_tap triggered via fallthrough");
3924        passwordless_tap!();
3925        debug!("passwordless_fido triggered via fallthrough");
3926        passwordless_fido!();
3927        debug!("passwordless_remote_ngc triggered via fallthrough");
3928        passwordless_remote_ngc!();
3929
3930        if !cred_type.account_exists()? {
3931            return Err(MsalError::GeneralFailure(
3932                "An account with that name does not exist.".to_string(),
3933            ));
3934        }
3935        if cred_type.is_personal_account() {
3936            dag_personal_fallback!();
3937        }
3938
3939        let auth_response = if let Some(ref federation_redirect_url) =
3940            cred_type.credentials.federation_redirect_url
3941        {
3942            let federation_url = match parse_adfs_federation_url(federation_redirect_url) {
3943                Ok(Some(url)) => url,
3944                Ok(None) => {
3945                    info!("Federated identity is not a supported AD FS HTTPS endpoint.");
3946                    dag_fallback!();
3947                }
3948                Err(e) => {
3949                    error!("Unable to parse federation endpoint: {e}");
3950                    dag_fallback!(e);
3951                }
3952            };
3953            info!(
3954                "Attempting AD FS forms authentication against {}",
3955                federation_url.host_str().unwrap_or("unknown host")
3956            );
3957            let password = password.ok_or(MsalError::PasswordRequired)?;
3958            match self
3959                .request_adfs_form_internal(federation_url, username, password)
3960                .await
3961                .and_then(|text| parse_ws_fed_form(&text))
3962            {
3963                Ok(form) => self.submit_ws_fed_form_internal(&form).await,
3964                Err(e) => Err(e),
3965            }
3966        } else {
3967            if !cred_type.credentials.has_password {
3968                info!("Password authentication is not supported.");
3969                dag_fallback!();
3970            }
3971            let sctx = match &auth_config.sctx {
3972                Some(sctx) => sctx.clone(),
3973                None => {
3974                    info!("sCtx is missing");
3975                    dag_fallback!();
3976                }
3977            };
3978            let sft = match &auth_config.sft {
3979                Some(sft) => sft.clone(),
3980                None => {
3981                    info!("sFt is missing");
3982                    dag_fallback!();
3983                }
3984            };
3985            let params = vec![
3986                ("login", username),
3987                ("passwd", password.ok_or(MsalError::PasswordRequired)?),
3988                ("ctx", &sctx),
3989                ("flowToken", &sft),
3990                ("canary", &auth_config.canary),
3991                ("client_id", self.client_id()),
3992                ("client-request-id", &request_id),
3993            ];
3994            self.handle_auth_config_req_internal(&params, &auth_config, options, false)
3995                .await
3996        };
3997        match auth_response {
3998            Ok(mut auth_config) => {
3999                if let Some(msg) = auth_config.service_exception_msg {
4000                    error!("{}", msg);
4001                    dag_fallback!();
4002                }
4003                if let Some(ref pgid) = auth_config.pgid {
4004                    if pgid == "KmsiInterrupt" {
4005                        let sctx = match &auth_config.sctx {
4006                            Some(sctx) => sctx.clone(),
4007                            None => {
4008                                info!("sCtx is missing");
4009                                dag_fallback!();
4010                            }
4011                        };
4012                        let sft = match &auth_config.sft {
4013                            Some(sft) => sft.clone(),
4014                            None => {
4015                                info!("sFt is missing");
4016                                dag_fallback!();
4017                            }
4018                        };
4019                        let params = vec![
4020                            ("LoginOptions", "1"),
4021                            ("ctx", &sctx),
4022                            ("flowToken", &sft),
4023                            ("canary", &auth_config.canary),
4024                            ("client-request-id", &request_id),
4025                        ];
4026                        auth_config = match self
4027                            .handle_auth_config_req_internal(&params, &auth_config, options, false)
4028                            .await
4029                        {
4030                            Ok(auth_config) => auth_config,
4031                            Err(e) => {
4032                                error!("{:?}", e);
4033                                dag_fallback!(e);
4034                            }
4035                        };
4036                    }
4037                }
4038                if let Some(ref pgid) = auth_config.pgid {
4039                    if pgid == "ConvergedProofUpRedirect" {
4040                        if let Some(remaining_days) = auth_config.remaining_days_to_skip_mfa_reg {
4041                            info!("MFA must be set up in {} days", remaining_days);
4042                            let params = vec![
4043                                ("LoginOptions", "1"),
4044                                ("ctx", &sctx),
4045                                ("flowToken", &sft),
4046                                ("canary", &auth_config.canary),
4047                                ("client-request-id", &request_id),
4048                            ];
4049                            auth_config = match self
4050                                .handle_auth_config_req_internal(
4051                                    &params,
4052                                    &auth_config,
4053                                    options,
4054                                    false,
4055                                )
4056                                .await
4057                            {
4058                                Ok(auth_config) => auth_config,
4059                                Err(e) => {
4060                                    error!("{:?}", e);
4061                                    dag_fallback!(e);
4062                                }
4063                            };
4064                        } else {
4065                            info!("MFA method must be registered.");
4066                            dag_fallback!();
4067                        }
4068                    }
4069                }
4070                if let Some(ref pgid) = auth_config.pgid {
4071                    if pgid == "ConvergedChangePassword" {
4072                        info!("Password is expired!");
4073                        #[cfg(feature = "changepassword")]
4074                        return Err(MsalError::ChangePassword);
4075                        #[cfg(not(feature = "changepassword"))]
4076                        dag_fallback!();
4077                    }
4078                }
4079                if let Some(ref arr_user_proofs) = auth_config.arr_user_proofs {
4080                    debug!("MFA methods available: {:?}", arr_user_proofs);
4081                    let skip_fido_for_mfa = user_has_any_cross_device_fido
4082                        || auth_config.is_passkey_support_enabled.unwrap_or(false);
4083
4084                    // Try to use provided MFA method if available
4085                    let selected_auth_method = if let Some(requested_method) = mfa_method {
4086                        arr_user_proofs
4087                            .iter()
4088                            .find(|proof| {
4089                                proof.auth_method_id == requested_method
4090                                    && (!skip_fido_for_mfa || proof.auth_method_id != "FidoKey")
4091                            })
4092                            .ok_or_else(|| {
4093                                let available = arr_user_proofs
4094                                    .iter()
4095                                    .map(|p| p.auth_method_id.as_str())
4096                                    .collect::<Vec<_>>();
4097                                MsalError::GeneralFailure(format!(
4098                                "Requested MFA method '{}' not available. Available methods: {}",
4099                                requested_method, available.join(", ")
4100                            ))
4101                            })?
4102                    } else if let Some(method) = arr_user_proofs.iter().find(|proof| {
4103                        proof.is_default
4104                            && (!skip_fido_for_mfa || proof.auth_method_id != "FidoKey")
4105                    }) {
4106                        method
4107                    } else if skip_fido_for_mfa {
4108                        // Skip FidoKey methods entirely if we can't use them
4109                        match arr_user_proofs
4110                            .iter()
4111                            .find(|proof| proof.auth_method_id == "PhoneAppNotification")
4112                            .or_else(|| {
4113                                arr_user_proofs
4114                                    .iter()
4115                                    .find(|proof| proof.auth_method_id != "FidoKey")
4116                            }) {
4117                            Some(method) => method,
4118                            None => {
4119                                info!("No usable MFA methods found (FIDO was cross-device)");
4120                                dag_fallback!();
4121                            }
4122                        }
4123                    } else if arr_user_proofs.is_empty() {
4124                        info!("No MFA methods found");
4125                        dag_fallback!();
4126                    } else {
4127                        // MS sometimes doesn't set is_default; fallback to the first
4128                        &arr_user_proofs[0]
4129                    };
4130
4131                    let sctx = match &auth_config.sctx {
4132                        Some(sctx) => sctx.clone(),
4133                        None => {
4134                            info!("sCtx is missing");
4135                            dag_fallback!();
4136                        }
4137                    };
4138                    let sft = match &auth_config.sft {
4139                        Some(sft) => sft.clone(),
4140                        None => {
4141                            info!("sFt is missing");
4142                            dag_fallback!();
4143                        }
4144                    };
4145                    let url_begin_auth = match &auth_config.url_begin_auth {
4146                        Some(url_begin_auth) => url_begin_auth.clone(),
4147                        None => {
4148                            info!("urlBeginAuth is missing");
4149                            dag_fallback!();
4150                        }
4151                    };
4152                    let url_post = match &auth_config.url_post {
4153                        Some(url_post) => url_post.clone(),
4154                        None => {
4155                            info!("urlPost is missing");
4156                            dag_fallback!();
4157                        }
4158                    };
4159                    let (flow_token, ctx, msg) = if selected_auth_method.auth_method_id == "FidoKey"
4160                    {
4161                        let fido_auth_config = self
4162                            .handle_auth_config_fido_get(username, &auth_config, &request_id)
4163                            .await?;
4164                        auth_config.fido_challenge = fido_auth_config.fido_challenge.clone();
4165                        auth_config.session_id = fido_auth_config.session_id.clone();
4166                        auth_config.cross_domain_canary =
4167                            fido_auth_config.cross_domain_canary.clone();
4168                        (sft, sctx, "".to_string())
4169                    } else if selected_auth_method.auth_method_id == "AccessPass" {
4170                        (sft, sctx, "Enter Temporary Access Pass: ".to_string())
4171                    } else if remote_ngc_push_attempted
4172                        && (selected_auth_method.auth_method_id == "PhoneAppNotification"
4173                            || selected_auth_method.auth_method_id == "CompanionAppsNotification")
4174                    {
4175                        // We already tried to send a push via get_one_time_code (remote NGC).
4176                        // If it failed, we might have sent a push but didn't get the entropy.
4177                        // Calling mfa_begin_auth_internal would send a SECOND push, causing
4178                        // the user to see two prompts on their phone.
4179                        // Fall back to Device Authorization Grant instead.
4180                        info!(
4181                            "Remote NGC push was attempted but failed. Avoiding duplicate push for {}. Falling back to DAG.",
4182                            selected_auth_method.auth_method_id
4183                        );
4184                        dag_fallback!();
4185                    } else {
4186                        let auth_response = match self
4187                            .mfa_begin_auth_internal(
4188                                &selected_auth_method.auth_method_id,
4189                                &url_begin_auth,
4190                                &sctx,
4191                                &sft,
4192                                &auth_config.canary,
4193                            )
4194                            .await
4195                        {
4196                            Ok(auth_response) => match auth_response.success {
4197                                true => auth_response,
4198                                false => {
4199                                    return Err(MsalError::GeneralFailure(
4200                                        "Begin Auth failed".to_string(),
4201                                    ))
4202                                }
4203                            },
4204                            Err(e) => {
4205                                error!("{:?}", e);
4206                                dag_fallback!(e);
4207                            }
4208                        };
4209                        let msg = match selected_auth_method.auth_method_id.as_str() {
4210                            "PhoneAppNotification" | "CompanionAppsNotification" => format!("Open your Authenticator app, and enter the number '{}' to sign in.", auth_response.entropy),
4211                            "PhoneAppOTP" =>
4212                                "Please type in the code displayed on your authenticator app from your device:".to_string(),
4213                            "ConsolidatedTelephony" | "OneWaySMS" =>
4214                                format!("We texted your phone {}. Please enter the code to sign in:", selected_auth_method.display),
4215                            "TwoWayVoiceMobile" =>
4216                                format!("We're calling your phone {}. Please answer it to continue.", selected_auth_method.display),
4217                            "TwoWayVoiceAlternateMobile" =>
4218                                format!("We're calling your phone {}. Please answer it to continue.", selected_auth_method.display),
4219                            "TwoWayVoiceOffice" =>
4220                                format!("We're calling your office phone {}. Please answer it to continue.", selected_auth_method.display),
4221                            method => {
4222                                info!("Unsupported MFA method {}", method);
4223                                dag_fallback!();
4224                            }
4225                        };
4226                        (auth_response.flow_token, auth_response.ctx, msg)
4227                    };
4228                    Ok(MFAAuthContinue {
4229                        msg,
4230                        entropy: None,
4231                        max_poll_attempts: auth_config.max_poll_attempts,
4232                        polling_interval: auth_config.polling_interval,
4233                        session_id: auth_config.session_id,
4234                        flow_token,
4235                        ctx,
4236                        canary: auth_config.canary,
4237                        url_end_auth: auth_config.url_end_auth,
4238                        url_post,
4239                        resource: resource.map(|s| s.to_string()),
4240                        dag: None,
4241                        fido_challenge: auth_config.fido_challenge.clone(),
4242                        fido_allow_list: auth_config.fido_allow_list.clone(),
4243                        cross_domain_canary: auth_config.cross_domain_canary.clone(),
4244                        url_session_state: None,
4245                        mfa_methods: arr_user_proofs
4246                            .iter()
4247                            .map(|proof| proof.auth_method_id.clone())
4248                            .collect(),
4249                        mfa_method_details: arr_user_proofs
4250                            .iter()
4251                            .map(|proof| proof.into())
4252                            .collect(),
4253                        selected_mfa_method_id: Some(selected_auth_method.auth_method_id.clone()),
4254                        auth_code: None,
4255                        #[allow(deprecated)]
4256                        fido_is_passkey: skip_fido_for_mfa,
4257                        skip_fido_for_mfa,
4258                        has_physical_security_key: false,
4259                        has_cross_device_passkey: false,
4260                    })
4261                } else {
4262                    info!("No MFA methods found");
4263                    dag_fallback!();
4264                }
4265            }
4266            Err(MsalError::AuthCodeReceived(auth_code)) => {
4267                // We received an auth code directly from the login response.
4268                // Create a flow that tells PAM to poll once, and we'll exchange
4269                // the auth code for tokens.
4270                Ok(MFAAuthContinue {
4271                    msg: "".to_string(),
4272                    entropy: None,
4273                    max_poll_attempts: Some(1),
4274                    polling_interval: Some(0),
4275                    session_id: String::new(),
4276                    flow_token: String::new(),
4277                    ctx: String::new(),
4278                    canary: String::new(),
4279                    url_end_auth: None,
4280                    url_post: String::new(),
4281                    url_session_state: None,
4282                    resource: resource.map(|s| s.to_string()),
4283                    dag: None,
4284                    fido_challenge: None,
4285                    fido_allow_list: None,
4286                    cross_domain_canary: None,
4287                    mfa_methods: vec![],
4288                    mfa_method_details: vec![],
4289                    selected_mfa_method_id: None,
4290                    auth_code: Some(auth_code),
4291                    #[allow(deprecated)]
4292                    fido_is_passkey: false,
4293                    skip_fido_for_mfa: false,
4294                    has_physical_security_key: false,
4295                    has_cross_device_passkey: false,
4296                })
4297            }
4298            Err(e) => {
4299                error!("{:?}", e);
4300                dag_fallback!(e);
4301            }
4302        }
4303    }
4304
4305    async fn get_one_time_code(
4306        &self,
4307        auth_config: &AuthConfig,
4308        remote_ngc_params: &RemoteNgcParams,
4309        request_id: &str,
4310    ) -> Result<RemoteNgcParams, MsalError> {
4311        let payload = json!({
4312            "Channel": "Authenticator",
4313            "FlowToken": &auth_config.sft,
4314            "OldDeviceCode": remote_ngc_params.session_identifier,
4315            "OriginalRequest": &auth_config.sctx,
4316        });
4317
4318        let url = match &auth_config.url_get_one_time_code {
4319            Some(url) => url.to_string(),
4320            None => format!("{}/GetOneTimeCode", self.authority()?),
4321        };
4322
4323        let resp = self
4324            .client()
4325            .post(url)
4326            .header(header::CONTENT_TYPE, "application/json; charset=UTF-8")
4327            .header("client-request-id", request_id)
4328            .header("Canary", &auth_config.canary)
4329            .json(&payload)
4330            .send()
4331            .await
4332            .map_err(|e| MsalError::request_failed(&e))?;
4333        if resp.status().is_success() {
4334            let json_resp: OneTimeCode = resp
4335                .json()
4336                .await
4337                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
4338            if let Some(error) = &json_resp.error {
4339                return Err(MsalError::GeneralFailure(format!(
4340                    "Failed to parse response for /GetOneTimeCode: {}",
4341                    error.message.clone()
4342                )));
4343            }
4344            json_resp.remote_ngc_params.ok_or(MsalError::GeneralFailure(
4345                "remote_ngc_params missing".to_string(),
4346            ))
4347        } else {
4348            let text = resp
4349                .text()
4350                .await
4351                .map_err(|e| MsalError::GeneralFailure(format!("Failed getting otc: {}", e)))?;
4352            Err(MsalError::GeneralFailure(format!(
4353                "Request to /GetOneTimeCode failed: {}",
4354                text
4355            )))
4356        }
4357    }
4358
4359    async fn get_cred_type(
4360        &self,
4361        username: &str,
4362        auth_config: &AuthConfig,
4363        request_id: &str,
4364        options: &[AuthOption],
4365    ) -> Result<CredType, MsalError> {
4366        let payload = json!({
4367            "username": username,
4368            "isOtherIdpSupported": true,
4369            "checkPhones": true,
4370            "isRemoteNGCSupported": options.contains(&AuthOption::Passwordless),
4371            "isCookieBannerShown": false,
4372            "isFidoSupported": options.contains(&AuthOption::Fido),
4373            "isAccessPassSupported": true,
4374            "originalRequest": &auth_config.sctx,
4375            "flowToken": &auth_config.sft,
4376        });
4377
4378        let url = match &auth_config.url_get_credential_type {
4379            Some(url) => url.to_string(),
4380            None => format!("{}/GetCredentialType", self.authority()?),
4381        };
4382
4383        let resp = self
4384            .client()
4385            .post(url)
4386            .header(header::CONTENT_TYPE, "application/json; charset=UTF-8")
4387            .header("client-request-id", request_id)
4388            .header(header::USER_AGENT, FIDO_USER_AGENT)
4389            .json(&payload)
4390            .send()
4391            .await
4392            .map_err(|e| MsalError::request_failed(&e))?;
4393        if resp.status().is_success() {
4394            let json_resp: CredType = resp
4395                .json()
4396                .await
4397                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
4398            json_resp.log_throttle_status();
4399            Ok(json_resp)
4400        } else {
4401            let json_resp: ErrorResponse = resp
4402                .json()
4403                .await
4404                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
4405            Err(MsalError::AcquireTokenFailed(json_resp))
4406        }
4407    }
4408
4409    async fn request_auth_config_internal(
4410        &self,
4411        scopes: Vec<&str>,
4412        request_id: &str,
4413        resource: Option<&str>,
4414        mfa: bool,
4415    ) -> Result<AuthConfig, MsalError> {
4416        let scope = format!("openid profile {}", scopes.join(" "));
4417        let redirect_uri = self.app.get_auth_redirect_uri(None, resource);
4418        let caller_app_redirect_uri = self
4419            .app
4420            .get_auth_redirect_uri(Some(LINUX_BROKER_APP_ID), resource);
4421
4422        debug!("request_auth_config_internal() client_id={} redirect_uri={} scope={} resource={} caller_app_redirect_uri={}",
4423            self.client_id(),
4424            redirect_uri.as_str(),
4425            &scope,
4426            resource.unwrap_or("https://graph.microsoft.com"),
4427            caller_app_redirect_uri.as_str()
4428        );
4429
4430        // TODO: the sso_reload, resource, caller_app_client_id, caller_app_redirect_uri, and amr_values parameters are only
4431        // supported with the "v1" /oauth2/authorize endpoint and are not supported by /oauth/v2.0/authorize
4432        // See: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow for details
4433        let mut params = vec![
4434            ("client_id", self.client_id()),
4435            ("response_type", "code"),
4436            ("redirect_uri", redirect_uri.as_str()),
4437            ("client-request-id", request_id),
4438            ("prompt", "login"),
4439            ("scope", &scope),
4440            ("response_mode", "query"),
4441            ("sso_reload", "True"),
4442            (
4443                "resource",
4444                (resource.unwrap_or("https://graph.microsoft.com")),
4445            ),
4446            ("caller_app_client_id", LINUX_BROKER_APP_ID),
4447            ("caller_app_redirect_uri", caller_app_redirect_uri.as_str()),
4448        ];
4449        // This will almost always be true. We have to disable it for password
4450        // changes though.
4451        if mfa {
4452            params.push(("amr_values", "ngcmfa"));
4453        }
4454        let url = Url::parse_with_params(
4455            &format!("{}/oauth2/authorize", self.authority()?),
4456            &params.to_vec(),
4457        )
4458        .map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
4459
4460        let resp = self
4461            .client()
4462            .get(url)
4463            .header(header::USER_AGENT, FIDO_USER_AGENT)
4464            .send()
4465            .await
4466            .map_err(|e| MsalError::request_failed(&e))?;
4467        if resp.status().is_success() {
4468            self.parse_auth_config(
4469                &resp.text().await.map_err(|e| {
4470                    MsalError::GeneralFailure(format!("Failed parsing auth config: {}", e))
4471                })?,
4472                true,
4473                false,
4474            )
4475        } else {
4476            Err(MsalError::GeneralFailure(
4477                "Failed requesting auth config".to_string(),
4478            ))
4479        }
4480    }
4481
4482    async fn mfa_begin_auth_internal(
4483        &self,
4484        mfa_method: &str,
4485        url_begin_auth: &str,
4486        ctx: &str,
4487        flow_token: &str,
4488        canary: &str,
4489    ) -> Result<AuthResponse, MsalError> {
4490        let payload = json!({
4491            "AuthMethodId": mfa_method,
4492            "ctx": ctx,
4493            "flowToken": flow_token,
4494            "Method": "BeginAuth",
4495        });
4496
4497        let resp = self
4498            .client()
4499            .post(url_begin_auth)
4500            .header(header::USER_AGENT, FIDO_USER_AGENT)
4501            .header(header::CONTENT_TYPE, "application/json; charset=utf-8")
4502            .header("canary", canary)
4503            .json(&payload)
4504            .send()
4505            .await
4506            .map_err(|e| MsalError::request_failed(&e))?;
4507        if resp.status().is_success() {
4508            let text = resp
4509                .text()
4510                .await
4511                .map_err(|e| MsalError::GeneralFailure(format!("{}", e)))?;
4512            let auth_response: AuthResponse =
4513                json_from_str(&text).map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
4514            if auth_response.success {
4515                Ok(auth_response)
4516            } else if let Some(error_code) = auth_response.error_code {
4517                Err(MsalError::AADSTSError(AADSTSError::new(error_code, None)))
4518            } else if let Some(msg) = auth_response.message {
4519                Err(MsalError::GeneralFailure(format!(
4520                    "BeginAuth failed with message: {}",
4521                    msg
4522                )))
4523            } else {
4524                Err(MsalError::GeneralFailure("BeginAuth failed".to_string()))
4525            }
4526        } else {
4527            Err(MsalError::GeneralFailure(
4528                "BeginAuth Authentication request failed".to_string(),
4529            ))
4530        }
4531    }
4532
4533    async fn await_working(&self, mut resp: Response) -> Result<(String, Response), MsalError> {
4534        // We have to read the response in chunks, because Response.text()
4535        // consumes the Response object.
4536        let mut body = Vec::new();
4537        while let Some(chunk) = resp.chunk().await.map_err(|e| {
4538            MsalError::GeneralFailure(format!("Error reading response chunks: {}", e))
4539        })? {
4540            body.extend(&chunk);
4541        }
4542        let mut text = String::from_utf8(body)
4543            .map_err(|e| MsalError::GeneralFailure(format!("UTF-8 error: {}", e)))?;
4544        for _ in 0..10 {
4545            if !text.contains("Click Submit to continue")
4546                && !text.contains("Working...")
4547                && !text.contains("Click here to finish the authorization process")
4548                && !text.contains("<input type=\"submit\"")
4549            {
4550                return Ok((text, resp));
4551            }
4552            sleep(Duration::from_secs(1));
4553            let (post_url, form_data) = tokio::task::spawn_blocking(
4554                move || -> Result<(String, HashMap<String, String>), MsalError> {
4555                    let document = Html::parse_document(&text);
4556                    let form_selector = Selector::parse("form")
4557                        .map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))?;
4558                    let input_selector = Selector::parse("input")
4559                        .map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))?;
4560
4561                    let form = document
4562                        .select(&form_selector)
4563                        .next()
4564                        .ok_or(MsalError::InvalidParse("Document parse failed".to_string()))?;
4565                    let post_url = form
4566                        .value()
4567                        .attr("action")
4568                        .ok_or(MsalError::InvalidParse("Form action not found".to_string()))?;
4569
4570                    let mut form_data = HashMap::new();
4571
4572                    for input in form.select(&input_selector) {
4573                        if let Some(name) = input.value().attr("name") {
4574                            if let Some(value) = input.value().attr("value") {
4575                                form_data.insert(name.to_string(), value.to_string());
4576                            }
4577                        }
4578                    }
4579
4580                    Ok((post_url.to_string(), form_data))
4581                },
4582            )
4583            .await
4584            .map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))??;
4585
4586            resp = self
4587                .client()
4588                .post(post_url)
4589                .form(&form_data)
4590                .send()
4591                .await
4592                .map_err(|e| MsalError::request_failed(&e))?;
4593            let mut body = Vec::new();
4594            while let Some(chunk) = resp.chunk().await.map_err(|e| {
4595                MsalError::GeneralFailure(format!("Error reading response chunks: {}", e))
4596            })? {
4597                body.extend(&chunk);
4598            }
4599            text = String::from_utf8(body)
4600                .map_err(|e| MsalError::GeneralFailure(format!("UTF-8 error: {}", e)))?;
4601        }
4602        Err(MsalError::GeneralFailure(
4603            "Pending request timed out after 10 seconds".to_string(),
4604        ))
4605    }
4606
4607    async fn auth_code_intercept_internal(
4608        &self,
4609        url: &str,
4610        payload: String,
4611    ) -> Result<String, MsalError> {
4612        let mut resp = self
4613            .client()
4614            .post(url)
4615            .header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
4616            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
4617            .body(payload)
4618            .send()
4619            .await
4620            .map_err(|e| MsalError::request_failed(&e))?;
4621        let text;
4622        (text, resp) = self.await_working(resp).await?;
4623        if resp.status().is_redirection() {
4624            let redirect = resp.headers()["location"]
4625                .to_str()
4626                .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
4627            let url =
4628                Url::parse(redirect).map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
4629            let params = url.query_pairs().collect::<Vec<_>>();
4630
4631            // Check for errors returned in the redirect URL parameters.
4632            if let Some((_, error_description)) =
4633                params.iter().find(|(k, _)| k == "error_description")
4634            {
4635                let error_code_regex = Regex::new(r"AADSTS(\d+):");
4636
4637                if let Ok(regex) = error_code_regex {
4638                    if let Some(captures) = regex.captures(error_description) {
4639                        if let Some(code_match) = captures.get(1) {
4640                            if let Ok(code) = code_match.as_str().parse::<u32>() {
4641                                return Err(MsalError::AADSTSError(AADSTSError::new(
4642                                    code,
4643                                    Some(error_description.to_string()),
4644                                )));
4645                            }
4646                        }
4647                    }
4648                }
4649
4650                return Err(MsalError::GeneralFailure(format!(
4651                    "Unknown error in request to {}: {}",
4652                    url, error_description
4653                )));
4654            }
4655
4656            let (_, code) =
4657                params
4658                    .iter()
4659                    .find(|(k, _)| k == "code")
4660                    .ok_or(MsalError::InvalidParse(
4661                        "Authorization code missing from redirect".to_string(),
4662                    ))?;
4663            Ok(code.to_string())
4664        } else if resp.status().is_success() {
4665            // MS may have returned an AuthConfig here with an error attached.
4666            // Return the error from that AuthConfig if possible. If a required
4667            // password change is indicated, raise an error.
4668            match self.parse_auth_config(&text, false, false) {
4669                #[cfg(feature = "changepassword")]
4670                Err(MsalError::ChangePassword) => Err(MsalError::ChangePassword),
4671                Err(MsalError::AADSTSError(e)) => Err(MsalError::AADSTSError(e)),
4672                Err(MsalError::SkipMfaRegistration(url_skip_mfa_registration, sft, canary)) => Err(
4673                    MsalError::SkipMfaRegistration(url_skip_mfa_registration, sft, canary),
4674                ),
4675                Err(error) => Err(MsalError::GeneralFailure(format!(
4676                    "MsalError in auth_code_intercept_internal(), {}: {}",
4677                    error, text
4678                ))),
4679                // It looks like pgid=KmsiInterrupt ("keep me signed in") can occur in some cases with a successful response, but no redirect
4680                Ok(value) => Ok(format!(
4681                    "auth_code_intercept_internal() succeeded without redirect, pgid={:?}",
4682                    value.pgid
4683                )),
4684            }
4685        } else {
4686            Err(MsalError::GeneralFailure(
4687                "ProcessAuth Authorization request failed".to_string(),
4688            ))
4689        }
4690    }
4691
4692    async fn request_authorization_passwordless_internal(
4693        &self,
4694        username: &str,
4695        flow: &MFAAuthContinue,
4696    ) -> Result<String, MsalError> {
4697        let entropy = format!(
4698            "{}",
4699            flow.entropy
4700                .ok_or(MsalError::GeneralFailure("Missing entropy".to_string()))?
4701        );
4702        let params = [
4703            ("code", &flow.session_id),
4704            ("psRNGCSLK", &flow.session_id),
4705            ("login", &username.to_string()),
4706            ("loginfmt", &username.to_string()),
4707            ("psRNGCEntropy", &entropy),
4708            ("flowToken", &flow.flow_token),
4709            ("canary", &flow.canary),
4710            ("ctx", &flow.ctx),
4711        ];
4712        let payload = params
4713            .iter()
4714            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
4715            .collect::<Vec<String>>()
4716            .join("&");
4717
4718        let url = match &flow.url_post.starts_with('/') {
4719            true => {
4720                let authority = self.authority()?.to_string();
4721                let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
4722                    "Failed to splice auth config url".to_string(),
4723                ))?;
4724                format!("{}/{}", &authority[..index], flow.url_post)
4725            }
4726            false => flow.url_post.clone(),
4727        };
4728
4729        let mut resp = self
4730            .client()
4731            .post(url)
4732            .header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
4733            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
4734            .body(payload)
4735            .send()
4736            .await
4737            .map_err(|e| MsalError::request_failed(&e))?;
4738        let _text;
4739        (_text, resp) = self.await_working(resp).await?;
4740        if resp.status().is_redirection() {
4741            let redirect = resp.headers()["location"]
4742                .to_str()
4743                .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
4744            let url =
4745                Url::parse(redirect).map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
4746            let params = url.query_pairs().collect::<Vec<_>>();
4747
4748            // Check for errors returned in the redirect URL parameters.
4749            if let Some((_, error_description)) =
4750                params.iter().find(|(k, _)| k == "error_description")
4751            {
4752                return Err(MsalError::GeneralFailure(format!("Error found in redirect URL parameters for request_authorization_passwordless_internal() redirect: {}", error_description)));
4753            }
4754
4755            let (_, code) =
4756                params
4757                    .iter()
4758                    .find(|(k, _)| k == "code")
4759                    .ok_or(MsalError::InvalidParse(
4760                        "Authorization code missing from redirect".to_string(),
4761                    ))?;
4762            Ok(code.to_string())
4763        } else {
4764            Err(MsalError::GeneralFailure(
4765                "ProcessAuth Authorization request failed".to_string(),
4766            ))
4767        }
4768    }
4769
4770    async fn request_authorization_internal(
4771        &self,
4772        username: &str,
4773        flow: &MFAAuthContinue,
4774        selected_mfa_method: &MfaMethodInfo,
4775    ) -> Result<String, MsalError> {
4776        let mfa_method = match selected_mfa_method.auth_method_id.as_str() {
4777            // ConsolidatedTelephony simply means OneWaySMS internally to Azure,
4778            // it seems. If we don't swap them during the ProcessAuth though,
4779            // this request is rejected. I observed this odd behavior in a
4780            // browser auth to Azure also.
4781            "ConsolidatedTelephony" => "OneWaySMS".to_string(),
4782            other => other.to_string(),
4783        };
4784        let params = [
4785            ("request", &flow.ctx),
4786            ("mfaAuthMethod", &mfa_method),
4787            ("login", &username.to_string()),
4788            ("flowToken", &flow.flow_token),
4789            ("canary", &flow.canary),
4790        ];
4791        let payload = params
4792            .iter()
4793            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
4794            .collect::<Vec<String>>()
4795            .join("&");
4796
4797        match self
4798            .auth_code_intercept_internal(&flow.url_post, payload)
4799            .await
4800        {
4801            Ok(code) => Ok(code),
4802            Err(MsalError::SkipMfaRegistration(url_skip_mfa_registration, sft, canary)) => {
4803                let params = [
4804                    (
4805                        "flowtoken",
4806                        &sft.ok_or(MsalError::GeneralFailure("Missing flow token".to_string()))?,
4807                    ),
4808                    ("ctx", &flow.ctx),
4809                    ("canary", &canary),
4810                ];
4811                let payload = params
4812                    .iter()
4813                    .map(|(k, v)| format!("{}={}", k, url_encode(v)))
4814                    .collect::<Vec<String>>()
4815                    .join("&");
4816                self.auth_code_intercept_internal(&url_skip_mfa_registration, payload)
4817                    .await
4818            }
4819            Err(e) => Err(e),
4820        }
4821    }
4822
4823    async fn exchange_authorization_code_for_access_token_internal(
4824        &self,
4825        authorization_code: String,
4826        resource: Option<&str>,
4827        custom_redirect_uri: Option<&str>,
4828    ) -> Result<UserToken, MsalError> {
4829        let redirect_uri = if let Some(custom_redirect_uri) = custom_redirect_uri {
4830            custom_redirect_uri.to_string()
4831        } else {
4832            self.app.get_auth_redirect_uri(None, resource)
4833        };
4834        let params = [
4835            ("client_id", self.client_id()),
4836            ("grant_type", "authorization_code"),
4837            ("code", &authorization_code),
4838            ("redirect_uri", &redirect_uri),
4839        ];
4840        let payload = params
4841            .iter()
4842            .map(|(k, v)| format!("{}={}", k, v))
4843            .collect::<Vec<String>>()
4844            .join("&");
4845
4846        let resp = self
4847            .client()
4848            .post(format!("{}/oauth2/token", self.authority()?))
4849            .header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
4850            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
4851            .body(payload)
4852            .send()
4853            .await
4854            .map_err(|e| MsalError::request_failed(&e))?;
4855        if resp.status().is_success() {
4856            let token: UserToken = resp
4857                .json()
4858                .await
4859                .map_err(|e| MsalError::InvalidJson(format!("Failed to parse UserToken: {}", e)))?;
4860
4861            Ok(token)
4862        } else {
4863            let text = resp.text().await.map_err(|e| {
4864                MsalError::RequestFailed(format!("Failed to read response text: {}", e))
4865            })?;
4866
4867            let json_resp: ErrorResponse = json_from_str(&text).map_err(|e| {
4868                MsalError::InvalidJson(format!(
4869                    "Failed to parse ErrorResponse: {}. Raw response: {}",
4870                    e, text
4871                ))
4872            })?;
4873            error!(
4874                "exchange_authorization_code_for_access_token_internal: {}",
4875                json_resp.error_description
4876            );
4877            Err(MsalError::AcquireTokenFailed(json_resp))
4878        }
4879    }
4880
4881    async fn exchange_accesspass_for_auth_code_internal(
4882        &self,
4883        username: &str,
4884        accesspass: &str,
4885        flow: &mut MFAAuthContinue,
4886    ) -> Result<String, MsalError> {
4887        let mut params = vec![
4888            ("login", username),
4889            ("loginfmt", username),
4890            ("accesspass", accesspass),
4891            ("canary", &flow.canary),
4892            ("hpgrequestid", &flow.session_id),
4893            ("flowToken", &flow.flow_token),
4894        ];
4895        if flow.url_post.contains("ProcessAuth") {
4896            params.push(("request", &flow.ctx));
4897        } else {
4898            params.push(("ctx", &flow.ctx));
4899        }
4900        let payload = params
4901            .iter()
4902            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
4903            .collect::<Vec<String>>()
4904            .join("&");
4905
4906        self.auth_code_intercept_internal(&flow.url_post, payload)
4907            .await
4908    }
4909
4910    async fn exchange_fido_assertion_for_auth_code_internal(
4911        &self,
4912        assertion: &str,
4913        flow: &mut MFAAuthContinue,
4914    ) -> Result<String, MsalError> {
4915        let cross_domain_canary = flow.cross_domain_canary.clone().ok_or(MsalError::Missing(
4916            "sCrossDomainCanary missing from response".to_string(),
4917        ))?;
4918        let params = [
4919            ("type", "23"),
4920            ("ps", "23"),
4921            ("assertion", assertion),
4922            ("lmcCanary", &cross_domain_canary),
4923            ("hpgrequestid", &flow.session_id),
4924            ("ctx", &flow.ctx),
4925            ("canary", &flow.canary),
4926            ("flowToken", &flow.flow_token),
4927        ];
4928        // Percent-encode the values: the assertion is a JSON string and the
4929        // canary/flowToken values are base64, so they contain characters
4930        // ('=', '+', '{', '"', ...) that corrupt a naively joined
4931        // application/x-www-form-urlencoded body.
4932        let payload = serde_urlencoded::to_string(params).map_err(|e| {
4933            MsalError::GeneralFailure(format!("Failed to encode FIDO assertion payload: {}", e))
4934        })?;
4935
4936        let mut resp = self
4937            .client()
4938            .post(&flow.url_post)
4939            .header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
4940            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
4941            .body(payload)
4942            .send()
4943            .await
4944            .map_err(|e| MsalError::request_failed(&e))?;
4945        let text;
4946        (text, resp) = self.await_working(resp).await?;
4947        debug!(
4948            "exchange_fido_assertion_for_auth_code_internal: status={}, body length={}",
4949            resp.status(),
4950            text.len()
4951        );
4952        if resp.status().is_redirection() {
4953            // First try the Location header directly, which is the standard
4954            // mechanism (matching auth_code_intercept_internal).
4955            if let Some(location) = resp.headers().get("location") {
4956                if let Ok(redirect) = location.to_str() {
4957                    if let Ok(url) = Url::parse(redirect) {
4958                        let params = url.query_pairs().collect::<Vec<_>>();
4959
4960                        if let Some((_, error_description)) =
4961                            params.iter().find(|(k, _)| k == "error_description")
4962                        {
4963                            return Err(MsalError::GeneralFailure(format!(
4964                                "Error in FIDO redirect: {}",
4965                                error_description
4966                            )));
4967                        }
4968
4969                        if let Some((_, code)) = params.iter().find(|(k, _)| k == "code") {
4970                            return Ok(code.to_string());
4971                        }
4972                    }
4973                }
4974            }
4975
4976            // Fall back to parsing the HTML body for an <a href> containing
4977            // the authorization code (legacy redirect body format).
4978            let document = Html::parse_document(&text);
4979            let selector = Selector::parse("a[href]").map_err(|_| {
4980                MsalError::InvalidParse("Failed parsing auth code response".to_string())
4981            })?;
4982            if let Some(element) = document.select(&selector).next() {
4983                if let Some(href_encoded) = element.value().attr("href") {
4984                    let href = percent_decode_str(href_encoded)
4985                        .decode_utf8()
4986                        .map_err(|e| {
4987                            MsalError::URLFormatFailed(format!("Failed decoding url: {:?}", e))
4988                        })?;
4989                    if let Ok(url) = Url::parse(&href) {
4990                        return url
4991                            .query_pairs()
4992                            .find_map(|(key, value)| {
4993                                if key == "code" {
4994                                    Some(value.into_owned())
4995                                } else {
4996                                    None
4997                                }
4998                            })
4999                            .ok_or(MsalError::GeneralFailure(format!(
5000                                "Authorization code not found in FIDO redirect. Body: {}",
5001                                text
5002                            )));
5003                    }
5004                }
5005            }
5006            Err(MsalError::GeneralFailure(format!(
5007                "Authorization code not found in FIDO redirect. Body: {}",
5008                text
5009            )))
5010        } else if resp.status().is_success() {
5011            // Check for a JavaScript document.location.replace redirect
5012            // containing the auth code (some Entra responses use this instead
5013            // of an HTTP redirect).
5014            let re = Regex::new(r#"document\.location\.replace\("([^"]+)"\)"#)
5015                .map_err(|e| MsalError::InvalidRegex(format!("{}", e)))?;
5016            if let Some(m) = re.captures(&text) {
5017                if let Some(redirect) = m.get(1) {
5018                    let redirect_decoded = Url::parse(&redirect.as_str().replace(r#"\u0026"#, "&"))
5019                        .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
5020                    for (k, v) in redirect_decoded.query_pairs().collect::<Vec<_>>() {
5021                        if k == "code" {
5022                            return Ok(v.to_string());
5023                        }
5024                        if k == "error_description" {
5025                            return Err(MsalError::GeneralFailure(v.to_string()));
5026                        }
5027                    }
5028                }
5029            }
5030
5031            // Try HTML <a href> parsing for error_description or code.
5032            let document = Html::parse_document(&text);
5033            let selector = Selector::parse("a[href]").map_err(|_| {
5034                MsalError::InvalidParse(format!("Failed parsing error response: {}", text))
5035            })?;
5036            if let Some(element) = document.select(&selector).next() {
5037                if let Some(href_encoded) = element.value().attr("href") {
5038                    let href = percent_decode_str(href_encoded)
5039                        .decode_utf8()
5040                        .map_err(|e| {
5041                            MsalError::URLFormatFailed(format!("Failed decoding url: {:?}", e))
5042                        })?;
5043                    if let Ok(url) = Url::parse(&href) {
5044                        for (key, value) in url.query_pairs() {
5045                            if key == "code" {
5046                                return Ok(value.to_string());
5047                            }
5048                            if key == "error_description" {
5049                                return Err(MsalError::GeneralFailure(format!(
5050                                    "error_description in FIDO response: {}",
5051                                    value
5052                                )));
5053                            }
5054                        }
5055                    }
5056                }
5057            }
5058
5059            // Try parsing an AuthConfig for specific errors.
5060            match self.parse_auth_config(&text, false, false) {
5061                #[cfg(feature = "changepassword")]
5062                Err(MsalError::ChangePassword) => return Err(MsalError::ChangePassword),
5063                Err(MsalError::AADSTSError(e)) => return Err(MsalError::AADSTSError(e)),
5064                Err(MsalError::ConsentRequested(e)) => return Err(MsalError::ConsentRequested(e)),
5065                Ok(auth_config) if auth_config.pgid.as_deref() == Some("ConvergedTFA") => {
5066                    return Err(MsalError::MFARequired);
5067                }
5068                _ => {}
5069            }
5070
5071            Err(MsalError::GeneralFailure(format!(
5072                "Authorization code not found in FIDO response. Body: {}",
5073                text
5074            )))
5075        } else {
5076            Err(MsalError::GeneralFailure(format!(
5077                "FIDO assertion request failed with status {}. Body: {}",
5078                resp.status(),
5079                text
5080            )))
5081        }
5082    }
5083
5084    /// Obtain token by a MFA flow object.
5085    ///
5086    /// # Arguments
5087    ///
5088    /// * `username` - Typically a UPN in the form of an email address.
5089    ///
5090    /// * `auth_data` - An optional token received for the MFA flow (some MFA
5091    ///   flows do not require input). For a FidoKey flow, this will be a fido
5092    ///   assertion.
5093    ///
5094    /// * `poll_attempt` - The polling attempt number.
5095    ///
5096    /// * `flow` - A MFAAuthContinue previously generated by
5097    ///   initiate_acquire_token_by_mfa_flow.
5098    ///
5099    /// # Returns
5100    ///
5101    /// * Success: A UserToken containing an access_token.
5102    /// * Failure: An MsalError, indicating the failure.
5103    pub async fn acquire_token_by_mfa_flow(
5104        &self,
5105        username: &str,
5106        auth_data: Option<&str>,
5107        poll_attempt: Option<u32>,
5108        flow: &mut MFAAuthContinue,
5109    ) -> Result<UserToken, MsalError> {
5110        // If we received an auth code directly from login (no MFA required),
5111        // exchange it for tokens immediately.
5112        if let Some(auth_code) = flow.auth_code.take() {
5113            return self
5114                .exchange_authorization_code_for_access_token_internal(
5115                    auth_code,
5116                    flow.resource.as_deref(),
5117                    None,
5118                )
5119                .await;
5120        }
5121
5122        if let Some(dag_flow) = &flow.dag {
5123            // This is a personal account DAG, not a work/school account DAG.
5124            if dag_flow.verification_uri.contains("www.microsoft.com/link") {
5125                return match self
5126                    .acquire_token_by_personal_device_flow(dag_flow.clone())
5127                    .await
5128                {
5129                    Ok(token) => {
5130                        if token.spn()?.to_lowercase() != username.to_lowercase() {
5131                            return Err(MsalError::GeneralFailure(
5132                                "The authenticating user did not match".to_string(),
5133                            ));
5134                        }
5135                        // Exchange the Portal MFA token for the requested token
5136                        // (unless the resource is enrollment.manage.microsoft.com,
5137                        // which a personal account cannot request).
5138                        if let Some(resource) = &flow.resource {
5139                            if !resource.contains("enrollment.manage.microsoft.com") {
5140                                let scope = format!("{}/.default", resource);
5141                                return self
5142                                    .acquire_token_by_refresh_token(
5143                                        &token.refresh_token,
5144                                        vec![&scope],
5145                                    )
5146                                    .await;
5147                            }
5148                        }
5149                        Ok(token)
5150                    }
5151                    Err(MsalError::AcquireTokenFailed(ref resp)) => {
5152                        if resp.error_codes.contains(&AUTH_PENDING) {
5153                            info!("Polling for acquire_token_by_personal_device_flow");
5154                            return Err(MsalError::MFAPollContinue);
5155                        }
5156                        error!(
5157                            "acquire_token_by_mfa_flow_internal: {}",
5158                            resp.error_description
5159                        );
5160                        Err(MsalError::AcquireTokenFailed(resp.clone()))
5161                    }
5162                    Err(e) => Err(e),
5163                };
5164            } else {
5165                // The initiate phase already fell back to a DAG
5166                return match self.acquire_token_by_device_flow(dag_flow.clone()).await {
5167                    Ok(token) => {
5168                        if token.spn()?.to_lowercase() != username.to_lowercase() {
5169                            return Err(MsalError::GeneralFailure(
5170                                "The authenticating user did not match".to_string(),
5171                            ));
5172                        }
5173                        // Exchange the Portal MFA token for the requested token
5174                        let token = if let Some(resource) = &flow.resource {
5175                            let scope = format!("{}/.default", resource);
5176                            self.acquire_token_by_refresh_token(&token.refresh_token, vec![&scope])
5177                                .await?
5178                        } else {
5179                            token
5180                        };
5181                        Ok(token)
5182                    }
5183                    Err(MsalError::AcquireTokenFailed(ref resp)) => {
5184                        if resp.error_codes.contains(&AUTH_PENDING) {
5185                            info!("Polling for acquire_token_by_device_flow");
5186                            return Err(MsalError::MFAPollContinue);
5187                        }
5188                        error!(
5189                            "acquire_token_by_mfa_flow_internal: {}",
5190                            resp.error_description
5191                        );
5192                        Err(MsalError::AcquireTokenFailed(resp.clone()))
5193                    }
5194                    Err(e) => Err(e),
5195                };
5196            }
5197        }
5198
5199        // Use the method that was selected during initiate_acquire_token
5200        let mfa_method = flow.selected_mfa_method_id.as_deref();
5201
5202        // Validate the selected method if provided
5203        if let Some(method) = mfa_method {
5204            if !flow.has_mfa_method(method) {
5205                return Err(MsalError::GeneralFailure(format!(
5206                    "Stored MFA method '{}' is not available. Available methods: {:?}",
5207                    method,
5208                    flow.get_available_mfa_methods().join(", ")
5209                )));
5210            }
5211        }
5212
5213        let selected_mfa_method = match mfa_method {
5214            Some(method) => flow.get_mfa_method_by_id(method),
5215            None => flow.get_default_mfa_method_details(),
5216        };
5217
5218        let selected_mfa_method = match selected_mfa_method {
5219            Some(value) => value,
5220            None => {
5221                let method_desc = mfa_method.unwrap_or("default");
5222                return Err(MsalError::GeneralFailure(format!(
5223                    "Unable to determine MFA method details - selected method was: {}",
5224                    method_desc
5225                )));
5226            }
5227        };
5228
5229        match auth_data {
5230            Some(auth_data) => {
5231                if selected_mfa_method.auth_method_id == "FidoKey" {
5232                    let auth_code = self
5233                        .exchange_fido_assertion_for_auth_code_internal(auth_data, flow)
5234                        .await?;
5235                    self.exchange_authorization_code_for_access_token_internal(
5236                        auth_code,
5237                        flow.resource.as_deref(),
5238                        None,
5239                    )
5240                    .await
5241                } else if selected_mfa_method.auth_method_id == "AccessPass" {
5242                    let auth_code = self
5243                        .exchange_accesspass_for_auth_code_internal(username, auth_data, flow)
5244                        .await?;
5245                    self.exchange_authorization_code_for_access_token_internal(
5246                        auth_code,
5247                        flow.resource.as_deref(),
5248                        None,
5249                    )
5250                    .await
5251                } else {
5252                    let payload = json!({
5253                        "AdditionalAuthData": auth_data.trim(),
5254                        "AuthMethodId": &selected_mfa_method.auth_method_id,
5255                        "SessionId": &flow.session_id,
5256                        "FlowToken": &flow.flow_token,
5257                        "Ctx": &flow.ctx,
5258                        "Method": "EndAuth",
5259                    });
5260                    let url_end_auth = match &flow.url_end_auth {
5261                        Some(url_end_auth) => url_end_auth,
5262                        None => {
5263                            return Err(MsalError::GeneralFailure(
5264                                "urlEndAuth is missing".to_string(),
5265                            ))
5266                        }
5267                    };
5268
5269                    let resp = self
5270                        .client()
5271                        .post(url_end_auth)
5272                        .header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
5273                        .header(header::CONTENT_TYPE, "application/json; charset=utf-8")
5274                        .header("canary", &flow.canary)
5275                        .json(&payload)
5276                        .send()
5277                        .await
5278                        .map_err(|e| match MsalError::request_failed(&e) {
5279                            MsalError::RequestFailed(msg) => MsalError::RequestFailed(format!(
5280                                "Request to {} failed: {}",
5281                                url_end_auth, msg
5282                            )),
5283                            other => other,
5284                        })?;
5285                    if resp.status().is_success() {
5286                        let text = resp.text().await.map_err(|e| {
5287                            MsalError::GeneralFailure(format!("Response decoding failed: {}", e))
5288                        })?;
5289                        // Check for an error in an auth Config
5290                        if let Ok(auth_config) = self.parse_auth_config(&text, false, false) {
5291                            if let Some(service_exception_msg) = auth_config.service_exception_msg {
5292                                return Err(MsalError::GeneralFailure(
5293                                    format!("Service exception during acquire_token_by_mfa_flow_internal(): {}", service_exception_msg),
5294                                ));
5295                            }
5296                        }
5297                        // Parse what should be a json response otherwise
5298                        let auth_response: AuthResponse = json_from_str(&text)
5299                            .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
5300                        if auth_response.success {
5301                            flow.ctx = auth_response.ctx;
5302                            flow.flow_token = auth_response.flow_token;
5303                            let auth_code = self
5304                                .request_authorization_internal(
5305                                    username,
5306                                    flow,
5307                                    &selected_mfa_method,
5308                                )
5309                                .await?;
5310                            self.exchange_authorization_code_for_access_token_internal(
5311                                auth_code,
5312                                flow.resource.as_deref(),
5313                                None,
5314                            )
5315                            .await
5316                        } else if let Some(msg) = auth_response.message {
5317                            // The submitted code was wrong/expired but the flow
5318                            // is still valid. Surface a structured error so
5319                            // consumers can re-prompt for the code. We don't
5320                            // gate on auth_response.retry here because Entra's
5321                            // code-submission path is not confirmed to always
5322                            // include it (unlike the polling branch), and
5323                            // every message-bearing failure on this path is
5324                            // retryable in practice.
5325                            Err(MsalError::MFAInvalidCode(msg))
5326                        } else {
5327                            Err(MsalError::GeneralFailure("EndAuth failed".to_string()))
5328                        }
5329                    } else {
5330                        Err(MsalError::GeneralFailure(
5331                            "EndAuth Authentication request failed".to_string(),
5332                        ))
5333                    }
5334                }
5335            }
5336            None => {
5337                let resp = if let Some(url_end_auth) = &flow.url_end_auth {
5338                    let url = Url::parse_with_params(
5339                        url_end_auth,
5340                        [
5341                            ("authMethodId", &selected_mfa_method.auth_method_id),
5342                            (
5343                                "pollCount",
5344                                &format!(
5345                                    "{}",
5346                                    poll_attempt.ok_or(MsalError::GeneralFailure(
5347                                        "Poll attempt required".to_string()
5348                                    ))?
5349                                ),
5350                            ),
5351                        ],
5352                    )
5353                    .map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
5354
5355                    self.client()
5356                        .get(url)
5357                        .header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
5358                        .header("x-ms-sessionId", &flow.session_id)
5359                        .header("x-ms-flowToken", &flow.flow_token)
5360                        .header("x-ms-ctx", &flow.ctx)
5361                        .send()
5362                        .await
5363                        .map_err(|e| MsalError::request_failed(&e))?
5364                } else if let Some(url_session_state) = &flow.url_session_state {
5365                    let url =
5366                        Url::parse_with_params(url_session_state, [("code", &flow.session_id)])
5367                            .map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
5368                    let payload = json!({
5369                        "DeviceCode": &flow.session_id,
5370                    });
5371
5372                    self.client()
5373                        .post(url)
5374                        .header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
5375                        .header(header::CONTENT_TYPE, "application/json")
5376                        .header("canary", &flow.canary)
5377                        .json(&payload)
5378                        .send()
5379                        .await
5380                        .map_err(|e| MsalError::request_failed(&e))?
5381                } else {
5382                    return Err(MsalError::GeneralFailure("Request invalid".to_string()));
5383                };
5384                if resp.status().is_success() {
5385                    let text = resp.text().await.map_err(|e| {
5386                        MsalError::GeneralFailure(format!("Response decoding failed: {}", e))
5387                    })?;
5388                    if flow.url_end_auth.is_some() {
5389                        // Check for an error in an auth Config
5390                        if let Ok(auth_config) = self.parse_auth_config(&text, false, false) {
5391                            if let Some(service_exception_msg) = auth_config.service_exception_msg {
5392                                return Err(MsalError::GeneralFailure(format!(
5393                                    "Service exception: {}",
5394                                    service_exception_msg
5395                                )));
5396                            }
5397                        }
5398                        // Parse what should be a json response otherwise
5399                        let auth_response: AuthResponse = json_from_str(&text)
5400                            .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
5401                        if auth_response.success {
5402                            flow.ctx = auth_response.ctx;
5403                            flow.flow_token = auth_response.flow_token;
5404                            let auth_code = self
5405                                .request_authorization_internal(
5406                                    username,
5407                                    flow,
5408                                    &selected_mfa_method,
5409                                )
5410                                .await?;
5411                            return self
5412                                .exchange_authorization_code_for_access_token_internal(
5413                                    auth_code,
5414                                    flow.resource.as_deref(),
5415                                    None,
5416                                )
5417                                .await;
5418                        } else if !auth_response.retry.ok_or(MsalError::GeneralFailure(
5419                            "Auth response Retry missing".to_string(),
5420                        ))? {
5421                            // The server explicitly said not to retry. Treat
5422                            // this terminal MFA response as an authorization
5423                            // denial rather than restarting a dead flow.
5424                            return Err(MsalError::AuthorizationDenied);
5425                        }
5426                        Err(MsalError::MFAPollContinue)
5427                    } else {
5428                        let status: DeviceCodeStatus = json_from_str(&text)
5429                            .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
5430                        if status.authorization_state == 0 {
5431                            Err(MsalError::MFAPollContinue)
5432                        } else if status.authorization_state == 1 {
5433                            Err(MsalError::AuthorizationDenied)
5434                        } else if status.authorization_state == 2 {
5435                            let auth_code = self
5436                                .request_authorization_passwordless_internal(username, flow)
5437                                .await?;
5438                            return self
5439                                .exchange_authorization_code_for_access_token_internal(
5440                                    auth_code,
5441                                    flow.resource.as_deref(),
5442                                    None,
5443                                )
5444                                .await;
5445                        } else {
5446                            Err(MsalError::GeneralFailure(format!(
5447                                "Unexpected authorization_state in DeviceCodeStatus {}: {}",
5448                                status.authorization_state, text
5449                            )))
5450                        }
5451                    }
5452                } else {
5453                    Err(MsalError::GeneralFailure(
5454                        "EndAuth Authentication request failed".to_string(),
5455                    ))
5456                }
5457            }
5458        }
5459    }
5460
5461    fn get_auth_redirect_uri(&self, client_id: Option<&str>, resource: Option<&str>) -> String {
5462        self.app.get_auth_redirect_uri(client_id, resource)
5463    }
5464}
5465
5466struct EnrollmentKeyWrapper {
5467    key: RS256Key,
5468    cert: Certificate,
5469}
5470
5471/// The private key a P2P certificate is bound to.
5472///
5473/// The device certificate is issued against the existing device enrolment key,
5474/// while a user certificate is issued against a key generated for the request.
5475/// A user key must be persisted by the caller alongside the certificate, or the
5476/// certificate is useless.
5477#[cfg(feature = "broker")]
5478#[derive(Clone, Serialize, Deserialize)]
5479pub enum P2PPrivateKey {
5480    ExistingDevice(LoadableMsDeviceEnrolmentKey),
5481    GeneratedUser(LoadableRS256Key),
5482}
5483
5484#[cfg(feature = "broker")]
5485#[derive(Clone, Serialize, Deserialize)]
5486pub struct P2PCertificate {
5487    pub certificate_der: Vec<u8>,
5488    /// The DER encoded issuing CA certificate, as returned by Entra.
5489    pub ca_certificate_der: Vec<u8>,
5490    /// The PEM encoded issuing CA certificate. `None` when the CA material
5491    /// returned by Entra could not be parsed as a certificate.
5492    pub ca_certificate_pem: Option<String>,
5493    pub subject: String,
5494    pub issuer: String,
5495    pub thumbprint_sha1: String,
5496    pub dns_names: Vec<String>,
5497    /// The certificate expiry, in seconds since the unix epoch.
5498    pub not_after_unix: i64,
5499    pub private_key: P2PPrivateKey,
5500}
5501
5502#[cfg(feature = "broker")]
5503impl fmt::Debug for P2PCertificate {
5504    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5505        f.debug_struct("P2PCertificate")
5506            .field("subject", &self.subject)
5507            .field("issuer", &self.issuer)
5508            .field("thumbprint_sha1", &self.thumbprint_sha1)
5509            .field("dns_names", &self.dns_names)
5510            .field("not_after_unix", &self.not_after_unix)
5511            .field("private_key", &"<redacted>")
5512            .finish()
5513    }
5514}
5515
5516/// PKCS#10 CertificationRequestInfo with an EMPTY constructed context [0] tag.
5517/// Microsoft requires the attributes field to be present as an empty CONSTRUCTED
5518/// tag, which is why this is hand rolled rather than using
5519/// `x509_cert::request::CertReqInfo`.
5520#[cfg(feature = "broker")]
5521#[derive(Clone, Debug, PartialEq, Eq, Sequence)]
5522struct P2PCertReqInfo {
5523    pub version: x509_cert::request::Version,
5524    pub subject: Name,
5525    pub public_key: SubjectPublicKeyInfoOwned,
5526    #[asn1(context_specific = "0", tag_mode = "IMPLICIT")]
5527    pub attributes: SetOfVec<Attribute>,
5528}
5529
5530#[cfg(feature = "broker")]
5531pub struct BrokerClientApplication {
5532    app: PublicClientApplication,
5533    transport_key: Option<LoadableMsOapxbcRsaKey>,
5534    cert_key: Option<LoadableMsDeviceEnrolmentKey>,
5535    on_behalf_of_client_id: Option<String>,
5536}
5537
5538#[cfg(feature = "broker")]
5539impl BrokerClientApplication {
5540    /// Create an instance of an application.
5541    ///
5542    /// # Arguments
5543    ///
5544    /// * `authority` - A URL that identifies a token authority. It should
5545    ///   be of the format <https://login.microsoftonline.com/your_tenant> By
5546    ///   default, we will use <https://login.microsoftonline.com/common>.
5547    ///
5548    /// * `client_id` - The optional client id of an app which you may
5549    ///   register in Azure Entra Id. If not specified, certain group
5550    ///   attributes will be unresolvable. This app may also delegate
5551    ///   permissions for your logon script access token.
5552    ///
5553    /// * `transport_key` - An optional LoadableMsOapxbcRsaKey transport key
5554    ///   from enrolling the device.
5555    ///
5556    /// * `cert_key` - An optional LoadableMsDeviceEnrolmentKey which was used to create
5557    ///   the enrollment CSR.
5558    ///
5559    /// NOTE: If `transport_key` and `cert_key` are not provided from a previous
5560    /// device enrollment, then enrollment will be required.
5561    pub fn new(
5562        authority: Option<&str>,
5563        client_id: Option<&str>,
5564        transport_key: Option<LoadableMsOapxbcRsaKey>,
5565        cert_key: Option<LoadableMsDeviceEnrolmentKey>,
5566        #[cfg(feature = "set_timeout")] timeout: Duration,
5567        #[cfg(feature = "ipvers")] ip_version: &[IpVersion],
5568    ) -> Result<Self, MsalError> {
5569        Ok(BrokerClientApplication {
5570            app: PublicClientApplication::new(
5571                BROKER_APP_ID,
5572                authority,
5573                #[cfg(feature = "set_timeout")]
5574                timeout,
5575                #[cfg(feature = "ipvers")]
5576                ip_version,
5577            )?,
5578            transport_key,
5579            cert_key,
5580            on_behalf_of_client_id: client_id.map(|s| s.to_string()),
5581        })
5582    }
5583
5584    fn client(&self) -> &Client {
5585        self.app.client()
5586    }
5587
5588    /// Clears all cookies stored in the internal cookie jar
5589    ///
5590    /// This forcibly removes all session and persistent cookies
5591    /// currently held by the HTTP client. Useful for resetting
5592    /// authentication state or periodically cycling cookies to
5593    /// avoid issues with long-lived sessions.
5594    ///
5595    /// # Remarks
5596    ///
5597    /// This function does not return an error on failure, but will
5598    /// log an error message if the internal cookie store cannot be
5599    /// locked.
5600    pub fn clear_cookies(&self) {
5601        self.app.clear_cookies()
5602    }
5603
5604    fn authority(&self) -> Result<String, MsalError> {
5605        self.app.authority()
5606    }
5607
5608    /// Changes the authority url set at initialization time
5609    ///
5610    /// # Arguments
5611    ///
5612    /// * `new_authority` - The new authority url to be used when communicating
5613    ///   with Entra Id.
5614    ///
5615    /// # Returns
5616    /// * Failure: An MsalError, indicating the failure.
5617    pub fn set_authority(&self, new_authority: &str) -> Result<(), MsalError> {
5618        self.app.set_authority(new_authority)
5619    }
5620
5621    fn transport_key(
5622        &self,
5623        tpm: &mut BoxedDynTpm,
5624        storage_key: &StorageKey,
5625    ) -> Result<MsOapxbcRsaKey, MsalError> {
5626        let transport_key = &self.transport_key.as_ref()
5627            .ok_or_else(||
5628                MsalError::ConfigError("The transport key was not found. Please provide the transport key during initialize of the BrokerClientApplication, or enroll the device.".to_string())
5629            )?;
5630
5631        tpm.msoapxbc_rsa_key_load(storage_key, transport_key)
5632            .map_err(|e| MsalError::TPMFail(format!("Failed to load Msoapxbc: {:?}", e)))
5633    }
5634
5635    /// Set the enrollment transport key
5636    ///
5637    /// # Arguments
5638    ///
5639    /// * `transport_key` - An optional LoadableMsOapxbcRsaKey transport key
5640    ///   from enrolling the device.
5641    pub fn set_transport_key(&mut self, transport_key: Option<LoadableMsOapxbcRsaKey>) {
5642        self.transport_key = transport_key;
5643    }
5644
5645    fn cert_key(
5646        &self,
5647        tpm: &mut BoxedDynTpm,
5648        storage_key: &StorageKey,
5649    ) -> Result<EnrollmentKeyWrapper, MsalError> {
5650        let cert_key = self.cert_key.clone()
5651            .ok_or_else(||
5652                MsalError::ConfigError("The certificate key was not found. Please provide the certificate key during initialize of the BrokerClientApplication, or enroll the device.".to_string())
5653            )?;
5654
5655        let (key, cert) = tpm
5656            .ms_device_enrolment_key_load(storage_key, cert_key)
5657            .map_err(|e| MsalError::TPMFail(format!("Failed to load IdentityKey: {:?}", e)))?;
5658        Ok(EnrollmentKeyWrapper { key, cert })
5659    }
5660
5661    /// Set the enrollment certificate key
5662    ///
5663    /// # Arguments
5664    ///
5665    /// * `cert_key` - An optional LoadableMsDeviceEnrolmentKey which was used to create
5666    ///   the enrollment CSR.
5667    pub fn set_cert_key(&mut self, cert_key: Option<LoadableMsDeviceEnrolmentKey>) {
5668        self.cert_key = cert_key;
5669    }
5670
5671    /// Request a device P2P certificate using the enrolled device certificate.
5672    ///
5673    /// # Arguments
5674    ///
5675    /// * `tenant_id` - A fallback Entra tenant id or verified domain, used only
5676    ///   when the device certificate carries no tenant OID. When the certificate
5677    ///   does carry one it takes precedence and this argument is ignored.
5678    /// * `device_name` - Device name used as the netbios name and default DNS name.
5679    /// * `dns_names` - Optional DNS names to request. Defaults to `device_name`.
5680    /// * `tpm` - The TPM object.
5681    /// * `storage_key` - The TPM MachineKey associated with this application.
5682    ///
5683    /// # Returns
5684    /// * Success: The issued P2P certificate material and metadata.
5685    /// * Failure: An MsalError, indicating the failure.
5686    pub async fn acquire_device_p2p_certificate(
5687        &self,
5688        tenant_id: &str,
5689        device_name: &str,
5690        dns_names: Option<&[&str]>,
5691        tpm: &mut BoxedDynTpm,
5692        storage_key: &StorageKey,
5693    ) -> Result<P2PCertificate, MsalError> {
5694        debug!("Acquiring a device P2P certificate");
5695
5696        let loadable_cert_key = self.cert_key.clone().ok_or_else(|| {
5697            MsalError::ConfigError(
5698                "The certificate key was not found. Please provide the certificate key during initialize of the BrokerClientApplication, or enroll the device.".to_string(),
5699            )
5700        })?;
5701        let cert_key = self.cert_key(tpm, storage_key)?;
5702        let cert_der = cert_key.cert.to_der().map_err(|e| {
5703            MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
5704        })?;
5705        let request_tenant_id =
5706            Self::device_certificate_tenant_id(&cert_der)?.unwrap_or_else(|| tenant_id.to_string());
5707        if request_tenant_id != tenant_id {
5708            warn!(
5709                "P2P device certificate tenant {} differs from requested tenant {}; using certificate tenant",
5710                request_tenant_id, tenant_id
5711            );
5712        }
5713        let token_endpoint = self.tenant_token_endpoint(&request_tenant_id)?;
5714        let nonce = self.request_nonce_from_endpoint(&token_endpoint).await?;
5715        let cert = X509::from_der(&cert_der).map_err(|e| {
5716            MsalError::CryptoFail(format!("Failed to create X509 from DER: {:?}", e))
5717        })?;
5718        let subject = Name::from_der(
5719            cert.subject_name()
5720                .to_der()
5721                .map_err(|e| MsalError::CryptoFail(format!("Failed encoding subject: {}", e)))?
5722                .as_slice(),
5723        )
5724        .map_err(|e| MsalError::CryptoFail(format!("Failed parsing subject: {:?}", e)))?;
5725        let (csr_der, public_key_der) = Self::create_p2p_csr(tpm, &cert_key.key, subject)?;
5726        let csr = STANDARD.encode(&csr_der);
5727        let dns_names_vec = dns_names
5728            .map(|names| names.iter().map(|name| name.to_string()).collect())
5729            .unwrap_or_else(|| vec![device_name.to_string()]);
5730
5731        let payload = P2PDeviceCertificatePayload::new(&nonce, &csr, device_name, &dns_names_vec);
5732        if let Ok(pretty) = to_string_pretty(&payload) {
5733            debug!("P2P Device Certificate Payload: {}", pretty);
5734        }
5735
5736        let signed_jwt = Self::sign_p2p_device_jwt(&payload, &cert_der, tpm, &cert_key.key)?;
5737        let response = self
5738            .post_p2p_certificate_request(&token_endpoint, &signed_jwt, "2.0")
5739            .await?;
5740
5741        Self::p2p_certificate_from_response(
5742            &response,
5743            P2PPrivateKey::ExistingDevice(loadable_cert_key),
5744            &public_key_der,
5745        )
5746    }
5747
5748    /// Request a user P2P certificate using a sealed Primary Refresh Token.
5749    ///
5750    /// # Arguments
5751    ///
5752    /// * `sealed_prt` - An encrypted Primary Refresh Token.
5753    /// * `tpm` - The TPM object.
5754    /// * `storage_key` - The TPM MachineKey associated with this application.
5755    ///
5756    /// # Returns
5757    /// * Success: The issued P2P certificate material and metadata.
5758    /// * Failure: An MsalError, indicating the failure.
5759    pub async fn acquire_user_p2p_certificate(
5760        &self,
5761        sealed_prt: &SealedData,
5762        tpm: &mut BoxedDynTpm,
5763        storage_key: &StorageKey,
5764    ) -> Result<P2PCertificate, MsalError> {
5765        debug!("Acquiring a user P2P certificate");
5766
5767        let transport_key = self.transport_key(tpm, storage_key)?;
5768        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
5769        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
5770        let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
5771        let tenant_id = if !prt.id_token.tid.is_empty() {
5772            prt.id_token.tid.clone()
5773        } else if let Some(utid) = prt.client_info.utid {
5774            utid.to_string()
5775        } else {
5776            return Err(MsalError::GeneralFailure(
5777                "No tenant id available for P2P user certificate request".to_string(),
5778            ));
5779        };
5780        let token_endpoint = self.tenant_token_endpoint(&tenant_id)?;
5781        let nonce = self.request_nonce_from_endpoint(&token_endpoint).await?;
5782        let session_key = prt.session_key()?;
5783
5784        let loadable_user_key = tpm
5785            .rs256_create(storage_key)
5786            .map_err(|e| MsalError::TPMFail(format!("Failed creating P2P user key: {:?}", e)))?;
5787        let user_key = tpm
5788            .rs256_load(storage_key, &loadable_user_key)
5789            .map_err(|e| MsalError::TPMFail(format!("Failed loading P2P user key: {:?}", e)))?;
5790        let subject = Name::from_str("CN=")
5791            .map_err(|e| MsalError::CryptoFail(format!("Failed parsing subject: {:?}", e)))?;
5792        let (csr_der, public_key_der) = Self::create_p2p_csr(tpm, &user_key, subject)?;
5793        let csr = STANDARD.encode(&csr_der);
5794
5795        let payload = P2PUserCertificatePayload::new(&prt, &nonce, &csr);
5796        if let Ok(pretty) = payload.redacted().and_then(|redacted| {
5797            to_string_pretty(&redacted).map_err(|e| MsalError::InvalidJson(format!("{}", e)))
5798        }) {
5799            debug!("P2P User Certificate Payload: {}", pretty);
5800        }
5801
5802        let signed_jwt =
5803            self.sign_p2p_user_jwt(&payload, tpm, storage_key, &transport_key, &session_key)?;
5804        let response = self
5805            .post_p2p_certificate_request(&token_endpoint, &signed_jwt, "1.0")
5806            .await?;
5807
5808        Self::p2p_certificate_from_response(
5809            &response,
5810            P2PPrivateKey::GeneratedUser(loadable_user_key),
5811            &public_key_der,
5812        )
5813    }
5814
5815    /// Enroll the device in the directory.
5816    ///
5817    /// # Arguments
5818    ///
5819    /// * `token` - Token obtained via either
5820    ///   acquire_token_by_username_password_for_device_enrollment
5821    ///   or acquire_token_by_device_flow.
5822    ///
5823    /// * `domain` - The domain the device is to be enrolled in.
5824    ///
5825    /// * `tpm` - The tpm object.
5826    ///
5827    /// * `storage_key` - The TPM MachineKey associated with this application.
5828    ///
5829    /// # Returns
5830    ///
5831    /// * Success: A LoadableMsOapxbcRsaKey transport key, a LoadableMsDeviceEnrolmentKey certificate key,
5832    ///   and a `device_id`.
5833    /// * Failure: An MsalError, indicating the failure.
5834    pub async fn enroll_device(
5835        &mut self,
5836        refresh_token: &str,
5837        attrs: EnrollAttrs,
5838        tpm: &mut BoxedDynTpm,
5839        storage_key: &StorageKey,
5840    ) -> Result<(LoadableMsOapxbcRsaKey, LoadableMsDeviceEnrolmentKey, String), MsalError> {
5841        // Acquire an actual enrollment token from the token received.
5842        let token = self
5843            .acquire_token_by_refresh_token_for_device_enrollment(refresh_token)
5844            .await?;
5845        // Create the transport and cert keys
5846        // Create the CSR
5847        let (in_progess_enrolment, csr) = tpm
5848            .ms_device_enrolment_begin(storage_key, "7E980AD9-B86D-4306-9425-9AC066FB014A")
5849            .map_err(|e| MsalError::TPMFail(format!("Failed creating certificate key: {:?}", e)))?;
5850
5851        // We need to make the csr into der here, or we can can just yield the der from ms_device_enrolment_begining.
5852        let csr_der = csr.to_der().map_err(|_|
5853                // TODO: David, is this the right error here?
5854                MsalError::GeneralFailure("Unable to convert X509 Request to DER".into()))?;
5855
5856        let loadable_transport_key = tpm
5857            .msoapxbc_rsa_key_create(storage_key)
5858            .map_err(|e| MsalError::TPMFail(format!("Failed creating transport key: {:?}", e)))?;
5859        self.transport_key = Some(loadable_transport_key.clone());
5860
5861        // Load the transport key
5862        let transport_key = match tpm.msoapxbc_rsa_key_load(storage_key, &loadable_transport_key) {
5863            Ok(transport_key) => transport_key,
5864            Err(e) => {
5865                return Err(MsalError::TPMFail(format!(
5866                    "Failed loading id key: {:?}",
5867                    e
5868                )))
5869            }
5870        };
5871
5872        let transport_key_der = tpm
5873            .msoapxbc_rsa_public_as_der(&transport_key)
5874            .map_err(|err| {
5875                MsalError::TPMFail(format!("Failed getting transport key as der: {:?}", err))
5876            })?;
5877
5878        let transport_key_rsa = Rsa::public_key_from_der(&transport_key_der)
5879            .map_err(|e| MsalError::TPMFail(format!("{}", e)))?;
5880
5881        let (cert, device_id) = match &token.access_token {
5882            Some(access_token) => {
5883                self.enroll_device_internal(access_token, attrs, &transport_key_rsa, &csr_der)
5884                    .await?
5885            }
5886            None => {
5887                return Err(MsalError::GeneralFailure(
5888                    "Access token not found".to_string(),
5889                ))
5890            }
5891        };
5892
5893        let cert_der = cert.to_der().map_err(|_|
5894                // TODO: David, is this the right error here?
5895                MsalError::GeneralFailure("Unable to convert X509 to DER".into()))?;
5896
5897        // To help prevent mismatches between the tpm crypto lib and openssl, we
5898        // want the certificate *der* here. This way you don't have to worry about it
5899        // as much when you load, and it saves you having to do the translation.
5900        let new_loadable_cert_key = tpm
5901            .ms_device_enrolment_finalise(storage_key, in_progess_enrolment, &cert_der)
5902            .map_err(|err| {
5903                MsalError::TPMFail(format!("Failed creating loadable identity key: {:?}", err))
5904            })?;
5905
5906        self.cert_key = Some(new_loadable_cert_key.clone());
5907        Ok((
5908            loadable_transport_key,
5909            new_loadable_cert_key,
5910            device_id.to_string(),
5911        ))
5912    }
5913
5914    async fn enroll_device_internal(
5915        &self,
5916        access_token: &str,
5917        attrs: EnrollAttrs,
5918        transport_key: &Rsa<Public>,
5919        csr_der: &Vec<u8>,
5920    ) -> Result<(X509, String), MsalError> {
5921        let services = Services::new(
5922            access_token,
5923            &attrs.target_domain,
5924            #[cfg(feature = "set_timeout")]
5925            self.app.app.timeout,
5926            #[cfg(feature = "ipvers")]
5927            &self.app.app.ip_version,
5928        )
5929        .await?;
5930        services
5931            .enroll_device(access_token, attrs, transport_key, csr_der)
5932            .await
5933    }
5934
5935    /// Gets a token for a given resource via user credentials.
5936    ///
5937    /// # Arguments
5938    ///
5939    /// * `username` - Typically a UPN in the form of an email address.
5940    ///
5941    /// * `password` - The password.
5942    ///
5943    /// * `scopes` - Scopes requested to access a protected API (a resource).
5944    ///
5945    /// * `request_resource` - A resource for obtaining an access token.
5946    ///   Default is the MS Graph API (00000002-0000-0000-c000-000000000000).
5947    ///
5948    /// * `on_behalf_of_client_id`: Request a resource or scope on behalf of
5949    ///   the specified client_id. Optional. This option requires libhimmelblau
5950    ///   be built with the `on_behalf_of` feature.
5951    ///
5952    /// * `tpm` - The tpm object.
5953    ///
5954    /// * `storage_key` - The TPM MachineKey associated with this application.
5955    ///
5956    /// # Returns
5957    /// * Success: A UserToken containing an access_token.
5958    /// * Failure: An MsalError, indicating the failure.
5959    pub async fn acquire_token_by_username_password(
5960        &self,
5961        username: &str,
5962        password: &str,
5963        scopes: Vec<&str>,
5964        request_resource: Option<String>,
5965        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
5966        tpm: &mut BoxedDynTpm,
5967        storage_key: &StorageKey,
5968    ) -> Result<UserToken, MsalError> {
5969        let v2_endpoint = !scopes.is_empty();
5970        if !scopes.is_empty() && request_resource.is_some() {
5971            return Err(MsalError::GeneralFailure(
5972                "Scopes cannot be specified with a request_resource".to_string(),
5973            ));
5974        }
5975        let prt = self
5976            .acquire_user_prt_by_username_password_internal(username, password, tpm, storage_key)
5977            .await?;
5978        let transport_key = self.transport_key(tpm, storage_key)?;
5979
5980        // This allows a transition, where existing msoapxbc keys will provide
5981        // their sealing content encryption keys, but newer keys will not, falling back
5982        // to the provided key.
5983        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
5984        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
5985
5986        let session_key = prt.session_key()?;
5987        let mut token = self
5988            .exchange_prt_for_access_token_internal(
5989                &prt,
5990                scopes.clone(),
5991                v2_endpoint,
5992                tpm,
5993                storage_key,
5994                &session_key,
5995                request_resource,
5996                #[cfg(feature = "on_behalf_of")]
5997                on_behalf_of_client_id,
5998                #[cfg(feature = "redirect_uri")]
5999                None,
6000                #[cfg(feature = "pop_support")]
6001                None,
6002                false,
6003            )
6004            .await?;
6005        token.client_info = prt.client_info.clone();
6006        token.prt = Some(self.seal_user_prt(&prt, tpm, prt_storage_key)?);
6007        Ok(token)
6008    }
6009
6010    /// Acquire token(s) based on a refresh token (RT) obtained from elsewhere.
6011    ///
6012    /// # Arguments
6013    ///
6014    /// * `refresh_token` - The old refresh token, as a string.
6015    ///
6016    /// * `scopes` - The scopes associated with this old RT.
6017    ///
6018    /// * `request_resource` - A resource for obtaining an access token.
6019    ///   Default is the MS Graph API (00000002-0000-0000-c000-000000000000).
6020    ///
6021    /// * `on_behalf_of_client_id`: Request a resource or scope on behalf of
6022    ///   the specified client_id. Optional. This option requires libhimmelblau
6023    ///   be built with the `on_behalf_of` feature.
6024    ///
6025    /// * `tpm` - The tpm object.
6026    ///
6027    /// * `storage_key` - The TPM MachineKey associated with this application.
6028    ///
6029    /// # Returns
6030    /// * Success: A UserToken, which means migration was successful.
6031    /// * Failure: An MsalError, indicating the failure.
6032    pub async fn acquire_token_by_refresh_token(
6033        &self,
6034        refresh_token: &str,
6035        scopes: Vec<&str>,
6036        request_resource: Option<String>,
6037        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
6038        tpm: &mut BoxedDynTpm,
6039        storage_key: &StorageKey,
6040    ) -> Result<UserToken, MsalError> {
6041        self.acquire_token_by_refresh_token_internal(
6042            refresh_token,
6043            scopes,
6044            request_resource,
6045            #[cfg(feature = "on_behalf_of")]
6046            on_behalf_of_client_id,
6047            false,
6048            tpm,
6049            storage_key,
6050        )
6051        .await
6052    }
6053
6054    async fn acquire_token_by_refresh_token_internal(
6055        &self,
6056        refresh_token: &str,
6057        scopes: Vec<&str>,
6058        request_resource: Option<String>,
6059        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
6060        demand_mfa: bool,
6061        tpm: &mut BoxedDynTpm,
6062        storage_key: &StorageKey,
6063    ) -> Result<UserToken, MsalError> {
6064        let prt = self
6065            .acquire_user_prt_by_refresh_token_internal(refresh_token, tpm, storage_key)
6066            .await?;
6067        let transport_key = self.transport_key(tpm, storage_key)?;
6068
6069        // This allows a transition, where existing msoapxbc keys will provide
6070        // their sealing content encryption keys, but newer keys will not, falling back
6071        // to the provided key.
6072        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
6073        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
6074
6075        let session_key = prt.session_key()?;
6076        let v2_endpoint = !scopes.is_empty();
6077        if !scopes.is_empty() && request_resource.is_some() {
6078            return Err(MsalError::GeneralFailure(
6079                "Scopes cannot be specified with a request_resource".to_string(),
6080            ));
6081        }
6082        let mut token = self
6083            .exchange_prt_for_access_token_internal(
6084                &prt,
6085                scopes.clone(),
6086                v2_endpoint,
6087                tpm,
6088                storage_key,
6089                &session_key,
6090                request_resource,
6091                #[cfg(feature = "on_behalf_of")]
6092                on_behalf_of_client_id,
6093                #[cfg(feature = "redirect_uri")]
6094                None,
6095                #[cfg(feature = "pop_support")]
6096                None,
6097                demand_mfa,
6098            )
6099            .await?;
6100        token.client_info = prt.client_info.clone();
6101        token.prt = Some(self.seal_user_prt(&prt, tpm, prt_storage_key)?);
6102        Ok(token)
6103    }
6104
6105    /// Initiate an OAuth 2.0 authorization code flow with PKCE.
6106    ///
6107    /// The caller is responsible for opening or presenting the returned URL
6108    /// and capturing the final redirect URI.
6109    pub fn initiate_authorization_code_pkce_flow(
6110        &self,
6111        scopes: Vec<&str>,
6112        redirect_uri: &str,
6113    ) -> Result<AuthorizationCodePkceFlow, MsalError> {
6114        self.app
6115            .initiate_authorization_code_pkce_flow(scopes, redirect_uri)
6116    }
6117
6118    /// Exchange a captured authorization-code redirect for a token.
6119    pub async fn acquire_token_by_authorization_code_pkce_flow(
6120        &self,
6121        flow: &AuthorizationCodePkceFlow,
6122        redirect_url: &str,
6123    ) -> Result<UserToken, MsalError> {
6124        self.app
6125            .acquire_token_by_authorization_code_pkce_flow(flow, redirect_url)
6126            .await
6127    }
6128
6129    /// Gets a token for enrollment via user credentials.
6130    ///
6131    /// # Arguments
6132    ///
6133    /// * `username` - Typically a UPN in the form of an email address.
6134    ///
6135    /// * `password` - The password.
6136    ///
6137    /// # Returns
6138    /// * Success: A UserToken containing an access_token.
6139    /// * Failure: An MsalError, indicating the failure.
6140    pub async fn acquire_token_by_username_password_for_device_enrollment(
6141        &self,
6142        username: &str,
6143        password: &str,
6144    ) -> Result<UserToken, MsalError> {
6145        let drs_scope = "https://enrollment.manage.microsoft.com/.default";
6146        self.app
6147            .acquire_token_by_username_password(username, password, vec![drs_scope])
6148            .await
6149    }
6150
6151    async fn acquire_token_by_refresh_token_for_device_enrollment(
6152        &self,
6153        refresh_token: &str,
6154    ) -> Result<UserToken, MsalError> {
6155        let drs_scope = format!("{}/.default", DRS_APP_ID);
6156        self.app
6157            .acquire_token_by_refresh_token(refresh_token, vec![&drs_scope])
6158            .await
6159    }
6160
6161    /// Initiate a Device Flow instance for enrollment, which will be
6162    /// used in acquire_token_by_device_flow.
6163    ///
6164    /// # Returns
6165    ///
6166    /// * Success: A DeviceAuthorizationResponse containing a user_code key,
6167    ///   among others
6168    /// * Failure: An MsalError, indicating the failure.
6169    pub async fn initiate_device_flow_for_device_enrollment(
6170        &self,
6171        #[cfg(feature = "optional_mfa")] options: &[AuthOption],
6172    ) -> Result<DeviceAuthorizationResponse, MsalError> {
6173        #[cfg(feature = "optional_mfa")]
6174        let portal_scope = if options.contains(&AuthOption::ForceMFA) {
6175            format!("{}/.default", AZURE_PORTAL_APP_ID)
6176        } else {
6177            format!("{}/.default", DRS_APP_ID)
6178        };
6179        #[cfg(not(feature = "optional_mfa"))]
6180        let portal_scope = format!("{}/.default", AZURE_PORTAL_APP_ID);
6181        self.app.initiate_device_flow(vec![&portal_scope]).await
6182    }
6183
6184    /// Obtain token for enrollment by a device flow object, with customizable
6185    /// polling effect.
6186    ///
6187    /// # Arguments
6188    ///
6189    /// * `flow` - A DeviceAuthorizationResponse previously generated by
6190    ///   initiate_device_flow.
6191    ///
6192    /// # Returns
6193    ///
6194    /// * Success: A UserToken containing an access_token.
6195    /// * Failure: An MsalError, indicating the failure.
6196    pub async fn acquire_token_by_device_flow(
6197        &self,
6198        flow: DeviceAuthorizationResponse,
6199    ) -> Result<UserToken, MsalError> {
6200        let portal_token = self.app.acquire_token_by_device_flow(flow).await?;
6201        let drs_scope = "https://enrollment.manage.microsoft.com/.default";
6202        self.app
6203            .acquire_token_by_refresh_token(&portal_token.refresh_token, vec![&drs_scope])
6204            .await
6205    }
6206
6207    /// Check if a user exists in Azure Entra ID
6208    ///
6209    /// # Arguments
6210    ///
6211    /// * `username` - Typically a UPN in the form of an email address.
6212    ///
6213    /// * `options` - Authentication options to enable, such as Fido and
6214    ///   Passwordless auth.
6215    ///
6216    /// # Returns
6217    /// * Success: An AuthInit object. Call `exists` to get the result.
6218    /// * Failure: An MsalError, indicating the failure.
6219    pub async fn check_user_exists(
6220        &self,
6221        username: &str,
6222        options: &[AuthOption],
6223    ) -> Result<AuthInit, MsalError> {
6224        // Use the Intune service principal as the resource for the initial
6225        // auth config request. This avoids Conditional Access policies that
6226        // block access to enrollment.manage.microsoft.com from non-compliant
6227        // devices, since CA exclusions for "Microsoft Intune" and "Microsoft
6228        // Intune Company Portal for Linux" will match this resource+client
6229        // combination. This app id is documented at:
6230        // https://learn.microsoft.com/en-us/entra/identity/users/groups-dynamic-membership
6231        let intune_resource = "0000000a-0000-0000-c000-000000000000";
6232        self.app
6233            .check_user_exists(username, Some(intune_resource), options)
6234            .await
6235    }
6236
6237    /// Initiate an MFA flow for enrollment via user credentials.
6238    ///
6239    /// # Arguments
6240    ///
6241    /// * `username` - Typically a UPN in the form of an email address.
6242    ///
6243    /// * `password` - The password.
6244    ///
6245    /// * `options` - Authentication options to enable, such as Fido.
6246    ///
6247    /// * `auth_init` - The result of `check_user_exists`, required if called
6248    ///   prior to `initiate_acquire_token_by_mfa_flow_for_device_enrollment`.
6249    ///
6250    /// * `selected_method` - Optional specific MFA method ID to use. Only available when
6251    ///   the `mfa_method_selection` feature is enabled.
6252    ///
6253    /// # Returns
6254    /// * Success: A MFAAuthContinue containing the information needed to continue the
6255    ///   authentication flow.
6256    /// * Failure: An MsalError, indicating the failure.
6257    pub async fn initiate_acquire_token_by_mfa_flow_for_device_enrollment(
6258        &self,
6259        username: &str,
6260        password: Option<&str>,
6261        options: &[AuthOption],
6262        auth_init: Option<AuthInit>,
6263        #[cfg(feature = "mfa_method_selection")] selected_method: Option<&str>,
6264    ) -> Result<MFAAuthContinue, MsalError> {
6265        // Use the Intune service principal as the resource, as per documentation
6266        // https://learn.microsoft.com/en-us/entra/identity/users/groups-dynamic-membership
6267        // This avoids Conditional Access policies that block non-compliant
6268        // devices from accessing enrollment.manage.microsoft.com during
6269        // the initial authentication. The enrollment-specific token is
6270        // obtained later via refresh_token exchange in enroll_device().
6271        let intune_resource = "0000000a-0000-0000-c000-000000000000";
6272        self.app
6273            .initiate_acquire_token_by_mfa_flow(
6274                username,
6275                password,
6276                vec![],
6277                Some(intune_resource),
6278                options,
6279                auth_init,
6280                #[cfg(feature = "mfa_method_selection")]
6281                selected_method,
6282            )
6283            .await
6284    }
6285
6286    /// Initiate an MFA flow via user credentials (without enrollment-specific
6287    /// resource scoping).
6288    ///
6289    /// This is the non-enrollment counterpart of
6290    /// `initiate_acquire_token_by_mfa_flow_for_device_enrollment`. The resulting
6291    /// token is scoped to the default resource (MS Graph) rather than Intune,
6292    /// making it suitable for scenarios where device enrollment is not desired.
6293    ///
6294    /// # Arguments
6295    ///
6296    /// * `username` - Typically a UPN in the form of an email address.
6297    ///
6298    /// * `password` - The password.
6299    ///
6300    /// * `options` - Authentication options to enable, such as Fido.
6301    ///
6302    /// * `auth_init` - The result of `check_user_exists`.
6303    ///
6304    /// * `selected_method` - Optional specific MFA method ID to use. Only available
6305    ///   when the `mfa_method_selection` feature is enabled.
6306    ///
6307    /// # Returns
6308    /// * Success: A MFAAuthContinue containing the information needed to continue the
6309    ///   authentication flow.
6310    /// * Failure: An MsalError, indicating the failure.
6311    pub async fn initiate_acquire_token_by_mfa_flow(
6312        &self,
6313        username: &str,
6314        password: Option<&str>,
6315        options: &[AuthOption],
6316        auth_init: Option<AuthInit>,
6317        #[cfg(feature = "mfa_method_selection")] selected_method: Option<&str>,
6318    ) -> Result<MFAAuthContinue, MsalError> {
6319        self.app
6320            .initiate_acquire_token_by_mfa_flow(
6321                username,
6322                password,
6323                vec![],
6324                None,
6325                options,
6326                auth_init,
6327                #[cfg(feature = "mfa_method_selection")]
6328                selected_method,
6329            )
6330            .await
6331    }
6332
6333    /// Obtain token by a MFA flow object.
6334    ///
6335    /// # Arguments
6336    ///
6337    /// * `username` - Typically a UPN in the form of an email address.
6338    ///
6339    /// * `auth_data` - An optional token received for the MFA flow (some MFA
6340    ///   flows do not require input).
6341    ///
6342    /// * `poll_attempt` - The polling attempt number.
6343    ///
6344    /// * `flow` - A MFAAuthContinue previously generated by
6345    ///   initiate_acquire_token_by_mfa_flow.
6346    ///
6347    /// # Returns
6348    ///
6349    /// * Success: A UserToken containing an access_token.
6350    /// * Failure: An MsalError, indicating the failure.
6351    pub async fn acquire_token_by_mfa_flow(
6352        &self,
6353        username: &str,
6354        auth_data: Option<&str>,
6355        poll_attempt: Option<u32>,
6356        flow: &mut MFAAuthContinue,
6357    ) -> Result<UserToken, MsalError> {
6358        self.app
6359            .acquire_token_by_mfa_flow(username, auth_data, poll_attempt, flow)
6360            .await
6361    }
6362
6363    // TODO: add condition here to support v2 endpoints?
6364    async fn request_nonce(&self) -> Result<String, MsalError> {
6365        let token_endpoint = format!("{}/oauth2/token", self.authority()?);
6366        self.request_nonce_from_endpoint(&token_endpoint).await
6367    }
6368
6369    fn tenant_token_endpoint(&self, tenant_id: &str) -> Result<String, MsalError> {
6370        if tenant_id.is_empty() || tenant_id.contains('/') {
6371            return Err(MsalError::ConfigError(
6372                "Invalid tenant id for P2P certificate request".to_string(),
6373            ));
6374        }
6375
6376        let mut authority = Url::parse(&self.authority()?)
6377            .map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
6378        authority.set_path(&format!("{}/oauth2/token", tenant_id));
6379        authority.set_query(None);
6380        authority.set_fragment(None);
6381        Ok(authority.to_string())
6382    }
6383
6384    async fn request_nonce_from_endpoint(&self, token_endpoint: &str) -> Result<String, MsalError> {
6385        let resp = self
6386            .client()
6387            .post(token_endpoint)
6388            .body("grant_type=srv_challenge")
6389            .send()
6390            .await
6391            .map_err(|e| MsalError::request_failed(&e))?;
6392        if resp.status().is_success() {
6393            let json_resp: Nonce = resp
6394                .json()
6395                .await
6396                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
6397            Ok(json_resp.nonce)
6398        } else {
6399            let json_resp: ErrorResponse = resp
6400                .json()
6401                .await
6402                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
6403            Err(MsalError::AcquireTokenFailed(json_resp))
6404        }
6405    }
6406
6407    fn device_certificate_tenant_id(cert_der: &[u8]) -> Result<Option<String>, MsalError> {
6408        const TENANT_ID_OID: &str = "1.2.840.113556.1.5.284.5";
6409
6410        let cert = x509_cert::Certificate::from_der(cert_der).map_err(|e| {
6411            MsalError::CryptoFail(format!("Failed parsing device certificate DER: {:?}", e))
6412        })?;
6413        let extensions = match cert.tbs_certificate.extensions.as_ref() {
6414            Some(extensions) => extensions,
6415            None => return Ok(None),
6416        };
6417        let raw = extensions
6418            .iter()
6419            .find(|ext| ext.extn_id.to_string() == TENANT_ID_OID)
6420            .map(|ext| ext.extn_value.as_bytes());
6421        let raw = match raw {
6422            Some(raw) => raw,
6423            None => return Ok(None),
6424        };
6425        // A tenant OID we can't make sense of is not fatal, since the caller
6426        // supplies a fallback tenant id.
6427        match Self::tenant_id_from_device_certificate_oid(raw) {
6428            Ok(tenant_id) => Ok(Some(tenant_id)),
6429            Err(e) => {
6430                warn!("Ignoring device certificate tenant OID: {:?}", e);
6431                Ok(None)
6432            }
6433        }
6434    }
6435
6436    fn tenant_id_from_device_certificate_oid(raw: &[u8]) -> Result<String, MsalError> {
6437        let tenant_bytes = Self::device_certificate_oid_guid(raw).ok_or_else(|| {
6438            MsalError::CryptoFail("Invalid device certificate tenant OID length".to_string())
6439        })?;
6440
6441        Ok(Uuid::from_bytes_le(tenant_bytes).to_string())
6442    }
6443
6444    /// The tenant OID value is an OCTET STRING wrapping the raw tenant GUID,
6445    /// but the encoding varies, so take whichever reading yields a GUID.
6446    fn device_certificate_oid_guid(raw: &[u8]) -> Option<[u8; 16]> {
6447        // Canonical DER, `04 10 <guid>`.
6448        if let Ok(octet_string) = OctetString::from_der(raw) {
6449            if let Ok(guid) = octet_string.as_bytes().try_into() {
6450                return Some(guid);
6451            }
6452        }
6453        // A BER long form length, `04 81 10 <guid>`, which the der crate
6454        // rejects as non canonical but Entra does emit.
6455        if let [0x04, 0x81, len, guid @ ..] = raw {
6456            if usize::from(*len) == guid.len() {
6457                if let Ok(guid) = guid.try_into() {
6458                    return Some(guid);
6459                }
6460            }
6461        }
6462        // The bare GUID, with no OCTET STRING wrapper at all.
6463        raw.try_into().ok()
6464    }
6465
6466    fn p2p_compact_signing_input<T: Serialize, U: Serialize>(
6467        header: &T,
6468        payload: &U,
6469    ) -> Result<String, MsalError> {
6470        let header_json = json_to_vec(header).map_err(|e| {
6471            MsalError::InvalidJson(format!("Failed serializing P2P JWT header: {}", e))
6472        })?;
6473        let payload_json = json_to_vec(payload).map_err(|e| {
6474            MsalError::InvalidJson(format!("Failed serializing P2P JWT payload: {}", e))
6475        })?;
6476
6477        Ok(format!(
6478            "{}.{}",
6479            URL_SAFE_NO_PAD.encode(header_json),
6480            URL_SAFE_NO_PAD.encode(payload_json)
6481        ))
6482    }
6483
6484    fn sign_p2p_device_jwt(
6485        payload: &P2PDeviceCertificatePayload,
6486        cert_der: &[u8],
6487        tpm: &mut BoxedDynTpm,
6488        signing_key: &RS256Key,
6489    ) -> Result<String, MsalError> {
6490        let x5c = STANDARD.encode(cert_der);
6491        let header = P2PDeviceCertificateHeader {
6492            alg: "RS256",
6493            typ: "JWT",
6494            x5c: &x5c,
6495        };
6496        debug!(
6497            "P2P Device Certificate Header: {}",
6498            json!({
6499                "alg": header.alg,
6500                "typ": header.typ,
6501                "x5c": "<base64 DER certificate>"
6502            })
6503        );
6504        let signing_input = Self::p2p_compact_signing_input(&header, payload)?;
6505        let signature = tpm
6506            .rs256_sign(signing_key, signing_input.as_bytes())
6507            .map_err(|e| MsalError::TPMFail(format!("Failed signing P2P device JWT: {:?}", e)))?;
6508        let signature_bytes: Box<[u8]> = signature.into();
6509
6510        Ok(format!(
6511            "{}.{}",
6512            signing_input,
6513            URL_SAFE_NO_PAD.encode(&signature_bytes)
6514        ))
6515    }
6516
6517    fn sign_p2p_user_jwt_with_key(
6518        payload: &P2PUserCertificatePayload,
6519        ctx: &[u8],
6520        hmac_key: &[u8],
6521    ) -> Result<String, MsalError> {
6522        let ctx = STANDARD.encode(ctx);
6523        let header = P2PUserCertificateHeader {
6524            alg: "HS256",
6525            typ: "JWT",
6526            ctx: &ctx,
6527        };
6528        debug!(
6529            "P2P User Certificate Header: {}",
6530            json!({
6531                "alg": header.alg,
6532                "typ": header.typ,
6533                "ctx": "<base64 context>"
6534            })
6535        );
6536        let signing_input = Self::p2p_compact_signing_input(&header, payload)?;
6537        let signature = Self::hmac_sha256(hmac_key, signing_input.as_bytes())?;
6538
6539        Ok(format!(
6540            "{}.{}",
6541            signing_input,
6542            URL_SAFE_NO_PAD.encode(signature)
6543        ))
6544    }
6545
6546    fn hmac_sha256(key: &[u8], data: &[u8]) -> Result<Vec<u8>, MsalError> {
6547        let key = PKey::hmac(key)
6548            .map_err(|e| MsalError::CryptoFail(format!("Failed creating HMAC key: {}", e)))?;
6549        let mut signer = Signer::new(MessageDigest::sha256(), &key)
6550            .map_err(|e| MsalError::CryptoFail(format!("Failed creating HMAC signer: {}", e)))?;
6551        signer
6552            .update(data)
6553            .map_err(|e| MsalError::CryptoFail(format!("Failed updating HMAC signer: {}", e)))?;
6554        signer
6555            .sign_to_vec()
6556            .map_err(|e| MsalError::CryptoFail(format!("Failed signing HMAC: {}", e)))
6557    }
6558
6559    /// Sign a P2P user certificate request with the PRT session key.
6560    ///
6561    /// This deliberately avoids `sign_session_key_jwt`, which goes through
6562    /// compact_jwt's `MsOapxbcSessionKey::sign`. That implementation embeds a
6563    /// `kid` header and uses a 32 byte context, while the broker sends exactly
6564    /// three header fields (`alg`, `typ`, `ctx`) and a 24 byte context. Entra
6565    /// rejects the P2P request otherwise. The KDF label and the
6566    /// `derive_key_aes256(key, label, ctx)` argument order below match
6567    /// compact_jwt, so the server derives the same key from our `ctx`.
6568    fn sign_p2p_user_jwt(
6569        &self,
6570        payload: &P2PUserCertificatePayload,
6571        tpm: &mut BoxedDynTpm,
6572        storage_key: &StorageKey,
6573        transport_key: &MsOapxbcRsaKey,
6574        session_key: &SessionKey,
6575    ) -> Result<String, MsalError> {
6576        const P2P_CTX_LEN: usize = 24;
6577        const AAD_KDF_LABEL: &[u8; 26] = b"AzureAD-SecureConversation";
6578
6579        let mut ctx = [0u8; P2P_CTX_LEN];
6580        rand_bytes(&mut ctx)
6581            .map_err(|e| MsalError::CryptoFail(format!("Failed creating P2P context: {}", e)))?;
6582
6583        let maybe_transport_storage_key = tpm.rs256_yield_cek(transport_key);
6584        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
6585        let session_key = MsOapxbcSessionKey::complete_tpm_rsa_oaep_key_agreement(
6586            tpm,
6587            prt_storage_key,
6588            transport_key,
6589            &session_key.session_key_jwe,
6590        )
6591        .map_err(|e| MsalError::CryptoFail(format!("Unable to decipher session_key_jwe: {}", e)))?;
6592        let MsOapxbcSessionKey::A256GCM { sealed_session_key } = session_key;
6593        let aes_key = tpm
6594            .unseal_data(prt_storage_key, &sealed_session_key)
6595            .map_err(|e| {
6596                MsalError::TPMFail(format!("Failed unsealing PRT session key: {:?}", e))
6597            })?;
6598        let derived_key = crypto_glue::nist_sp800_108_kdf_hmac_sha256::derive_key_aes256(
6599            &aes_key,
6600            AAD_KDF_LABEL,
6601            &ctx,
6602        )
6603        .ok_or_else(|| {
6604            MsalError::CryptoFail("Failed deriving P2P user JWT signing key".to_string())
6605        })?;
6606
6607        Self::sign_p2p_user_jwt_with_key(payload, &ctx, &derived_key)
6608    }
6609
6610    fn create_p2p_csr(
6611        tpm: &mut BoxedDynTpm,
6612        signing_key: &RS256Key,
6613        subject: Name,
6614    ) -> Result<(Vec<u8>, Vec<u8>), MsalError> {
6615        let public_key = tpm
6616            .rs256_public(signing_key)
6617            .map_err(|e| MsalError::TPMFail(format!("Failed getting public key: {:?}", e)))?;
6618        let public_key_der = public_key
6619            .to_public_key_der()
6620            .map_err(|e| MsalError::CryptoFail(format!("Failed encoding public key: {:?}", e)))?;
6621        let spki = SubjectPublicKeyInfoOwned::try_from(public_key_der.as_bytes())
6622            .map_err(|e| MsalError::CryptoFail(format!("Failed parsing SPKI: {:?}", e)))?;
6623
6624        let cert_req_info = P2PCertReqInfo {
6625            version: x509_cert::request::Version::V1,
6626            subject,
6627            public_key: spki,
6628            attributes: SetOfVec::new(),
6629        };
6630        let tbs_der = cert_req_info
6631            .to_der()
6632            .map_err(|e| MsalError::CryptoFail(format!("Failed encoding CSR info: {:?}", e)))?;
6633        let signature = tpm
6634            .rs256_sign(signing_key, &tbs_der)
6635            .map_err(|e| MsalError::TPMFail(format!("Failed signing CSR: {:?}", e)))?;
6636        let signature_bytes: Box<[u8]> = signature.into();
6637        let signature_algorithm = AlgorithmIdentifierOwned {
6638            oid: rfc5912::SHA_256_WITH_RSA_ENCRYPTION,
6639            parameters: Some(der::asn1::AnyRef::from(der::asn1::Null).into()),
6640        };
6641
6642        #[derive(Sequence)]
6643        struct P2PCertReq {
6644            info: P2PCertReqInfo,
6645            algorithm: AlgorithmIdentifierOwned,
6646            signature: BitString,
6647        }
6648
6649        let cert_req = P2PCertReq {
6650            info: cert_req_info,
6651            algorithm: signature_algorithm,
6652            signature: BitString::from_bytes(&signature_bytes).map_err(|e| {
6653                MsalError::CryptoFail(format!("Failed creating CSR signature: {:?}", e))
6654            })?,
6655        };
6656        let csr_der = cert_req
6657            .to_der()
6658            .map_err(|e| MsalError::CryptoFail(format!("Failed encoding CSR: {:?}", e)))?;
6659
6660        Ok((csr_der, public_key_der.to_vec()))
6661    }
6662
6663    fn x509_name_to_string(name: &openssl::x509::X509NameRef) -> String {
6664        name.entries()
6665            .map(|entry| {
6666                let key = entry.object().nid().short_name().unwrap_or("OID");
6667                let value = entry
6668                    .data()
6669                    .to_string()
6670                    .map(|value| value.to_string())
6671                    .unwrap_or_else(|_| "<non-utf8>".to_string());
6672                format!("{}={}", key, value)
6673            })
6674            .collect::<Vec<String>>()
6675            .join(", ")
6676    }
6677
6678    fn p2p_certificate_from_response(
6679        response: &P2PCertificateResponse,
6680        private_key: P2PPrivateKey,
6681        expected_public_key_der: &[u8],
6682    ) -> Result<P2PCertificate, MsalError> {
6683        let certificate_der = STANDARD
6684            .decode(&response.x5c)
6685            .map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?;
6686        let cert = X509::from_der(&certificate_der)
6687            .map_err(|e| MsalError::CryptoFail(format!("Failed parsing P2P certificate: {}", e)))?;
6688        let cert_public_key_der = cert
6689            .public_key()
6690            .and_then(|key| key.public_key_to_der())
6691            .map_err(|e| {
6692                MsalError::CryptoFail(format!("Failed reading P2P certificate public key: {}", e))
6693            })?;
6694        if cert_public_key_der != expected_public_key_der {
6695            return Err(MsalError::CryptoFail(
6696                "P2P certificate public key does not match CSR key".to_string(),
6697            ));
6698        }
6699
6700        let thumbprint_sha1 = hash(MessageDigest::sha1(), &certificate_der)
6701            .map_err(|e| MsalError::CryptoFail(format!("{}", e)))?
6702            .iter()
6703            .map(|byte| format!("{:02X}", byte))
6704            .collect::<String>();
6705        let dns_names = cert
6706            .subject_alt_names()
6707            .map(|names| {
6708                names
6709                    .iter()
6710                    .filter_map(|name| name.dnsname().map(|dns| dns.to_string()))
6711                    .collect()
6712            })
6713            .unwrap_or_default();
6714        let not_after_unix = Self::asn1_time_to_unix(cert.not_after())?;
6715
6716        // x5c_ca carries the DER encoded issuing CA certificate. Failing to
6717        // parse it isn't fatal, since the issued certificate is what the caller
6718        // asked for, but don't claim it's a certificate when it doesn't parse.
6719        let ca_certificate_der = STANDARD
6720            .decode(&response.x5c_ca)
6721            .map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?;
6722        let ca_certificate_pem = match X509::from_der(&ca_certificate_der) {
6723            Ok(_) => Some(format!(
6724                "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
6725                response.x5c_ca
6726            )),
6727            Err(e) => {
6728                warn!("Failed parsing the P2P issuing CA certificate: {}", e);
6729                None
6730            }
6731        };
6732
6733        Ok(P2PCertificate {
6734            certificate_der,
6735            ca_certificate_der,
6736            ca_certificate_pem,
6737            subject: Self::x509_name_to_string(cert.subject_name()),
6738            issuer: Self::x509_name_to_string(cert.issuer_name()),
6739            thumbprint_sha1,
6740            dns_names,
6741            not_after_unix,
6742            private_key,
6743        })
6744    }
6745
6746    fn asn1_time_to_unix(time: &Asn1TimeRef) -> Result<i64, MsalError> {
6747        let epoch = Asn1Time::from_unix(0)
6748            .map_err(|e| MsalError::CryptoFail(format!("Failed creating unix epoch: {}", e)))?;
6749        let diff = epoch.diff(time).map_err(|e| {
6750            MsalError::CryptoFail(format!("Failed comparing certificate time: {}", e))
6751        })?;
6752
6753        Ok(i64::from(diff.days) * 86400 + i64::from(diff.secs))
6754    }
6755
6756    async fn post_p2p_certificate_request(
6757        &self,
6758        token_endpoint: &str,
6759        signed_jwt: &str,
6760        windows_api_version: &str,
6761    ) -> Result<P2PCertificateResponse, MsalError> {
6762        let params = [
6763            ("windows_api_version", windows_api_version),
6764            ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
6765            ("request", signed_jwt),
6766        ];
6767        let payload = params
6768            .iter()
6769            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
6770            .collect::<Vec<String>>()
6771            .join("&");
6772
6773        let mut debug_payload = params;
6774        debug_payload[2] = ("request", "**********");
6775        if let Ok(pretty) = to_string_pretty(&debug_payload) {
6776            debug!("POST {}: {}", token_endpoint, pretty);
6777        }
6778
6779        let resp = self
6780            .client()
6781            .post(token_endpoint)
6782            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
6783            .body(payload)
6784            .send()
6785            .await
6786            .map_err(|e| MsalError::request_failed(&e))?;
6787        if resp.status().is_success() {
6788            resp.json()
6789                .await
6790                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))
6791        } else {
6792            let json_resp: ErrorResponse = resp
6793                .json()
6794                .await
6795                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
6796            Err(MsalError::AcquireTokenFailed(json_resp))
6797        }
6798    }
6799
6800    async fn build_jwt_by_username_password(
6801        &self,
6802        username: &str,
6803        password: &str,
6804        cert: Option<&X509>,
6805    ) -> Result<Jws, MsalError> {
6806        let nonce = self.request_nonce().await?;
6807
6808        let mut builder = JwsBuilder::from(
6809            serde_json::to_vec(&UsernamePasswordAuthenticationPayload::new(
6810                username, password, &nonce,
6811            ))
6812            .map_err(|e| {
6813                MsalError::InvalidJson(format!("Failed serializing UsernamePassword JWT: {}", e))
6814            })?,
6815        )
6816        .set_typ(Some("JWT"));
6817
6818        if let Some(cert) = cert {
6819            builder = builder.set_x5c(Some(vec![cert
6820                .to_der()
6821                .map_err(|e| MsalError::CryptoFail(format!("{}", e)))?]));
6822        }
6823
6824        let jwt = builder.build();
6825
6826        if let Ok(mut debug_jwt) = jwt.from_json::<Value>() {
6827            debug_jwt["password"] = "**********".into();
6828            if let Ok(pretty) = to_string_pretty(&debug_jwt) {
6829                debug!("Username/Password JWT: {}", pretty);
6830            }
6831        }
6832
6833        Ok(jwt)
6834    }
6835
6836    /// Gets a Primary Refresh Token (PRT) via user credentials.
6837    ///
6838    /// # Arguments
6839    ///
6840    /// * `username` - Typically a UPN in the form of an email address.
6841    ///
6842    /// * `password` - The password.
6843    ///
6844    /// * `tpm` - The tpm object.
6845    ///
6846    /// * `storage_key` - The TPM MachineKey associated with this application.
6847    ///
6848    /// # Returns
6849    /// * Success: An encrypted PrimaryRefreshToken, containing a refresh_token and tgt.
6850    /// * Failure: An MsalError, indicating the failure.
6851    pub async fn acquire_user_prt_by_username_password(
6852        &self,
6853        username: &str,
6854        password: &str,
6855        tpm: &mut BoxedDynTpm,
6856        storage_key: &StorageKey,
6857    ) -> Result<SealedData, MsalError> {
6858        let prt = self
6859            .acquire_user_prt_by_username_password_internal(username, password, tpm, storage_key)
6860            .await?;
6861        let transport_key = self.transport_key(tpm, storage_key)?;
6862
6863        // This allows a transition, where existing msoapxbc keys will provide
6864        // their sealing content encryption keys, but newer keys will not, falling back
6865        // to the provided key.
6866        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
6867        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
6868
6869        self.seal_user_prt(&prt, tpm, prt_storage_key)
6870    }
6871
6872    async fn acquire_user_prt_by_username_password_internal(
6873        &self,
6874        username: &str,
6875        password: &str,
6876        tpm: &mut BoxedDynTpm,
6877        storage_key: &StorageKey,
6878    ) -> Result<PrimaryRefreshToken, MsalError> {
6879        debug!("Acquiring User PRT via Username/Password");
6880
6881        let cert_key = self.cert_key(tpm, storage_key)?;
6882        let cert_der = cert_key.cert.to_der().map_err(|e| {
6883            MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
6884        })?;
6885        let cert = X509::from_der(&cert_der).map_err(|e| {
6886            MsalError::CryptoFail(format!("Failed to create X509 from DER: {:?}", e))
6887        })?;
6888        let jwt = self
6889            .build_jwt_by_username_password(username, password, Some(&cert))
6890            .await?;
6891        let signed_jwt = self.sign_jwt(&jwt, tpm, storage_key).await?;
6892
6893        self.acquire_user_prt_jwt(&signed_jwt).await
6894    }
6895
6896    async fn build_jwt_by_refresh_token(
6897        &self,
6898        refresh_token: &str,
6899        cert: Option<&X509>,
6900    ) -> Result<Jws, MsalError> {
6901        let nonce = self.request_nonce().await?;
6902
6903        let mut builder = JwsBuilder::from(
6904            serde_json::to_vec(&RefreshTokenAuthenticationPayload::new(
6905                refresh_token,
6906                &nonce,
6907            ))
6908            .map_err(|e| {
6909                MsalError::InvalidJson(format!("Failed serializing RefreshToken JWT: {}", e))
6910            })?,
6911        )
6912        .set_typ(Some("JWT"));
6913
6914        if let Some(cert) = cert {
6915            builder = builder.set_x5c(Some(vec![cert
6916                .to_der()
6917                .map_err(|e| MsalError::CryptoFail(format!("{}", e)))?]));
6918        }
6919
6920        let jwt = builder.build();
6921
6922        if let Ok(mut debug_jwt) = jwt.from_json::<Value>() {
6923            debug_jwt["refresh_token"] = "**********".into();
6924            if let Ok(pretty) = to_string_pretty(&debug_jwt) {
6925                debug!("Refresh Token JWT: {}", pretty);
6926            }
6927        }
6928
6929        Ok(jwt)
6930    }
6931
6932    /// Gets a Primary Refresh Token (PRT) via a refresh token (RT) obtained
6933    /// previously.
6934    ///
6935    /// # Arguments
6936    ///
6937    /// * `refresh_token` - The old refresh token, as a string.
6938    ///
6939    /// * `tpm` - The tpm object.
6940    ///
6941    /// * `storage_key` - The TPM MachineKey associated with this application.
6942    ///
6943    /// # Returns
6944    /// * Success: An encrypted PrimaryRefreshToken, containing a refresh_token and tgt.
6945    /// * Failure: An MsalError, indicating the failure.
6946    pub async fn acquire_user_prt_by_refresh_token(
6947        &self,
6948        refresh_token: &str,
6949        tpm: &mut BoxedDynTpm,
6950        storage_key: &StorageKey,
6951    ) -> Result<SealedData, MsalError> {
6952        let prt = self
6953            .acquire_user_prt_by_refresh_token_internal(refresh_token, tpm, storage_key)
6954            .await?;
6955        let transport_key = self.transport_key(tpm, storage_key)?;
6956
6957        // This allows a transition, where existing msoapxbc keys will provide
6958        // their sealing content encryption keys, but newer keys will not, falling back
6959        // to the provided key.
6960        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
6961        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
6962
6963        self.seal_user_prt(&prt, tpm, prt_storage_key)
6964    }
6965
6966    async fn acquire_user_prt_by_refresh_token_internal(
6967        &self,
6968        refresh_token: &str,
6969        tpm: &mut BoxedDynTpm,
6970        storage_key: &StorageKey,
6971    ) -> Result<PrimaryRefreshToken, MsalError> {
6972        debug!("Acquiring User PRT via Refresh Token");
6973
6974        let cert_key = self.cert_key(tpm, storage_key)?;
6975        let cert_der = cert_key.cert.to_der().map_err(|e| {
6976            MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
6977        })?;
6978        let cert = X509::from_der(&cert_der).map_err(|e| {
6979            MsalError::CryptoFail(format!("Failed to create X509 from DER: {:?}", e))
6980        })?;
6981        let jwt = self
6982            .build_jwt_by_refresh_token(refresh_token, Some(&cert))
6983            .await?;
6984        let signed_jwt = self.sign_jwt(&jwt, tpm, storage_key).await?;
6985
6986        self.acquire_user_prt_jwt(&signed_jwt).await
6987    }
6988
6989    async fn sign_jwt(
6990        &self,
6991        jwt: &Jws,
6992        tpm: &mut BoxedDynTpm,
6993        storage_key: &StorageKey,
6994    ) -> Result<String, MsalError> {
6995        let cert_key = self.cert_key(tpm, storage_key)?;
6996        let mut jws_tpm_signer = match JwsTpmRs256Signer::new(tpm, &cert_key.key) {
6997            Ok(jws_tpm_signer) => jws_tpm_signer,
6998            Err(e) => {
6999                return Err(MsalError::TPMFail(format!(
7000                    "Failed loading tpm signer: {}",
7001                    e
7002                )))
7003            }
7004        };
7005
7006        let signed_jwt = match jws_tpm_signer.sign(jwt) {
7007            Ok(signed_jwt) => signed_jwt,
7008            Err(e) => return Err(MsalError::TPMFail(format!("Failed signing jwk: {}", e))),
7009        };
7010
7011        Ok(format!("{}", signed_jwt))
7012    }
7013
7014    async fn acquire_user_prt_jwt(
7015        &self,
7016        signed_jwt: &str,
7017    ) -> Result<PrimaryRefreshToken, MsalError> {
7018        debug!("Acquiring User PRT via JWT");
7019
7020        // [MS-OAPXBC] 3.2.5.1.2 POST (Request for Primary Refresh Token)
7021        let params = [
7022            ("windows_api_version", "2.0"),
7023            ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
7024            ("request", signed_jwt),
7025            ("client_info", "1"),
7026            ("tgt", "true"),
7027        ];
7028        let payload = params
7029            .iter()
7030            .map(|(k, v)| format!("{}={}", k, v))
7031            .collect::<Vec<String>>()
7032            .join("&");
7033
7034        // TODO: add condition here to support v2 endpoints?
7035        let url = format!("{}/oauth2/token", self.authority()?);
7036
7037        let mut debug_payload = params;
7038        debug_payload[2] = ("request", "**********");
7039        if let Ok(pretty) = to_string_pretty(&debug_payload) {
7040            debug!("POST {}: {}", url, pretty);
7041        }
7042
7043        let resp = self
7044            .client()
7045            .post(url)
7046            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
7047            .body(payload)
7048            .send()
7049            .await
7050            .map_err(|e| MsalError::request_failed(&e))?;
7051        if resp.status().is_success() {
7052            let json_resp: PrimaryRefreshToken = resp
7053                .json()
7054                .await
7055                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
7056            Ok(json_resp)
7057        } else {
7058            let json_resp: ErrorResponse = resp
7059                .json()
7060                .await
7061                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
7062            Err(MsalError::AcquireTokenFailed(json_resp))
7063        }
7064    }
7065
7066    async fn sign_session_key_jwt(
7067        &self,
7068        jwt: &Jws,
7069        tpm: &mut BoxedDynTpm,
7070        storage_key: &StorageKey,
7071        session_key: &SessionKey,
7072    ) -> Result<String, MsalError> {
7073        let transport_key = self.transport_key(tpm, storage_key)?;
7074
7075        // This allows a transition, where existing msoapxbc keys will provide
7076        // their sealing content encryption keys, but newer keys will not, falling back
7077        // to the provided key.
7078        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
7079        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
7080
7081        let signed_jwt = session_key.sign(tpm, &transport_key, prt_storage_key, jwt)?;
7082
7083        Ok(format!("{}", signed_jwt))
7084    }
7085
7086    /// Request a Microsoft-issued OpenSSH user certificate using a sealed PRT.
7087    ///
7088    /// This reuses the PRT JWT-bearer exchange to obtain a normal refresh token,
7089    /// then performs the Azure SSH certificate token exchange. The caller owns
7090    /// the matching RSA private key.
7091    pub async fn exchange_prt_for_ssh_certificate(
7092        &self,
7093        sealed_prt: &SealedData,
7094        openssh_public_key: &str,
7095        tpm: &mut BoxedDynTpm,
7096        storage_key: &StorageKey,
7097    ) -> Result<EntraSshCertificate, MsalError> {
7098        let jwk = ssh_rsa_public_key_to_jwk(openssh_public_key)?;
7099        let transport_key = self.transport_key(tpm, storage_key)?;
7100        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
7101        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
7102        let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
7103        let session_key = prt.session_key()?;
7104        let redirect_uri = self.app.get_auth_redirect_uri(Some(AZURE_CLI_APP_ID), None);
7105        let bearer_token = self
7106            .exchange_prt_for_refresh_token_jwt_bearer(
7107                &prt,
7108                tpm,
7109                storage_key,
7110                &session_key,
7111                AZURE_CLI_APP_ID,
7112                &redirect_uri,
7113                None,
7114            )
7115            .await?;
7116        self.acquire_ssh_certificate_with_refresh_token(&bearer_token.refresh_token, &jwk)
7117            .await
7118    }
7119
7120    async fn acquire_ssh_certificate_with_refresh_token(
7121        &self,
7122        refresh_token: &str,
7123        jwk: &SshRsaJwk,
7124    ) -> Result<EntraSshCertificate, MsalError> {
7125        let payload = build_ssh_certificate_request_form(refresh_token, jwk)?;
7126        let url = format!("{}/oauth2/v2.0/token", self.authority()?);
7127        debug!("POST SSH certificate request {} for key {}", url, jwk.kid);
7128
7129        let response = self
7130            .client()
7131            .post(&url)
7132            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
7133            .header(header::ACCEPT, "application/json")
7134            .body(payload)
7135            .send()
7136            .await
7137            .map_err(|e| MsalError::request_failed(&e))?;
7138        if response.status().is_success() {
7139            let response: SshCertificateTokenResponse = response
7140                .json()
7141                .await
7142                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
7143            parse_ssh_certificate_response(response, jwk)
7144        } else {
7145            let response: ErrorResponse = response
7146                .json()
7147                .await
7148                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
7149            Err(MsalError::AcquireTokenFailed(response))
7150        }
7151    }
7152
7153    /// Given the primary refresh token, this method requests an access token.
7154    ///
7155    /// # Arguments
7156    ///
7157    /// * `sealed_prt` -  An encrypted primary refresh token that was
7158    ///   previously received from the server.
7159    ///
7160    /// * `scope` - The scope that the client requests for the access token.
7161    ///
7162    /// * `request_resource` - A resource for obtaining an access token.
7163    ///   Default is the MS Graph API (00000002-0000-0000-c000-000000000000).
7164    ///
7165    /// * `on_behalf_of_client_id`: Request a resource or scope on behalf of
7166    ///   the specified client_id. Optional. This option requires libhimmelblau
7167    ///   be built with the `on_behalf_of` feature.
7168    ///
7169    /// * `tpm` - The tpm object.
7170    ///
7171    /// * `storage_key` - The TPM MachineKey associated with this application.
7172    ///
7173    /// # Returns
7174    /// * Success: A UserToken containing an access_token.
7175    /// * Failure: An MsalError, indicating the failure.
7176    #[allow(clippy::too_many_arguments)]
7177    pub async fn exchange_prt_for_access_token(
7178        &self,
7179        sealed_prt: &SealedData,
7180        scope: Vec<&str>,
7181        request_resource: Option<String>,
7182        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
7183        tpm: &mut BoxedDynTpm,
7184        storage_key: &StorageKey,
7185        #[cfg(feature = "redirect_uri")] redirect_uri: Option<&str>,
7186        #[cfg(feature = "pop_support")] req_cnf: Option<&str>,
7187    ) -> Result<UserToken, MsalError> {
7188        #[cfg(not(feature = "pop_support"))]
7189        let _req_cnf: Option<&str> = None;
7190
7191        let v2_endpoint = !scope.is_empty();
7192        if !scope.is_empty() && request_resource.is_some() {
7193            return Err(MsalError::GeneralFailure(
7194                "Scopes cannot be specified with a request_resource".to_string(),
7195            ));
7196        }
7197        let transport_key = self.transport_key(tpm, storage_key)?;
7198
7199        // This allows a transition, where existing msoapxbc keys will provide
7200        // their sealing content encryption keys, but newer keys will not, falling back
7201        // to the provided key.
7202        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
7203        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
7204
7205        let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
7206        let session_key = prt.session_key()?;
7207        let mut token = self
7208            .exchange_prt_for_access_token_internal(
7209                &prt,
7210                scope,
7211                v2_endpoint,
7212                tpm,
7213                // TODO: Is this the correct storage key to pass here? I think it may not be?
7214                storage_key,
7215                &session_key,
7216                request_resource,
7217                #[cfg(feature = "on_behalf_of")]
7218                on_behalf_of_client_id,
7219                #[cfg(feature = "redirect_uri")]
7220                redirect_uri,
7221                #[cfg(feature = "pop_support")]
7222                req_cnf,
7223                false,
7224            )
7225            .await?;
7226        token.client_info = prt.client_info.clone();
7227        Ok(token)
7228    }
7229
7230    #[allow(clippy::too_many_arguments)]
7231    async fn exchange_prt_for_access_token_internal(
7232        &self,
7233        prt: &PrimaryRefreshToken,
7234        scope: Vec<&str>,
7235        v2_endpoint: bool,
7236        tpm: &mut BoxedDynTpm,
7237        storage_key: &StorageKey,
7238        session_key: &SessionKey,
7239        request_resource: Option<String>,
7240        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
7241        #[cfg(feature = "redirect_uri")] redirect_uri: Option<&str>,
7242        #[cfg(feature = "pop_support")] req_cnf: Option<&str>,
7243        demand_mfa: bool,
7244    ) -> Result<UserToken, MsalError> {
7245        debug!("Exchanging a PRT for an Access Token");
7246
7247        #[cfg(not(feature = "pop_support"))]
7248        let req_cnf: Option<&str> = None;
7249
7250        // When PoP (req_cnf) is requested, use the JWT bearer grant directly
7251        // to the token endpoint. This is needed for scopes like
7252        // ms-device-service:// that require PoP and don't work with the
7253        // authorize -> auth_code -> token flow.
7254        if let Some(req_cnf_val) = req_cnf {
7255            return self
7256                .exchange_prt_for_access_token_jwt_bearer(
7257                    prt,
7258                    scope,
7259                    v2_endpoint,
7260                    tpm,
7261                    storage_key,
7262                    session_key,
7263                    request_resource,
7264                    req_cnf_val,
7265                    #[cfg(feature = "on_behalf_of")]
7266                    on_behalf_of_client_id,
7267                    #[cfg(feature = "redirect_uri")]
7268                    redirect_uri,
7269                )
7270                .await;
7271        }
7272
7273        let request_id = Uuid::new_v4().to_string();
7274        let auth_code = self
7275            .exchange_prt_for_auth_code(
7276                prt,
7277                scope.clone(),
7278                &request_id,
7279                request_resource.as_deref(),
7280                v2_endpoint,
7281                session_key,
7282                #[cfg(feature = "on_behalf_of")]
7283                on_behalf_of_client_id,
7284                tpm,
7285                storage_key,
7286                #[cfg(feature = "redirect_uri")]
7287                redirect_uri,
7288                req_cnf,
7289                demand_mfa,
7290            )
7291            .await?;
7292
7293        self.exchange_auth_code_for_access_token_internal(
7294            scope,
7295            &request_id,
7296            v2_endpoint,
7297            auth_code,
7298            request_resource.as_deref(),
7299            #[cfg(feature = "on_behalf_of")]
7300            on_behalf_of_client_id,
7301            #[cfg(feature = "redirect_uri")]
7302            redirect_uri,
7303            req_cnf,
7304        )
7305        .await
7306    }
7307
7308    #[allow(clippy::too_many_arguments)]
7309    async fn exchange_prt_for_refresh_token_jwt_bearer(
7310        &self,
7311        prt: &PrimaryRefreshToken,
7312        tpm: &mut BoxedDynTpm,
7313        storage_key: &StorageKey,
7314        session_key: &SessionKey,
7315        client_id: &str,
7316        redirect_uri: &str,
7317        request_resource: Option<&str>,
7318    ) -> Result<UserToken, MsalError> {
7319        let nonce = self.request_nonce().await?;
7320        let jwt_payload = ExchangePRTForATPayload::new(
7321            prt,
7322            &nonce,
7323            "openid profile offline_access",
7324            client_id,
7325            redirect_uri,
7326            request_resource,
7327        )?;
7328        let jwt = JwsBuilder::from(serde_json::to_vec(&jwt_payload).map_err(|e| {
7329            MsalError::InvalidJson(format!("Failed serializing ExchangePRTForAT JWT: {}", e))
7330        })?)
7331        .set_typ(Some("JWT"))
7332        .build();
7333
7334        if let Ok(mut payload) = jwt.from_json::<Value>() {
7335            payload["refresh_token"] = "**********".into();
7336            if let Ok(pretty) = to_string_pretty(&payload) {
7337                debug!("Exchange PRT for refresh token payload: {}", pretty);
7338            }
7339        }
7340
7341        let signed_jwt = self
7342            .sign_session_key_jwt(&jwt, tpm, storage_key, session_key)
7343            .await?;
7344        let params = [
7345            ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
7346            ("windows_api_version", "2.2"),
7347            ("request", signed_jwt.as_str()),
7348            ("client_id", client_id),
7349            ("redirect_uri", redirect_uri),
7350            ("client_info", "1"),
7351        ];
7352        let payload = params
7353            .iter()
7354            .map(|(key, value)| format!("{}={}", key, value))
7355            .collect::<Vec<String>>()
7356            .join("&");
7357        let url = format!("{}/oauth2/token", self.authority()?);
7358        let mut debug_params = params;
7359        debug_params[2] = ("request", "**********");
7360        if let Ok(pretty) = to_string_pretty(&debug_params) {
7361            debug!("POST PRT refresh-token exchange {}: {}", url, pretty);
7362        }
7363
7364        let response = self
7365            .client()
7366            .post(&url)
7367            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
7368            .body(payload)
7369            .send()
7370            .await
7371            .map_err(|e| MsalError::request_failed(&e))?;
7372        let token: UserToken = if response.status().is_success() {
7373            let response_text = response
7374                .text()
7375                .await
7376                .map_err(|e| MsalError::GeneralFailure(format!("{}", e)))?;
7377            let transport_key = self.transport_key(tpm, storage_key)?;
7378            let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
7379            let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
7380            if let Ok(jwe) = JweCompact::from_str(&response_text) {
7381                let decrypted =
7382                    session_key.decipher_prt_v2(tpm, &transport_key, prt_storage_key, &jwe)?;
7383                json_from_str(
7384                    std::str::from_utf8(decrypted.payload())
7385                        .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
7386                )
7387                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?
7388            } else {
7389                json_from_str(&response_text)
7390                    .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?
7391            }
7392        } else {
7393            let response: ErrorResponse = response
7394                .json()
7395                .await
7396                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
7397            return Err(MsalError::AcquireTokenFailed(response));
7398        };
7399        debug!(
7400            "PRT refresh-token exchange succeeded, refresh_token present={}",
7401            !token.refresh_token.is_empty()
7402        );
7403        Ok(token)
7404    }
7405
7406    /// Exchange a PRT for an access token using a two-step PoP flow:
7407    /// 1. Use JWT bearer grant with PRT to get a bearer token + refresh_token
7408    ///    for basic scopes (openid profile offline_access)
7409    /// 2. Use that refresh_token with grant_type=refresh_token + target scope
7410    ///    + token_type=pop + req_cnf to get the PoP access token
7411    ///
7412    /// This is required because Azure rejects PRT-based flows (both
7413    /// authorize -> auth_code and JWT bearer) for certain scopes like
7414    /// ms-device-service:// with AADSTS50132 (SsoArtifactInvalidOrExpired).
7415    /// Using a standard refresh_token from a prior token acquisition bypasses
7416    /// this restriction.
7417    #[allow(clippy::too_many_arguments)]
7418    async fn exchange_prt_for_access_token_jwt_bearer(
7419        &self,
7420        prt: &PrimaryRefreshToken,
7421        scope: Vec<&str>,
7422        v2_endpoint: bool,
7423        tpm: &mut BoxedDynTpm,
7424        storage_key: &StorageKey,
7425        session_key: &SessionKey,
7426        request_resource: Option<String>,
7427        req_cnf: &str,
7428        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
7429        #[cfg(feature = "redirect_uri")] redirect_uri_override: Option<&str>,
7430    ) -> Result<UserToken, MsalError> {
7431        debug!("Exchanging a PRT for an Access Token via JWT bearer (PoP)");
7432
7433        #[cfg(not(feature = "on_behalf_of"))]
7434        let on_behalf_of_client_id: Option<&str> = None;
7435        #[cfg(not(feature = "redirect_uri"))]
7436        let redirect_uri_override: Option<&str> = None;
7437
7438        // Resolve client_id and redirect_uri using the same logic as the
7439        // authorize -> auth_code flow
7440        let (client_id, redirect_uri) = if let Some(uri) = redirect_uri_override {
7441            let cid = if v2_endpoint {
7442                if let Some(obo) = on_behalf_of_client_id {
7443                    obo.to_string()
7444                } else if let Some(obo) = &self.on_behalf_of_client_id {
7445                    obo.clone()
7446                } else {
7447                    LINUX_BROKER_APP_ID.to_string()
7448                }
7449            } else {
7450                self.app.client_id().to_string()
7451            };
7452            (cid, uri.to_string())
7453        } else if v2_endpoint {
7454            if let Some(obo) = on_behalf_of_client_id {
7455                (
7456                    obo.to_string(),
7457                    self.app
7458                        .get_auth_redirect_uri(Some(obo), request_resource.as_deref()),
7459                )
7460            } else if let Some(obo) = &self.on_behalf_of_client_id {
7461                (obo.clone(), HIMMELBLAU_REDIRECT_URI.to_string())
7462            } else {
7463                (
7464                    LINUX_BROKER_APP_ID.to_string(),
7465                    self.app.get_auth_redirect_uri(
7466                        Some(LINUX_BROKER_APP_ID),
7467                        request_resource.as_deref(),
7468                    ),
7469                )
7470            }
7471        } else {
7472            (
7473                self.app.client_id().to_string(),
7474                self.app
7475                    .get_auth_redirect_uri(None, request_resource.as_deref()),
7476            )
7477        };
7478
7479        let bearer_token = self
7480            .exchange_prt_for_refresh_token_jwt_bearer(
7481                prt,
7482                tpm,
7483                storage_key,
7484                session_key,
7485                &client_id,
7486                &redirect_uri,
7487                request_resource.as_deref(),
7488            )
7489            .await?;
7490
7491        // Step 2: Use the refresh_token from step 1 with
7492        // grant_type=refresh_token + target scope + token_type=pop + req_cnf
7493        let target_scopes = if v2_endpoint {
7494            format!("openid profile offline_access {}", scope.join(" "))
7495        } else {
7496            "openid".to_string()
7497        };
7498
7499        let mut step2_params = vec![
7500            ("client_id", client_id.as_str()),
7501            ("scope", target_scopes.as_str()),
7502            ("grant_type", "refresh_token"),
7503            ("refresh_token", &bearer_token.refresh_token),
7504            ("client_info", "1"),
7505            ("token_type", "pop"),
7506            ("req_cnf", req_cnf),
7507        ];
7508        if !v2_endpoint {
7509            if let Some(resource) = request_resource.as_deref() {
7510                step2_params.push(("resource", resource));
7511            }
7512        }
7513        let step2_payload = step2_params
7514            .iter()
7515            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
7516            .collect::<Vec<String>>()
7517            .join("&");
7518
7519        // Step 2 uses the v2 endpoint when v2 scopes are present, so the
7520        // target scope (e.g. ms-device-service://) is correctly resolved.
7521        let step2_url = if v2_endpoint {
7522            format!("{}/oauth2/v2.0/token", self.authority()?)
7523        } else {
7524            format!("{}/oauth2/token", self.authority()?)
7525        };
7526
7527        let mut debug_params2 = step2_params.clone();
7528        debug_params2[3] = ("refresh_token", "**********");
7529        if let Ok(pretty) = to_string_pretty(&debug_params2) {
7530            debug!("Step 2 POST {}: {}", step2_url, pretty);
7531        }
7532
7533        let resp2 = self
7534            .client()
7535            .post(&step2_url)
7536            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
7537            .header(header::ACCEPT, "application/json")
7538            .body(step2_payload)
7539            .send()
7540            .await
7541            .map_err(|e| MsalError::request_failed(&e))?;
7542
7543        if resp2.status().is_success() {
7544            let token: UserToken = resp2
7545                .json()
7546                .await
7547                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
7548            Ok(token)
7549        } else {
7550            let json_resp: ErrorResponse = resp2
7551                .json()
7552                .await
7553                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
7554            Err(MsalError::AcquireTokenFailed(json_resp))
7555        }
7556    }
7557
7558    /// Given the primary refresh token, this method requests a new primary
7559    /// refresh token
7560    ///
7561    /// # Arguments
7562    ///
7563    /// * `sealed_prt` -  An encrypted primary refresh token that was
7564    ///   previously received from the server.
7565    ///
7566    /// * `tpm` - The tpm object.
7567    ///
7568    /// * `storage_key` - The TPM MachineKey associated with this application.
7569    ///
7570    /// * `request_tgt` - Whether to include a request for a TGT.
7571    ///
7572    /// # Returns
7573    /// * Success: An encrypted PrimaryRefreshToken, containing a refresh_token
7574    ///   and optionally a tgt. The session key is copied from the old PRT.
7575    /// * Failure: An MsalError, indicating the failure.
7576    pub async fn exchange_prt_for_prt(
7577        &self,
7578        sealed_prt: &SealedData,
7579        tpm: &mut BoxedDynTpm,
7580        storage_key: &StorageKey,
7581        request_tgt: bool,
7582    ) -> Result<SealedData, MsalError> {
7583        debug!("Exchanging a PRT for a new PRT");
7584
7585        let transport_key = self.transport_key(tpm, storage_key)?;
7586
7587        // This allows a transition, where existing msoapxbc keys will provide
7588        // their sealing content encryption keys, but newer keys will not, falling back
7589        // to the provided key.
7590        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
7591        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
7592
7593        let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
7594        let session_key = prt.session_key()?;
7595        let nonce = self.request_nonce().await?;
7596        let jwt = JwsBuilder::from(
7597            serde_json::to_vec(&ExchangePRTPayload::new(&prt, &nonce, None, true)?).map_err(
7598                |e| MsalError::InvalidJson(format!("Failed serializing ExchangePRT JWT: {}", e)),
7599            )?,
7600        )
7601        .set_typ(Some("JWT"))
7602        .build();
7603
7604        if let Ok(mut payload) = jwt.from_json::<Value>() {
7605            payload["refresh_token"] = "**********".into();
7606            if let Ok(pretty) = to_string_pretty(&payload) {
7607                debug!("Exchange PRT Payload JWT: {}", pretty);
7608            }
7609        }
7610
7611        let signed_jwt = self
7612            .sign_session_key_jwt(&jwt, tpm, storage_key, &session_key)
7613            .await?;
7614
7615        let mut params = vec![
7616            ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
7617            ("windows_api_version", "2.2"),
7618            ("request", &signed_jwt),
7619            ("client_info", "1"),
7620        ];
7621        if request_tgt {
7622            params.push(("tgt", "true"));
7623        }
7624        let payload = params
7625            .iter()
7626            .map(|(k, v)| format!("{}={}", k, v))
7627            .collect::<Vec<String>>()
7628            .join("&");
7629
7630        // TODO: add condition here to support v2 endpoints?
7631        let url = format!("{}/oauth2/token", self.authority()?);
7632
7633        let mut debug_payload = params.clone();
7634        debug_payload[2] = ("request", "**********");
7635        if let Ok(pretty) = to_string_pretty(&debug_payload) {
7636            debug!("POST {}: {}", url, pretty);
7637        }
7638
7639        let resp = self
7640            .client()
7641            .post(url)
7642            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
7643            .body(payload)
7644            .send()
7645            .await
7646            .map_err(|e| MsalError::request_failed(&e))?;
7647        if resp.status().is_success() {
7648            let enc = resp
7649                .text()
7650                .await
7651                .map_err(|e| MsalError::GeneralFailure(format!("{}", e)))?;
7652            let jwe = JweCompact::from_str(&enc)
7653                .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
7654            let mut new_prt: PrimaryRefreshToken = json_from_str(
7655                std::str::from_utf8(
7656                    session_key
7657                        .decipher_prt_v2(tpm, &transport_key, prt_storage_key, &jwe)?
7658                        .payload(),
7659                )
7660                .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
7661            )
7662            .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
7663            prt.clone_session_key(&mut new_prt);
7664            self.seal_user_prt(&new_prt, tpm, prt_storage_key)
7665        } else {
7666            let json_resp: ErrorResponse = resp
7667                .json()
7668                .await
7669                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
7670            Err(MsalError::AcquireTokenFailed(json_resp))
7671        }
7672    }
7673
7674    /// Provision a new Hello for Business Key
7675    ///
7676    /// # Arguments
7677    ///
7678    /// * `token` - Token obtained via either
7679    ///   acquire_token_by_username_password_for_device_enrollment
7680    ///   or acquire_token_by_device_flow.
7681    ///
7682    /// * `tpm` - The tpm object.
7683    ///
7684    /// * `storage_key` - The TPM MachineKey associated with this application.
7685    ///
7686    /// * `pin` - The PIN code which will be used to unlock the key.
7687    ///
7688    /// # Returns
7689    /// * Success: Either the existing LoadableMsHelloKey, or a new created
7690    ///   key if none was provided.
7691    /// * Failure: An MsalError, indicating the failure.
7692    pub async fn provision_hello_for_business_key(
7693        &self,
7694        token: &UserToken,
7695        tpm: &mut BoxedDynTpm,
7696        storage_key: &StorageKey,
7697        pin: &str,
7698    ) -> Result<LoadableMsHelloKey, MsalError> {
7699        debug!("Provisioning a Hello for Business Key");
7700
7701        // If we at least have the mfa amr already, we can request a token
7702        // containing ngcmfa. If we have neither, we cannot proceed.
7703        if !token.amr_ngcmfa()? && !token.amr_mfa()? {
7704            error!("Key provisioning is impossible without an ngcmfa amr!");
7705            return Err(MsalError::GeneralFailure(
7706                "Token is missing an ngcmfa amr".to_string(),
7707            ));
7708        }
7709
7710        let pin = PinValue::new(pin)
7711            .map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
7712
7713        // Discover the KeyProvisioningService
7714        let access_token = match &token.access_token {
7715            Some(access_token) => access_token.clone(),
7716            None => {
7717                return Err(MsalError::GeneralFailure(
7718                    "Access token missing".to_string(),
7719                ))
7720            }
7721        };
7722        let services = Services::new(
7723            &access_token,
7724            &token.tenant_id()?,
7725            #[cfg(feature = "set_timeout")]
7726            self.app.app.timeout,
7727            #[cfg(feature = "ipvers")]
7728            &self.app.app.ip_version,
7729        )
7730        .await?;
7731        let resource_id = services.key_provisioning_resource_id();
7732
7733        // Acquire an access token for the key provisioning service
7734        let token = self
7735            .acquire_token_by_refresh_token_internal(
7736                &token.refresh_token,
7737                vec![],
7738                Some(resource_id),
7739                #[cfg(feature = "on_behalf_of")]
7740                None,
7741                true, // Hello enrollment must demand MFA
7742                tpm,
7743                storage_key,
7744            )
7745            .await?;
7746
7747        // Create a new hello key (using the TPM)
7748        let loadable_win_hello_key = tpm.ms_hello_key_create(storage_key, &pin).map_err(|e| {
7749            MsalError::TPMFail(format!("Failed creating Windows Hello Key: {:?}", e))
7750        })?;
7751
7752        // TODO!!! This now has the unlocked storage key associated to the user that you can use
7753        // to protect PRT's.
7754        let (win_hello_key, _win_hello_storage_key) = tpm
7755            .ms_hello_key_load(storage_key, &loadable_win_hello_key, &pin)
7756            .map_err(|e| {
7757                MsalError::TPMFail(format!("Failed loading Windows Hello Key: {:?}", e))
7758            })?;
7759
7760        let win_hello_pub_der = tpm
7761            .ms_hello_rsa_public_as_der(&win_hello_key)
7762            .map_err(|e| {
7763                MsalError::TPMFail(format!("Failed getting Windows Hello Key as der: {:?}", e))
7764            })?;
7765
7766        let win_hello_rsa = Rsa::public_key_from_der(&win_hello_pub_der)
7767            .map_err(|e| MsalError::TPMFail(format!("{}", e)))?;
7768
7769        let access_token = match &token.access_token {
7770            Some(access_token) => access_token.clone(),
7771            None => {
7772                return Err(MsalError::GeneralFailure(
7773                    "Access token missing".to_string(),
7774                ))
7775            }
7776        };
7777
7778        match services.provision_key(&access_token, &win_hello_rsa).await {
7779            Ok(()) => Ok(loadable_win_hello_key.clone()),
7780            Err(_) => Err(MsalError::GeneralFailure(
7781                "Failed registering Windows Hello Key".to_string(),
7782            )),
7783        }
7784    }
7785
7786    /// Gets a token for a given resource via a Hello for Business Key
7787    ///
7788    /// # Arguments
7789    ///
7790    /// * `username` - Typically a UPN in the form of an email address.
7791    ///
7792    /// * `key` - A LoadableMsHelloKey provisioned using
7793    ///   provision_hello_for_business_key.
7794    ///
7795    /// * `scopes` - Scopes requested to access a protected API (a resource).
7796    ///
7797    /// * `request_resource` - A resource for obtaining an access token.
7798    ///   Default is the MS Graph API (00000002-0000-0000-c000-000000000000).
7799    ///
7800    /// * `on_behalf_of_client_id`: Request a resource or scope on behalf of
7801    ///   the specified client_id. Optional. This option requires libhimmelblau
7802    ///   be built with the `on_behalf_of` feature.
7803    ///
7804    /// * `tpm` - The tpm object.
7805    ///
7806    /// * `storage_key` - The TPM MachineKey associated with this application.
7807    ///
7808    /// * `pin` - The PIN code required to unlock the key.
7809    ///
7810    /// # Returns
7811    /// * Success: A UserToken containing an access_token.
7812    /// * Failure: An MsalError, indicating the failure.
7813    #[allow(clippy::too_many_arguments)]
7814    pub async fn acquire_token_by_hello_for_business_key(
7815        &self,
7816        username: &str,
7817        key: &LoadableMsHelloKey,
7818        scopes: Vec<&str>,
7819        request_resource: Option<String>,
7820        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
7821        tpm: &mut BoxedDynTpm,
7822        storage_key: &StorageKey,
7823        pin: &str,
7824    ) -> Result<UserToken, MsalError> {
7825        let v2_endpoint = !scopes.is_empty();
7826        if !scopes.is_empty() && request_resource.is_some() {
7827            return Err(MsalError::GeneralFailure(
7828                "Scopes cannot be specified with a request_resource".to_string(),
7829            ));
7830        }
7831
7832        let pin = PinValue::new(pin)
7833            .map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
7834
7835        let prt = self
7836            .acquire_user_prt_by_hello_for_business_key_internal(
7837                username,
7838                key,
7839                tpm,
7840                storage_key,
7841                &pin,
7842            )
7843            .await?;
7844        let transport_key = self.transport_key(tpm, storage_key)?;
7845
7846        // This allows a transition, where existing msoapxbc keys will provide
7847        // their sealing content encryption keys, but newer keys will not, falling back
7848        // to the provided key.
7849        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
7850        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
7851
7852        let session_key = prt.session_key()?;
7853        let mut token = self
7854            .exchange_prt_for_access_token_internal(
7855                &prt,
7856                scopes.clone(),
7857                v2_endpoint,
7858                tpm,
7859                storage_key,
7860                &session_key,
7861                request_resource,
7862                #[cfg(feature = "on_behalf_of")]
7863                on_behalf_of_client_id,
7864                #[cfg(feature = "redirect_uri")]
7865                None,
7866                #[cfg(feature = "pop_support")]
7867                None,
7868                false,
7869            )
7870            .await?;
7871        token.client_info = prt.client_info.clone();
7872        // TODO: You should change this to the hello key's related storage key.
7873        token.prt = Some(self.seal_user_prt(&prt, tpm, prt_storage_key)?);
7874        Ok(token)
7875    }
7876
7877    async fn build_jwt_by_hello_for_business_key(
7878        &self,
7879        username: &str,
7880        loadable_key: &LoadableMsHelloKey,
7881        tpm: &mut BoxedDynTpm,
7882        storage_key: &StorageKey,
7883        pin: &PinValue,
7884    ) -> Result<Jws, MsalError> {
7885        debug!("Building a Hello for Business JWT");
7886
7887        let mut nonce = self.request_nonce().await?;
7888        let (key, _win_hello_storage_key) = tpm
7889            .ms_hello_key_load(storage_key, loadable_key, pin)
7890            .map_err(|e| MsalError::TPMFail(format!("{:?}", e)))?;
7891        let win_hello_pub_der = tpm.ms_hello_rsa_public_as_der(&key).map_err(|e| {
7892            MsalError::TPMFail(format!("Failed getting Windows Hello Key as der: {:?}", e))
7893        })?;
7894        let win_hello_rsa = Rsa::public_key_from_der(&win_hello_pub_der)
7895            .map_err(|e| MsalError::TPMFail(format!("{}", e)))?;
7896        let win_hello_blob: Vec<u8> = BcryptRsaKeyBlob::new(
7897            2048,
7898            &win_hello_rsa.e().to_vec(),
7899            &win_hello_rsa.n().to_vec(),
7900        )
7901        .try_into()?;
7902        let kid = STANDARD.encode(
7903            hash(MessageDigest::sha256(), &win_hello_blob)
7904                .map_err(|e| MsalError::CryptoFail(format!("{}", e)))?,
7905        );
7906        let assertion_jwt = JwsBuilder::from(
7907            serde_json::to_vec(
7908                &HelloForBusinessAssertion::new(username, &nonce)
7909                    .map_err(|e| MsalError::GeneralFailure(format!("{:?}", e)))?,
7910            )
7911            .map_err(|e| {
7912                MsalError::InvalidJson(format!(
7913                    "Failed serializing Hello for Business Assertion JWT: {}",
7914                    e
7915                ))
7916            })?,
7917        )
7918        .set_typ(Some("JWT"))
7919        .set_use(Some("ngc"))
7920        .set_kid(Some(&kid))
7921        .build();
7922
7923        if let Ok(payload) = assertion_jwt.from_json::<Value>() {
7924            if let Ok(pretty) = to_string_pretty(&payload) {
7925                debug!("Hello for Business Assertion: {}", pretty);
7926            }
7927        }
7928
7929        let mut jws_tpm_signer = match JwsTpmRs256Signer::new(tpm, &key) {
7930            Ok(jws_tpm_signer) => jws_tpm_signer,
7931            Err(e) => {
7932                return Err(MsalError::TPMFail(format!(
7933                    "Failed loading tpm signer: {}",
7934                    e
7935                )))
7936            }
7937        };
7938        let signed_assertion = match jws_tpm_signer.sign(&assertion_jwt) {
7939            Ok(signed_jwt) => signed_jwt,
7940            Err(e) => return Err(MsalError::TPMFail(format!("Failed signing jwk: {}", e))),
7941        };
7942        let assertion = format!("{}", signed_assertion);
7943
7944        nonce = self.request_nonce().await?;
7945
7946        let cert_key = self.cert_key(tpm, storage_key)?;
7947        let cert_der = cert_key.cert.to_der().map_err(|e| {
7948            MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
7949        })?;
7950        let jwt = JwsBuilder::from(
7951            serde_json::to_vec(&HelloForBusinessPayload::new(username, &assertion, &nonce))
7952                .map_err(|e| {
7953                    MsalError::InvalidJson(format!(
7954                        "Failed serializing Hello for Business JWT: {}",
7955                        e
7956                    ))
7957                })?,
7958        )
7959        .set_typ(Some("JWT"))
7960        .set_x5c(Some(vec![cert_der]))
7961        .build();
7962
7963        if let Ok(mut jwt_debug) = jwt.from_json::<Value>() {
7964            jwt_debug["assertion"] = "**********".into();
7965            if let Ok(pretty) = to_string_pretty(&jwt_debug) {
7966                debug!("Hello for Business Payload: {}", pretty);
7967            }
7968        }
7969
7970        Ok(jwt)
7971    }
7972
7973    async fn acquire_user_prt_by_hello_for_business_key_internal(
7974        &self,
7975        username: &str,
7976        key: &LoadableMsHelloKey,
7977        tpm: &mut BoxedDynTpm,
7978        storage_key: &StorageKey,
7979        pin: &PinValue,
7980    ) -> Result<PrimaryRefreshToken, MsalError> {
7981        debug!("Acquiring a User PRT via a Hello for Business Key");
7982
7983        let jwt = self
7984            .build_jwt_by_hello_for_business_key(username, key, tpm, storage_key, pin)
7985            .await?;
7986        let signed_jwt = self.sign_jwt(&jwt, tpm, storage_key).await?;
7987
7988        self.acquire_user_prt_jwt(&signed_jwt).await
7989    }
7990
7991    /// Gets a Primary Refresh Token (PRT) via a Hello for Business Key
7992    ///
7993    /// # Arguments
7994    ///
7995    /// * `username` - Typically a UPN in the form of an email address.
7996    ///
7997    /// * `key` - A LoadableMsHelloKey provisioned using
7998    ///   provision_hello_for_business_key.
7999    ///
8000    /// * `tpm` - The tpm object.
8001    ///
8002    /// * `storage_key` - The TPM MachineKey associated with this application.
8003    ///
8004    /// * `pin` - The PIN code required to unlock the key.
8005    ///
8006    /// # Returns
8007    /// * Success: An encrypted PrimaryRefreshToken, containing a refresh_token and tgt.
8008    /// * Failure: An MsalError, indicating the failure.
8009    pub async fn acquire_user_prt_by_hello_for_business_key(
8010        &self,
8011        username: &str,
8012        key: &LoadableMsHelloKey,
8013        tpm: &mut BoxedDynTpm,
8014        storage_key: &StorageKey,
8015        pin: &str,
8016    ) -> Result<SealedData, MsalError> {
8017        let pin = PinValue::new(pin)
8018            .map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
8019
8020        let prt = self
8021            .acquire_user_prt_by_hello_for_business_key_internal(
8022                username,
8023                key,
8024                tpm,
8025                storage_key,
8026                &pin,
8027            )
8028            .await?;
8029        let transport_key = self.transport_key(tpm, storage_key)?;
8030
8031        // This allows a transition, where existing msoapxbc keys will provide
8032        // their sealing content encryption keys, but newer keys will not, falling back
8033        // to the provided key.
8034        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
8035        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
8036
8037        self.seal_user_prt(&prt, tpm, prt_storage_key)
8038    }
8039
8040    #[allow(clippy::too_many_arguments)]
8041    async fn exchange_prt_for_auth_code_internal(
8042        &self,
8043        scope: Vec<&str>,
8044        request_id: &str,
8045        resource: Option<&str>,
8046        v2_endpoint: bool,
8047        signed_prt_payload: Option<String>,
8048        signed_device_payload: Option<String>,
8049        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
8050        #[cfg(feature = "redirect_uri")] redirect_uri_override: Option<&str>,
8051        _req_cnf: Option<&str>,
8052        demand_mfa: bool,
8053    ) -> Result<String, MsalError> {
8054        #[cfg(not(feature = "on_behalf_of"))]
8055        let on_behalf_of_client_id: Option<&str> = None;
8056        #[cfg(not(feature = "redirect_uri"))]
8057        let redirect_uri_override: Option<&str> = None;
8058
8059        let scope = format!("openid profile {}", scope.join(" "));
8060        let (client_id, redirect_uri) = if let Some(uri) = redirect_uri_override {
8061            // Use the caller-supplied redirect URI with the appropriate client_id
8062            let cid = if v2_endpoint {
8063                if let Some(obo) = on_behalf_of_client_id {
8064                    obo.to_string()
8065                } else if let Some(obo) = &self.on_behalf_of_client_id {
8066                    obo.clone()
8067                } else {
8068                    LINUX_BROKER_APP_ID.to_string()
8069                }
8070            } else {
8071                self.app.client_id().to_string()
8072            };
8073            (cid, uri.to_string())
8074        } else if v2_endpoint {
8075            if let Some(on_behalf_of_client_id) = on_behalf_of_client_id {
8076                (
8077                    on_behalf_of_client_id.to_string(),
8078                    self.app
8079                        .get_auth_redirect_uri(Some(on_behalf_of_client_id), resource),
8080                )
8081            } else if let Some(on_behalf_of_client_id) = &self.on_behalf_of_client_id {
8082                (
8083                    on_behalf_of_client_id.clone(),
8084                    HIMMELBLAU_REDIRECT_URI.to_string(),
8085                )
8086            } else {
8087                (
8088                    LINUX_BROKER_APP_ID.to_string(),
8089                    self.app
8090                        .get_auth_redirect_uri(Some(LINUX_BROKER_APP_ID), resource),
8091                )
8092            }
8093        } else {
8094            (
8095                self.app.client_id().to_string(),
8096                self.app.get_auth_redirect_uri(None, resource),
8097            )
8098        };
8099
8100        let mut params = vec![
8101            ("client_id", client_id.as_str()),
8102            ("response_type", "code"),
8103            ("redirect_uri", redirect_uri.as_str()),
8104            ("client-request-id", request_id),
8105        ];
8106        if v2_endpoint {
8107            params.push(("scope", &scope));
8108        } else if let Some(resource) = resource {
8109            params.push(("resource", resource));
8110        } else {
8111            params.push(("resource", "https://graph.microsoft.com"));
8112        }
8113        if !v2_endpoint && demand_mfa {
8114            params.push(("amr_values", "ngcmfa"));
8115        }
8116        let payload = params
8117            .iter()
8118            .map(|(k, v)| format!("{}={}", k, url_encode(v)))
8119            .collect::<Vec<String>>()
8120            .join("&");
8121
8122        let url = if v2_endpoint {
8123            format!("{}/oAuth2/v2.0/authorize?{}", self.authority()?, payload)
8124        } else {
8125            format!("{}/oauth2/authorize?{}", self.authority()?, payload)
8126        };
8127        debug!("GET {}", url);
8128
8129        let mut req = self.client().get(url).header(header::USER_AGENT, "");
8130        if let Some(signed_prt_payload) = signed_prt_payload {
8131            req = req.header("x-ms-RefreshTokenCredential", signed_prt_payload);
8132        }
8133        if let Some(signed_device_payload) = signed_device_payload {
8134            req = req.header("x-ms-DeviceCredential", signed_device_payload);
8135        }
8136        let mut resp = req
8137            .send()
8138            .await
8139            .map_err(|e| MsalError::request_failed(&e))?;
8140        let text;
8141        (text, resp) = self.app.await_working(resp).await?;
8142        if resp.status().is_redirection() {
8143            let document = Html::parse_document(&text);
8144            let selector = Selector::parse("a[href]").map_err(|_| {
8145                MsalError::InvalidParse("Failed parsing auth code response".to_string())
8146            })?;
8147            if let Some(element) = document.select(&selector).next() {
8148                if let Some(href_encoded) = element.value().attr("href") {
8149                    let href = percent_decode_str(href_encoded)
8150                        .decode_utf8()
8151                        .map_err(|e| {
8152                            MsalError::URLFormatFailed(format!("Failed decoding url: {:?}", e))
8153                        })?;
8154                    if let Ok(url) = Url::parse(&href) {
8155                        return url
8156                            .query_pairs()
8157                            .find_map(|(key, value)| {
8158                                if key == "code" {
8159                                    Some(value.into_owned())
8160                                } else {
8161                                    None
8162                                }
8163                            })
8164                            .ok_or(MsalError::GeneralFailure(
8165                                "Authorization code not found".to_string(),
8166                            ));
8167                    }
8168                }
8169            }
8170
8171            // MS may have returned an AuthConfig here with an error attached.
8172            // Return the error from that AuthConfig if possible. If a required
8173            // password change or MFA is indicated, raise an error.
8174            match self.app.parse_auth_config(&text, false, false) {
8175                #[cfg(feature = "changepassword")]
8176                Err(MsalError::ChangePassword) => return Err(MsalError::ChangePassword),
8177                Err(MsalError::AADSTSError(e)) => return Err(MsalError::AADSTSError(e)),
8178                Err(MsalError::ConsentRequested(e)) => return Err(MsalError::ConsentRequested(e)),
8179                Ok(auth_config) if auth_config.pgid.as_deref() == Some("ConvergedTFA") => {
8180                    return Err(MsalError::MFARequired);
8181                }
8182                _ => {}
8183            }
8184
8185            Err(MsalError::GeneralFailure(format!(
8186                "Authorization code not found in: {}",
8187                text
8188            )))
8189        } else if resp.status().is_success() {
8190            let re = Regex::new(r#"document\.location\.replace\("([^"]+)"\)"#)
8191                .map_err(|e| MsalError::InvalidRegex(format!("{}", e)))?;
8192            if let Some(m) = re.captures(&text) {
8193                if let Some(redirect) = m.get(1) {
8194                    let redirect_decoded = Url::parse(&redirect.as_str().replace(r#"\u0026"#, "&"))
8195                        .map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
8196                    for (k, v) in redirect_decoded.query_pairs().collect::<Vec<_>>() {
8197                        if k == "code" {
8198                            return Ok(v.to_string());
8199                        }
8200                        if k == "error_description" {
8201                            return Err(MsalError::GeneralFailure(v.to_string()));
8202                        }
8203                    }
8204                }
8205            }
8206
8207            // MS may have returned an AuthConfig here with an error attached.
8208            // Return the error from that AuthConfig if possible. If a required
8209            // password change or MFA is indicated, raise an error.
8210            match self.app.parse_auth_config(&text, false, false) {
8211                #[cfg(feature = "changepassword")]
8212                Err(MsalError::ChangePassword) => return Err(MsalError::ChangePassword),
8213                Err(MsalError::AADSTSError(e)) => return Err(MsalError::AADSTSError(e)),
8214                Err(MsalError::ConsentRequested(e)) => return Err(MsalError::ConsentRequested(e)),
8215                Ok(auth_config) if auth_config.pgid.as_deref() == Some("ConvergedTFA") => {
8216                    return Err(MsalError::MFARequired);
8217                }
8218                _ => {}
8219            }
8220
8221            Err(MsalError::GeneralFailure(format!(
8222                "Authorization code not found in: {}",
8223                text
8224            )))
8225        } else {
8226            let json_resp: ErrorResponse = resp
8227                .json()
8228                .await
8229                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
8230            Err(MsalError::AcquireTokenFailed(json_resp))
8231        }
8232    }
8233
8234    /// Creates a single sign-on (SSO) JWT Cookie from an encrypted Primary
8235    /// Refresh Token (PRT).
8236    ///
8237    /// # Arguments
8238    ///
8239    /// * `prt` - The encrypted Primary Refresh Token (PRT) that will be used
8240    ///   to generate the SSO cookie.
8241    ///
8242    /// * `tpm` - The TPM object used to interface with the hardware for
8243    ///   cryptographic operations.
8244    ///
8245    /// * `storage_key` - The TPM MachineKey associated with the current
8246    ///   device/application.
8247    ///
8248    /// # Returns
8249    /// * Success: A JWT (as a String) that can be used for single sign-on
8250    ///   (SSO) authentication.
8251    /// * Failure: An MsalError, indicating the failure.
8252    ///
8253    /// Given the primary refresh token, this method creates a PRT SSO
8254    /// cookie that can be used for browser single sign-on (SSO).
8255    ///
8256    /// # Arguments
8257    ///
8258    /// * `sealed_prt` - An encrypted primary refresh token.
8259    ///
8260    /// * `sso_nonce` - An optional SSO nonce extracted from a login URL's
8261    ///   `sso_nonce` query parameter. When `None`, the cookie is generated
8262    ///   with an `iat` (issued-at) claim instead, suitable for proactive
8263    ///   SSO where the cookie is set as a static header. When `Some`, the
8264    ///   provided nonce is included as `request_nonce` in the cookie.
8265    ///
8266    /// * `tpm` - The TPM object.
8267    ///
8268    /// * `storage_key` - The TPM MachineKey associated with this application.
8269    ///
8270    /// # Returns
8271    /// * Success: A JWT (as a String) that can be used for single sign-on
8272    ///   (SSO) authentication.
8273    /// * Failure: An MsalError, indicating the failure.
8274    pub async fn acquire_prt_sso_cookie(
8275        &self,
8276        prt: &SealedData,
8277        tpm: &mut BoxedDynTpm,
8278        storage_key: &StorageKey,
8279    ) -> Result<String, MsalError> {
8280        self.acquire_prt_sso_cookie_with_nonce(prt, None, tpm, storage_key)
8281            .await
8282    }
8283
8284    /// Like [`acquire_prt_sso_cookie`](Self::acquire_prt_sso_cookie), but
8285    /// accepts an optional SSO nonce.
8286    ///
8287    /// When `sso_nonce` is `None`, the cookie uses an `iat` (issued-at)
8288    /// claim, making it suitable for proactive SSO (e.g., the linux-entra-sso
8289    /// extension's static header injection). When `Some`, the provided
8290    /// nonce is included as `request_nonce`.
8291    pub async fn acquire_prt_sso_cookie_with_nonce(
8292        &self,
8293        prt: &SealedData,
8294        sso_nonce: Option<&str>,
8295        tpm: &mut BoxedDynTpm,
8296        storage_key: &StorageKey,
8297    ) -> Result<String, MsalError> {
8298        debug!("Creating a prt sso cookie");
8299
8300        let transport_key = self.transport_key(tpm, storage_key)?;
8301
8302        // This allows a transition, where existing msoapxbc keys will provide
8303        // their sealing content encryption keys, but newer keys will not, falling back
8304        // to the provided key.
8305        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
8306        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
8307
8308        let prt = self.unseal_user_prt(prt, tpm, prt_storage_key)?;
8309        let session_key = prt.session_key()?;
8310
8311        let jwt = JwsBuilder::from(
8312            serde_json::to_vec(&RefreshTokenCredentialPayload::new(&prt, sso_nonce)?).map_err(
8313                |e| MsalError::InvalidJson(format!("Failed serializing Authorization JWT: {}", e)),
8314            )?,
8315        )
8316        .set_typ(Some("JWT"))
8317        .build();
8318
8319        self.sign_session_key_jwt(&jwt, tpm, storage_key, &session_key)
8320            .await
8321    }
8322
8323    #[allow(clippy::too_many_arguments)]
8324    async fn exchange_prt_for_auth_code(
8325        &self,
8326        prt: &PrimaryRefreshToken,
8327        scope: Vec<&str>,
8328        request_id: &str,
8329        resource: Option<&str>,
8330        v2_endpoint: bool,
8331        session_key: &SessionKey,
8332        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
8333        tpm: &mut BoxedDynTpm,
8334        storage_key: &StorageKey,
8335        #[cfg(feature = "redirect_uri")] redirect_uri: Option<&str>,
8336        req_cnf: Option<&str>,
8337        demand_mfa: bool,
8338    ) -> Result<String, MsalError> {
8339        debug!("Exchanging a PRT for an Authorization Code");
8340
8341        let nonce = self.request_nonce().await?;
8342
8343        let jwt = JwsBuilder::from(
8344            serde_json::to_vec(&RefreshTokenCredentialPayload::new(prt, Some(&nonce))?).map_err(
8345                |e| MsalError::InvalidJson(format!("Failed serializing Authorization JWT: {}", e)),
8346            )?,
8347        )
8348        .set_typ(Some("JWT"))
8349        .build();
8350        let signed_prt_payload = self
8351            .sign_session_key_jwt(&jwt, tpm, storage_key, session_key)
8352            .await?;
8353        if let Ok(mut payload) = jwt.from_json::<Value>() {
8354            payload["refresh_token"] = "**********".into();
8355            if let Ok(pretty) = to_string_pretty(&payload) {
8356                debug!("Refresh Token Credential Payload: {}", pretty);
8357            }
8358        }
8359
8360        let cert_key = self.cert_key(tpm, storage_key)?;
8361        let cert_der = cert_key.cert.to_der().map_err(|e| {
8362            MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
8363        })?;
8364        let jwt = JwsBuilder::from(
8365            serde_json::to_vec(&DeviceCredentialPayload::new(&nonce)?).map_err(|e| {
8366                MsalError::InvalidJson(format!("Failed serializing Authorization JWT: {}", e))
8367            })?,
8368        )
8369        .set_typ(Some("JWT"))
8370        .set_x5c(Some(vec![cert_der]))
8371        .build();
8372        let signed_device_payload = self.sign_jwt(&jwt, tpm, storage_key).await?;
8373        if let Ok(payload) = jwt.from_json::<Value>() {
8374            if let Ok(pretty) = to_string_pretty(&payload) {
8375                debug!("Device Credential Payload: {}", pretty);
8376            }
8377        }
8378
8379        let result = self
8380            .exchange_prt_for_auth_code_internal(
8381                scope.clone(),
8382                request_id,
8383                resource,
8384                v2_endpoint,
8385                Some(signed_prt_payload.clone()),
8386                Some(signed_device_payload.clone()),
8387                #[cfg(feature = "on_behalf_of")]
8388                on_behalf_of_client_id,
8389                #[cfg(feature = "redirect_uri")]
8390                redirect_uri,
8391                req_cnf,
8392                demand_mfa,
8393            )
8394            .await;
8395
8396        // AADSTS16000 (InteractionRequired) can occur due to stale session
8397        // cookies. Clear cookies and retry once.
8398        match result {
8399            Err(MsalError::AADSTSError(ref e)) if e.code == 16000 => {
8400                warn!("PRT exchange failed with AADSTS16000, clearing cookies and retrying");
8401                self.clear_cookies();
8402                self.exchange_prt_for_auth_code_internal(
8403                    scope,
8404                    request_id,
8405                    resource,
8406                    v2_endpoint,
8407                    Some(signed_prt_payload),
8408                    Some(signed_device_payload),
8409                    #[cfg(feature = "on_behalf_of")]
8410                    on_behalf_of_client_id,
8411                    #[cfg(feature = "redirect_uri")]
8412                    redirect_uri,
8413                    req_cnf,
8414                    demand_mfa,
8415                )
8416                .await
8417            }
8418            other => other,
8419        }
8420    }
8421
8422    #[allow(clippy::too_many_arguments)]
8423    async fn exchange_auth_code_for_access_token_internal(
8424        &self,
8425        scope: Vec<&str>,
8426        request_id: &str,
8427        v2_endpoint: bool,
8428        authorization_code: String,
8429        request_resource: Option<&str>,
8430        #[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
8431        #[cfg(feature = "redirect_uri")] redirect_uri_override: Option<&str>,
8432        req_cnf: Option<&str>,
8433    ) -> Result<UserToken, MsalError> {
8434        debug!("Exchanging an Authorization Code for an Access Token");
8435        #[cfg(not(feature = "on_behalf_of"))]
8436        let on_behalf_of_client_id: Option<&str> = None;
8437        #[cfg(not(feature = "redirect_uri"))]
8438        let redirect_uri_override: Option<&str> = None;
8439
8440        let scopes_str = format!("openid profile offline_access {}", scope.join(" "));
8441        let (client_id, redirect_uri) = if let Some(uri) = redirect_uri_override {
8442            // Use the caller-supplied redirect URI with the appropriate client_id
8443            let cid = if v2_endpoint {
8444                if let Some(obo) = on_behalf_of_client_id {
8445                    obo.to_string()
8446                } else if let Some(obo) = &self.on_behalf_of_client_id {
8447                    obo.clone()
8448                } else {
8449                    LINUX_BROKER_APP_ID.to_string()
8450                }
8451            } else {
8452                self.app.client_id().to_string()
8453            };
8454            (cid, uri.to_string())
8455        } else if v2_endpoint {
8456            if let Some(on_behalf_of_client_id) = on_behalf_of_client_id {
8457                (
8458                    on_behalf_of_client_id.to_string(),
8459                    self.app
8460                        .get_auth_redirect_uri(Some(on_behalf_of_client_id), request_resource),
8461                )
8462            } else if let Some(on_behalf_of_client_id) = &self.on_behalf_of_client_id {
8463                (
8464                    on_behalf_of_client_id.clone(),
8465                    HIMMELBLAU_REDIRECT_URI.to_string(),
8466                )
8467            } else {
8468                (
8469                    LINUX_BROKER_APP_ID.to_string(),
8470                    self.app
8471                        .get_auth_redirect_uri(Some(LINUX_BROKER_APP_ID), request_resource),
8472                )
8473            }
8474        } else {
8475            (
8476                self.app.client_id().to_string(),
8477                self.app.get_auth_redirect_uri(None, request_resource),
8478            )
8479        };
8480        let mut params = vec![
8481            ("client_id", client_id.as_str()),
8482            ("grant_type", "authorization_code"),
8483            ("code", &authorization_code),
8484            ("redirect_uri", &redirect_uri),
8485            ("client-request-id", request_id),
8486        ];
8487        if v2_endpoint {
8488            params.push(("scope", &scopes_str));
8489        } else if let Some(request_resource) = request_resource {
8490            params.push(("resource", request_resource));
8491        } else {
8492            params.push(("resource", "https://graph.microsoft.com"));
8493        }
8494        if let Some(req_cnf) = req_cnf {
8495            params.push(("req_cnf", req_cnf));
8496        }
8497        let payload = params
8498            .iter()
8499            .map(|(k, v)| format!("{}={}", k, v))
8500            .collect::<Vec<String>>()
8501            .join("&");
8502
8503        let url = if v2_endpoint {
8504            format!("{}/oAuth2/v2.0/token", self.authority()?)
8505        } else {
8506            format!("{}/oauth2/token", self.authority()?)
8507        };
8508        let mut debug_payload = params;
8509        debug_payload[2] = ("code", "**********");
8510        if let Ok(pretty) = to_string_pretty(&debug_payload) {
8511            debug!("POST {}: {}", url, pretty);
8512        }
8513
8514        let resp = self
8515            .client()
8516            .post(url)
8517            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
8518            .body(payload)
8519            .send()
8520            .await
8521            .map_err(|e| MsalError::request_failed(&e))?;
8522        if resp.status().is_success() {
8523            let token: UserToken = resp
8524                .json()
8525                .await
8526                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
8527
8528            Ok(token)
8529        } else {
8530            let json_resp: ErrorResponse = resp
8531                .json()
8532                .await
8533                .map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
8534            Err(MsalError::AcquireTokenFailed(json_resp))
8535        }
8536    }
8537
8538    /// Change the password for an Entra Id user.
8539    ///
8540    /// This function allows changing the password for an Entra Id user. It
8541    /// requires the current username and password to validate the user's
8542    /// identity before updating the password.
8543    ///
8544    /// # Arguments
8545    ///
8546    /// * `username` - The username associated with the account for which the
8547    ///   password is being changed.
8548    /// * `password` - The current password for the account.
8549    /// * `new_password` - The new password that will replace the current
8550    ///   password.
8551    ///
8552    /// # Returns
8553    ///
8554    /// * Success: An empty Ok result indicating the password has been changed
8555    ///   successfully.
8556    /// * Failure: An MsalError, indicating problems such as authentication
8557    ///   failures or password complexity requirements not met.
8558    #[cfg(feature = "changepassword")]
8559    pub async fn handle_password_change(
8560        &self,
8561        username: &str,
8562        password: &str,
8563        new_password: &str,
8564    ) -> Result<(), MsalError> {
8565        self.app
8566            .handle_password_change(username, password, new_password)
8567            .await
8568    }
8569
8570    /// Fetch the name (GECOS) from the PRT
8571    ///
8572    /// # Arguments
8573    ///
8574    /// * `prt` - The encrypted Primary Refresh Token (PRT) that will be used
8575    ///   to generate the SSO cookie.
8576    ///
8577    /// * `tpm` - The TPM object used to interface with the hardware for
8578    ///   cryptographic operations.
8579    ///
8580    /// * `storage_key` - The TPM MachineKey associated with the current
8581    ///   device/application.
8582    ///
8583    /// # Returns
8584    ///
8585    /// * Success: The user Azure spn
8586    /// * Failure: An MsalError, indicating the failure.
8587    pub fn name_from_prt(
8588        &self,
8589        sealed_data: &SealedData,
8590        tpm: &mut BoxedDynTpm,
8591        storage_key: &StorageKey,
8592    ) -> Result<String, MsalError> {
8593        let transport_key = self.transport_key(tpm, storage_key)?;
8594
8595        // This allows a transition, where existing msoapxbc keys will provide
8596        // their sealing content encryption keys, but newer keys will not, falling back
8597        // to the provided key.
8598        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
8599        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
8600
8601        let prt = self.unseal_user_prt(sealed_data, tpm, prt_storage_key)?;
8602        Ok(prt.name())
8603    }
8604
8605    /// Fetch the spn from the PRT
8606    ///
8607    /// # Arguments
8608    ///
8609    /// * `prt` - The encrypted Primary Refresh Token (PRT) that will be used
8610    ///   to generate the SSO cookie.
8611    ///
8612    /// * `tpm` - The TPM object used to interface with the hardware for
8613    ///   cryptographic operations.
8614    ///
8615    /// * `storage_key` - The TPM MachineKey associated with the current
8616    ///   device/application.
8617    ///
8618    /// # Returns
8619    ///
8620    /// * Success: The user Azure spn
8621    /// * Failure: An MsalError, indicating the failure.
8622    pub fn spn_from_prt(
8623        &self,
8624        sealed_data: &SealedData,
8625        tpm: &mut BoxedDynTpm,
8626        storage_key: &StorageKey,
8627    ) -> Result<String, MsalError> {
8628        let transport_key = self.transport_key(tpm, storage_key)?;
8629
8630        // This allows a transition, where existing msoapxbc keys will provide
8631        // their sealing content encryption keys, but newer keys will not, falling back
8632        // to the provided key.
8633        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
8634        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
8635
8636        let prt = self.unseal_user_prt(sealed_data, tpm, prt_storage_key)?;
8637        prt.spn()
8638    }
8639
8640    /// Fetch the UUID from the PRT
8641    ///
8642    /// # Arguments
8643    ///
8644    /// * `prt` - The encrypted Primary Refresh Token (PRT) that will be used
8645    ///   to generate the SSO cookie.
8646    ///
8647    /// * `tpm` - The TPM object used to interface with the hardware for
8648    ///   cryptographic operations.
8649    ///
8650    /// * `storage_key` - The TPM MachineKey associated with the current
8651    ///   device/application.
8652    ///
8653    /// # Returns
8654    ///
8655    /// * Success: The user Azure UUID
8656    /// * Failure: An MsalError, indicating the failure.
8657    pub fn uuid_from_prt(
8658        &self,
8659        sealed_data: &SealedData,
8660        tpm: &mut BoxedDynTpm,
8661        storage_key: &StorageKey,
8662    ) -> Result<Uuid, MsalError> {
8663        let transport_key = self.transport_key(tpm, storage_key)?;
8664
8665        // This allows a transition, where existing msoapxbc keys will provide
8666        // their sealing content encryption keys, but newer keys will not, falling back
8667        // to the provided key.
8668        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
8669        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
8670
8671        let prt = self.unseal_user_prt(sealed_data, tpm, prt_storage_key)?;
8672        prt.uuid()
8673    }
8674
8675    /// Gets the Cloud TGT from a sealed PRT and stores it in the Kerberos CCache
8676    ///
8677    /// # Arguments
8678    ///
8679    /// * `sealed_prt` -  An encrypted primary refresh token that was
8680    ///   previously received from the server.
8681    ///
8682    /// * `filename` - The filename for the Kerberos Credential Cache.
8683    ///
8684    /// * `tpm` - The tpm object.
8685    ///
8686    /// * `storage_key` - The TPM MachineKey associated with this application.
8687    ///
8688    /// # Returns
8689    /// * Failure: An MsalError, indicating the failure.
8690    pub fn store_cloud_tgt(
8691        &self,
8692        _sealed_prt: &SealedData,
8693        _filename: &str,
8694        _tpm: &mut BoxedDynTpm,
8695        _storage_key: &StorageKey,
8696    ) -> Result<(), MsalError> {
8697        Err(MsalError::NotImplemented)
8698    }
8699
8700    /// Gets the AD TGT from a sealed PRT and stores it in the Kerberos CCache
8701    ///
8702    /// # Arguments
8703    ///
8704    /// * `sealed_prt` -  An encrypted primary refresh token that was
8705    ///   previously received from the server.
8706    ///
8707    /// * `filename` - The filename for the Kerberos Credential Cache.
8708    ///
8709    /// * `tpm` - The tpm object.
8710    ///
8711    /// * `storage_key` - The TPM MachineKey associated with this application.
8712    ///
8713    /// # Returns
8714    /// * Failure: An MsalError, indicating the failure.
8715    pub fn store_ad_tgt(
8716        &self,
8717        _sealed_prt: &SealedData,
8718        _filename: &str,
8719        _tpm: &mut BoxedDynTpm,
8720        _storage_key: &StorageKey,
8721    ) -> Result<(), MsalError> {
8722        Err(MsalError::NotImplemented)
8723    }
8724
8725    /// Converts KerberosCredentials to MIT ccache file format bytes
8726    fn kerberos_credentials_to_ccache_bytes(
8727        credentials: &KerberosCredentials,
8728    ) -> Result<Vec<u8>, MsalError> {
8729        // Generate a unique temp file path
8730        let temp_path = format!("/tmp/himmelblau_ccache_{}", Uuid::new_v4().as_hyphenated());
8731        let ccache_name = format!("FILE:{}", temp_path);
8732
8733        // Create and initialize the ccache
8734        let mut ccache = ccache_resolve(Some(&ccache_name)).map_err(|e| {
8735            error!("Failed to resolve ccache: {:?}", e);
8736            MsalError::CryptoFail("Failed to resolve ccache".to_string())
8737        })?;
8738
8739        ccache.init(credentials.name(), None).map_err(|e| {
8740            error!("Failed to init ccache: {:?}", e);
8741            MsalError::CryptoFail("Failed to init ccache".to_string())
8742        })?;
8743
8744        ccache.store(credentials).map_err(|e| {
8745            error!("Failed to store credentials in ccache: {:?}", e);
8746            MsalError::CryptoFail("Failed to store credentials in ccache".to_string())
8747        })?;
8748
8749        // Read the file bytes
8750        let mut file = fs::File::open(&temp_path).map_err(|e| {
8751            error!("Failed to open ccache file: {:?}", e);
8752            MsalError::CryptoFail("Failed to open ccache file".to_string())
8753        })?;
8754
8755        let mut bytes = Vec::new();
8756        file.read_to_end(&mut bytes).map_err(|e| {
8757            error!("Failed to read ccache file: {:?}", e);
8758            MsalError::CryptoFail("Failed to read ccache file".to_string())
8759        })?;
8760
8761        // Clean up the temp file
8762        let _ = fs::remove_file(&temp_path);
8763
8764        Ok(bytes)
8765    }
8766
8767    /// Gets the Cloud TGT from a sealed PRT and returns it in a Kerberos CCache
8768    ///
8769    /// # Arguments
8770    ///
8771    /// * `sealed_prt` -  An encrypted primary refresh token that was
8772    ///   previously received from the server.
8773    ///
8774    /// * `tpm` - The tpm object.
8775    ///
8776    /// * `storage_key` - The TPM MachineKey associated with this application.
8777    ///
8778    /// # Returns
8779    /// * Success: Byte representation of a Kerberos CCache
8780    /// * Failure: An MsalError, indicating the failure.
8781    pub fn fetch_cloud_ccache(
8782        &self,
8783        sealed_prt: &SealedData,
8784        tpm: &mut BoxedDynTpm,
8785        storage_key: &StorageKey,
8786    ) -> Result<Vec<u8>, MsalError> {
8787        let credentials = self.fetch_cloud_tgt(sealed_prt, tpm, storage_key)?;
8788        Self::kerberos_credentials_to_ccache_bytes(&credentials)
8789    }
8790
8791    /// Gets the AD TGT from a sealed PRT and returns it in a Kerberos CCache
8792    ///
8793    /// # Arguments
8794    ///
8795    /// * `sealed_prt` -  An encrypted primary refresh token that was
8796    ///   previously received from the server.
8797    ///
8798    /// * `tpm` - The tpm object.
8799    ///
8800    /// * `storage_key` - The TPM MachineKey associated with this application.
8801    ///
8802    /// # Returns
8803    /// * Success: Byte representation of a Kerberos CCache
8804    /// * Failure: An MsalError, indicating the failure.
8805    pub fn fetch_ad_ccache(
8806        &self,
8807        sealed_prt: &SealedData,
8808        tpm: &mut BoxedDynTpm,
8809        storage_key: &StorageKey,
8810    ) -> Result<Vec<u8>, MsalError> {
8811        let credentials = self.fetch_ad_tgt(sealed_prt, tpm, storage_key)?;
8812        Self::kerberos_credentials_to_ccache_bytes(&credentials)
8813    }
8814
8815    /// Gets the Cloud TGT from a sealed PRT and returns it
8816    ///
8817    /// # Arguments
8818    ///
8819    /// * `sealed_prt` -  An encrypted primary refresh token that was
8820    ///   previously received from the server.
8821    ///
8822    /// * `tpm` - The tpm object.
8823    ///
8824    /// * `storage_key` - The TPM MachineKey associated with this application.
8825    ///
8826    /// # Returns
8827    /// * Success: Boxed KerberosCredentials struct
8828    /// * Failure: An MsalError, indicating the failure.
8829    pub fn fetch_cloud_tgt(
8830        &self,
8831        sealed_prt: &SealedData,
8832        tpm: &mut BoxedDynTpm,
8833        storage_key: &StorageKey,
8834    ) -> Result<Box<KerberosCredentials>, MsalError> {
8835        let transport_key = self.transport_key(tpm, storage_key)?;
8836
8837        // This allows a transition, where existing msoapxbc keys will provide
8838        // their sealing content encryption keys, but newer keys will not, falling back
8839        // to the provided key.
8840        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
8841        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
8842
8843        let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
8844        if let Some(error) = &prt.tgt_cloud.error {
8845            return Err(MsalError::Missing(error.to_string()));
8846        }
8847
8848        let session_key = prt.session_key()?;
8849        let client_key =
8850            prt.tgt_cloud
8851                .derived_key(tpm, &transport_key, storage_key, &session_key)?;
8852
8853        let as_rep = prt.tgt_cloud.as_rep()?;
8854        let kdc_reply = as_rep
8855            .enc_part
8856            .decrypt_enc_kdc_rep(&client_key)
8857            .map_err(|e| {
8858                let msg = format!("Failed to decrypt KDC reply part from AS reply: {:?}", e);
8859                MsalError::CryptoFail(msg)
8860            })?;
8861
8862        let creds = KerberosCredentials::new(as_rep.name, as_rep.ticket, kdc_reply);
8863        Ok(Box::new(creds))
8864    }
8865
8866    /// Gets the AD TGT from a sealed PRT and returns it
8867    ///
8868    /// # Arguments
8869    ///
8870    /// * `sealed_prt` -  An encrypted primary refresh token that was
8871    ///   previously received from the server.
8872    ///
8873    /// * `tpm` - The tpm object.
8874    ///
8875    /// * `storage_key` - The TPM MachineKey associated with this application.
8876    ///
8877    /// # Returns
8878    /// * Success: Boxed KerberosCredentials struct
8879    /// * Failure: An MsalError, indicating the failure.
8880    pub fn fetch_ad_tgt(
8881        &self,
8882        sealed_prt: &SealedData,
8883        tpm: &mut BoxedDynTpm,
8884        storage_key: &StorageKey,
8885    ) -> Result<Box<KerberosCredentials>, MsalError> {
8886        let transport_key = self.transport_key(tpm, storage_key)?;
8887
8888        // This allows a transition, where existing msoapxbc keys will provide
8889        // their sealing content encryption keys, but newer keys will not, falling back
8890        // to the provided key.
8891        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
8892        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
8893
8894        let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
8895        let session_key = prt.session_key()?;
8896
8897        let (client_key, as_rep) = match &prt.tgt_on_prem {
8898            OnPremTgt::Structured { tgt_ad } => {
8899                if let Some(error) = tgt_ad.error.as_ref() {
8900                    return Err(MsalError::Missing(error.clone()));
8901                }
8902                let client_key =
8903                    tgt_ad.derived_key(tpm, &transport_key, storage_key, &session_key)?;
8904                let as_rep = tgt_ad.as_rep()?;
8905                (client_key, as_rep)
8906            }
8907            OnPremTgt::Raw(raw_tgt) => {
8908                let client_key =
8909                    raw_tgt.derived_key(tpm, &transport_key, storage_key, &session_key)?;
8910                let as_rep = raw_tgt.as_rep()?;
8911                (client_key, as_rep)
8912            }
8913            OnPremTgt::Absent => {
8914                return Err(MsalError::Missing(
8915                    "No on-prem partial ticket bundled".to_string(),
8916                ))
8917            }
8918        };
8919
8920        let kdc_reply = as_rep
8921            .enc_part
8922            .decrypt_enc_kdc_rep(&client_key)
8923            .map_err(|e| {
8924                let msg = format!("Failed to decrypt KDC reply part from AS reply: {:?}", e);
8925                MsalError::CryptoFail(msg)
8926            })?;
8927
8928        let creds = KerberosCredentials::new(as_rep.name, as_rep.ticket, kdc_reply);
8929        Ok(Box::new(creds))
8930    }
8931
8932    /// Get the Kerberos top level names from a sealed PRT
8933    ///
8934    /// # Arguments
8935    ///
8936    /// * `sealed_prt` -  An encrypted primary refresh token that was
8937    ///   previously received from the server.
8938    ///
8939    /// * `tpm` - The tpm object.
8940    ///
8941    /// * `storage_key` - The TPM MachineKey associated with this application.
8942    ///
8943    /// # Returns
8944    /// * Success: The Kerberos top level names
8945    /// * Failure: An MsalError, indicating the failure.
8946    pub fn unseal_prt_kerberos_top_level_names(
8947        &self,
8948        sealed_prt: &SealedData,
8949        tpm: &mut BoxedDynTpm,
8950        storage_key: &StorageKey,
8951    ) -> Result<String, MsalError> {
8952        let transport_key = self.transport_key(tpm, storage_key)?;
8953        // This allows a transition, where existing msoapxbc keys will provide
8954        // their sealing content encryption keys, but newer keys will not, falling back
8955        // to the provided key.
8956        // NOTE: This dance is to make sure that transport_storage_key lives long enough.
8957        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
8958        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
8959
8960        let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
8961        let kerberos_top_level_names =
8962            prt.kerberos_top_level_names
8963                .clone()
8964                .ok_or(MsalError::Missing(
8965                    "kerberos_top_level_names missing from PRT".to_string(),
8966                ))?;
8967        Ok(kerberos_top_level_names.clone())
8968    }
8969
8970    fn seal_user_prt(
8971        &self,
8972        prt: &PrimaryRefreshToken,
8973        tpm: &mut BoxedDynTpm,
8974        storage_key: &StorageKey,
8975    ) -> Result<SealedData, MsalError> {
8976        let prt_data = json_to_vec(prt)
8977            .map(Zeroizing::new)
8978            .map_err(|e| MsalError::InvalidJson(format!("Failed serializing PRT {:?}", e)))?;
8979        tpm.seal_data(storage_key, prt_data)
8980            .map_err(|e| MsalError::TPMFail(format!("Failed sealing PRT {:?}", e)))
8981    }
8982
8983    fn unseal_user_prt(
8984        &self,
8985        sealed_data: &SealedData,
8986        tpm: &mut BoxedDynTpm,
8987        storage_key: &StorageKey,
8988    ) -> Result<PrimaryRefreshToken, MsalError> {
8989        let prt_data = tpm
8990            .unseal_data(storage_key, sealed_data)
8991            .map_err(|e| MsalError::TPMFail(format!("Failed unsealing PRT {:?}", e)))?;
8992        json_from_slice(&prt_data)
8993            .map_err(|e| MsalError::InvalidJson(format!("Failed deserializing PRT {:?}", e)))
8994    }
8995
8996    /// Seal a user Primary Refresh Token (PRT) with a Linux Hello key, bound to the user PIN
8997    ///
8998    /// This function first decrypts the provided PRT, which is normally encrypted
8999    /// using the host credentials, and then re-encrypts (seals) it using the user’s
9000    /// Hello PIN. This allows safely persisting the PRT to disk such that it is only
9001    /// decryptable by the same user on the same host, requiring both the physical TPM
9002    /// and the user’s Hello PIN for access. This mitigates the risk of PRT exfiltration
9003    /// by ensuring that an attacker would need both the PIN and the hardware TPM.
9004    ///
9005    /// This is particularly useful to support scenarios such as offline authentication
9006    /// where the network is unavailable. It allows a user to authenticate and retrieve
9007    /// Single Sign-On (SSO) credentials immediately when the network becomes available,
9008    /// without re-entering full credentials, even if their Hello PIN was entered before
9009    /// network connectivity was established.
9010    ///
9011    /// # Arguments
9012    ///
9013    /// * `prt` - The existing sealed PRT, encrypted using the host credentials.
9014    /// * `hello_key` - The user's loadable Linux Hello key.
9015    /// * `pin` - The Hello PIN provided by the user.
9016    /// * `tpm` - The TPM object used for cryptographic operations.
9017    /// * `storage_key` - The TPM MachineKey used to load the Hello key and perform sealing.
9018    ///
9019    /// # Returns
9020    /// * Success: A new `SealedData` containing the PRT encrypted (sealed) using the user’s Hello PIN.
9021    /// * Failure: An `MsalError` indicating why the operation failed.
9022    pub fn seal_user_prt_with_hello_key(
9023        &self,
9024        prt: &SealedData,
9025        hello_key: &LoadableMsHelloKey,
9026        pin: &str,
9027        tpm: &mut BoxedDynTpm,
9028        storage_key: &StorageKey,
9029    ) -> Result<SealedData, MsalError> {
9030        let transport_key = self.transport_key(tpm, storage_key)?;
9031
9032        // This allows a transition, where existing msoapxbc keys will provide
9033        // their sealing content encryption keys, but newer keys will not, falling back
9034        // to the provided key.
9035        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
9036        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
9037
9038        let prt = self.unseal_user_prt(prt, tpm, prt_storage_key)?;
9039        let prt_data = json_to_vec(&prt)
9040            .map(Zeroizing::new)
9041            .map_err(|e| MsalError::InvalidJson(format!("Failed serializing PRT {:?}", e)))?;
9042        let pin = PinValue::new(pin)
9043            .map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
9044        let (_key, win_hello_storage_key) = tpm
9045            .ms_hello_key_load(storage_key, hello_key, &pin)
9046            .map_err(|e| MsalError::TPMFail(format!("{:?}", e)))?;
9047        tpm.seal_data(&win_hello_storage_key, prt_data)
9048            .map_err(|e| MsalError::TPMFail(format!("Failed sealing PRT {:?}", e)))
9049    }
9050
9051    /// Unseal a user Primary Refresh Token (PRT) that is protected with the user Hello PIN
9052    ///
9053    /// This function decrypts (unseals) a PRT that was previously encrypted using
9054    /// the user’s Hello PIN. It loads the Hello key into the TPM using the provided
9055    /// PIN, then decrypts the sealed PRT data. After obtaining the decrypted PRT,
9056    /// it immediately re-encrypts it using the standard host credentials, so it can
9057    /// be used in memory for SSO or other operations while still maintaining security
9058    /// if kept transient.
9059    ///
9060    /// This mechanism ensures that even when a PRT is persisted on disk for offline
9061    /// scenarios, it remains protected by both the hardware TPM and the user’s Hello PIN.
9062    ///
9063    /// # Arguments
9064    ///
9065    /// * `sealed_data` - The PRT sealed (encrypted) using the user’s Hello PIN and TPM.
9066    /// * `hello_key` - The user's loadable Linux Hello key.
9067    /// * `pin` - The Hello PIN provided by the user.
9068    /// * `tpm` - The TPM object used for cryptographic operations.
9069    /// * `storage_key` - The TPM MachineKey used to load the Hello key and perform unsealing.
9070    ///
9071    /// # Returns
9072    /// * Success: A new `SealedData` containing the PRT re-sealed using host credentials for in-memory use.
9073    /// * Failure: An `MsalError` indicating why the operation failed.
9074    pub fn unseal_user_prt_with_hello_key(
9075        &self,
9076        sealed_data: &SealedData,
9077        hello_key: &LoadableMsHelloKey,
9078        pin: &str,
9079        tpm: &mut BoxedDynTpm,
9080        storage_key: &StorageKey,
9081    ) -> Result<SealedData, MsalError> {
9082        let pin = PinValue::new(pin)
9083            .map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
9084        let (_key, win_hello_storage_key) = tpm
9085            .ms_hello_key_load(storage_key, hello_key, &pin)
9086            .map_err(|e| MsalError::TPMFail(format!("{:?}", e)))?;
9087        let prt_data = tpm
9088            .unseal_data(&win_hello_storage_key, sealed_data)
9089            .map_err(|e| MsalError::TPMFail(format!("Failed unsealing PRT {:?}", e)))?;
9090        let prt = json_from_slice(&prt_data)
9091            .map_err(|e| MsalError::InvalidJson(format!("Failed deserializing PRT {:?}", e)))?;
9092
9093        let transport_key = self.transport_key(tpm, storage_key)?;
9094
9095        // This allows a transition, where existing msoapxbc keys will provide
9096        // their sealing content encryption keys, but newer keys will not, falling back
9097        // to the provided key.
9098        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
9099        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
9100
9101        self.seal_user_prt(&prt, tpm, prt_storage_key)
9102    }
9103
9104    pub async fn resolve_nametosid(
9105        &self,
9106        username: &str,
9107        tpm: &mut BoxedDynTpm,
9108        machine_key: &StorageKey,
9109    ) -> Result<SidToName, MsalError> {
9110        let nonce = self.request_nonce().await?;
9111
9112        let os_release = match OsRelease::new() {
9113            Ok(os_release) => Some(format!(
9114                "{} {}",
9115                os_release.pretty_name, os_release.version_id
9116            )),
9117            Err(_) => None,
9118        };
9119
9120        let jwt_body = json!({
9121            "win_ver": os_release,
9122            "version": "1.0",
9123            "nonce": nonce,
9124            "username": username,
9125        });
9126
9127        let jwt = JwsBuilder::from(
9128            serde_json::to_vec(&jwt_body)
9129                .map_err(|e| MsalError::InvalidJson(format!("Failed to serialize JWT: {}", e)))?,
9130        )
9131        .set_typ(Some("JWT"))
9132        .build();
9133
9134        let signed_jwt = self.sign_jwt(&jwt, tpm, machine_key).await?;
9135
9136        let form_body = serde_urlencoded::to_string([
9137            ("windows_api_version", "2.2"),
9138            ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
9139            ("signedRequest", &signed_jwt),
9140        ])
9141        .map_err(|e| MsalError::InvalidJson(format!("Failed to encode form: {}", e)))?;
9142
9143        let url = format!("{}/sidtoname", self.authority()?);
9144
9145        let resp = self
9146            .client()
9147            .get(url)
9148            .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
9149            .body(Body::from(form_body))
9150            .send()
9151            .await
9152            .map_err(|e| MsalError::request_failed(&e))?;
9153
9154        if resp.status().is_success() {
9155            let json_resp: SidToName = resp
9156                .json()
9157                .await
9158                .map_err(|e| MsalError::RequestFailed(format!("{:?}", e)))?;
9159            Ok(json_resp)
9160        } else {
9161            Err(MsalError::RequestFailed(format!("{}", resp.status())))
9162        }
9163    }
9164
9165    /// Determine whether the Primary Refresh Token (PRT) is expired.
9166    ///
9167    /// This function decrypts the provided `SealedData` PRT and checks
9168    /// whether the token has passed its `expires_on` timestamp.
9169    ///
9170    /// # Arguments
9171    ///
9172    /// * `prt` - The sealed and encrypted Primary Refresh Token.
9173    /// * `tpm` - The TPM object used for cryptographic operations.
9174    /// * `storage_key` - The TPM MachineKey used to load the Hello key and perform unsealing.
9175    ///
9176    /// # Returns
9177    /// * Success: Whether or not the token is expired.
9178    /// * Failure: An MsalError, indicating the failure.
9179    pub fn is_prt_expired(
9180        &self,
9181        prt: &SealedData,
9182        tpm: &mut BoxedDynTpm,
9183        storage_key: &StorageKey,
9184    ) -> Result<bool, MsalError> {
9185        let transport_key = self.transport_key(tpm, storage_key)?;
9186
9187        // This allows a transition, where existing msoapxbc keys will provide
9188        // their sealing content encryption keys, but newer keys will not, falling back
9189        // to the provided key.
9190        let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
9191        let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
9192
9193        let prt = self.unseal_user_prt(prt, tpm, prt_storage_key)?;
9194
9195        Ok(prt.is_expired())
9196    }
9197}
9198
9199#[cfg(test)]
9200#[allow(clippy::unwrap_used, clippy::expect_used)]
9201mod tests {
9202    use super::*;
9203
9204    #[cfg(feature = "broker")]
9205    use kanidm_hsm_crypto::{provider::SoftTpm, AuthValue};
9206    #[cfg(feature = "broker")]
9207    use openssl::{
9208        asn1::{Asn1Object, Asn1OctetString, Asn1Time},
9209        bn::BigNum,
9210        pkey::{PKey, Private},
9211        sign::Verifier,
9212        x509::{X509Builder, X509Extension, X509NameBuilder},
9213    };
9214
9215    fn test_cred_type(if_exists_result: i32, throttle_status: u8) -> CredType {
9216        serde_json::from_value(json!({
9217            "Credentials": {
9218                "PrefCredential": 1,
9219                "HasPassword": true
9220            },
9221            "ThrottleStatus": throttle_status,
9222            "IfExistsResult": if_exists_result
9223        }))
9224        .expect("test credential type should deserialize")
9225    }
9226
9227    #[test]
9228    fn adfs_url_detection_requires_a_secure_complete_path_segment() {
9229        for value in [
9230            "https://fs.example.com/adfs/ls/?wa=wsignin1.0",
9231            "https://fs.example.com/ADFS/LS/",
9232            "https://fs.example.com:8443/prefix/adfs/ls",
9233        ] {
9234            assert!(parse_adfs_federation_url(value).unwrap().is_some());
9235        }
9236
9237        for value in [
9238            "http://fs.example.com/adfs/ls/",
9239            "https://user:secret@fs.example.com/adfs/ls/",
9240            "https://adfs.example.com/login/",
9241            "https://fs.example.com/notadfs/ls/",
9242            "https://fs.example.com/login/?next=/adfs/ls/",
9243            "https://fs.example.com/adfs/ls/#fragment",
9244        ] {
9245            assert!(parse_adfs_federation_url(value).unwrap().is_none());
9246        }
9247        assert!(parse_adfs_federation_url("not a URL").is_err());
9248    }
9249
9250    #[test]
9251    fn ws_fed_form_parser_selects_complete_form_and_decodes_entities() {
9252        let html = r#"
9253            <html><body>
9254              <form><input name="UserName" value="ignored"></form>
9255              <form action="https://untrusted.example/collect">
9256                <input type="hidden" name="WCTX" value="ctx&amp;value">
9257                <input type="hidden" name="wresult" value="&lt;Assertion&gt;ok&lt;/Assertion&gt;">
9258                <input type="hidden" name="WA" value="wsignin1.0">
9259                <input type="hidden" name="Password" value="must-not-be-forwarded">
9260              </form>
9261            </body></html>
9262        "#;
9263        let form = parse_ws_fed_form(html).unwrap();
9264        assert_eq!(form.wa, "wsignin1.0");
9265        assert_eq!(form.wctx, "ctx&value");
9266        assert_eq!(form.wresult, "<Assertion>ok</Assertion>");
9267    }
9268
9269    #[test]
9270    fn ws_fed_form_parser_rejects_login_and_incomplete_forms() {
9271        for html in [
9272            r#"<form><input name="UserName"><input name="Password"></form>"#,
9273            r#"<form><input name="wa" value="wsignin1.0"><input name="wctx" value="ctx"></form>"#,
9274            r#"<form><input name="wa" value="wrong"><input name="wctx" value="ctx"><input name="wresult" value="assertion"></form>"#,
9275            r#"<form><input name="wa" value="wsignin1.0"><input name="wctx" value=""><input name="wresult" value="assertion"></form>"#,
9276        ] {
9277            assert!(parse_ws_fed_form(html).is_err());
9278        }
9279    }
9280
9281    #[test]
9282    fn login_srf_is_derived_from_authority_origin() {
9283        assert_eq!(
9284            entra_login_srf_url("https://login.microsoftonline.com/common/?ignored=true")
9285                .unwrap()
9286                .as_str(),
9287            "https://login.microsoftonline.com/login.srf"
9288        );
9289        assert_eq!(
9290            entra_login_srf_url("https://login.microsoftonline.us:8443/tenant")
9291                .unwrap()
9292                .as_str(),
9293            "https://login.microsoftonline.us:8443/login.srf"
9294        );
9295        assert!(entra_login_srf_url("http://login.example.com/common").is_err());
9296    }
9297
9298    #[test]
9299    fn adfs_redirects_stay_on_https_origin_and_control_password_replay() {
9300        let origin = Url::parse("https://fs.example.com/adfs/ls/").unwrap();
9301        let (next, method) = resolve_adfs_redirect(
9302            &origin,
9303            &origin,
9304            "../continue",
9305            302,
9306            AdfsRequestMethod::PostCredentials,
9307        )
9308        .unwrap();
9309        assert_eq!(next.as_str(), "https://fs.example.com/adfs/continue");
9310        assert_eq!(method, AdfsRequestMethod::Get);
9311
9312        let (_, method) = resolve_adfs_redirect(
9313            &origin,
9314            &origin,
9315            "/adfs/resubmit",
9316            307,
9317            AdfsRequestMethod::PostCredentials,
9318        )
9319        .unwrap();
9320        assert_eq!(method, AdfsRequestMethod::PostCredentials);
9321
9322        for location in [
9323            "http://fs.example.com/adfs/continue",
9324            "https://other.example.com/adfs/continue",
9325            "https://user:secret@fs.example.com/adfs/continue",
9326            "/adfs/continue#fragment",
9327        ] {
9328            assert!(resolve_adfs_redirect(
9329                &origin,
9330                &origin,
9331                location,
9332                302,
9333                AdfsRequestMethod::PostCredentials,
9334            )
9335            .is_err());
9336        }
9337    }
9338
9339    #[test]
9340    fn usable_account_result_is_not_rejected_by_backend_throttle_status() {
9341        for throttle_status in [0, 1, 2, u8::MAX] {
9342            let cred_type = test_cred_type(0, throttle_status);
9343            assert!(cred_type.account_exists().unwrap());
9344        }
9345    }
9346
9347    #[test]
9348    fn throttled_account_lookup_remains_retryable_error() {
9349        let cred_type = test_cred_type(2, 0);
9350        assert!(matches!(
9351            cred_type.account_exists(),
9352            Err(MsalError::AADSTSError(err)) if err.code == 90055
9353        ));
9354    }
9355
9356    #[test]
9357    fn retryable_account_lookup_failures_keep_existing_error() {
9358        for if_exists_result in [-1, 4] {
9359            let cred_type = test_cred_type(if_exists_result, 0);
9360            assert!(matches!(
9361                cred_type.account_exists(),
9362                Err(MsalError::AADSTSError(err)) if err.code == 90006
9363            ));
9364        }
9365    }
9366
9367    fn build_access_token(payload_json: &str) -> String {
9368        let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none"}"#.as_bytes());
9369        let payload = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
9370        format!("{}.{}.", header, payload)
9371    }
9372
9373    #[cfg(feature = "broker")]
9374    const TEST_TENANT_GUID: &str = "6973bb37-65f8-440e-b39e-dd57da64f6cf";
9375    /// `TEST_TENANT_GUID` in the little endian layout Entra uses for the
9376    /// device certificate tenant OID.
9377    #[cfg(feature = "broker")]
9378    const TEST_TENANT_BYTES_LE: [u8; 16] = [
9379        0x37, 0xbb, 0x73, 0x69, 0xf8, 0x65, 0x0e, 0x44, 0xb3, 0x9e, 0xdd, 0x57, 0xda, 0x64, 0xf6,
9380        0xcf,
9381    ];
9382    #[cfg(feature = "broker")]
9383    const TENANT_ID_OID: &str = "1.2.840.113556.1.5.284.5";
9384
9385    #[cfg(feature = "broker")]
9386    fn test_tpm() -> (BoxedDynTpm, StorageKey) {
9387        let mut tpm = BoxedDynTpm::new(SoftTpm::new());
9388        let auth_str = AuthValue::generate().unwrap();
9389        let auth_value = AuthValue::from_str(&auth_str).unwrap();
9390        let loadable_machine_key = tpm.root_storage_key_create(&auth_value).unwrap();
9391        let machine_key = tpm
9392            .root_storage_key_load(&auth_value, &loadable_machine_key)
9393            .unwrap();
9394
9395        (tpm, machine_key)
9396    }
9397
9398    #[cfg(feature = "broker")]
9399    fn test_prt(refresh_token: &str) -> PrimaryRefreshToken {
9400        PrimaryRefreshToken {
9401            token_type: "Bearer".to_string(),
9402            expires_in: "3600".to_string(),
9403            ext_expires_in: "3600".to_string(),
9404            expires_on: "9999999999".to_string(),
9405            refresh_token: refresh_token.to_string(),
9406            refresh_token_expires_in: 3600,
9407            session_key_jwe: None,
9408            id_token: IdToken::default(),
9409            client_info: ClientInfo::default(),
9410            device_tenant_id: None,
9411            tgt_on_prem: OnPremTgt::Absent,
9412            tgt_cloud: StructuredTgt::default(),
9413            kerberos_top_level_names: None,
9414        }
9415    }
9416
9417    #[cfg(feature = "broker")]
9418    fn self_signed_cert(pkey: &PKey<Private>, extensions: &[X509Extension]) -> Vec<u8> {
9419        let mut name = X509NameBuilder::new().unwrap();
9420        name.append_entry_by_text("CN", "p2p-test").unwrap();
9421        let name = name.build();
9422        let mut builder = X509Builder::new().unwrap();
9423        builder.set_version(2).unwrap();
9424        let serial = BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap();
9425        builder.set_serial_number(&serial).unwrap();
9426        builder.set_subject_name(&name).unwrap();
9427        builder.set_issuer_name(&name).unwrap();
9428        builder.set_pubkey(pkey).unwrap();
9429        builder
9430            .set_not_before(Asn1Time::days_from_now(0).unwrap().as_ref())
9431            .unwrap();
9432        builder
9433            .set_not_after(Asn1Time::days_from_now(1).unwrap().as_ref())
9434            .unwrap();
9435        for extension in extensions {
9436            builder.append_extension2(extension).unwrap();
9437        }
9438        builder.sign(pkey, MessageDigest::sha256()).unwrap();
9439
9440        builder.build().to_der().unwrap()
9441    }
9442
9443    /// Build the device certificate tenant OID extension, holding `value` as
9444    /// the raw extnValue octets.
9445    #[cfg(feature = "broker")]
9446    fn tenant_oid_extension(value: &[u8]) -> X509Extension {
9447        let oid = Asn1Object::from_str(TENANT_ID_OID).unwrap();
9448        let value = Asn1OctetString::new_from_bytes(value).unwrap();
9449
9450        X509Extension::new_from_der(&oid, false, &value).unwrap()
9451    }
9452
9453    #[cfg(feature = "broker")]
9454    fn test_broker_app(authority: &str) -> BrokerClientApplication {
9455        BrokerClientApplication::new(
9456            Some(authority),
9457            None,
9458            None,
9459            None,
9460            #[cfg(feature = "set_timeout")]
9461            Duration::from_secs(30),
9462            #[cfg(feature = "ipvers")]
9463            &[],
9464        )
9465        .unwrap()
9466    }
9467
9468    #[cfg(feature = "broker")]
9469    fn decode_jwt_header(jwt: &str) -> Value {
9470        let header = jwt.split('.').next().unwrap();
9471        let header = URL_SAFE_NO_PAD.decode(header).unwrap();
9472        json_from_slice(&header).unwrap()
9473    }
9474
9475    fn build_user_token(access_token: String) -> UserToken {
9476        UserToken {
9477            token_type: "Bearer".to_string(),
9478            scope: None,
9479            expires_in: 3600,
9480            ext_expires_in: 3600,
9481            access_token: Some(access_token),
9482            refresh_token: "refresh-token".to_string(),
9483            id_token: IdToken::default(),
9484            client_info: ClientInfo::default(),
9485            #[cfg(feature = "broker")]
9486            prt: None,
9487        }
9488    }
9489
9490    #[test]
9491    fn pkce_challenge_matches_rfc7636_vector() {
9492        assert_eq!(
9493            pkce_code_challenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"),
9494            "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
9495        );
9496    }
9497
9498    #[test]
9499    fn initiate_authorization_code_pkce_flow_builds_expected_url() {
9500        let app = PublicClientApplication::new(
9501            "client-id",
9502            Some("https://login.microsoftonline.com/tenant"),
9503            #[cfg(feature = "set_timeout")]
9504            Duration::from_secs(3),
9505            #[cfg(feature = "ipvers")]
9506            &[],
9507        )
9508        .unwrap();
9509
9510        let flow = app
9511            .initiate_authorization_code_pkce_flow(
9512                vec!["https://graph.microsoft.com/User.Read"],
9513                "http://localhost/callback",
9514            )
9515            .unwrap();
9516        let url = Url::parse(&flow.auth_url).unwrap();
9517        let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
9518
9519        assert_eq!(
9520            url.as_str().split('?').next().unwrap(),
9521            "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize"
9522        );
9523        assert_eq!(params["client_id"], "client-id");
9524        assert_eq!(params["response_type"], "code");
9525        assert_eq!(params["redirect_uri"], "http://localhost/callback");
9526        assert_eq!(params["response_mode"], "query");
9527        assert_eq!(
9528            params["scope"],
9529            "openid profile offline_access https://graph.microsoft.com/User.Read"
9530        );
9531        assert_eq!(params["state"], flow.state);
9532        assert_eq!(
9533            params["code_challenge"],
9534            pkce_code_challenge(&flow.code_verifier)
9535        );
9536        assert_eq!(params["code_challenge_method"], "S256");
9537        assert!(flow.code_verifier.len() >= 43);
9538    }
9539
9540    #[tokio::test]
9541    async fn acquire_token_by_authorization_code_pkce_flow_rejects_wrong_state() {
9542        let app = PublicClientApplication::new(
9543            "client-id",
9544            Some("https://login.microsoftonline.com/tenant"),
9545            #[cfg(feature = "set_timeout")]
9546            Duration::from_secs(3),
9547            #[cfg(feature = "ipvers")]
9548            &[],
9549        )
9550        .unwrap();
9551        let flow = app
9552            .initiate_authorization_code_pkce_flow(vec!["User.Read"], "http://localhost/callback")
9553            .unwrap();
9554
9555        let result = app
9556            .acquire_token_by_authorization_code_pkce_flow(
9557                &flow,
9558                "http://localhost/callback?code=abc&state=wrong",
9559            )
9560            .await;
9561
9562        assert!(matches!(result, Err(MsalError::InvalidParse(_))));
9563    }
9564
9565    #[tokio::test]
9566    async fn acquire_token_by_authorization_code_pkce_flow_maps_redirect_error() {
9567        let app = PublicClientApplication::new(
9568            "client-id",
9569            Some("https://login.microsoftonline.com/tenant"),
9570            #[cfg(feature = "set_timeout")]
9571            Duration::from_secs(3),
9572            #[cfg(feature = "ipvers")]
9573            &[],
9574        )
9575        .unwrap();
9576        let flow = app
9577            .initiate_authorization_code_pkce_flow(vec!["User.Read"], "http://localhost/callback")
9578            .unwrap();
9579        let redirect = format!(
9580            "http://localhost/callback?error=access_denied&error_description=nope&state={}",
9581            flow.state
9582        );
9583
9584        let result = app
9585            .acquire_token_by_authorization_code_pkce_flow(&flow, &redirect)
9586            .await;
9587
9588        assert!(matches!(result, Err(MsalError::AcquireTokenFailed(_))));
9589        if let Err(MsalError::AcquireTokenFailed(error)) = result {
9590            assert_eq!(error.error, "access_denied");
9591            assert_eq!(error.error_description, "nope");
9592        }
9593    }
9594
9595    #[test]
9596    fn authorization_code_pkce_token_form_contains_expected_fields() {
9597        let app = PublicClientApplication::new(
9598            "client-id",
9599            Some("https://login.microsoftonline.com/tenant"),
9600            #[cfg(feature = "set_timeout")]
9601            Duration::from_secs(3),
9602            #[cfg(feature = "ipvers")]
9603            &[],
9604        )
9605        .unwrap();
9606        let flow = app
9607            .initiate_authorization_code_pkce_flow(vec!["User.Read"], "http://localhost/callback")
9608            .unwrap();
9609        let form: HashMap<&str, &str> = app
9610            .authorization_code_pkce_token_form(&flow, "auth-code")
9611            .into_iter()
9612            .collect();
9613
9614        assert_eq!(form["client_id"], "client-id");
9615        assert_eq!(form["grant_type"], "authorization_code");
9616        assert_eq!(form["code"], "auth-code");
9617        assert_eq!(form["redirect_uri"], "http://localhost/callback");
9618        assert_eq!(form["scope"], "openid profile offline_access User.Read");
9619        assert_eq!(form["code_verifier"], flow.code_verifier);
9620        assert_eq!(form["client_info"], "1");
9621    }
9622
9623    #[cfg(feature = "broker")]
9624    #[test]
9625    fn p2p_device_payload_serializes_expected_fields() {
9626        let payload = P2PDeviceCertificatePayload::new(
9627            "nonce",
9628            "csr",
9629            "host1",
9630            &["host1.example.com".to_string()],
9631        );
9632        let value: Value = serde_json::to_value(payload).unwrap();
9633
9634        assert_eq!(value["client_id"], BROKER_CLIENT_IDENT);
9635        assert_eq!(value["request_nonce"], "nonce");
9636        assert_eq!(value["grant_type"], "device_auth");
9637        assert_eq!(value["cert_token_use"], "device_cert");
9638        assert_eq!(
9639            value["csr_type"],
9640            "http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10"
9641        );
9642        assert_eq!(value["csr"], "csr");
9643        assert_eq!(value["netbios_name"], "host1");
9644        assert_eq!(value["dns_names"][0], "host1.example.com");
9645    }
9646
9647    #[cfg(feature = "broker")]
9648    #[test]
9649    fn p2p_device_jwt_header_matches_broker_shape() {
9650        let (mut tpm, machine_key) = test_tpm();
9651        let loadable_key = tpm.rs256_create(&machine_key).unwrap();
9652        let key = tpm.rs256_load(&machine_key, &loadable_key).unwrap();
9653        let cert_der = vec![0x30, 0x03, 0x02, 0x01, 0x00];
9654        let payload =
9655            P2PDeviceCertificatePayload::new("nonce", "csr", "host1", &["host1".to_string()]);
9656
9657        let jwt = BrokerClientApplication::sign_p2p_device_jwt(&payload, &cert_der, &mut tpm, &key)
9658            .unwrap();
9659        let header = decode_jwt_header(&jwt);
9660        let header = header.as_object().unwrap();
9661
9662        assert_eq!(header.len(), 3);
9663        assert_eq!(header["alg"], "RS256");
9664        assert_eq!(header["typ"], "JWT");
9665        // The broker sends x5c as a single string, not the RFC 7515 array.
9666        assert_eq!(header["x5c"], STANDARD.encode(cert_der));
9667        assert!(!header.contains_key("kid"));
9668    }
9669
9670    #[cfg(feature = "broker")]
9671    #[test]
9672    fn p2p_device_jwt_signature_verifies() {
9673        let (mut tpm, machine_key) = test_tpm();
9674        let loadable_key = tpm.rs256_create(&machine_key).unwrap();
9675        let key = tpm.rs256_load(&machine_key, &loadable_key).unwrap();
9676        let payload =
9677            P2PDeviceCertificatePayload::new("nonce", "csr", "host1", &["host1".to_string()]);
9678
9679        let jwt =
9680            BrokerClientApplication::sign_p2p_device_jwt(&payload, &[0x30, 0x00], &mut tpm, &key)
9681                .unwrap();
9682        let (signing_input, signature) = jwt.rsplit_once('.').unwrap();
9683        let signature = URL_SAFE_NO_PAD.decode(signature).unwrap();
9684
9685        let public_key = PKey::public_key_from_der(&tpm.rs256_public_der(&key).unwrap()).unwrap();
9686        let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key).unwrap();
9687        verifier.update(signing_input.as_bytes()).unwrap();
9688
9689        assert!(verifier.verify(&signature).unwrap());
9690    }
9691
9692    #[cfg(feature = "broker")]
9693    #[test]
9694    fn p2p_user_payload_serializes_expected_fields() {
9695        let prt = test_prt("prt-refresh-token");
9696
9697        let payload = P2PUserCertificatePayload::new(&prt, "nonce", "csr");
9698        let value: Value = serde_json::to_value(payload).unwrap();
9699
9700        assert_eq!(value["iss"], "aad:brokerplugin");
9701        assert_eq!(value["aud"], "login.microsoftonline.com");
9702        assert_eq!(value["grant_type"], "refresh_token");
9703        assert_eq!(value["scope"], "openid aza ugs");
9704        assert_eq!(value["refresh_token"], "prt-refresh-token");
9705        assert_eq!(value["client_id"], BROKER_CLIENT_IDENT);
9706        assert_eq!(value["cert_token_use"], "user_cert");
9707        assert_eq!(value["csr"], "csr");
9708    }
9709
9710    #[cfg(feature = "broker")]
9711    #[test]
9712    fn p2p_user_payload_redacts_refresh_token() {
9713        let prt = test_prt("prt-refresh-token");
9714        let payload = P2PUserCertificatePayload::new(&prt, "nonce", "csr");
9715
9716        let redacted = payload.redacted().unwrap();
9717
9718        assert_eq!(redacted["refresh_token"], "**********");
9719        // Only the logged copy is redacted, the payload still carries the PRT.
9720        assert_eq!(redacted["csr"], "csr");
9721        assert_eq!(
9722            serde_json::to_value(&payload).unwrap()["refresh_token"],
9723            "prt-refresh-token"
9724        );
9725    }
9726
9727    #[cfg(feature = "broker")]
9728    #[test]
9729    fn p2p_user_jwt_header_matches_broker_shape() {
9730        let prt = test_prt("prt-refresh-token");
9731        let payload = P2PUserCertificatePayload::new(&prt, "nonce", "csr");
9732        let ctx = [0xA5; 24];
9733        let hmac_key = [0x5A; 32];
9734
9735        let jwt =
9736            BrokerClientApplication::sign_p2p_user_jwt_with_key(&payload, &ctx, &hmac_key).unwrap();
9737        let header = decode_jwt_header(&jwt);
9738        let header = header.as_object().unwrap();
9739
9740        assert_eq!(header.len(), 3);
9741        assert_eq!(header["alg"], "HS256");
9742        assert_eq!(header["typ"], "JWT");
9743        assert_eq!(
9744            STANDARD
9745                .decode(header["ctx"].as_str().unwrap())
9746                .unwrap()
9747                .len(),
9748            24
9749        );
9750        assert!(!header.contains_key("kid"));
9751    }
9752
9753    #[cfg(feature = "broker")]
9754    #[test]
9755    fn p2p_device_tenant_oid_uses_little_endian_guid() {
9756        let raw = [
9757            0x04, 0x81, 0x10, 0x37, 0xbb, 0x73, 0x69, 0xf8, 0x65, 0x0e, 0x44, 0xb3, 0x9e, 0xdd,
9758            0x57, 0xda, 0x64, 0xf6, 0xcf,
9759        ];
9760
9761        assert_eq!(
9762            BrokerClientApplication::tenant_id_from_device_certificate_oid(&raw).unwrap(),
9763            TEST_TENANT_GUID
9764        );
9765    }
9766
9767    #[cfg(feature = "broker")]
9768    #[test]
9769    fn p2p_tenant_oid_accepts_all_encodings() {
9770        // Long form length, short form length, and a bare GUID with no
9771        // OCTET STRING wrapper at all.
9772        let mut long_form = vec![0x04, 0x81, 0x10];
9773        long_form.extend_from_slice(&TEST_TENANT_BYTES_LE);
9774        let mut short_form = vec![0x04, 0x10];
9775        short_form.extend_from_slice(&TEST_TENANT_BYTES_LE);
9776
9777        for raw in [long_form, short_form, TEST_TENANT_BYTES_LE.to_vec()] {
9778            assert_eq!(
9779                BrokerClientApplication::tenant_id_from_device_certificate_oid(&raw).unwrap(),
9780                TEST_TENANT_GUID
9781            );
9782        }
9783    }
9784
9785    #[cfg(feature = "broker")]
9786    #[test]
9787    fn p2p_device_tenant_id_absent_extension() {
9788        let rsa = Rsa::generate(2048).unwrap();
9789        let pkey = PKey::from_rsa(rsa).unwrap();
9790        let cert_der = self_signed_cert(&pkey, &[]);
9791
9792        assert_eq!(
9793            BrokerClientApplication::device_certificate_tenant_id(&cert_der).unwrap(),
9794            None
9795        );
9796    }
9797
9798    #[cfg(feature = "broker")]
9799    #[test]
9800    fn p2p_device_tenant_id_extracts_from_extension() {
9801        let rsa = Rsa::generate(2048).unwrap();
9802        let pkey = PKey::from_rsa(rsa).unwrap();
9803        let tenant_value = OctetString::new(TEST_TENANT_BYTES_LE)
9804            .unwrap()
9805            .to_der()
9806            .unwrap();
9807        let cert_der = self_signed_cert(&pkey, &[tenant_oid_extension(&tenant_value)]);
9808
9809        assert_eq!(
9810            BrokerClientApplication::device_certificate_tenant_id(&cert_der).unwrap(),
9811            Some(TEST_TENANT_GUID.to_string())
9812        );
9813    }
9814
9815    #[cfg(feature = "broker")]
9816    #[test]
9817    fn p2p_device_tenant_id_ignores_malformed_oid() {
9818        let rsa = Rsa::generate(2048).unwrap();
9819        let pkey = PKey::from_rsa(rsa).unwrap();
9820        // Some devices carry a verified domain here rather than a GUID. That
9821        // must fall back to the caller supplied tenant id, not fail the flow.
9822        let cert_der = self_signed_cert(&pkey, &[tenant_oid_extension(b"contoso.onmicrosoft.com")]);
9823
9824        assert_eq!(
9825            BrokerClientApplication::device_certificate_tenant_id(&cert_der).unwrap(),
9826            None
9827        );
9828    }
9829
9830    #[cfg(feature = "broker")]
9831    #[test]
9832    fn p2p_tenant_endpoint_uses_current_authority_host() {
9833        let app = test_broker_app("https://login.microsoftonline.com/common");
9834        assert_eq!(
9835            app.tenant_token_endpoint("11111111-1111-1111-1111-111111111111")
9836                .unwrap(),
9837            "https://login.microsoftonline.com/11111111-1111-1111-1111-111111111111/oauth2/token"
9838        );
9839
9840        let app = test_broker_app("https://login.microsoftonline.us/organizations");
9841        assert_eq!(
9842            app.tenant_token_endpoint("contoso.com").unwrap(),
9843            "https://login.microsoftonline.us/contoso.com/oauth2/token"
9844        );
9845        assert!(app.tenant_token_endpoint("../bad").is_err());
9846    }
9847
9848    #[cfg(feature = "broker")]
9849    #[test]
9850    fn p2p_tenant_endpoint_rejects_empty_tenant() {
9851        let app = test_broker_app("https://login.microsoftonline.com/common");
9852
9853        assert!(app.tenant_token_endpoint("").is_err());
9854    }
9855
9856    #[cfg(feature = "broker")]
9857    #[test]
9858    fn p2p_csr_is_der_and_matches_key() {
9859        let (mut tpm, machine_key) = test_tpm();
9860        let loadable_key = tpm.rs256_create(&machine_key).unwrap();
9861        let key = tpm.rs256_load(&machine_key, &loadable_key).unwrap();
9862        let subject = Name::from_str("CN=test-device").unwrap();
9863
9864        let (csr_der, public_key_der) =
9865            BrokerClientApplication::create_p2p_csr(&mut tpm, &key, subject).unwrap();
9866
9867        assert!(!csr_der.is_empty());
9868        assert_eq!(public_key_der, tpm.rs256_public_der(&key).unwrap());
9869    }
9870
9871    #[cfg(feature = "broker")]
9872    #[test]
9873    fn p2p_user_csr_accepts_blank_common_name() {
9874        let (mut tpm, machine_key) = test_tpm();
9875        let loadable_key = tpm.rs256_create(&machine_key).unwrap();
9876        let key = tpm.rs256_load(&machine_key, &loadable_key).unwrap();
9877        let subject = Name::from_str("CN=").unwrap();
9878
9879        let (csr_der, public_key_der) =
9880            BrokerClientApplication::create_p2p_csr(&mut tpm, &key, subject).unwrap();
9881
9882        assert!(!csr_der.is_empty());
9883        assert_eq!(public_key_der, tpm.rs256_public_der(&key).unwrap());
9884    }
9885
9886    #[cfg(feature = "broker")]
9887    fn test_private_key() -> P2PPrivateKey {
9888        P2PPrivateKey::GeneratedUser(LoadableRS256Key::Soft2048V2 {
9889            key: vec![],
9890            tag: [0; 16],
9891            iv: [0; 16],
9892        })
9893    }
9894
9895    #[cfg(feature = "broker")]
9896    #[test]
9897    fn p2p_response_parses_certificate_metadata() {
9898        let rsa = Rsa::generate(2048).unwrap();
9899        let pkey = PKey::from_rsa(rsa).unwrap();
9900        let public_key_der = pkey.public_key_to_der().unwrap();
9901        let cert_der = self_signed_cert(&pkey, &[]);
9902
9903        let cert = BrokerClientApplication::p2p_certificate_from_response(
9904            &P2PCertificateResponse {
9905                x5c: STANDARD.encode(&cert_der),
9906                x5c_ca: STANDARD.encode(&cert_der),
9907            },
9908            test_private_key(),
9909            &public_key_der,
9910        )
9911        .unwrap();
9912
9913        assert_eq!(cert.certificate_der, cert_der);
9914        assert_eq!(cert.subject, "CN=p2p-test");
9915        assert_eq!(cert.issuer, "CN=p2p-test");
9916        assert_eq!(cert.thumbprint_sha1.len(), 40);
9917        // The certificate is valid for a day, so it expires in the future.
9918        assert!(cert.not_after_unix > 0);
9919    }
9920
9921    #[cfg(feature = "broker")]
9922    #[test]
9923    fn p2p_response_parses_ca_certificate() {
9924        let rsa = Rsa::generate(2048).unwrap();
9925        let pkey = PKey::from_rsa(rsa).unwrap();
9926        let public_key_der = pkey.public_key_to_der().unwrap();
9927        let cert_der = self_signed_cert(&pkey, &[]);
9928        let ca_der = self_signed_cert(&pkey, &[]);
9929
9930        let cert = BrokerClientApplication::p2p_certificate_from_response(
9931            &P2PCertificateResponse {
9932                x5c: STANDARD.encode(&cert_der),
9933                x5c_ca: STANDARD.encode(&ca_der),
9934            },
9935            test_private_key(),
9936            &public_key_der,
9937        )
9938        .unwrap();
9939
9940        assert_eq!(cert.ca_certificate_der, ca_der);
9941        assert_eq!(
9942            cert.ca_certificate_pem,
9943            Some(format!(
9944                "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
9945                STANDARD.encode(&ca_der)
9946            ))
9947        );
9948
9949        // A CA blob we can't parse is not fatal, but it must not be handed
9950        // back labelled as a certificate.
9951        let cert = BrokerClientApplication::p2p_certificate_from_response(
9952            &P2PCertificateResponse {
9953                x5c: STANDARD.encode(&cert_der),
9954                x5c_ca: "AQID".to_string(),
9955            },
9956            test_private_key(),
9957            &public_key_der,
9958        )
9959        .unwrap();
9960
9961        assert_eq!(cert.ca_certificate_der, vec![0x01, 0x02, 0x03]);
9962        assert_eq!(cert.ca_certificate_pem, None);
9963    }
9964
9965    #[cfg(feature = "broker")]
9966    #[test]
9967    fn p2p_response_rejects_public_key_mismatch() {
9968        let pkey = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
9969        let other = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
9970        let cert_der = self_signed_cert(&pkey, &[]);
9971
9972        // Entra returning a certificate bound to a key we don't hold means the
9973        // certificate is unusable, so this must not be accepted.
9974        assert!(BrokerClientApplication::p2p_certificate_from_response(
9975            &P2PCertificateResponse {
9976                x5c: STANDARD.encode(&cert_der),
9977                x5c_ca: STANDARD.encode(&cert_der),
9978            },
9979            test_private_key(),
9980            &other.public_key_to_der().unwrap(),
9981        )
9982        .is_err());
9983    }
9984
9985    #[test]
9986    fn user_token_spn_uses_upn_when_present() {
9987        let access_token = build_access_token(
9988            r#"{"amr":["pwd"],"tid":"11111111-1111-1111-1111-111111111111","upn":"user@example.com"}"#,
9989        );
9990        let token = build_user_token(access_token);
9991
9992        assert_eq!(token.spn().unwrap_or_default(), "user@example.com");
9993    }
9994
9995    #[test]
9996    fn user_token_spn_falls_back_to_unique_name() {
9997        let access_token = build_access_token(
9998            r#"{"amr":["pwd"],"tid":"11111111-1111-1111-1111-111111111111","unique_name":"alias@example.com"}"#,
9999        );
10000        let token = build_user_token(access_token);
10001
10002        assert_eq!(token.spn().unwrap_or_default(), "alias@example.com");
10003    }
10004
10005    #[test]
10006    fn user_token_spn_prefers_upn_when_both_fields_present() {
10007        let access_token = build_access_token(
10008            r#"{"amr":["pwd"],"tid":"11111111-1111-1111-1111-111111111111","upn":"primary@example.com","unique_name":"alias@example.com"}"#,
10009        );
10010        let token = build_user_token(access_token);
10011
10012        assert_eq!(token.spn().unwrap_or_default(), "primary@example.com");
10013    }
10014
10015    fn build_mfa_auth_continue(
10016        methods: Vec<MfaMethodInfo>,
10017        skip_fido_for_mfa: bool,
10018    ) -> MFAAuthContinue {
10019        let mfa_methods = methods.iter().map(|m| m.auth_method_id.clone()).collect();
10020        MFAAuthContinue {
10021            mfa_method_details: methods,
10022            mfa_methods,
10023            skip_fido_for_mfa,
10024            ..Default::default()
10025        }
10026    }
10027
10028    /// User has SMS as preferred MFA, plus a cross-device passkey (MS Authenticator
10029    /// QR scan) and PhoneAppNotification. No physical security key.
10030    /// Tenant enforces MFA. Expected: SMS is selected, FIDO is skipped.
10031    #[test]
10032    fn mfa_prefers_sms_when_default_and_fido_is_cross_device_passkey() {
10033        let methods = vec![
10034            MfaMethodInfo {
10035                auth_method_id: "OneWaySMS".to_string(),
10036                display: "+X XXXXXXXX90".to_string(),
10037                is_default: true,
10038            },
10039            MfaMethodInfo {
10040                auth_method_id: "FidoKey".to_string(),
10041                display: "MS Authenticator passkey".to_string(),
10042                is_default: false,
10043            },
10044            MfaMethodInfo {
10045                auth_method_id: "PhoneAppNotification".to_string(),
10046                display: "Microsoft Authenticator".to_string(),
10047                is_default: false,
10048            },
10049        ];
10050
10051        let mfa = build_mfa_auth_continue(methods, true);
10052
10053        assert_eq!(mfa.mfa_method(), "OneWaySMS");
10054
10055        let default = mfa.get_default_mfa_method_details();
10056        assert!(default.is_some(), "default MFA method should exist");
10057        if let Some(default) = default {
10058            assert_eq!(default.auth_method_id, "OneWaySMS");
10059            assert!(default.is_default);
10060        }
10061
10062        assert!(mfa.has_mfa_method("FidoKey"));
10063        let fido = mfa.get_mfa_method_by_id("FidoKey");
10064        assert!(fido.is_some(), "FidoKey method should exist");
10065        if let Some(fido) = fido {
10066            assert!(mfa.should_skip_fido_method(&fido));
10067        }
10068
10069        assert_eq!(mfa.mfa_method_count(), 3);
10070    }
10071
10072    // User has only a cross-device passkey (MS Authenticator), no physical
10073    // security key. Legacy flag is set. Graph is unavailable.
10074    // Expected: security key flow should NOT be attempted.
10075    // Legacy flag enables security key when FIDO params present
10076    #[test]
10077    fn passwordless_fido_skipped_when_cross_device_passkey_and_sms_preferred() {
10078        let options = vec![AuthOption::PasswordlessFido, AuthOption::Fido];
10079
10080        let result = should_attempt_passwordless_security_key(
10081            &options, true, // has_fido_params
10082        );
10083
10084        assert!(
10085            result,
10086            "Should attempt security key when FIDO params present and legacy flag enabled"
10087        );
10088    }
10089
10090    #[test]
10091    fn security_key_skipped_when_no_fido_params() {
10092        let options = vec![AuthOption::PasswordlessSecurityKey];
10093        let result = should_attempt_passwordless_security_key(&options, false);
10094        assert!(
10095            !result,
10096            "Should not attempt security key without FIDO params"
10097        );
10098    }
10099
10100    #[test]
10101    fn security_key_skipped_when_not_enabled() {
10102        let options = vec![AuthOption::Fido];
10103        let result = should_attempt_passwordless_security_key(&options, true);
10104        assert!(
10105            !result,
10106            "Should not attempt security key when not enabled in config"
10107        );
10108    }
10109
10110    #[test]
10111    fn qr_bluetooth_attempted_when_cross_device_passkey() {
10112        let options = vec![AuthOption::PasswordlessQrBluetooth];
10113        let result = should_attempt_passwordless_qr_bluetooth(&options, true, true);
10114        assert!(
10115            result,
10116            "Should attempt QR/Bluetooth when user has cross-device passkey"
10117        );
10118    }
10119
10120    #[test]
10121    fn qr_bluetooth_skipped_when_no_cross_device_passkey() {
10122        let options = vec![AuthOption::PasswordlessQrBluetooth];
10123        let result = should_attempt_passwordless_qr_bluetooth(&options, true, false);
10124        assert!(
10125            !result,
10126            "Should not attempt QR/Bluetooth when user has no cross-device passkey"
10127        );
10128    }
10129
10130    #[test]
10131    fn qr_bluetooth_skipped_when_not_enabled() {
10132        let options = vec![AuthOption::PasswordlessFido];
10133        let result = should_attempt_passwordless_qr_bluetooth(&options, true, true);
10134        assert!(
10135            !result,
10136            "Legacy PasswordlessFido should not enable QR/Bluetooth"
10137        );
10138    }
10139
10140    #[test]
10141    fn both_flows_when_fido_params_and_cross_device() {
10142        let options = vec![
10143            AuthOption::PasswordlessSecurityKey,
10144            AuthOption::PasswordlessQrBluetooth,
10145        ];
10146        let security_key = should_attempt_passwordless_security_key(&options, true);
10147        let qr_bluetooth = should_attempt_passwordless_qr_bluetooth(&options, true, true);
10148        assert!(security_key, "Should attempt security key");
10149        assert!(qr_bluetooth, "Should attempt QR/Bluetooth");
10150    }
10151
10152    #[test]
10153    fn prt_serialize_deserialize() {
10154        let prt = PrimaryRefreshToken {
10155            token_type: "Bearer".to_string(),
10156            expires_in: "3600".to_string(),
10157            ext_expires_in: "3600".to_string(),
10158            expires_on: "9999999999".to_string(),
10159            refresh_token: "refresh_token".to_string(),
10160            refresh_token_expires_in: 3600,
10161            session_key_jwe: None,
10162            id_token: IdToken::default(),
10163            client_info: ClientInfo::default(),
10164            device_tenant_id: None,
10165            tgt_on_prem: OnPremTgt::Absent {},
10166            tgt_cloud: StructuredTgt::default(),
10167            kerberos_top_level_names: None,
10168        };
10169        let prt_json = r#"
10170        {
10171            "token_type":"Bearer",
10172            "expires_in":"3600",
10173            "ext_expires_in":"3600",
10174            "expires_on":"9999999999",
10175            "refresh_token":"refresh_token",
10176            "refresh_token_expires_in":3600,
10177            "session_key_jwe":null,
10178            "id_token":{
10179                "name":"",
10180                "oid":"",
10181                "preferred_username":null,
10182                "puid":null,
10183                "tenant_region_scope":null,
10184                "tid":""
10185            },
10186            "client_info":{
10187                "uid":null,
10188                "utid":null
10189            },
10190            "device_tenant_id":null,
10191            "tgt_cloud":{
10192                "clientKey":null,
10193                "keyType":0,
10194                "error":null,
10195                "messageBuffer":null,
10196                "realm":null,
10197                "sn":null,
10198                "cn":null,
10199                "sessionKeyType":0,
10200                "accountType":0
10201            },
10202            "kerberos_top_level_names":null
10203        }
10204        "#
10205        .to_string()
10206        .replace("\n", "")
10207        .replace(" ", "");
10208        let se = serde_json::to_string(&prt).expect("Failed to serialize");
10209        assert_eq!(prt_json, se);
10210
10211        let de: PrimaryRefreshToken = serde_json::from_str(&se).expect("Falied to deserialize");
10212        assert_eq!(prt, de);
10213    }
10214
10215    #[test]
10216    fn prt_serialize_deserialize_tgt_ad() {
10217        let prt = PrimaryRefreshToken {
10218            token_type: "Bearer".to_string(),
10219            expires_in: "3600".to_string(),
10220            ext_expires_in: "3600".to_string(),
10221            expires_on: "9999999999".to_string(),
10222            refresh_token: "refresh_token".to_string(),
10223            refresh_token_expires_in: 3600,
10224            session_key_jwe: None,
10225            id_token: IdToken::default(),
10226            client_info: ClientInfo::default(),
10227            device_tenant_id: None,
10228            tgt_on_prem: OnPremTgt::Structured {
10229                tgt_ad: StructuredTgt {
10230                    client_key: Some("a".to_string()),
10231                    key_type: 18,
10232                    error: None,
10233                    message_buffer: Some("b".to_string()),
10234                    realm: Some("c".to_string()),
10235                    sn: Some("d".to_string()),
10236                    cn: Some("e".to_string()),
10237                    session_key_type: 18,
10238                    account_type: 1,
10239                },
10240            },
10241            tgt_cloud: StructuredTgt::default(),
10242            kerberos_top_level_names: None,
10243        };
10244        let prt_json = r#"
10245        {
10246            "token_type":"Bearer",
10247            "expires_in":"3600",
10248            "ext_expires_in":"3600",
10249            "expires_on":"9999999999",
10250            "refresh_token":"refresh_token",
10251            "refresh_token_expires_in":3600,
10252            "session_key_jwe":null,
10253            "id_token":{
10254                "name":"",
10255                "oid":"",
10256                "preferred_username":null,
10257                "puid":null,
10258                "tenant_region_scope":null,
10259                "tid":""
10260            },
10261            "client_info":{
10262                "uid":null,
10263                "utid":null
10264            },
10265            "device_tenant_id":null,
10266            "tgt_ad":{
10267                "clientKey":"a",
10268                "keyType":18,
10269                "error":null,
10270                "messageBuffer":"b",
10271                "realm":"c",
10272                "sn":"d",
10273                "cn":"e",
10274                "sessionKeyType":18,
10275                "accountType":1
10276            },
10277            "tgt_cloud":{
10278                "clientKey":null,
10279                "keyType":0,
10280                "error":null,
10281                "messageBuffer":null,
10282                "realm":null,
10283                "sn":null,
10284                "cn":null,
10285                "sessionKeyType":0,
10286                "accountType":0
10287            },
10288            "kerberos_top_level_names":null
10289        }
10290        "#
10291        .to_string()
10292        .replace("\n", "")
10293        .replace(" ", "");
10294        let se = serde_json::to_string(&prt).expect("Failed to serialize");
10295        assert_eq!(prt_json, se);
10296
10297        let de: PrimaryRefreshToken = serde_json::from_str(&se).expect("Falied to deserialize");
10298        assert_eq!(prt, de);
10299    }
10300
10301    #[test]
10302    fn prt_serialize_deserialize_tgt_message_buffer() {
10303        let prt = PrimaryRefreshToken {
10304            token_type: "Bearer".to_string(),
10305            expires_in: "3600".to_string(),
10306            ext_expires_in: "3600".to_string(),
10307            expires_on: "9999999999".to_string(),
10308            refresh_token: "refresh_token".to_string(),
10309            refresh_token_expires_in: 3600,
10310            session_key_jwe: None,
10311            id_token: IdToken::default(),
10312            client_info: ClientInfo::default(),
10313            device_tenant_id: None,
10314            tgt_on_prem: OnPremTgt::Raw(RawTgt {
10315                tgt_message_buffer: "a".to_string(),
10316                tgt_client_key: "b".to_string(),
10317                tgt_key_type: 18,
10318            }),
10319            tgt_cloud: StructuredTgt::default(),
10320            kerberos_top_level_names: None,
10321        };
10322        let prt_json = r#"
10323        {
10324            "token_type":"Bearer",
10325            "expires_in":"3600",
10326            "ext_expires_in":"3600",
10327            "expires_on":"9999999999",
10328            "refresh_token":"refresh_token",
10329            "refresh_token_expires_in":3600,
10330            "session_key_jwe":null,
10331            "id_token":{
10332                "name":"",
10333                "oid":"",
10334                "preferred_username":null,
10335                "puid":null,
10336                "tenant_region_scope":null,
10337                "tid":""
10338            },
10339            "client_info":{
10340                "uid":null,
10341                "utid":null
10342            },
10343            "device_tenant_id":null,
10344            "tgt_message_buffer":"a",
10345            "tgt_client_key":"b",
10346            "tgt_key_type":18,
10347            "tgt_cloud":{
10348                "clientKey":null,
10349                "keyType":0,
10350                "error":null,
10351                "messageBuffer":null,
10352                "realm":null,
10353                "sn":null,
10354                "cn":null,
10355                "sessionKeyType":0,
10356                "accountType":0
10357            },
10358            "kerberos_top_level_names":null
10359        }
10360        "#
10361        .to_string()
10362        .replace("\n", "")
10363        .replace(" ", "");
10364        let se = serde_json::to_string(&prt).expect("Failed to serialize");
10365        assert_eq!(prt_json, se);
10366
10367        let de: PrimaryRefreshToken = serde_json::from_str(&se).expect("Falied to deserialize");
10368        assert_eq!(prt, de);
10369    }
10370
10371    #[test]
10372    fn prt_deserialize_invalid() {
10373        let se = r#"
10374        {
10375            "token_type":"Bearer",
10376            "expires_in":"3600",
10377            "ext_expires_in":"3600",
10378            "expires_on":"9999999999",
10379            "refresh_token":"refresh_token",
10380            "refresh_token_expires_in":3600,
10381            "session_key_jwe":null,
10382            "id_token":{
10383                "name":"",
10384                "oid":"",
10385                "preferred_username":null,
10386                "puid":null,
10387                "tenant_region_scope":null,
10388                "tid":""
10389            },
10390            "client_info":{
10391                "uid":null,
10392                "utid":null
10393            },
10394            "device_tenant_id":null,
10395            "tgt_message_buffer":"a",
10396            "tgt_client_key":"b",
10397            "tgt_key_type":18,
10398            "tgt_ad":{
10399                "clientKey":"a",
10400                "keyType":18,
10401                "error":null,
10402                "messageBuffer":"b",
10403                "realm":"c",
10404                "sn":"d",
10405                "cn":"e",
10406                "sessionKeyType":18,
10407                "accountType":1
10408            },
10409            "tgt_cloud":{
10410                "clientKey":null,
10411                "keyType":0,
10412                "error":null,
10413                "messageBuffer":null,
10414                "realm":null,
10415                "sn":null,
10416                "cn":null,
10417                "sessionKeyType":0,
10418                "accountType":0
10419            },
10420            "kerberos_top_level_names":null
10421        }
10422        "#
10423        .to_string()
10424        .replace("\n", "")
10425        .replace(" ", "");
10426        let de = serde_json::from_str::<PrimaryRefreshToken>(&se);
10427        assert!(de.is_err());
10428        //Error("on-prem TGT is a mix of raw and structured, or is incomplete", line: 1, column: 729)
10429    }
10430}