use crate::aadsts_err_gen::AADSTSError;
use crate::error::{ErrorResponse, MsalError, AUTH_PENDING};
#[cfg(feature = "broker")]
use crate::ssh::{
build_ssh_certificate_request_form, parse_ssh_certificate_response, ssh_rsa_public_key_to_jwk,
EntraSshCertificate, SshCertificateTokenResponse, SshRsaJwk, AZURE_CLI_APP_ID,
};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
#[cfg(feature = "broker")]
use crypto_glue::traits::SpkiEncodePublicKey;
#[cfg(feature = "broker")]
use crypto_glue::x509::oiddb::rfc5912;
use crypto_glue::x509::Certificate;
#[cfg(feature = "broker")]
use der::asn1::{BitString, OctetString, SetOfVec};
#[cfg(feature = "broker")]
use der::{Decode, Encode, Sequence};
use kanidm_hsm_crypto::structures::RS256Key;
use percent_encoding::percent_decode_str;
use reqwest::redirect::Policy;
#[cfg(feature = "proxyable")]
use reqwest::Proxy;
use reqwest::{header, Body, Client, Response, Url};
use scraper::{Html, Selector};
use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
use serde::ser::{SerializeMap, Serializer};
use serde::{Deserialize, Serialize};
use serde_json::{from_str as json_from_str, json, Value};
#[cfg(feature = "set_timeout")]
use std::cmp::min;
use std::collections::HashMap;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use std::sync::RwLock;
use std::thread::sleep;
use std::time::Duration;
use tracing::{error, info, warn};
use urlencoding::encode as url_encode;
use uuid::Uuid;
#[cfg(feature = "broker")]
use x509_cert::attr::Attribute;
#[cfg(feature = "broker")]
use x509_cert::name::Name;
#[cfg(feature = "broker")]
use x509_cert::spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned};
use zeroize::{Zeroize, ZeroizeOnDrop};
#[cfg(feature = "broker")]
use compact_jwt::{
compact::JweCompact,
crypto::{JwsTpmRs256Signer, MsOapxbcSessionKey},
jwe::Jwe,
jws::{Jws, JwsBuilder},
traits::{JwsMutSigner, JwsSignable},
};
#[cfg(feature = "broker")]
use kanidm_hsm_crypto::{
provider::{BoxedDynTpm, Tpm, TpmMsExtensions, TpmRS256},
structures::{
LoadableMsDeviceEnrolmentKey, LoadableMsHelloKey, LoadableMsOapxbcRsaKey, LoadableRS256Key,
MsOapxbcRsaKey, SealedData, StorageKey,
},
PinValue,
};
#[cfg(feature = "broker")]
use openssl::asn1::{Asn1Time, Asn1TimeRef};
#[cfg(feature = "broker")]
use openssl::hash::{hash, MessageDigest};
#[cfg(feature = "broker")]
use openssl::pkey::{PKey, Public};
use openssl::rand::rand_bytes;
#[cfg(feature = "broker")]
use openssl::rsa::Rsa;
use openssl::sha::sha256;
#[cfg(feature = "broker")]
use openssl::sign::Signer;
#[cfg(feature = "broker")]
use openssl::x509::X509;
#[cfg(feature = "broker")]
use os_release::OsRelease;
#[cfg(feature = "broker")]
use regex::Regex;
#[cfg(feature = "broker")]
use serde_json::{from_slice as json_from_slice, to_vec as json_to_vec};
#[cfg(feature = "broker")]
use std::convert::TryInto;
#[cfg(feature = "broker")]
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(feature = "broker")]
use tracing::debug;
#[cfg(feature = "broker")]
use crate::discovery::Services;
#[cfg(feature = "broker")]
use crate::discovery::{BcryptRsaKeyBlob, EnrollAttrs};
#[cfg(feature = "broker")]
use libkrimes::ccache::resolve as ccache_resolve;
#[cfg(feature = "broker")]
use libkrimes::proto::{AuthenticationReply, DerivedKey, KerberosCredentials, KerberosReply};
#[cfg(feature = "broker")]
use std::fs;
#[cfg(feature = "broker")]
use std::io::Read;
#[cfg(feature = "broker")]
use base64::engine::general_purpose::STANDARD;
#[cfg(feature = "broker")]
use compact_jwt::JwtError;
#[cfg(feature = "broker")]
use serde_json::to_string_pretty;
#[cfg(feature = "broker")]
use zeroize::Zeroizing;
use reqwest_cookie_store::{CookieStore, CookieStoreMutex};
use std::sync::Arc;
#[cfg(feature = "broker")]
const BROKER_CLIENT_IDENT: &str = "38aa3b87-a06d-4817-b275-7a316988d93b";
#[cfg(feature = "broker")]
pub const BROKER_APP_ID: &str = "29d9ed98-a469-4536-ade2-f981bc1d605e";
#[cfg(feature = "broker")]
pub const LINUX_BROKER_APP_ID: &str = "b743a22d-6705-4147-8670-d92fa515ee2b";
#[cfg(feature = "broker")]
const DRS_APP_ID: &str = "01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9";
#[cfg(feature = "broker")]
const AZURE_PORTAL_APP_ID: &str = "c44b4083-3bb0-49c1-b47d-974e53cbdf3c";
const HIMMELBLAU_REDIRECT_URI: &str = "himmelblau://Himmelblau.EntraId.BrokerPlugin";
#[derive(Debug, Deserialize)]
pub struct SidToName {
pub upn: String,
pub email: Option<String>,
pub name: String,
pub family_name: Option<String>,
pub sid: String,
pub onprem_sam_account_name: Option<String>,
pub domain_netbios_name: Option<String>,
pub domain_dns_name: Option<String>,
}
#[cfg(feature = "broker")]
const FIDO_USER_AGENT: &str =
"Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0";
#[derive(Default, Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
pub struct DeviceAuthorizationResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub verification_uri_complete: Option<String>,
pub expires_in: u32,
pub interval: Option<u32>,
pub message: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
struct ArrUserProofs {
#[serde(rename = "authMethodId")]
auth_method_id: String,
#[serde(rename = "isDefault")]
is_default: bool,
display: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MfaMethodInfo {
pub auth_method_id: String,
pub display: String,
pub is_default: bool,
}
impl From<&ArrUserProofs> for MfaMethodInfo {
fn from(proof: &ArrUserProofs) -> Self {
MfaMethodInfo {
auth_method_id: proof.auth_method_id.clone(),
display: proof.display.clone(),
is_default: proof.is_default,
}
}
}
#[derive(Clone, Deserialize)]
struct AuthConfig {
#[serde(rename = "sessionId")]
session_id: String,
#[serde(rename = "sFT")]
sft: Option<String>,
#[serde(rename = "sCtx")]
sctx: Option<String>,
#[serde(rename = "urlPost")]
url_post: Option<String>,
canary: String,
#[serde(rename = "iAllowedIdentities")]
allowed_identities: Option<u32>,
#[serde(rename = "strServiceExceptionMessage")]
service_exception_msg: Option<String>,
pgid: Option<String>,
#[serde(rename = "urlSkipMfaRegistration")]
url_skip_mfa_registration: Option<String>,
#[serde(rename = "iRemainingDaysToSkipMfaRegistration")]
remaining_days_to_skip_mfa_reg: Option<u32>,
#[serde(rename = "arrUserProofs")]
arr_user_proofs: Option<Vec<ArrUserProofs>>,
#[serde(rename = "arrFidoAllowList")]
fido_allow_list: Option<Vec<String>>,
#[serde(rename = "urlEndAuth")]
url_end_auth: Option<String>,
#[serde(rename = "urlBeginAuth")]
url_begin_auth: Option<String>,
#[serde(rename = "urlFidoLogin")]
url_fido_login: Option<String>,
#[serde(rename = "urlResume")]
url_resume: Option<String>,
#[serde(rename = "iMaxPollAttempts")]
max_poll_attempts: Option<u32>,
#[serde(rename = "iPollingInterval")]
polling_interval: Option<u32>,
#[serde(rename = "sErrorCode")]
error_code: Option<String>,
#[serde(rename = "iErrorCode")]
error_code2: Option<u32>,
#[serde(rename = "sErrTxt")]
err_txt: Option<String>,
#[serde(rename = "sFidoChallenge")]
fido_challenge: Option<String>,
#[serde(rename = "sCrossDomainCanary")]
cross_domain_canary: Option<String>,
#[serde(rename = "urlGetOneTimeCode")]
url_get_one_time_code: Option<String>,
#[serde(rename = "urlGetCredentialType")]
url_get_credential_type: Option<String>,
#[serde(rename = "urlSessionState")]
url_session_state: Option<String>,
#[cfg(feature = "changepassword")]
#[serde(rename = "urlAsyncSsprBegin")]
url_async_sspr_begin: Option<String>,
#[cfg(feature = "changepassword")]
#[serde(rename = "urlAsyncSsprPoll")]
url_async_sspr_poll: Option<String>,
#[serde(rename = "fIsPasskeySupportEnabled")]
is_passkey_support_enabled: Option<bool>,
}
#[derive(Deserialize, Serialize, Default)]
pub struct MFAAuthContinue {
pub msg: String,
pub entropy: Option<u8>,
pub max_poll_attempts: Option<u32>,
pub polling_interval: Option<u32>,
pub session_id: String,
pub flow_token: String,
pub ctx: String,
pub canary: String,
pub url_end_auth: Option<String>,
pub url_post: String,
pub url_session_state: Option<String>,
pub resource: Option<String>,
pub dag: Option<DeviceAuthorizationResponse>,
pub fido_challenge: Option<String>,
pub fido_allow_list: Option<Vec<String>>,
pub cross_domain_canary: Option<String>,
pub mfa_methods: Vec<String>,
pub mfa_method_details: Vec<MfaMethodInfo>,
pub selected_mfa_method_id: Option<String>,
pub auth_code: Option<String>,
#[deprecated(note = "Use `skip_fido_for_mfa` instead")]
pub fido_is_passkey: bool,
pub skip_fido_for_mfa: bool,
pub has_physical_security_key: bool,
pub has_cross_device_passkey: bool,
}
impl From<DeviceAuthorizationResponse> for MFAAuthContinue {
fn from(item: DeviceAuthorizationResponse) -> Self {
let msg = match &item.message {
Some(msg) => msg.to_string(),
None => format!(
"Using a browser on another device, visit:\n{}\n \
And enter the code:\n{}",
item.verification_uri, item.user_code
),
};
let polling_interval = item.interval.unwrap_or(5);
let max_poll_attempts = item.expires_in / polling_interval;
MFAAuthContinue {
msg,
max_poll_attempts: Some(max_poll_attempts),
polling_interval: Some(polling_interval * 1000),
dag: Some(item),
..Default::default()
}
}
}
impl MFAAuthContinue {
pub fn mfa_method(&self) -> String {
if let Some(method) = self.get_default_mfa_method_details() {
method.auth_method_id
} else if !self.mfa_methods.is_empty() {
for method in self.get_mfa_method_details() {
if !self.should_skip_fido_method(&method) {
return method.auth_method_id.clone();
}
}
"".to_string()
} else {
"".to_string()
}
}
pub fn get_available_mfa_methods(&self) -> Vec<String> {
self.mfa_methods.clone()
}
pub fn get_mfa_method_details(&self) -> Vec<MfaMethodInfo> {
self.mfa_method_details.clone()
}
pub fn has_mfa_method(&self, method_id: &str) -> bool {
self.get_available_mfa_methods()
.contains(&method_id.to_string())
}
pub fn mfa_method_count(&self) -> usize {
self.get_available_mfa_methods().len()
}
fn should_skip_fido_method(&self, method: &MfaMethodInfo) -> bool {
if method.auth_method_id == "FidoKey" {
self.skip_fido_for_mfa
} else {
false
}
}
pub fn get_default_mfa_method_details(&self) -> Option<MfaMethodInfo> {
if let Some(details) = self
.get_mfa_method_details()
.into_iter()
.find(|method| method.is_default && !self.should_skip_fido_method(method))
{
Some(details)
} else if !self.mfa_methods.is_empty() {
for method in self.get_mfa_method_details() {
if !self.should_skip_fido_method(&method) {
return Some(method);
}
}
None
} else {
None
}
}
pub fn get_mfa_method_by_id(&self, method_id: &str) -> Option<MfaMethodInfo> {
self.get_mfa_method_details()
.into_iter()
.find(|method| method.auth_method_id == method_id)
}
}
#[derive(Deserialize)]
struct AuthResponse {
#[serde(rename = "Success")]
success: bool,
#[serde(rename = "Retry")]
retry: Option<bool>,
#[serde(rename = "Message")]
message: Option<String>,
#[serde(rename = "ErrCode")] error_code: Option<u32>,
#[serde(rename = "Ctx")]
ctx: String,
#[serde(rename = "FlowToken")]
flow_token: String,
#[serde(rename = "Entropy")]
entropy: u8,
}
#[derive(Deserialize)]
struct DeviceCodeStatus {
#[serde(rename = "AuthorizationState")]
authorization_state: u8,
}
#[derive(Clone, Deserialize)]
struct RemoteNgcParams {
#[serde(rename = "SessionIdentifier")]
session_identifier: String,
#[serde(rename = "Entropy")]
entropy: u8,
}
#[derive(Deserialize)]
struct OTCError {
message: String,
}
#[derive(Deserialize)]
struct OneTimeCode {
#[serde(rename = "RemoteNgcParams")]
remote_ngc_params: Option<RemoteNgcParams>,
error: Option<OTCError>,
}
#[derive(Clone, Deserialize)]
struct FidoParams {
#[serde(rename = "AllowList")]
fido_allow_list: Vec<String>,
#[serde(rename = "HasCrossDeviceCapablePasskey")]
has_cross_device_capable_passkey: Option<bool>,
}
#[allow(dead_code)]
#[derive(Clone, Deserialize)]
struct Credentials {
#[serde(rename = "FederationRedirectUrl")]
federation_redirect_url: Option<String>,
#[serde(rename = "HasPassword")]
has_password: bool,
#[serde(rename = "RemoteNgcParams")]
remote_ngc_params: Option<RemoteNgcParams>,
#[serde(rename = "FidoParams")]
fido_params: Option<FidoParams>,
#[serde(rename = "PrefCredential")]
pref_credential: u8,
#[serde(rename = "HasAccessPass")]
has_access_pass: Option<bool>,
#[serde(rename = "HasFido")]
has_fido: Option<bool>,
#[serde(rename = "HasRemoteNGC")]
has_remote_ngc: Option<bool>,
}
#[derive(Clone, Deserialize)]
struct CredType {
#[serde(rename = "Credentials")]
credentials: Credentials,
#[serde(rename = "ThrottleStatus")]
throttle_status: u8,
#[serde(rename = "IfExistsResult")]
if_exists_result: i32,
}
impl CredType {
fn log_throttle_status(&self) {
match self.throttle_status {
0 => {}
1 => debug!("GetCredentialType reports AAD backend throttling"),
2 => debug!("GetCredentialType reports MSA backend throttling"),
other => warn!("GetCredentialType returned unknown ThrottleStatus={other}"),
}
}
pub fn account_exists(&self) -> Result<bool, MsalError> {
match self.if_exists_result {
0 | 5 | 6 => Ok(true),
1 => Ok(false),
2 => Err(MsalError::AADSTSError(AADSTSError::new(90055, None))),
-1 | 4 => Err(MsalError::AADSTSError(AADSTSError::new(90006, None))),
other => {
warn!(
"GetCredentialType returned unknown IfExistsResult={other}, ThrottleStatus={}",
self.throttle_status
);
Ok(true)
}
}
}
pub fn is_personal_account(&self) -> bool {
self.if_exists_result == 5
}
}
const MAX_ADFS_RESPONSE_BYTES: usize = 2 * 1024 * 1024;
const MAX_ADFS_REDIRECTS: usize = 5;
#[derive(Debug, PartialEq, Eq)]
struct WsFedForm {
wa: String,
wresult: String,
wctx: String,
}
fn parse_adfs_federation_url(value: &str) -> Result<Option<Url>, MsalError> {
let url = Url::parse(value)
.map_err(|e| MsalError::URLFormatFailed(format!("Invalid federation URL: {e}")))?;
let is_adfs = url
.path_segments()
.map(|segments| {
segments
.into_iter()
.any(|segment| segment.eq_ignore_ascii_case("adfs"))
})
.unwrap_or(false);
if url.scheme() != "https"
|| url.host_str().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.fragment().is_some()
|| !is_adfs
{
return Ok(None);
}
Ok(Some(url))
}
fn same_origin(left: &Url, right: &Url) -> bool {
left.scheme() == right.scheme()
&& left.host_str() == right.host_str()
&& left.port_or_known_default() == right.port_or_known_default()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AdfsRequestMethod {
Get,
PostCredentials,
}
fn resolve_adfs_redirect(
origin: &Url,
current: &Url,
location: &str,
status: u16,
method: AdfsRequestMethod,
) -> Result<(Url, AdfsRequestMethod), MsalError> {
let next = current
.join(location)
.map_err(|e| MsalError::URLFormatFailed(format!("Invalid AD FS redirect URL: {e}")))?;
if next.scheme() != "https"
|| !same_origin(origin, &next)
|| !next.username().is_empty()
|| next.password().is_some()
|| next.fragment().is_some()
{
return Err(MsalError::GeneralFailure(
"AD FS attempted an unsafe redirect".to_string(),
));
}
let next_method = if matches!(status, 307 | 308) {
method
} else {
AdfsRequestMethod::Get
};
Ok((next, next_method))
}
fn entra_login_srf_url(authority: &str) -> Result<Url, MsalError> {
let mut url = Url::parse(authority)
.map_err(|e| MsalError::URLFormatFailed(format!("Invalid authority URL: {e}")))?;
if url.scheme() != "https" || url.host_str().is_none() {
return Err(MsalError::URLFormatFailed(
"Entra authority must be an absolute HTTPS URL".to_string(),
));
}
url.set_path("/login.srf");
url.set_query(None);
url.set_fragment(None);
Ok(url)
}
fn parse_ws_fed_form(text: &str) -> Result<WsFedForm, MsalError> {
let document = Html::parse_document(text);
let form_selector = Selector::parse("form")
.map_err(|e| MsalError::InvalidParse(format!("Failed parsing AD FS form: {e:?}")))?;
let input_selector = Selector::parse("input")
.map_err(|e| MsalError::InvalidParse(format!("Failed parsing AD FS inputs: {e:?}")))?;
for form in document.select(&form_selector) {
let mut wa = None;
let mut wresult = None;
let mut wctx = None;
for input in form.select(&input_selector) {
let Some(name) = input.value().attr("name") else {
continue;
};
let Some(value) = input.value().attr("value") else {
continue;
};
if name.eq_ignore_ascii_case("wa") {
wa = Some(value.to_string());
} else if name.eq_ignore_ascii_case("wresult") {
wresult = Some(value.to_string());
} else if name.eq_ignore_ascii_case("wctx") {
wctx = Some(value.to_string());
}
}
if let (Some(wa), Some(wresult), Some(wctx)) = (wa, wresult, wctx) {
if wa == "wsignin1.0" && !wresult.is_empty() && !wctx.is_empty() {
return Ok(WsFedForm { wa, wresult, wctx });
}
}
}
Err(MsalError::GeneralFailure(
"AD FS did not return a supported WS-Federation sign-in response".to_string(),
))
}
#[derive(Default, Clone, Deserialize, Serialize)]
#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
pub struct IdToken {
pub name: String,
pub oid: String,
pub preferred_username: Option<String>,
pub puid: Option<String>,
pub tenant_region_scope: Option<String>,
pub tid: String,
#[serde(skip_serializing)]
pub raw: Option<String>,
}
fn decode_string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Deserialize<'de> + FromStr<Err = MsalError>,
D: Deserializer<'de>,
{
struct StringOrStruct<T>(PhantomData<fn() -> T>);
impl<'de, T> Visitor<'de> for StringOrStruct<T>
where
T: Deserialize<'de> + FromStr<Err = MsalError>,
{
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<T, E>
where
E: de::Error,
{
FromStr::from_str(value)
.map_err(|e| serde::de::Error::custom(format!("Failed to parse string: {:?}", e)))
}
fn visit_map<M>(self, map: M) -> Result<T, M::Error>
where
M: MapAccess<'de>,
{
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
}
}
deserializer.deserialize_any(StringOrStruct(PhantomData))
}
impl FromStr for IdToken {
type Err = MsalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut siter = s.splitn(3, '.');
if siter.next().is_none() {
return Err(MsalError::InvalidParse(
"Failed parsing id_token header".to_string(),
));
}
let payload_str = match siter.next() {
Some(payload_str) => URL_SAFE_NO_PAD
.decode(payload_str)
.map_err(|e| MsalError::InvalidParse(format!("Failed parsing id_token: {}", e)))
.and_then(|bytes| {
String::from_utf8(bytes).map_err(|e| {
MsalError::InvalidParse(format!("Failed parsing id_token: {}", e))
})
})?,
None => {
return Err(MsalError::InvalidParse(
"Failed parsing id_token payload".to_string(),
));
}
};
let mut payload: IdToken = json_from_str(&payload_str).map_err(|e| {
MsalError::InvalidParse(format!("Failed parsing id_token from json: {}", e))
})?;
payload.raw = Some(s.to_string());
Ok(payload)
}
}
#[derive(Clone, Default, Deserialize, Serialize)]
#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
pub struct ClientInfo {
pub uid: Option<Uuid>,
pub utid: Option<Uuid>,
}
impl FromStr for ClientInfo {
type Err = MsalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let client_info: Value = URL_SAFE_NO_PAD
.decode(s)
.map_err(|e| MsalError::InvalidParse(format!("Failed parsing client_info: {}", e)))
.and_then(|bytes| {
String::from_utf8(bytes).map_err(|e| {
MsalError::InvalidParse(format!("Failed parsing client_info: {}", e))
})
})
.and_then(|client_info_str| {
json_from_str(&client_info_str).map_err(|e| {
MsalError::InvalidParse(format!("Failed parsing client_info: {}", e))
})
})?;
let uid_str = client_info["uid"].to_string();
let uid = Uuid::parse_str(uid_str.trim_matches('"'))
.map_err(|e| MsalError::InvalidParse(format!("Failed parsing client_info: {}", e)))?;
let utid_str = client_info["utid"].to_string();
let utid = Uuid::parse_str(utid_str.trim_matches('"'))
.map_err(|e| MsalError::InvalidParse(format!("Failed parsing client_info: {}", e)))?;
Ok(ClientInfo {
uid: Some(uid),
utid: Some(utid),
})
}
}
fn v2_scope_to_v1_resource(scope: &str) -> String {
if let Some(scheme_end) = scope.find("://") {
let authority_start = scheme_end + 3;
let after_authority = &scope[authority_start..];
if let Some(slash_pos) = after_authority.find('/') {
let permission = &after_authority[slash_pos + 1..];
if !permission.is_empty() {
return scope[..authority_start + slash_pos].to_string();
}
}
}
scope.trim_end_matches('/').to_string()
}
fn decode_number_from_string<'de, D>(d: D) -> Result<u32, D::Error>
where
D: Deserializer<'de>,
{
let v: Value = Deserialize::deserialize(d)?;
match v {
Value::Number(n) => Ok(n
.as_u64()
.ok_or(serde::de::Error::custom("Expected number or string"))?
as u32),
Value::String(s) => s
.parse::<u32>()
.map_err(|e| serde::de::Error::custom(format!("{}", e))),
_ => Err(serde::de::Error::custom("Expected number or string")),
}
}
#[derive(Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct AccessTokenPayload {
amr: Vec<String>,
tid: String,
unique_name: Option<String>,
upn: Option<String>,
}
#[derive(Clone, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct UserToken {
pub token_type: String,
pub scope: Option<String>,
#[serde(deserialize_with = "decode_number_from_string")]
pub expires_in: u32,
#[serde(deserialize_with = "decode_number_from_string")]
pub ext_expires_in: u32,
pub access_token: Option<String>,
pub refresh_token: String,
#[serde(deserialize_with = "decode_string_or_struct", default)]
#[zeroize(skip)]
pub id_token: IdToken,
#[serde(deserialize_with = "decode_string_or_struct", default)]
#[zeroize(skip)]
pub client_info: ClientInfo,
#[cfg(feature = "broker")]
#[zeroize(skip)]
pub prt: Option<SealedData>,
}
#[derive(Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
pub struct AuthorizationCodePkceFlow {
pub auth_url: String,
pub redirect_uri: String,
pub state: String,
scope: String,
code_verifier: String,
}
impl AuthorizationCodePkceFlow {
pub fn auth_url(&self) -> &str {
&self.auth_url
}
pub fn redirect_uri(&self) -> &str {
&self.redirect_uri
}
pub fn state(&self) -> &str {
&self.state
}
}
fn generate_base64url_random(bytes_len: usize) -> Result<String, MsalError> {
let mut bytes = vec![0u8; bytes_len];
rand_bytes(&mut bytes)
.map_err(|e| MsalError::CryptoFail(format!("Failed generating random bytes: {}", e)))?;
Ok(URL_SAFE_NO_PAD.encode(bytes))
}
fn pkce_code_challenge(code_verifier: &str) -> String {
URL_SAFE_NO_PAD.encode(sha256(code_verifier.as_bytes()))
}
impl UserToken {
pub fn tenant_id(&self) -> Result<String, MsalError> {
if !self.id_token.tid.is_empty() {
Ok(self.id_token.tid.clone())
} else if let Some(utid) = self.client_info.utid {
Ok(utid.to_string())
} else if let Some(access_token) = &self.access_token {
let mut siter = access_token.splitn(3, '.');
siter.next(); let payload: AccessTokenPayload = json_from_str(
&String::from_utf8(
URL_SAFE_NO_PAD
.decode(siter.next().ok_or_else(|| {
MsalError::InvalidParse("Payload not present".to_string())
})?)
.map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(payload.tid.clone())
} else {
Err(MsalError::GeneralFailure(
"No tid available for UserToken".to_string(),
))
}
}
pub fn uuid(&self) -> Result<Uuid, MsalError> {
Uuid::parse_str(&self.id_token.oid).map_err(|e| MsalError::InvalidParse(format!("{}", e)))
}
pub fn spn(&self) -> Result<String, MsalError> {
match &self.id_token.preferred_username {
Some(spn) => Ok(spn.to_string()),
None => match &self.access_token {
Some(access_token) => {
let mut siter = access_token.splitn(3, '.');
siter.next(); let payload: AccessTokenPayload = json_from_str(
&String::from_utf8(
URL_SAFE_NO_PAD
.decode(siter.next().ok_or_else(|| {
MsalError::InvalidParse("Payload not present".to_string())
})?)
.map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
if let Some(upn) = &payload.upn {
Ok(upn.clone())
} else if let Some(unique_name) = &payload.unique_name {
Ok(unique_name.clone())
} else {
Err(MsalError::GeneralFailure(
"No spn available for UserToken".to_string(),
))
}
}
None => Err(MsalError::GeneralFailure(
"No spn available for UserToken".to_string(),
)),
},
}
}
pub fn amr_mfa(&self) -> Result<bool, MsalError> {
match &self.access_token {
Some(access_token) => {
let mut siter = access_token.splitn(3, '.');
siter.next(); let payload: AccessTokenPayload = json_from_str(
&String::from_utf8(
URL_SAFE_NO_PAD
.decode(siter.next().ok_or_else(|| {
MsalError::InvalidParse("Payload not present".to_string())
})?)
.map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(payload.amr.iter().any(|s| s == "ngcmfa" || s == "mfa"))
}
None => Err(MsalError::GeneralFailure(
"No access token available for UserToken".to_string(),
)),
}
}
pub fn amr_ngcmfa(&self) -> Result<bool, MsalError> {
match &self.access_token {
Some(access_token) => {
let mut siter = access_token.splitn(3, '.');
siter.next(); let payload: AccessTokenPayload = json_from_str(
&String::from_utf8(
URL_SAFE_NO_PAD
.decode(siter.next().ok_or_else(|| {
MsalError::InvalidParse("Payload not present".to_string())
})?)
.map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(payload.amr.iter().any(|s| s == "ngcmfa"))
}
None => Err(MsalError::GeneralFailure(
"No access token available for UserToken".to_string(),
)),
}
}
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
struct UsernamePasswordAuthenticationPayload {
client_id: String,
request_nonce: String,
scope: String,
win_ver: Option<String>,
grant_type: String,
username: String,
password: String,
}
#[cfg(feature = "broker")]
impl UsernamePasswordAuthenticationPayload {
fn new(username: &str, password: &str, request_nonce: &str) -> Self {
let os_release = match OsRelease::new() {
Ok(os_release) => Some(format!(
"{} {}",
os_release.pretty_name, os_release.version_id
)),
Err(_) => None,
};
UsernamePasswordAuthenticationPayload {
client_id: BROKER_CLIENT_IDENT.to_string(),
request_nonce: request_nonce.to_string(),
scope: "openid aza ugs".to_string(),
win_ver: os_release,
grant_type: "password".to_string(),
username: username.to_string(),
password: password.to_string(),
}
}
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
struct RefreshTokenAuthenticationPayload {
client_id: String,
request_nonce: String,
scope: String,
win_ver: Option<String>,
grant_type: String,
refresh_token: String,
}
#[cfg(feature = "broker")]
impl RefreshTokenAuthenticationPayload {
fn new(refresh_token: &str, request_nonce: &str) -> Self {
let os_release = match OsRelease::new() {
Ok(os_release) => Some(format!(
"{} {}",
os_release.pretty_name, os_release.version_id
)),
Err(_) => None,
};
RefreshTokenAuthenticationPayload {
client_id: BROKER_APP_ID.to_string(),
request_nonce: request_nonce.to_string(),
scope: "openid aza ugs".to_string(),
win_ver: os_release,
grant_type: "refresh_token".to_string(),
refresh_token: refresh_token.to_string(),
}
}
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
struct HelloForBusinessAssertion {
iss: String,
aud: String,
iat: u64,
exp: u64,
scope: String,
request_nonce: String,
}
#[cfg(feature = "broker")]
impl HelloForBusinessAssertion {
fn new(username: &str, request_nonce: &str) -> Result<Self, MsalError> {
let iat: u64 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| MsalError::GeneralFailure(format!("Failed choosing iat: {}", e)))?
.as_secs();
Ok(HelloForBusinessAssertion {
iss: username.to_string(),
aud: "common".to_string(),
iat: iat - 300,
exp: iat + 300,
scope: "openid aza ugs".to_string(),
request_nonce: request_nonce.to_string(),
})
}
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
struct HelloForBusinessPayload {
client_id: String,
request_nonce: String,
scope: String,
win_ver: Option<String>,
grant_type: String,
username: String,
assertion: String,
}
#[cfg(feature = "broker")]
impl HelloForBusinessPayload {
fn new(username: &str, assertion: &str, request_nonce: &str) -> Self {
let os_release = match OsRelease::new() {
Ok(os_release) => Some(format!(
"{} {}",
os_release.pretty_name, os_release.version_id
)),
Err(_) => None,
};
HelloForBusinessPayload {
client_id: BROKER_APP_ID.to_string(),
request_nonce: request_nonce.to_string(),
scope: "openid aza ugs".to_string(),
win_ver: os_release,
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer".to_string(),
username: username.to_string(),
assertion: assertion.to_string(),
}
}
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default)]
struct ExchangePRTPayload {
win_ver: Option<String>,
scope: String,
resource: Option<String>,
request_nonce: String,
refresh_token: String,
iss: String,
grant_type: String,
client_id: String,
aud: String,
}
#[cfg(feature = "broker")]
impl ExchangePRTPayload {
fn new(
prt: &PrimaryRefreshToken,
nonce: &str,
resource: Option<String>,
request_prt: bool,
) -> Result<Self, MsalError> {
let mut scopes = "openid ugs".to_string();
if request_prt {
scopes = format!("{} aza", scopes);
}
let os_release = match OsRelease::new() {
Ok(os_release) => Some(format!(
"{} {}",
os_release.pretty_name, os_release.version_id
)),
Err(_) => None,
};
Ok(ExchangePRTPayload {
win_ver: os_release,
scope: scopes,
resource,
request_nonce: nonce.to_string(),
refresh_token: prt.refresh_token.clone(),
iss: "aad:brokerplugin".to_string(),
grant_type: "refresh_token".to_string(),
client_id: BROKER_CLIENT_IDENT.to_string(),
aud: "login.microsoftonline.com".to_string(),
})
}
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default)]
struct ExchangePRTForATPayload {
win_ver: Option<String>,
scope: String,
#[serde(skip_serializing_if = "Option::is_none")]
resource: Option<String>,
request_nonce: String,
refresh_token: String,
iss: String,
grant_type: String,
client_id: String,
redirect_uri: String,
aud: String,
}
#[cfg(feature = "broker")]
impl ExchangePRTForATPayload {
fn new(
prt: &PrimaryRefreshToken,
nonce: &str,
scopes: &str,
client_id: &str,
redirect_uri: &str,
resource: Option<&str>,
) -> Result<Self, MsalError> {
let os_release = match OsRelease::new() {
Ok(os_release) => Some(format!(
"{} {}",
os_release.pretty_name, os_release.version_id
)),
Err(_) => None,
};
Ok(ExchangePRTForATPayload {
win_ver: os_release,
scope: scopes.to_string(),
resource: resource.map(|r| r.to_string()),
request_nonce: nonce.to_string(),
refresh_token: prt.refresh_token.clone(),
iss: "aad:brokerplugin".to_string(),
grant_type: "refresh_token".to_string(),
client_id: client_id.to_string(),
redirect_uri: redirect_uri.to_string(),
aud: "login.microsoftonline.com".to_string(),
})
}
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
struct RefreshTokenCredentialPayload {
#[serde(skip_serializing_if = "Option::is_none")]
iat: Option<i64>,
refresh_token: String,
#[serde(skip_serializing_if = "Option::is_none")]
request_nonce: Option<String>,
ua_client_id: Option<String>,
ua_redirect_uri: Option<String>,
x_client_platform: Option<String>,
win_ver: Option<String>,
windows_api_version: Option<String>,
}
#[cfg(feature = "broker")]
impl RefreshTokenCredentialPayload {
fn new(prt: &PrimaryRefreshToken, nonce: Option<&str>) -> Result<Self, MsalError> {
let os_release = match OsRelease::new() {
Ok(os_release) => Some(format!(
"{} {}",
os_release.pretty_name, os_release.version_id
)),
Err(_) => None,
};
let (iat, request_nonce) = match nonce {
Some(n) => (None, Some(n.to_string())),
None => {
let iat: i64 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| MsalError::GeneralFailure(format!("Failed choosing iat: {}", e)))?
.as_secs()
.try_into()
.map_err(|e| {
MsalError::GeneralFailure(format!("Failed choosing iat: {}", e))
})?;
(Some(iat), None)
}
};
Ok(RefreshTokenCredentialPayload {
iat,
refresh_token: prt.refresh_token.clone(),
request_nonce,
ua_client_id: None,
ua_redirect_uri: None,
x_client_platform: None,
win_ver: os_release,
windows_api_version: Some("2.0.1".to_string()),
})
}
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
struct DeviceCredentialPayload {
grant_type: String,
iss: String,
request_nonce: String,
ua_client_id: Option<String>,
ua_redirect_uri: Option<String>,
x_client_platform: Option<String>,
win_ver: Option<String>,
windows_api_version: Option<String>,
}
#[cfg(feature = "broker")]
impl DeviceCredentialPayload {
fn new(nonce: &str) -> Result<Self, MsalError> {
let os_release = match OsRelease::new() {
Ok(os_release) => Some(format!(
"{} {}",
os_release.pretty_name, os_release.version_id
)),
Err(_) => None,
};
Ok(DeviceCredentialPayload {
grant_type: "device_auth".to_string(),
iss: "aad:brokerplugin".to_string(),
request_nonce: nonce.to_string(),
ua_client_id: None,
ua_redirect_uri: None,
x_client_platform: None,
win_ver: os_release,
windows_api_version: Some("2.0.1".to_string()),
})
}
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
struct P2PDeviceCertificatePayload {
client_id: String,
request_nonce: String,
win_ver: Option<String>,
grant_type: String,
cert_token_use: String,
csr_type: String,
csr: String,
netbios_name: String,
dns_names: Vec<String>,
}
#[cfg(feature = "broker")]
impl P2PDeviceCertificatePayload {
fn new(nonce: &str, csr: &str, device_name: &str, dns_names: &[String]) -> Self {
let os_release = match OsRelease::new() {
Ok(os_release) => Some(format!(
"{} {}",
os_release.pretty_name, os_release.version_id
)),
Err(_) => None,
};
P2PDeviceCertificatePayload {
client_id: BROKER_CLIENT_IDENT.to_string(),
request_nonce: nonce.to_string(),
win_ver: os_release,
grant_type: "device_auth".to_string(),
cert_token_use: "device_cert".to_string(),
csr_type: "http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10"
.to_string(),
csr: csr.to_string(),
netbios_name: device_name.to_string(),
dns_names: dns_names.to_vec(),
}
}
}
#[cfg(feature = "broker")]
#[derive(Serialize)]
struct P2PDeviceCertificateHeader<'a> {
alg: &'static str,
typ: &'static str,
x5c: &'a str,
}
#[cfg(feature = "broker")]
#[derive(Serialize)]
struct P2PUserCertificateHeader<'a> {
alg: &'static str,
typ: &'static str,
ctx: &'a str,
}
#[cfg(feature = "broker")]
#[derive(Serialize, Clone, Default, Zeroize, ZeroizeOnDrop)]
struct P2PUserCertificatePayload {
iss: String,
grant_type: String,
aud: String,
request_nonce: String,
scope: String,
refresh_token: String,
client_id: String,
cert_token_use: String,
csr_type: String,
csr: String,
}
#[cfg(feature = "broker")]
impl P2PUserCertificatePayload {
fn new(prt: &PrimaryRefreshToken, nonce: &str, csr: &str) -> Self {
P2PUserCertificatePayload {
iss: "aad:brokerplugin".to_string(),
grant_type: "refresh_token".to_string(),
aud: "login.microsoftonline.com".to_string(),
request_nonce: nonce.to_string(),
scope: "openid aza ugs".to_string(),
refresh_token: prt.refresh_token.clone(),
client_id: BROKER_CLIENT_IDENT.to_string(),
cert_token_use: "user_cert".to_string(),
csr_type: "http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10"
.to_string(),
csr: csr.to_string(),
}
}
fn redacted(&self) -> Result<Value, MsalError> {
let mut value = serde_json::to_value(self).map_err(|e| {
MsalError::InvalidJson(format!("Failed serializing P2P user payload: {}", e))
})?;
value["refresh_token"] = "**********".into();
Ok(value)
}
}
#[cfg(feature = "broker")]
#[derive(Deserialize)]
struct P2PCertificateResponse {
x5c: String,
x5c_ca: String,
}
#[cfg(feature = "broker")]
#[derive(Debug, Deserialize)]
struct Nonce {
#[serde(rename = "Nonce")]
nonce: String,
}
#[cfg(feature = "broker")]
trait Tgt {
fn derived_key(
&self,
tpm: &mut BoxedDynTpm,
transport_key: &MsOapxbcRsaKey,
storage_key: &StorageKey,
session_key: &SessionKey,
) -> Result<DerivedKey, MsalError>;
fn as_rep(&self) -> Result<AuthenticationReply, MsalError>;
}
#[cfg(feature = "broker")]
impl FromStr for StructuredTgt {
type Err = MsalError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
json_from_str(s).map_err(|e| MsalError::InvalidParse(format!("Failed parsing tgt: {}", e)))
}
}
#[cfg(feature = "broker")]
#[derive(Default, Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
pub struct StructuredTgt {
#[serde(rename = "clientKey")]
client_key: Option<String>,
#[serde(rename = "keyType")]
key_type: u32,
error: Option<String>,
#[serde(rename = "messageBuffer")]
message_buffer: Option<String>,
pub realm: Option<String>,
pub sn: Option<String>,
pub cn: Option<String>,
#[serde(rename = "sessionKeyType")]
pub session_key_type: u32,
#[serde(rename = "accountType")]
pub account_type: u32,
}
#[cfg(feature = "broker")]
impl Tgt for StructuredTgt {
fn derived_key(
&self,
tpm: &mut BoxedDynTpm,
transport_key: &MsOapxbcRsaKey,
storage_key: &StorageKey,
session_key: &SessionKey,
) -> Result<DerivedKey, MsalError> {
let client_key = match self.client_key.as_deref() {
Some(k) => {
if k.contains('.') {
let jwe = JweCompact::from_str(k)
.map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))?;
session_key
.decipher_tgt_client_key(tpm, transport_key, storage_key, &jwe)
.map_err(|e| {
MsalError::CryptoFail(format!(
"Failed to unwrap the TGT session key: {:?}",
e
))
})?
} else {
Zeroizing::new(STANDARD.decode(k).map_err(|e| {
MsalError::CryptoFail(format!(
"Failed to decode base64 client key: {:?}",
e
))
})?)
}
}
None => {
return Err(MsalError::CryptoFail(
"TGT client key is missing".to_string(),
))
}
};
match self.key_type {
18 => {
let k: [u8; 32] = client_key.as_slice().try_into().map_err(|_| {
MsalError::CryptoFail("Unexpected TGT session key length".to_string())
})?;
let dk = DerivedKey::Aes256CtsHmacSha196 {
k,
i: 0,
s: String::new(),
kvno: 1,
};
Ok(dk)
}
_ => Err(MsalError::CryptoFail(format!(
"Unexpected TGT session key type {}",
self.key_type
))),
}
}
fn as_rep(&self) -> Result<AuthenticationReply, MsalError> {
let buf = match self.message_buffer.as_deref() {
Some(buf) => STANDARD
.decode(buf)
.map_err(|e| MsalError::CryptoFail(format!("{:?}", e)))?,
None => {
return Err(MsalError::CryptoFail(
"TGT message buffer is missing".to_string(),
))
}
};
let reply = match KerberosReply::try_from(buf.as_slice()) {
Ok(r) => r,
Err(e) => {
return Err(MsalError::GeneralFailure(format!(
"Failed to decode the cloud kerberos reply: {:?}",
e
)));
}
};
match reply {
KerberosReply::AS(as_rep) => Ok(as_rep),
_ => Err(MsalError::GeneralFailure(
"Unexpected kerberos reply message".to_string(),
)),
}
}
}
#[derive(Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
struct RawTgt {
tgt_message_buffer: String,
tgt_client_key: String,
tgt_key_type: u32,
}
#[cfg(feature = "broker")]
impl Tgt for RawTgt {
fn derived_key(
&self,
tpm: &mut BoxedDynTpm,
transport_key: &MsOapxbcRsaKey,
storage_key: &StorageKey,
session_key: &SessionKey,
) -> Result<DerivedKey, MsalError> {
let jwe = JweCompact::from_str(&self.tgt_client_key)
.map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))?;
let long_term_krb_session_key = session_key
.decipher_tgt_client_key(tpm, transport_key, storage_key, &jwe)
.map_err(|e| {
MsalError::CryptoFail(format!("Failed to unwrap the TGT session key: {:?}", e))
})?;
let dk = match self.tgt_key_type {
18 => {
let k: [u8; 32] =
long_term_krb_session_key
.as_slice()
.try_into()
.map_err(|_| {
MsalError::CryptoFail("Unexpected TGT session key length".to_string())
})?;
DerivedKey::Aes256CtsHmacSha196 {
k,
i: 0,
s: String::new(),
kvno: 1,
}
}
_ => {
return Err(MsalError::CryptoFail(format!(
"Unexpected TGT session key type {}",
self.tgt_key_type
)));
}
};
Ok(dk)
}
fn as_rep(&self) -> Result<AuthenticationReply, MsalError> {
let buf = STANDARD
.decode(&self.tgt_message_buffer)
.map_err(|e| MsalError::CryptoFail(format!("{:?}", e)))?;
let reply = match KerberosReply::try_from(buf.as_slice()) {
Ok(r) => r,
Err(e) => {
return Err(MsalError::GeneralFailure(format!(
"Failed to decode the cloud kerberos reply: {:?}",
e
)));
}
};
match reply {
KerberosReply::AS(as_rep) => Ok(as_rep),
_ => Err(MsalError::GeneralFailure(
"Unexpected kerberos reply message".to_string(),
)),
}
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
enum OnPremTgt {
Raw(RawTgt),
Structured {
tgt_ad: StructuredTgt,
},
Absent,
}
impl Serialize for OnPremTgt {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
OnPremTgt::Raw(raw) => {
let mut m = serializer.serialize_map(Some(3))?;
m.serialize_entry("tgt_message_buffer", &raw.tgt_message_buffer)?;
m.serialize_entry("tgt_client_key", &raw.tgt_client_key)?;
m.serialize_entry("tgt_key_type", &raw.tgt_key_type)?;
m.end()
}
OnPremTgt::Structured { tgt_ad } => {
let mut m = serializer.serialize_map(Some(1))?;
m.serialize_entry("tgt_ad", tgt_ad)?;
m.end()
}
OnPremTgt::Absent => serializer.serialize_map(Some(0))?.end(),
}
}
}
impl<'de> Deserialize<'de> for OnPremTgt {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = OnPremTgt;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("on-prem TGT fields (raw or structured)")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut tgt_message_buffer = None;
let mut tgt_client_key = None;
let mut tgt_key_type = None;
let mut tgt_ad: Option<StructuredTgt> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"tgt_message_buffer" => tgt_message_buffer = Some(map.next_value()?),
"tgt_client_key" => tgt_client_key = Some(map.next_value()?),
"tgt_key_type" => tgt_key_type = Some(map.next_value()?),
"tgt_ad" => tgt_ad = Some(map.next_value()?),
_ => {
map.next_value::<IgnoredAny>()?;
}
}
}
match (tgt_ad, tgt_message_buffer, tgt_client_key, tgt_key_type) {
(Some(tgt_ad), None, None, None) => Ok(OnPremTgt::Structured { tgt_ad }),
(None, Some(buf), Some(key), Some(kt)) => Ok(OnPremTgt::Raw(RawTgt {
tgt_message_buffer: buf,
tgt_client_key: key,
tgt_key_type: kt,
})),
(None, None, None, None) => Ok(OnPremTgt::Absent),
_ => Err(de::Error::custom(
"on-prem TGT is a mix of raw and structured, or is incomplete",
)),
}
}
}
deserializer.deserialize_map(V)
}
}
#[cfg(feature = "broker")]
#[derive(Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
struct PrimaryRefreshToken {
token_type: String,
expires_in: String,
ext_expires_in: String,
expires_on: String,
refresh_token: String,
refresh_token_expires_in: u64,
session_key_jwe: Option<String>,
#[serde(deserialize_with = "decode_string_or_struct")]
#[zeroize(skip)]
id_token: IdToken,
#[serde(deserialize_with = "decode_string_or_struct", default)]
#[zeroize(skip)]
client_info: ClientInfo,
device_tenant_id: Option<String>,
#[serde(flatten)]
tgt_on_prem: OnPremTgt,
#[serde(deserialize_with = "decode_string_or_struct", default)]
tgt_cloud: StructuredTgt,
kerberos_top_level_names: Option<String>,
}
#[cfg(feature = "broker")]
impl PrimaryRefreshToken {
fn name(&self) -> String {
self.id_token.name.clone()
}
fn spn(&self) -> Result<String, MsalError> {
match &self.id_token.preferred_username {
Some(spn) => Ok(spn.to_string()),
None => Err(MsalError::GeneralFailure(
"No spn available for PRT".to_string(),
)),
}
}
fn uuid(&self) -> Result<Uuid, MsalError> {
Uuid::parse_str(&self.id_token.oid).map_err(|e| MsalError::InvalidParse(format!("{}", e)))
}
fn session_key(&self) -> Result<SessionKey, MsalError> {
match &self.session_key_jwe {
Some(session_key_jwe) => SessionKey::new(session_key_jwe),
None => Err(MsalError::CryptoFail("session_key_jwe missing".to_string())),
}
}
fn clone_session_key(&self, new_prt: &mut PrimaryRefreshToken) {
new_prt.session_key_jwe.clone_from(&self.session_key_jwe);
}
fn is_expired(&self) -> bool {
match self.expires_on.parse::<u64>() {
Ok(expiry_ts) => match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(now) => {
let now = now.as_secs();
now >= expiry_ts
}
Err(_) => true,
},
Err(e) => {
error!(?e, "Failed parsing PRT expires_on '{}'", self.expires_on);
true
}
}
}
}
#[cfg(feature = "broker")]
struct SessionKey {
session_key_jwe: JweCompact,
}
#[cfg(feature = "broker")]
impl SessionKey {
fn new(session_key_jwe: &str) -> Result<Self, MsalError> {
Ok(SessionKey {
session_key_jwe: JweCompact::from_str(session_key_jwe)
.map_err(|e| MsalError::InvalidParse(format!("Failed parsing jwe: {}", e)))?,
})
}
fn decipher_prt_v2(
&self,
tpm: &mut BoxedDynTpm,
transport_key: &MsOapxbcRsaKey,
storage_key: &StorageKey,
jwe: &JweCompact,
) -> Result<Jwe, MsalError> {
let maybe_transport_storage_key = tpm.rs256_yield_cek(transport_key);
let storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let session_key = MsOapxbcSessionKey::complete_tpm_rsa_oaep_key_agreement(
tpm,
storage_key,
transport_key,
&self.session_key_jwe,
)
.map_err(|e| MsalError::CryptoFail(format!("Unable to decipher session_key_jwe: {}", e)))?;
session_key
.decipher_prt_v2(tpm, storage_key, jwe)
.map_err(|e| MsalError::CryptoFail(format!("Failed to decipher Jwe: {}", e)))
}
#[allow(dead_code)]
fn decipher_tgt_client_key(
&self,
tpm: &mut BoxedDynTpm,
transport_key: &MsOapxbcRsaKey,
storage_key: &StorageKey,
jwe: &JweCompact,
) -> Result<Zeroizing<Vec<u8>>, MsalError> {
let maybe_transport_storage_key = tpm.rs256_yield_cek(transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let session_key = MsOapxbcSessionKey::complete_tpm_rsa_oaep_key_agreement(
tpm,
prt_storage_key,
transport_key,
&self.session_key_jwe,
)
.map_err(|e| MsalError::CryptoFail(format!("Unable to decipher session_key_jwe: {}", e)))?;
match session_key.decipher_prt_v2(tpm, prt_storage_key, jwe) {
Ok(decrypted) => Ok(Zeroizing::new(decrypted.payload().to_vec())),
Err(JwtError::OpenSSLError) => match session_key.decipher(tpm, prt_storage_key, jwe) {
Ok(decrypted) => Ok(Zeroizing::new(decrypted.payload().to_vec())),
Err(e) => Err(MsalError::CryptoFail(format!(
"Failed to decipher Jwe: {}",
e
))),
},
Err(e) => Err(MsalError::CryptoFail(format!(
"Failed to decipher Jwe: {}",
e
))),
}
}
fn sign<V: JwsSignable>(
&self,
tpm: &mut BoxedDynTpm,
transport_key: &MsOapxbcRsaKey,
storage_key: &StorageKey,
jws: &V,
) -> Result<V::Signed, MsalError> {
let maybe_transport_storage_key = tpm.rs256_yield_cek(transport_key);
let storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let session_key = MsOapxbcSessionKey::complete_tpm_rsa_oaep_key_agreement(
tpm,
storage_key,
transport_key,
&self.session_key_jwe,
)
.map_err(|e| MsalError::CryptoFail(format!("Unable to decipher session_key_jwe: {}", e)))?;
session_key
.sign(tpm, storage_key, jws)
.map_err(|e| MsalError::CryptoFail(format!("Failed signing jwk: {}", e)))
}
}
pub(crate) fn should_attempt_passwordless_security_key(
options: &[AuthOption],
has_fido_params: bool,
) -> bool {
let enabled = options.contains(&AuthOption::PasswordlessSecurityKey)
|| options.contains(&AuthOption::PasswordlessFido);
if !enabled {
debug!("passwordless_security_key: skipped (not enabled in config)");
return false;
}
if !has_fido_params {
debug!("passwordless_security_key: skipped (no FIDO params from GetCredentialType)");
return false;
}
debug!("passwordless_security_key: user has FIDO params, attempting");
true
}
pub(crate) fn should_attempt_passwordless_qr_bluetooth(
options: &[AuthOption],
has_fido_params: bool,
user_has_any_cross_device_fido: bool,
) -> bool {
if !options.contains(&AuthOption::PasswordlessQrBluetooth) {
debug!("passwordless_qr_bluetooth: skipped (not enabled in config)");
return false;
}
if !has_fido_params {
debug!("passwordless_qr_bluetooth: skipped (no FIDO params from GetCredentialType)");
return false;
}
if !user_has_any_cross_device_fido {
debug!("passwordless_qr_bluetooth: skipped (user has no cross-device passkey)");
return false;
}
debug!("passwordless_qr_bluetooth: user has cross-device passkey, attempting");
true
}
#[repr(C)]
#[derive(PartialEq)]
pub enum AuthOption {
Fido,
Passwordless,
PasswordlessFido,
PasswordlessSecurityKey,
PasswordlessQrBluetooth,
NoDAGFallback,
#[cfg(feature = "optional_mfa")]
ForceMFA,
#[cfg(feature = "optional_mfa")]
RemoteSession,
}
#[cfg(feature = "ipvers")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IpVersion {
V4,
V6,
}
pub(crate) struct ClientApplication {
pub(crate) client: Client,
pub(crate) client_id: String,
authority: RwLock<String>,
jar: Arc<CookieStoreMutex>,
#[cfg(feature = "ipvers")]
pub(crate) ip_version: Vec<IpVersion>,
#[cfg(feature = "set_timeout")]
pub(crate) timeout: Duration,
}
impl ClientApplication {
pub(crate) fn new(
client_id: &str,
authority: Option<&str>,
#[cfg(feature = "set_timeout")] timeout: Duration,
#[cfg(feature = "ipvers")] ip_version: &[IpVersion],
) -> Result<Self, MsalError> {
let jar = Arc::new(CookieStoreMutex::new(CookieStore::default()));
#[cfg(feature = "set_timeout")]
let (timeout, connect_timeout) = { (timeout, min(timeout / 2, Duration::from_secs(3))) };
#[cfg(not(feature = "set_timeout"))]
let (timeout, connect_timeout) = (Duration::from_secs(3), Duration::from_secs(1));
#[allow(unused_mut)]
let mut builder = reqwest::Client::builder()
.connect_timeout(connect_timeout)
.timeout(timeout)
.redirect(Policy::none())
.cookie_provider(jar.clone());
#[cfg(feature = "proxyable")]
{
if let Some(proxy_var) = std::env::var("HTTPS_PROXY")
.ok()
.or_else(|| std::env::var("ALL_PROXY").ok())
{
let proxy = Proxy::https(proxy_var)
.map_err(|e| MsalError::GeneralFailure(format!("{:?}", e)))?;
builder = builder.proxy(proxy).danger_accept_invalid_certs(true);
}
}
#[cfg(feature = "ipvers")]
{
let has_v4 = ip_version.contains(&IpVersion::V4);
let has_v6 = ip_version.contains(&IpVersion::V6);
if has_v4 && !has_v6 {
builder =
builder.local_address(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED))
} else if !has_v4 && has_v6 {
builder =
builder.local_address(std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED))
}
}
let client = builder
.build()
.map_err(|e| MsalError::RequestFailed(format!("{}", e)))?;
Ok(ClientApplication {
client,
client_id: client_id.to_string(),
authority: RwLock::new(match authority {
Some(authority) => authority.to_string(),
None => "https://login.microsoftonline.com/common".to_string(),
}),
jar,
#[cfg(feature = "ipvers")]
ip_version: ip_version.to_vec(),
#[cfg(feature = "set_timeout")]
timeout,
})
}
pub(crate) fn clear_cookies(&self) {
match self.jar.lock() {
Ok(mut jar) => jar.clear(),
Err(e) => error!("Failed to clear cookies: {:?}", e),
}
}
pub(crate) fn authority(&self) -> Result<String, MsalError> {
self.authority
.read()
.map_err(|e| {
MsalError::GeneralFailure(format!(
"Failed to lock authority URL for reading: {:?}",
e
))
})
.map(|authority| authority.clone())
}
pub(crate) fn set_authority(&self, new_authority: &str) -> Result<(), MsalError> {
self.authority
.write()
.map_err(|e| {
MsalError::GeneralFailure(format!(
"Failed to acquire authority write lock: {:?}",
e
))
})
.map(|mut auth| *auth = new_authority.to_string())
}
async fn acquire_token_by_username_password(
&self,
username: &str,
password: &str,
scopes: Vec<&str>,
) -> Result<UserToken, MsalError> {
let mut all_scopes = vec!["openid", "profile", "offline_access"];
all_scopes.extend(scopes);
let scopes_str = all_scopes.join(" ");
let params = [
("client_id", self.client_id.as_str()),
("scope", &scopes_str),
("username", username),
("password", password),
("grant_type", "password"),
("client_info", "1"),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let resp = self
.client
.post(format!("{}/oauth2/v2.0/token", self.authority()?))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::ACCEPT, "application/json")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let token: UserToken = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(token)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
async fn acquire_token_by_refresh_token(
&self,
refresh_token: &str,
scopes: Vec<&str>,
) -> Result<UserToken, MsalError> {
let mut all_scopes = vec!["openid", "profile", "offline_access"];
all_scopes.extend(scopes);
let scopes_str = all_scopes.join(" ");
let params = [
("client_id", self.client_id.as_str()),
("scope", &scopes_str),
("grant_type", "refresh_token"),
("refresh_token", refresh_token),
("client_info", "1"),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let resp = self
.client
.post(format!("{}/oauth2/v2.0/token", self.authority()?))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::ACCEPT, "application/json")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let token: UserToken = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(token)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
fn get_auth_redirect_uri(&self, client_id: Option<&str>, resource: Option<&str>) -> String {
let client_id = client_id.unwrap_or(self.client_id.as_str());
let resource = resource.unwrap_or("");
match client_id {
"1fec8e78-bce4-4aaf-ab1b-5451cc387264" => {
"https://login.microsoftonline.com/common/oauth2/nativeclient".to_string()
},
"9bc3ab49-b65d-410a-85ad-de819febfddc" => {
"https://oauth.spops.microsoft.com/".to_string()
},
"c44b4083-3bb0-49c1-b47d-974e53cbdf3c" => {
"https://portal.azure.com/signin/index/?feature.prefetchtokens=true&feature.showservicehealthalerts=true&feature.usemsallogin=true".to_string()
},
"0000000c-0000-0000-c000-000000000000" => {
"https://account.activedirectory.windowsazure.com/".to_string()
},
"19db86c3-b2b9-44cc-b339-36da233a3be2" => {
"https://mysignins.microsoft.com".to_string()
},
"29d9ed98-a469-4536-ade2-f981bc1d605e" => {
if resource.contains("enrollment.manage.microsoft.com") {
"ms-aadj-redir://auth/drs".to_string()
} else {
"msauth://Microsoft.AAD.BrokerPlugin".to_string()
}
},
"b743a22d-6705-4147-8670-d92fa515ee2b" => {
"companyportal://com.microsoft.CompanyPortal".to_string()
}
"d3590ed6-52b3-4102-aeff-aad2292ab01c" => {
"ms-appx-web://Microsoft.AAD.BrokerPlugin/d3590ed6-52b3-4102-aeff-aad2292ab01c".to_string()
},
"0c1307d4-29d6-4389-a11c-5cbe7f65d7fa" => {
"https://azureapp".to_string()
},
"33be1cef-03fb-444b-8fd3-08ca1b4d803f" => {
"https://admin.onedrive.com/".to_string()
},
"ab9b8c07-8f02-4f72-87fa-80105867a763" => {
"https://login.windows.net/common/oauth2/nativeclient".to_string()
},
"3d5cffa9-04da-4657-8cab-c7f074657cad" => {
"http://localhost/m365/commerce".to_string()
},
"4990cffe-04e8-4e8b-808a-1175604b879f" => {
"https://partner.microsoft.com/aad/authPostGateway".to_string()
},
"fb78d390-0c51-40cd-8e17-fdbfab77341b" |
"fdd7719f-d61e-4592-b501-793734eb8a0e" |
"a0c73c16-a7e3-4564-9a95-2bdf47383716" => {
"https://login.microsoftonline.com/common/oauth2/nativeclient".to_string()
},
"3b511579-5e00-46e1-a89e-a6f0870e2f5a" => {
"https://windows365.microsoft.com/signin-oidc".to_string()
},
"08e18876-6177-487e-b8b5-cf950c1e598c" => {
"https://*-admin.sharepoint.com/_forms/spfxsinglesignon.aspx".to_string()
},
"dd762716-544d-4aeb-a526-687b73838a22" => {
"ms-appx-web://microsoft.aad.brokerplugin/dd762716-544d-4aeb-a526-687b73838a22".to_string()
},
"4765445b-32c6-49b0-83e6-1d93765276ca" => {
"https://www.office.com/landingv2".to_string()
},
_ => {
"https://login.microsoftonline.com/common/oauth2/nativeclient".to_string()
},
}
}
}
#[derive(Clone)]
pub struct AuthInit {
auth_config: AuthConfig,
cred_type: CredType,
}
impl AuthInit {
#[deprecated(
since = "0.8.25",
note = "use AuthInit::try_exists() to distinguish nonexistent accounts from transient Entra ID lookup failures"
)]
pub fn exists(&self) -> bool {
match self.cred_type.account_exists() {
Ok(exists) => exists,
Err(err) => {
warn!("Unable to determine account existence from GetCredentialType: {err:?}");
false
}
}
}
pub fn try_exists(&self) -> Result<bool, MsalError> {
self.cred_type.account_exists()
}
pub fn is_personal_account(&self) -> bool {
self.cred_type.is_personal_account()
}
pub fn passwordless(&self) -> bool {
self.cred_type.credentials.has_access_pass.unwrap_or(false)
|| (self.cred_type.credentials.has_remote_ngc.unwrap_or(false)
&& self.cred_type.credentials.remote_ngc_params.is_some())
|| (self.cred_type.credentials.has_fido.unwrap_or(false)
&& self.cred_type.credentials.fido_params.is_some())
|| self.cred_type.is_personal_account()
}
}
#[cfg(feature = "changepassword")]
#[derive(Deserialize)]
struct SsprResponse {
#[serde(rename = "IsJobPending")]
is_job_pending: bool,
#[serde(rename = "Ctx")]
ctx: String,
#[serde(rename = "FlowToken")]
flow_token: String,
#[serde(rename = "CoupledDataCenter")]
coupled_data_center: String,
#[serde(rename = "CoupledScaleUnit")]
coupled_scale_unit: String,
#[serde(rename = "ErrorMessage")]
error_message: Option<String>,
}
pub struct PublicClientApplication {
app: ClientApplication,
}
impl PublicClientApplication {
pub fn new(
client_id: &str,
authority: Option<&str>,
#[cfg(feature = "set_timeout")] timeout: Duration,
#[cfg(feature = "ipvers")] ip_version: &[IpVersion],
) -> Result<Self, MsalError> {
Ok(PublicClientApplication {
app: ClientApplication::new(
client_id,
authority,
#[cfg(feature = "set_timeout")]
timeout,
#[cfg(feature = "ipvers")]
ip_version,
)?,
})
}
fn client(&self) -> &Client {
&self.app.client
}
fn client_id(&self) -> &str {
&self.app.client_id
}
pub fn clear_cookies(&self) {
self.app.clear_cookies()
}
fn authority(&self) -> Result<String, MsalError> {
self.app.authority()
}
pub fn set_authority(&self, new_authority: &str) -> Result<(), MsalError> {
self.app.set_authority(new_authority)
}
pub async fn acquire_token_by_username_password(
&self,
username: &str,
password: &str,
scopes: Vec<&str>,
) -> Result<UserToken, MsalError> {
self.app
.acquire_token_by_username_password(username, password, scopes)
.await
}
pub async fn acquire_token_by_refresh_token(
&self,
refresh_token: &str,
scopes: Vec<&str>,
) -> Result<UserToken, MsalError> {
self.app
.acquire_token_by_refresh_token(refresh_token, scopes)
.await
}
pub fn initiate_authorization_code_pkce_flow(
&self,
scopes: Vec<&str>,
redirect_uri: &str,
) -> Result<AuthorizationCodePkceFlow, MsalError> {
if redirect_uri.trim().is_empty() {
return Err(MsalError::ConfigError(
"redirect_uri must not be empty".to_string(),
));
}
let mut all_scopes = vec!["openid", "profile", "offline_access"];
all_scopes.extend(scopes);
let scope = all_scopes.join(" ");
let code_verifier = generate_base64url_random(32)?;
let code_challenge = pkce_code_challenge(&code_verifier);
let state = generate_base64url_random(32)?;
let url = Url::parse_with_params(
&format!("{}/oauth2/v2.0/authorize", self.authority()?),
[
("client_id", self.client_id()),
("response_type", "code"),
("redirect_uri", redirect_uri),
("response_mode", "query"),
("scope", &scope),
("state", &state),
("code_challenge", &code_challenge),
("code_challenge_method", "S256"),
],
)
.map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
Ok(AuthorizationCodePkceFlow {
auth_url: url.to_string(),
redirect_uri: redirect_uri.to_string(),
state,
scope,
code_verifier,
})
}
pub async fn acquire_token_by_authorization_code_pkce_flow(
&self,
flow: &AuthorizationCodePkceFlow,
redirect_url: &str,
) -> Result<UserToken, MsalError> {
let url =
Url::parse(redirect_url).map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
if let Some(error) = params.get("error") {
return Err(MsalError::AcquireTokenFailed(ErrorResponse {
error: error.clone(),
error_description: params.get("error_description").cloned().unwrap_or_default(),
suberror: params.get("suberror").cloned(),
error_codes: Vec::new(),
}));
}
let returned_state = params
.get("state")
.ok_or_else(|| MsalError::InvalidParse("state missing from redirect".to_string()))?;
if returned_state != &flow.state {
return Err(MsalError::InvalidParse(
"state returned by redirect does not match the PKCE flow".to_string(),
));
}
let code = params
.get("code")
.ok_or_else(|| MsalError::InvalidParse("code missing from redirect".to_string()))?;
let form = self.authorization_code_pkce_token_form(flow, code);
let resp = self
.client()
.post(format!("{}/oauth2/v2.0/token", self.authority()?))
.header(header::ACCEPT, "application/json")
.form(&form)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let token: UserToken = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(token)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
fn authorization_code_pkce_token_form<'a>(
&'a self,
flow: &'a AuthorizationCodePkceFlow,
code: &'a str,
) -> [(&'static str, &'a str); 7] {
[
("client_id", self.client_id()),
("grant_type", "authorization_code"),
("code", code),
("redirect_uri", flow.redirect_uri.as_str()),
("scope", flow.scope.as_str()),
("code_verifier", flow.code_verifier.as_str()),
("client_info", "1"),
]
}
pub async fn initiate_device_flow(
&self,
scopes: Vec<&str>,
) -> Result<DeviceAuthorizationResponse, MsalError> {
let mut all_scopes = vec!["openid", "profile", "offline_access"];
all_scopes.extend(scopes);
let scopes_str = all_scopes.join(" ");
let params = [("client_id", self.client_id()), ("scope", &scopes_str)];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let resp = self
.client()
.post(format!("{}/oauth2/v2.0/devicecode", self.authority()?))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::ACCEPT, "application/json")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let json_resp: DeviceAuthorizationResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(json_resp)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
async fn initiate_personal_device_flow(
&self,
scopes: Vec<&str>,
) -> Result<DeviceAuthorizationResponse, MsalError> {
let mut all_scopes = vec!["openid", "profile", "offline_access"];
all_scopes.extend(scopes);
let scopes_str = all_scopes.join(" ");
let params = [("client_id", self.client_id()), ("scope", &scopes_str)];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let resp = self
.client()
.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::ACCEPT, "application/json")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let json_resp: DeviceAuthorizationResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(json_resp)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
pub async fn acquire_token_by_device_flow(
&self,
flow: DeviceAuthorizationResponse,
) -> Result<UserToken, MsalError> {
let params = [
("client_id", self.client_id()),
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
("device_code", &flow.device_code),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let resp = self
.client()
.post(format!("{}/oauth2/v2.0/token", self.authority()?))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::ACCEPT, "application/json")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let token: UserToken = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(token)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
async fn acquire_token_by_personal_device_flow(
&self,
flow: DeviceAuthorizationResponse,
) -> Result<UserToken, MsalError> {
let params = [
("client_id", self.client_id()),
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
("device_code", &flow.device_code),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let resp = self
.client()
.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::ACCEPT, "application/json")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let token: UserToken = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(token)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
#[allow(unused_variables)]
fn parse_auth_config(
&self,
text: &str,
initial: bool,
password_change: bool,
) -> Result<AuthConfig, MsalError> {
let document = Html::parse_document(text);
for script in
document.select(&Selector::parse("script").map_err(|e| {
MsalError::GeneralFailure(format!("Failed parsing auth config: {}", e))
})?)
{
let text = script.inner_html();
if let Some(config_index) = text.find(r"$Config=") {
let sconfig = &text[config_index + 8..];
if let Some(end_index) = sconfig.rfind(r"//]]>") {
let config = &sconfig[..end_index - 2];
let auth_config: AuthConfig = json_from_str(config).map_err(|e| {
MsalError::InvalidJson(format!("Failed parsing auth config: {}", e))
})?;
if !initial {
let error_code = match auth_config.error_code {
Some(ref error_code) => {
Some(error_code.parse::<u32>().map_err(|e| {
MsalError::InvalidParse(format!(
"error_code {}: {:?}",
error_code, e
))
})?)
}
None => auth_config.error_code2,
};
if let Some(error_code) = error_code {
let description =
auth_config.err_txt.or(auth_config.service_exception_msg);
if let Some(err_txt) = description.clone() {
if !err_txt.is_empty() {
error!("{}", err_txt);
}
}
if error_code == 50203 {
if let Some(url_skip_mfa_registration) =
auth_config.url_skip_mfa_registration
{
return Err(MsalError::SkipMfaRegistration(
url_skip_mfa_registration,
auth_config.sft,
auth_config.canary,
));
}
}
return Err(MsalError::AADSTSError(AADSTSError::new(
error_code,
description,
)));
}
}
#[cfg(feature = "changepassword")]
if !password_change {
if let Some(ref pgid) = auth_config.pgid {
if pgid == "ConvergedChangePassword" {
return Err(MsalError::ChangePassword);
}
}
}
if let Some(ref pgid) = auth_config.pgid {
if pgid == "ConvergedConsent" {
return Err(MsalError::ConsentRequested(
"The client application requires additional consent to proceed."
.to_string(),
));
}
}
return Ok(auth_config);
}
}
}
Err(MsalError::GeneralFailure(
"Auth config was not found".to_string(),
))
}
#[cfg(feature = "changepassword")]
pub async fn handle_password_change(
&self,
username: &str,
password: &str,
new_password: &str,
) -> Result<(), MsalError> {
let request_id = Uuid::new_v4().to_string();
let auth_config = self
.request_auth_config_internal(vec![], &request_id, None, false)
.await?;
let ctx = auth_config
.sctx
.clone()
.ok_or(MsalError::GeneralFailure("ctx is missing".to_string()))?;
let flow_token = auth_config
.sft
.clone()
.ok_or(MsalError::GeneralFailure("sft is missing".to_string()))?;
let params = vec![
("login", username),
("passwd", password),
("ctx", &ctx),
("flowToken", &flow_token),
("canary", &auth_config.canary),
("client_id", self.client_id()),
("client-request-id", &request_id),
];
let auth_config = self
.handle_auth_config_req_internal(¶ms, &auth_config, &[], true)
.await?;
let payload = json!({
"Ctx": &auth_config
.sctx
.ok_or(MsalError::GeneralFailure("ctx is missing".to_string()))?,
"FlowToken": &auth_config
.sft
.ok_or(MsalError::GeneralFailure("sft is missing".to_string()))?,
"OldPassword": password,
"NewPassword": new_password,
});
let url_async_sspr_begin = match &auth_config.url_async_sspr_begin {
Some(url_async_sspr_begin) => url_async_sspr_begin.clone(),
None => {
return Err(MsalError::GeneralFailure(
"url_async_sspr_begin missing from auth config".to_string(),
))
}
};
let url = match url_async_sspr_begin.starts_with('/') {
true => {
let authority = self.authority()?.to_string();
let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
"Failed to splice auth config url".to_string(),
))?;
format!("{}/{}", &authority[..index], url_async_sspr_begin)
}
false => url_async_sspr_begin.clone(),
};
let resp = self
.client()
.post(&url)
.json(&payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let mut sspr_response: SsprResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
let url_async_sspr_poll = match &auth_config.url_async_sspr_poll {
Some(url_async_sspr_poll) => url_async_sspr_poll.clone(),
None => {
return Err(MsalError::GeneralFailure(
"url_async_sspr_poll missing from auth config".to_string(),
))
}
};
let url = match url_async_sspr_poll.starts_with('/') {
true => {
let authority = self.authority()?.to_string();
let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
"Failed to splice auth config url".to_string(),
))?;
format!("{}/{}", &authority[..index], url_async_sspr_poll)
}
false => url_async_sspr_poll.clone(),
};
while sspr_response.is_job_pending {
sleep(Duration::from_secs(1));
let poll_body = json!({
"Ctx": sspr_response.ctx,
"FlowToken": sspr_response.flow_token,
"CoupledDataCenter": sspr_response.coupled_data_center,
"CoupledScaleUnit": sspr_response.coupled_scale_unit,
});
sspr_response = self
.client()
.post(&url)
.json(&poll_body)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
if let Some(e) = sspr_response.error_message {
return Err(MsalError::GeneralFailure(format!(
"Failed changing password: {}",
e
)));
}
}
let url_post = match &auth_config.url_post {
Some(url_post) => url_post.clone(),
None => {
return Err(MsalError::GeneralFailure(
"urlPost missing from auth config".to_string(),
))
}
};
let url = match url_post.starts_with('/') {
true => {
let authority = self.authority()?.to_string();
let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
"Failed to splice auth config url".to_string(),
))?;
format!("{}/{}", &authority[..index], url_post)
}
false => url_post.clone(),
};
let final_body = json!({
"Ctx": sspr_response.ctx,
"FlowToken": sspr_response.flow_token,
"currentpasswd": password,
"confirmnewpasswd": new_password,
"canary": auth_config.canary,
});
let resp = self
.client()
.post(&url)
.json(&final_body)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
Ok(())
} else {
let text = resp.text().await.map_err(|e| {
MsalError::GeneralFailure(format!("Failed changing password: {}", e))
})?;
Err(MsalError::GeneralFailure(format!(
"Failed changing password: {}",
text
)))
}
} else {
let text = resp.text().await.map_err(|e| {
MsalError::GeneralFailure(format!("Failed changing password: {}", e))
})?;
Err(MsalError::GeneralFailure(format!(
"Failed changing password: {}",
text
)))
}
}
async fn handle_auth_config_fido_get(
&self,
username: &str,
auth_config: &AuthConfig,
request_id: &str,
) -> Result<AuthConfig, MsalError> {
let url_post = match &auth_config.url_post {
Some(url_post) => url_post.clone(),
None => {
return Err(MsalError::GeneralFailure(
"urlPost missing from auth config".to_string(),
))
}
};
let url_resume = match &auth_config.url_resume {
Some(url_resume) => url_resume.clone(),
None => {
return Err(MsalError::GeneralFailure(
"urlResume missing from auth config".to_string(),
))
}
};
let credentials_json = match auth_config.fido_allow_list.as_deref() {
Some([credentials_json, ..]) => credentials_json,
_ => {
return Err(MsalError::GeneralFailure(
"arrFidoAllowList missing from auth config".to_string(),
))
}
};
let sctx = match &auth_config.sctx {
Some(sctx) => sctx.clone(),
None => {
return Err(MsalError::GeneralFailure(
"sCtx missing from auth config".to_string(),
));
}
};
let sft = match &auth_config.sft {
Some(sft) => sft.clone(),
None => {
return Err(MsalError::GeneralFailure(
"sFt missing from auth config".to_string(),
));
}
};
let allowed_identities = match &auth_config.allowed_identities {
Some(allowed_identities) => format!("{}", allowed_identities),
None => {
return Err(MsalError::GeneralFailure(
"iAllowedIdentities missing from auth config".to_string(),
));
}
};
let params = [
("flow", "mfa"),
("allowedIdentities", &allowed_identities),
("canary", &sft),
("serverChallenge", &sft),
("postBackUrl", &url_post),
("postBackUrlAad", &url_post),
("cancelUrl", &url_resume),
("resumeUrl", &url_resume),
("correlationId", request_id),
("credentialsJson", credentials_json),
("ctx", &sctx),
("username", username),
("loginCanary", &auth_config.canary),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let url_fido_login = match &auth_config.url_fido_login {
Some(url_fido_login) => url_fido_login.clone(),
None => {
return Err(MsalError::GeneralFailure(
"urlFidoLogin missing from auth config".to_string(),
))
}
};
let mut resp = self
.client()
.post(url_fido_login)
.header(header::USER_AGENT, FIDO_USER_AGENT)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
let text;
(text, resp) = self.await_working(resp).await?;
if resp.status().is_success() {
self.parse_auth_config(&text, false, false)
} else {
Err(MsalError::GeneralFailure(resp.text().await.map_err(
|e| MsalError::GeneralFailure(format!("Request to FIDO login URL failed: {}", e)),
)?))
}
}
async fn handle_auth_config_req_internal(
&self,
req_params: &[(&str, &str)],
auth_config: &AuthConfig,
options: &[AuthOption],
password_change: bool,
) -> Result<AuthConfig, MsalError> {
let payload = req_params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let url_post = match &auth_config.url_post {
Some(url_post) => url_post.clone(),
None => {
return Err(MsalError::GeneralFailure(
"urlPost missing from auth config".to_string(),
))
}
};
let url = match url_post.starts_with('/') {
true => {
let authority = self.authority()?.to_string();
let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
"Failed to splice auth config url".to_string(),
))?;
format!("{}/{}", &authority[..index], url_post)
}
false => url_post.clone(),
};
let user_agent = if options.contains(&AuthOption::Fido) {
FIDO_USER_AGENT
} else {
env!("CARGO_PKG_NAME")
};
let resp = self
.client()
.post(url)
.header(header::USER_AGENT, user_agent)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
self.handle_auth_config_response_internal(resp, password_change)
.await
}
async fn handle_auth_config_response_internal(
&self,
mut resp: Response,
password_change: bool,
) -> Result<AuthConfig, MsalError> {
let text;
(text, resp) = self.await_working(resp).await?;
if resp.status().is_success() {
self.parse_auth_config(&text, false, password_change)
} else if resp.status().is_redirection() {
let redirect = resp
.headers()
.get(header::LOCATION)
.ok_or_else(|| MsalError::InvalidParse("Redirect location is missing".to_string()))?
.to_str()
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
let url =
Url::parse(redirect).map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
let params = url.query_pairs().collect::<Vec<_>>();
if let Some((_, error_description)) =
params.iter().find(|(k, _)| k == "error_description")
{
let error_code_regex = Regex::new(r"AADSTS(\d+):");
if let Ok(regex) = error_code_regex {
if let Some(captures) = regex.captures(error_description) {
if let Some(code_match) = captures.get(1) {
if let Ok(code) = code_match.as_str().parse::<u32>() {
return Err(MsalError::AADSTSError(AADSTSError::new(
code,
Some(error_description.to_string()),
)));
}
}
}
}
return Err(MsalError::GeneralFailure(format!(
"Unknown error in request to {}: {}",
url, error_description
)));
}
let (_, code) =
params
.iter()
.find(|(k, _)| k == "code")
.ok_or(MsalError::InvalidParse(
"Authorization code missing from redirect".to_string(),
))?;
debug!("Received auth code directly from login redirect");
Err(MsalError::AuthCodeReceived(code.to_string()))
} else {
Err(MsalError::GeneralFailure(resp.text().await.map_err(
|e| {
MsalError::GeneralFailure(format!(
"Request for handle_auth_config_req_internal() failed: {}",
e
))
},
)?))
}
}
async fn read_adfs_response_body(&self, mut resp: Response) -> Result<String, MsalError> {
let mut body = Vec::new();
while let Some(chunk) = resp
.chunk()
.await
.map_err(|e| MsalError::RequestFailed(format!("Failed reading AD FS response: {e}")))?
{
if body.len().saturating_add(chunk.len()) > MAX_ADFS_RESPONSE_BYTES {
return Err(MsalError::GeneralFailure(
"AD FS response exceeded the 2 MiB limit".to_string(),
));
}
body.extend_from_slice(&chunk);
}
String::from_utf8(body)
.map_err(|e| MsalError::InvalidParse(format!("AD FS response was not UTF-8: {e}")))
}
async fn request_adfs_form_internal(
&self,
federation_url: Url,
username: &str,
password: &str,
) -> Result<String, MsalError> {
let origin = federation_url.clone();
let mut url = federation_url;
let mut method = AdfsRequestMethod::PostCredentials;
for redirect_count in 0..=MAX_ADFS_REDIRECTS {
let response = if method == AdfsRequestMethod::PostCredentials {
self.client()
.post(url.clone())
.header(header::USER_AGENT, FIDO_USER_AGENT)
.form(&[
("UserName", username),
("Password", password),
("Kmsi", ""),
("AuthMethod", "FormsAuthentication"),
])
.send()
.await
} else {
self.client()
.get(url.clone())
.header(header::USER_AGENT, FIDO_USER_AGENT)
.send()
.await
}
.map_err(|e| MsalError::request_failed(&e))?;
if response.status().is_redirection() {
if redirect_count == MAX_ADFS_REDIRECTS {
return Err(MsalError::GeneralFailure(
"AD FS redirect limit exceeded".to_string(),
));
}
let location = response
.headers()
.get(header::LOCATION)
.ok_or_else(|| {
MsalError::InvalidParse(
"AD FS redirect did not include a location".to_string(),
)
})?
.to_str()
.map_err(|e| {
MsalError::InvalidParse(format!("Invalid AD FS redirect location: {e}"))
})?;
(url, method) = resolve_adfs_redirect(
&origin,
&url,
location,
response.status().as_u16(),
method,
)?;
continue;
}
if !response.status().is_success() {
return Err(MsalError::GeneralFailure(format!(
"AD FS authentication failed with HTTP status {}",
response.status()
)));
}
let content_type = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(|value| value.to_ascii_lowercase())
.unwrap_or_default();
if !content_type.starts_with("text/html")
&& !content_type.starts_with("application/xhtml+xml")
{
return Err(MsalError::GeneralFailure(
"AD FS returned an unsupported content type".to_string(),
));
}
return self.read_adfs_response_body(response).await;
}
Err(MsalError::GeneralFailure(
"AD FS redirect limit exceeded".to_string(),
))
}
async fn submit_ws_fed_form_internal(&self, form: &WsFedForm) -> Result<AuthConfig, MsalError> {
let login_srf = entra_login_srf_url(&self.authority()?)?;
let resp = self
.client()
.post(login_srf)
.header(header::USER_AGENT, FIDO_USER_AGENT)
.form(&[
("wa", form.wa.as_str()),
("wresult", form.wresult.as_str()),
("wctx", form.wctx.as_str()),
])
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
self.handle_auth_config_response_internal(resp, false).await
}
pub async fn check_user_exists(
&self,
username: &str,
resource: Option<&str>,
options: &[AuthOption],
) -> Result<AuthInit, MsalError> {
let request_id = Uuid::new_v4().to_string();
#[cfg(feature = "optional_mfa")]
let force_mfa = options.contains(&AuthOption::ForceMFA);
#[cfg(not(feature = "optional_mfa"))]
let force_mfa = true;
let auth_config = self
.request_auth_config_internal(vec![], &request_id, resource, force_mfa)
.await?;
let cred_type = self
.get_cred_type(username, &auth_config, &request_id, options)
.await?;
Ok(AuthInit {
auth_config,
cred_type,
})
}
pub async fn initiate_acquire_token_by_mfa_flow(
&self,
username: &str,
password: Option<&str>,
scopes: Vec<&str>,
resource: Option<&str>,
options: &[AuthOption],
auth_init: Option<AuthInit>,
#[cfg(feature = "mfa_method_selection")] mfa_method: Option<&str>,
) -> Result<MFAAuthContinue, MsalError> {
#[cfg(not(feature = "mfa_method_selection"))]
let mfa_method: Option<&str> = None;
#[cfg(feature = "optional_mfa")]
let force_mfa = options.contains(&AuthOption::ForceMFA);
#[cfg(not(feature = "optional_mfa"))]
let force_mfa = true;
macro_rules! dag_fallback {
() => {
if !options.contains(&AuthOption::NoDAGFallback) {
let mut dag_scopes: Vec<String> =
scopes.into_iter().map(|s| s.to_string()).collect();
let has_resource_scope =
dag_scopes.iter().any(|s| s.contains("://"));
if force_mfa && !has_resource_scope {
dag_scopes.push(format!("{}/.default", AZURE_PORTAL_APP_ID));
}
info!("MFA auth failed, falling back to Device Authorization Grant.");
let flow = self
.initiate_device_flow(dag_scopes.iter().map(|i| i.as_str()).collect())
.await?;
let mut flow: MFAAuthContinue = flow.into();
flow.resource = resource.map(|s| s.to_string());
return Ok(flow);
} else {
return Err(MsalError::MFADAGFallbackDisabled);
}
};
($err:expr) => {
if !options.contains(&AuthOption::NoDAGFallback) {
#[cfg(feature = "changepassword")]
if let MsalError::ChangePassword = $err {
return Err($err);
}
if let MsalError::AADSTSError(ref e) = $err {
#[cfg(feature = "optional_mfa")]
if options.contains(&AuthOption::RemoteSession)
&& [50072, 50203].contains(&e.code)
{
error!(
"Remote session with unenrolled MFA user denied. \
User must enroll in MFA before remote authentication is permitted."
);
return Err($err);
}
if ![16000, 50072, 50203].contains(&e.code) {
return Err($err);
}
}
let mut dag_scopes: Vec<String> =
scopes.into_iter().map(|s| s.to_string()).collect();
let has_resource_scope =
dag_scopes.iter().any(|s| s.contains("://"));
if force_mfa && !has_resource_scope {
dag_scopes.push(format!("{}/.default", AZURE_PORTAL_APP_ID));
}
info!("MFA auth failed, falling back to Device Authorization Grant.");
let flow = self
.initiate_device_flow(dag_scopes.iter().map(|i| i.as_str()).collect())
.await?;
let mut flow: MFAAuthContinue = flow.into();
flow.resource = resource.map(|s| s.to_string());
return Ok(flow);
} else {
return Err($err);
}
};
}
macro_rules! dag_personal_fallback {
() => {
let flow = self.initiate_personal_device_flow(scopes).await?;
let mut flow: MFAAuthContinue = flow.into();
flow.resource = resource.map(|s| s.to_string());
return Ok(flow);
};
}
const OIDC_SCOPES: &[&str] = &["openid", "profile", "email", "offline_access"];
let _derived_resource: Option<String>;
let resource: Option<&str> = match resource {
Some(r) => Some(r),
None => {
_derived_resource = scopes
.iter()
.find(|&&s| !OIDC_SCOPES.contains(&s))
.map(|&s| v2_scope_to_v1_resource(s));
_derived_resource.as_deref()
}
};
let request_id = Uuid::new_v4().to_string();
let (mut auth_config, cred_type) = if let Some(auth_init) = auth_init {
(auth_init.auth_config, auth_init.cred_type)
} else {
let auth_config = match self
.request_auth_config_internal(scopes.clone(), &request_id, resource, force_mfa)
.await
{
Ok(auth_config) => auth_config,
Err(e) => {
error!("{:?}", e);
dag_fallback!();
}
};
let cred_type = match self
.get_cred_type(username, &auth_config, &request_id, options)
.await
{
Ok(cred_type) => cred_type,
Err(e) => {
error!("{:?}", e);
dag_fallback!(e);
}
};
(auth_config, cred_type)
};
let sctx = match &auth_config.sctx {
Some(sctx) => sctx.clone(),
None => {
info!("sCtx is missing");
dag_fallback!();
}
};
let sft = match &auth_config.sft {
Some(sft) => sft.clone(),
None => {
info!("sFt is missing");
dag_fallback!();
}
};
macro_rules! passwordless_tap {
() => {
if cred_type.credentials.has_access_pass.unwrap_or(false) {
debug!("passwordless_tap: attempting (has_access_pass=true)");
let msg = "Enter Temporary Access Pass: ".to_string();
let url_post = match &auth_config.url_post {
Some(url_post) => url_post.clone(),
None => {
return Err(MsalError::GeneralFailure(
"urlBeginAuth is missing".to_string(),
))
}
};
return Ok(MFAAuthContinue {
msg,
entropy: None,
max_poll_attempts: None,
polling_interval: None,
session_id: auth_config.session_id,
flow_token: sft,
ctx: sctx,
canary: auth_config.canary,
url_end_auth: None,
url_post,
resource: resource.map(|s| s.to_string()),
dag: None,
fido_challenge: None,
fido_allow_list: None,
cross_domain_canary: None,
url_session_state: auth_config.url_session_state,
mfa_methods: vec!["AccessPass".to_string()].into(),
mfa_method_details: vec![MfaMethodInfo {
auth_method_id: "AccessPass".to_string(),
display: "AccessPass".to_string(),
is_default: true,
}],
selected_mfa_method_id: Some("AccessPass".to_string()),
auth_code: None,
#[allow(deprecated)]
fido_is_passkey: false,
skip_fido_for_mfa: false,
has_physical_security_key: false,
has_cross_device_passkey: false,
});
} else {
debug!("passwordless_tap: skipped (has_access_pass=false)");
}
};
}
let mut passwordless_remote_ngc_called = false;
let mut remote_ngc_push_attempted = false;
macro_rules! passwordless_remote_ngc {
() => {
if !passwordless_remote_ngc_called {
passwordless_remote_ngc_called = true;
if let Some(ref remote_ngc_params) = cred_type.credentials.remote_ngc_params {
debug!("passwordless_remote_ngc: attempting (remote_ngc_params present)");
remote_ngc_push_attempted = true;
if let Ok(remote_ngc_params) = self
.get_one_time_code(&auth_config, &remote_ngc_params, &request_id)
.await
{
let msg = format!(
"Open your Authenticator app, and enter the number '{}' to sign in.",
remote_ngc_params.entropy
);
let url_post = match &auth_config.url_post {
Some(url_post) => url_post.clone(),
None => {
return Err(MsalError::GeneralFailure(
"urlBeginAuth is missing".to_string(),
))
}
};
return Ok(MFAAuthContinue {
msg,
entropy: Some(remote_ngc_params.entropy),
max_poll_attempts: auth_config.max_poll_attempts,
polling_interval: Some(5000),
session_id: remote_ngc_params.session_identifier,
flow_token: sft,
ctx: sctx,
canary: auth_config.canary,
url_end_auth: None,
url_post,
resource: resource.map(|s| s.to_string()),
dag: None,
fido_challenge: None,
fido_allow_list: None,
cross_domain_canary: None,
url_session_state: auth_config.url_session_state,
mfa_methods: vec!["PhoneAppNotification".to_string()].into(),
mfa_method_details: vec![MfaMethodInfo {
auth_method_id: "PhoneAppNotification".to_string(),
display: "PhoneAppNotification".to_string(),
is_default: true
}],
selected_mfa_method_id: Some("PhoneAppNotification".to_string()),
auth_code: None,
#[allow(deprecated)]
fido_is_passkey: false,
skip_fido_for_mfa: false,
has_physical_security_key: false,
has_cross_device_passkey: false,
});
}
} else {
debug!("passwordless_remote_ngc: skipped (remote_ngc_params absent)");
}
} else {
debug!("passwordless_remote_ngc: skipped (already called)");
}
};
}
debug!("Credential type: pref_credential={}, has_password={}, has_fido={:?}, has_remote_ngc={:?}, has_access_pass={:?}, is_passkey_support_enabled={:?}",
cred_type.credentials.pref_credential,
cred_type.credentials.has_password,
cred_type.credentials.has_fido,
cred_type.credentials.has_remote_ngc,
cred_type.credentials.has_access_pass,
auth_config.is_passkey_support_enabled,
);
if let Some(ref fido_params) = cred_type.credentials.fido_params {
debug!(
"FIDO params: has_cross_device_capable_passkey={:?}, allow_list_count={}",
fido_params.has_cross_device_capable_passkey,
fido_params.fido_allow_list.len(),
);
}
let user_has_any_cross_device_fido = cred_type
.credentials
.fido_params
.as_ref()
.map(|fido_params| {
fido_params
.has_cross_device_capable_passkey
.unwrap_or(false)
})
.unwrap_or(false);
let attempt_security_key = should_attempt_passwordless_security_key(
options,
cred_type.credentials.fido_params.is_some(),
);
let attempt_qr_bluetooth = should_attempt_passwordless_qr_bluetooth(
options,
cred_type.credentials.fido_params.is_some(),
user_has_any_cross_device_fido,
);
macro_rules! passwordless_fido {
() => {
if attempt_security_key || attempt_qr_bluetooth {
let fido_params = cred_type.credentials.fido_params.as_ref().unwrap();
let url_post = match &auth_config.url_post {
Some(url_post) => url_post.clone(),
None => {
return Err(MsalError::GeneralFailure(
"urlBeginAuth is missing".to_string(),
))
}
};
auth_config.fido_allow_list = Some(fido_params.fido_allow_list.clone());
let fido_auth_config = self
.handle_auth_config_fido_get(username, &auth_config, &request_id)
.await?;
return Ok(MFAAuthContinue {
msg: "".to_string(),
entropy: None,
max_poll_attempts: auth_config.max_poll_attempts,
polling_interval: Some(5000),
session_id: fido_auth_config.session_id,
flow_token: sft,
ctx: sctx,
canary: auth_config.canary,
url_end_auth: auth_config.url_end_auth,
url_post,
resource: resource.map(|s| s.to_string()),
dag: None,
fido_challenge: fido_auth_config.fido_challenge,
fido_allow_list: Some(fido_params.fido_allow_list.clone()),
cross_domain_canary: fido_auth_config.cross_domain_canary,
url_session_state: auth_config.url_session_state,
mfa_methods: vec!["FidoKey".to_string()].into(),
mfa_method_details: vec![MfaMethodInfo {
auth_method_id: "FidoKey".to_string(),
display: "FidoKey".to_string(),
is_default: true,
}],
selected_mfa_method_id: Some("FidoKey".to_string()),
auth_code: None,
#[allow(deprecated)]
fido_is_passkey: false,
skip_fido_for_mfa: false,
has_physical_security_key: attempt_security_key,
has_cross_device_passkey: attempt_qr_bluetooth,
});
}
};
}
match cred_type.credentials.pref_credential {
13 => passwordless_tap!(),
2 | 7 => {
debug!(
"passwordless_fido triggered via pref_credential={}",
cred_type.credentials.pref_credential
);
passwordless_fido!();
passwordless_remote_ngc!();
}
_ => {}
}
debug!("passwordless_tap triggered via fallthrough");
passwordless_tap!();
debug!("passwordless_fido triggered via fallthrough");
passwordless_fido!();
debug!("passwordless_remote_ngc triggered via fallthrough");
passwordless_remote_ngc!();
if !cred_type.account_exists()? {
return Err(MsalError::GeneralFailure(
"An account with that name does not exist.".to_string(),
));
}
if cred_type.is_personal_account() {
dag_personal_fallback!();
}
let auth_response = if let Some(ref federation_redirect_url) =
cred_type.credentials.federation_redirect_url
{
let federation_url = match parse_adfs_federation_url(federation_redirect_url) {
Ok(Some(url)) => url,
Ok(None) => {
info!("Federated identity is not a supported AD FS HTTPS endpoint.");
dag_fallback!();
}
Err(e) => {
error!("Unable to parse federation endpoint: {e}");
dag_fallback!(e);
}
};
info!(
"Attempting AD FS forms authentication against {}",
federation_url.host_str().unwrap_or("unknown host")
);
let password = password.ok_or(MsalError::PasswordRequired)?;
match self
.request_adfs_form_internal(federation_url, username, password)
.await
.and_then(|text| parse_ws_fed_form(&text))
{
Ok(form) => self.submit_ws_fed_form_internal(&form).await,
Err(e) => Err(e),
}
} else {
if !cred_type.credentials.has_password {
info!("Password authentication is not supported.");
dag_fallback!();
}
let sctx = match &auth_config.sctx {
Some(sctx) => sctx.clone(),
None => {
info!("sCtx is missing");
dag_fallback!();
}
};
let sft = match &auth_config.sft {
Some(sft) => sft.clone(),
None => {
info!("sFt is missing");
dag_fallback!();
}
};
let params = vec![
("login", username),
("passwd", password.ok_or(MsalError::PasswordRequired)?),
("ctx", &sctx),
("flowToken", &sft),
("canary", &auth_config.canary),
("client_id", self.client_id()),
("client-request-id", &request_id),
];
self.handle_auth_config_req_internal(¶ms, &auth_config, options, false)
.await
};
match auth_response {
Ok(mut auth_config) => {
if let Some(msg) = auth_config.service_exception_msg {
error!("{}", msg);
dag_fallback!();
}
if let Some(ref pgid) = auth_config.pgid {
if pgid == "KmsiInterrupt" {
let sctx = match &auth_config.sctx {
Some(sctx) => sctx.clone(),
None => {
info!("sCtx is missing");
dag_fallback!();
}
};
let sft = match &auth_config.sft {
Some(sft) => sft.clone(),
None => {
info!("sFt is missing");
dag_fallback!();
}
};
let params = vec![
("LoginOptions", "1"),
("ctx", &sctx),
("flowToken", &sft),
("canary", &auth_config.canary),
("client-request-id", &request_id),
];
auth_config = match self
.handle_auth_config_req_internal(¶ms, &auth_config, options, false)
.await
{
Ok(auth_config) => auth_config,
Err(e) => {
error!("{:?}", e);
dag_fallback!(e);
}
};
}
}
if let Some(ref pgid) = auth_config.pgid {
if pgid == "ConvergedProofUpRedirect" {
if let Some(remaining_days) = auth_config.remaining_days_to_skip_mfa_reg {
info!("MFA must be set up in {} days", remaining_days);
let params = vec![
("LoginOptions", "1"),
("ctx", &sctx),
("flowToken", &sft),
("canary", &auth_config.canary),
("client-request-id", &request_id),
];
auth_config = match self
.handle_auth_config_req_internal(
¶ms,
&auth_config,
options,
false,
)
.await
{
Ok(auth_config) => auth_config,
Err(e) => {
error!("{:?}", e);
dag_fallback!(e);
}
};
} else {
info!("MFA method must be registered.");
dag_fallback!();
}
}
}
if let Some(ref pgid) = auth_config.pgid {
if pgid == "ConvergedChangePassword" {
info!("Password is expired!");
#[cfg(feature = "changepassword")]
return Err(MsalError::ChangePassword);
#[cfg(not(feature = "changepassword"))]
dag_fallback!();
}
}
if let Some(ref arr_user_proofs) = auth_config.arr_user_proofs {
debug!("MFA methods available: {:?}", arr_user_proofs);
let skip_fido_for_mfa = user_has_any_cross_device_fido
|| auth_config.is_passkey_support_enabled.unwrap_or(false);
let selected_auth_method = if let Some(requested_method) = mfa_method {
arr_user_proofs
.iter()
.find(|proof| {
proof.auth_method_id == requested_method
&& (!skip_fido_for_mfa || proof.auth_method_id != "FidoKey")
})
.ok_or_else(|| {
let available = arr_user_proofs
.iter()
.map(|p| p.auth_method_id.as_str())
.collect::<Vec<_>>();
MsalError::GeneralFailure(format!(
"Requested MFA method '{}' not available. Available methods: {}",
requested_method, available.join(", ")
))
})?
} else if let Some(method) = arr_user_proofs.iter().find(|proof| {
proof.is_default
&& (!skip_fido_for_mfa || proof.auth_method_id != "FidoKey")
}) {
method
} else if skip_fido_for_mfa {
match arr_user_proofs
.iter()
.find(|proof| proof.auth_method_id == "PhoneAppNotification")
.or_else(|| {
arr_user_proofs
.iter()
.find(|proof| proof.auth_method_id != "FidoKey")
}) {
Some(method) => method,
None => {
info!("No usable MFA methods found (FIDO was cross-device)");
dag_fallback!();
}
}
} else if arr_user_proofs.is_empty() {
info!("No MFA methods found");
dag_fallback!();
} else {
&arr_user_proofs[0]
};
let sctx = match &auth_config.sctx {
Some(sctx) => sctx.clone(),
None => {
info!("sCtx is missing");
dag_fallback!();
}
};
let sft = match &auth_config.sft {
Some(sft) => sft.clone(),
None => {
info!("sFt is missing");
dag_fallback!();
}
};
let url_begin_auth = match &auth_config.url_begin_auth {
Some(url_begin_auth) => url_begin_auth.clone(),
None => {
info!("urlBeginAuth is missing");
dag_fallback!();
}
};
let url_post = match &auth_config.url_post {
Some(url_post) => url_post.clone(),
None => {
info!("urlPost is missing");
dag_fallback!();
}
};
let (flow_token, ctx, msg) = if selected_auth_method.auth_method_id == "FidoKey"
{
let fido_auth_config = self
.handle_auth_config_fido_get(username, &auth_config, &request_id)
.await?;
auth_config.fido_challenge = fido_auth_config.fido_challenge.clone();
auth_config.session_id = fido_auth_config.session_id.clone();
auth_config.cross_domain_canary =
fido_auth_config.cross_domain_canary.clone();
(sft, sctx, "".to_string())
} else if selected_auth_method.auth_method_id == "AccessPass" {
(sft, sctx, "Enter Temporary Access Pass: ".to_string())
} else if remote_ngc_push_attempted
&& (selected_auth_method.auth_method_id == "PhoneAppNotification"
|| selected_auth_method.auth_method_id == "CompanionAppsNotification")
{
info!(
"Remote NGC push was attempted but failed. Avoiding duplicate push for {}. Falling back to DAG.",
selected_auth_method.auth_method_id
);
dag_fallback!();
} else {
let auth_response = match self
.mfa_begin_auth_internal(
&selected_auth_method.auth_method_id,
&url_begin_auth,
&sctx,
&sft,
&auth_config.canary,
)
.await
{
Ok(auth_response) => match auth_response.success {
true => auth_response,
false => {
return Err(MsalError::GeneralFailure(
"Begin Auth failed".to_string(),
))
}
},
Err(e) => {
error!("{:?}", e);
dag_fallback!(e);
}
};
let msg = match selected_auth_method.auth_method_id.as_str() {
"PhoneAppNotification" | "CompanionAppsNotification" => format!("Open your Authenticator app, and enter the number '{}' to sign in.", auth_response.entropy),
"PhoneAppOTP" =>
"Please type in the code displayed on your authenticator app from your device:".to_string(),
"ConsolidatedTelephony" | "OneWaySMS" =>
format!("We texted your phone {}. Please enter the code to sign in:", selected_auth_method.display),
"TwoWayVoiceMobile" =>
format!("We're calling your phone {}. Please answer it to continue.", selected_auth_method.display),
"TwoWayVoiceAlternateMobile" =>
format!("We're calling your phone {}. Please answer it to continue.", selected_auth_method.display),
"TwoWayVoiceOffice" =>
format!("We're calling your office phone {}. Please answer it to continue.", selected_auth_method.display),
method => {
info!("Unsupported MFA method {}", method);
dag_fallback!();
}
};
(auth_response.flow_token, auth_response.ctx, msg)
};
Ok(MFAAuthContinue {
msg,
entropy: None,
max_poll_attempts: auth_config.max_poll_attempts,
polling_interval: auth_config.polling_interval,
session_id: auth_config.session_id,
flow_token,
ctx,
canary: auth_config.canary,
url_end_auth: auth_config.url_end_auth,
url_post,
resource: resource.map(|s| s.to_string()),
dag: None,
fido_challenge: auth_config.fido_challenge.clone(),
fido_allow_list: auth_config.fido_allow_list.clone(),
cross_domain_canary: auth_config.cross_domain_canary.clone(),
url_session_state: None,
mfa_methods: arr_user_proofs
.iter()
.map(|proof| proof.auth_method_id.clone())
.collect(),
mfa_method_details: arr_user_proofs
.iter()
.map(|proof| proof.into())
.collect(),
selected_mfa_method_id: Some(selected_auth_method.auth_method_id.clone()),
auth_code: None,
#[allow(deprecated)]
fido_is_passkey: skip_fido_for_mfa,
skip_fido_for_mfa,
has_physical_security_key: false,
has_cross_device_passkey: false,
})
} else {
info!("No MFA methods found");
dag_fallback!();
}
}
Err(MsalError::AuthCodeReceived(auth_code)) => {
Ok(MFAAuthContinue {
msg: "".to_string(),
entropy: None,
max_poll_attempts: Some(1),
polling_interval: Some(0),
session_id: String::new(),
flow_token: String::new(),
ctx: String::new(),
canary: String::new(),
url_end_auth: None,
url_post: String::new(),
url_session_state: None,
resource: resource.map(|s| s.to_string()),
dag: None,
fido_challenge: None,
fido_allow_list: None,
cross_domain_canary: None,
mfa_methods: vec![],
mfa_method_details: vec![],
selected_mfa_method_id: None,
auth_code: Some(auth_code),
#[allow(deprecated)]
fido_is_passkey: false,
skip_fido_for_mfa: false,
has_physical_security_key: false,
has_cross_device_passkey: false,
})
}
Err(e) => {
error!("{:?}", e);
dag_fallback!(e);
}
}
}
async fn get_one_time_code(
&self,
auth_config: &AuthConfig,
remote_ngc_params: &RemoteNgcParams,
request_id: &str,
) -> Result<RemoteNgcParams, MsalError> {
let payload = json!({
"Channel": "Authenticator",
"FlowToken": &auth_config.sft,
"OldDeviceCode": remote_ngc_params.session_identifier,
"OriginalRequest": &auth_config.sctx,
});
let url = match &auth_config.url_get_one_time_code {
Some(url) => url.to_string(),
None => format!("{}/GetOneTimeCode", self.authority()?),
};
let resp = self
.client()
.post(url)
.header(header::CONTENT_TYPE, "application/json; charset=UTF-8")
.header("client-request-id", request_id)
.header("Canary", &auth_config.canary)
.json(&payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let json_resp: OneTimeCode = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
if let Some(error) = &json_resp.error {
return Err(MsalError::GeneralFailure(format!(
"Failed to parse response for /GetOneTimeCode: {}",
error.message.clone()
)));
}
json_resp.remote_ngc_params.ok_or(MsalError::GeneralFailure(
"remote_ngc_params missing".to_string(),
))
} else {
let text = resp
.text()
.await
.map_err(|e| MsalError::GeneralFailure(format!("Failed getting otc: {}", e)))?;
Err(MsalError::GeneralFailure(format!(
"Request to /GetOneTimeCode failed: {}",
text
)))
}
}
async fn get_cred_type(
&self,
username: &str,
auth_config: &AuthConfig,
request_id: &str,
options: &[AuthOption],
) -> Result<CredType, MsalError> {
let payload = json!({
"username": username,
"isOtherIdpSupported": true,
"checkPhones": true,
"isRemoteNGCSupported": options.contains(&AuthOption::Passwordless),
"isCookieBannerShown": false,
"isFidoSupported": options.contains(&AuthOption::Fido),
"isAccessPassSupported": true,
"originalRequest": &auth_config.sctx,
"flowToken": &auth_config.sft,
});
let url = match &auth_config.url_get_credential_type {
Some(url) => url.to_string(),
None => format!("{}/GetCredentialType", self.authority()?),
};
let resp = self
.client()
.post(url)
.header(header::CONTENT_TYPE, "application/json; charset=UTF-8")
.header("client-request-id", request_id)
.header(header::USER_AGENT, FIDO_USER_AGENT)
.json(&payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let json_resp: CredType = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
json_resp.log_throttle_status();
Ok(json_resp)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
async fn request_auth_config_internal(
&self,
scopes: Vec<&str>,
request_id: &str,
resource: Option<&str>,
mfa: bool,
) -> Result<AuthConfig, MsalError> {
let scope = format!("openid profile {}", scopes.join(" "));
let redirect_uri = self.app.get_auth_redirect_uri(None, resource);
let caller_app_redirect_uri = self
.app
.get_auth_redirect_uri(Some(LINUX_BROKER_APP_ID), resource);
debug!("request_auth_config_internal() client_id={} redirect_uri={} scope={} resource={} caller_app_redirect_uri={}",
self.client_id(),
redirect_uri.as_str(),
&scope,
resource.unwrap_or("https://graph.microsoft.com"),
caller_app_redirect_uri.as_str()
);
let mut params = vec![
("client_id", self.client_id()),
("response_type", "code"),
("redirect_uri", redirect_uri.as_str()),
("client-request-id", request_id),
("prompt", "login"),
("scope", &scope),
("response_mode", "query"),
("sso_reload", "True"),
(
"resource",
(resource.unwrap_or("https://graph.microsoft.com")),
),
("caller_app_client_id", LINUX_BROKER_APP_ID),
("caller_app_redirect_uri", caller_app_redirect_uri.as_str()),
];
if mfa {
params.push(("amr_values", "ngcmfa"));
}
let url = Url::parse_with_params(
&format!("{}/oauth2/authorize", self.authority()?),
¶ms.to_vec(),
)
.map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
let resp = self
.client()
.get(url)
.header(header::USER_AGENT, FIDO_USER_AGENT)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
self.parse_auth_config(
&resp.text().await.map_err(|e| {
MsalError::GeneralFailure(format!("Failed parsing auth config: {}", e))
})?,
true,
false,
)
} else {
Err(MsalError::GeneralFailure(
"Failed requesting auth config".to_string(),
))
}
}
async fn mfa_begin_auth_internal(
&self,
mfa_method: &str,
url_begin_auth: &str,
ctx: &str,
flow_token: &str,
canary: &str,
) -> Result<AuthResponse, MsalError> {
let payload = json!({
"AuthMethodId": mfa_method,
"ctx": ctx,
"flowToken": flow_token,
"Method": "BeginAuth",
});
let resp = self
.client()
.post(url_begin_auth)
.header(header::USER_AGENT, FIDO_USER_AGENT)
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.header("canary", canary)
.json(&payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let text = resp
.text()
.await
.map_err(|e| MsalError::GeneralFailure(format!("{}", e)))?;
let auth_response: AuthResponse =
json_from_str(&text).map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
if auth_response.success {
Ok(auth_response)
} else if let Some(error_code) = auth_response.error_code {
Err(MsalError::AADSTSError(AADSTSError::new(error_code, None)))
} else if let Some(msg) = auth_response.message {
Err(MsalError::GeneralFailure(format!(
"BeginAuth failed with message: {}",
msg
)))
} else {
Err(MsalError::GeneralFailure("BeginAuth failed".to_string()))
}
} else {
Err(MsalError::GeneralFailure(
"BeginAuth Authentication request failed".to_string(),
))
}
}
async fn await_working(&self, mut resp: Response) -> Result<(String, Response), MsalError> {
let mut body = Vec::new();
while let Some(chunk) = resp.chunk().await.map_err(|e| {
MsalError::GeneralFailure(format!("Error reading response chunks: {}", e))
})? {
body.extend(&chunk);
}
let mut text = String::from_utf8(body)
.map_err(|e| MsalError::GeneralFailure(format!("UTF-8 error: {}", e)))?;
for _ in 0..10 {
if !text.contains("Click Submit to continue")
&& !text.contains("Working...")
&& !text.contains("Click here to finish the authorization process")
&& !text.contains("<input type=\"submit\"")
{
return Ok((text, resp));
}
sleep(Duration::from_secs(1));
let (post_url, form_data) = tokio::task::spawn_blocking(
move || -> Result<(String, HashMap<String, String>), MsalError> {
let document = Html::parse_document(&text);
let form_selector = Selector::parse("form")
.map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))?;
let input_selector = Selector::parse("input")
.map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))?;
let form = document
.select(&form_selector)
.next()
.ok_or(MsalError::InvalidParse("Document parse failed".to_string()))?;
let post_url = form
.value()
.attr("action")
.ok_or(MsalError::InvalidParse("Form action not found".to_string()))?;
let mut form_data = HashMap::new();
for input in form.select(&input_selector) {
if let Some(name) = input.value().attr("name") {
if let Some(value) = input.value().attr("value") {
form_data.insert(name.to_string(), value.to_string());
}
}
}
Ok((post_url.to_string(), form_data))
},
)
.await
.map_err(|e| MsalError::InvalidParse(format!("{:?}", e)))??;
resp = self
.client()
.post(post_url)
.form(&form_data)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
let mut body = Vec::new();
while let Some(chunk) = resp.chunk().await.map_err(|e| {
MsalError::GeneralFailure(format!("Error reading response chunks: {}", e))
})? {
body.extend(&chunk);
}
text = String::from_utf8(body)
.map_err(|e| MsalError::GeneralFailure(format!("UTF-8 error: {}", e)))?;
}
Err(MsalError::GeneralFailure(
"Pending request timed out after 10 seconds".to_string(),
))
}
async fn auth_code_intercept_internal(
&self,
url: &str,
payload: String,
) -> Result<String, MsalError> {
let mut resp = self
.client()
.post(url)
.header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
let text;
(text, resp) = self.await_working(resp).await?;
if resp.status().is_redirection() {
let redirect = resp.headers()["location"]
.to_str()
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
let url =
Url::parse(redirect).map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
let params = url.query_pairs().collect::<Vec<_>>();
if let Some((_, error_description)) =
params.iter().find(|(k, _)| k == "error_description")
{
let error_code_regex = Regex::new(r"AADSTS(\d+):");
if let Ok(regex) = error_code_regex {
if let Some(captures) = regex.captures(error_description) {
if let Some(code_match) = captures.get(1) {
if let Ok(code) = code_match.as_str().parse::<u32>() {
return Err(MsalError::AADSTSError(AADSTSError::new(
code,
Some(error_description.to_string()),
)));
}
}
}
}
return Err(MsalError::GeneralFailure(format!(
"Unknown error in request to {}: {}",
url, error_description
)));
}
let (_, code) =
params
.iter()
.find(|(k, _)| k == "code")
.ok_or(MsalError::InvalidParse(
"Authorization code missing from redirect".to_string(),
))?;
Ok(code.to_string())
} else if resp.status().is_success() {
match self.parse_auth_config(&text, false, false) {
#[cfg(feature = "changepassword")]
Err(MsalError::ChangePassword) => Err(MsalError::ChangePassword),
Err(MsalError::AADSTSError(e)) => Err(MsalError::AADSTSError(e)),
Err(MsalError::SkipMfaRegistration(url_skip_mfa_registration, sft, canary)) => Err(
MsalError::SkipMfaRegistration(url_skip_mfa_registration, sft, canary),
),
Err(error) => Err(MsalError::GeneralFailure(format!(
"MsalError in auth_code_intercept_internal(), {}: {}",
error, text
))),
Ok(value) => Ok(format!(
"auth_code_intercept_internal() succeeded without redirect, pgid={:?}",
value.pgid
)),
}
} else {
Err(MsalError::GeneralFailure(
"ProcessAuth Authorization request failed".to_string(),
))
}
}
async fn request_authorization_passwordless_internal(
&self,
username: &str,
flow: &MFAAuthContinue,
) -> Result<String, MsalError> {
let entropy = format!(
"{}",
flow.entropy
.ok_or(MsalError::GeneralFailure("Missing entropy".to_string()))?
);
let params = [
("code", &flow.session_id),
("psRNGCSLK", &flow.session_id),
("login", &username.to_string()),
("loginfmt", &username.to_string()),
("psRNGCEntropy", &entropy),
("flowToken", &flow.flow_token),
("canary", &flow.canary),
("ctx", &flow.ctx),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let url = match &flow.url_post.starts_with('/') {
true => {
let authority = self.authority()?.to_string();
let index = authority.rfind('/').ok_or(MsalError::GeneralFailure(
"Failed to splice auth config url".to_string(),
))?;
format!("{}/{}", &authority[..index], flow.url_post)
}
false => flow.url_post.clone(),
};
let mut resp = self
.client()
.post(url)
.header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
let _text;
(_text, resp) = self.await_working(resp).await?;
if resp.status().is_redirection() {
let redirect = resp.headers()["location"]
.to_str()
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
let url =
Url::parse(redirect).map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
let params = url.query_pairs().collect::<Vec<_>>();
if let Some((_, error_description)) =
params.iter().find(|(k, _)| k == "error_description")
{
return Err(MsalError::GeneralFailure(format!("Error found in redirect URL parameters for request_authorization_passwordless_internal() redirect: {}", error_description)));
}
let (_, code) =
params
.iter()
.find(|(k, _)| k == "code")
.ok_or(MsalError::InvalidParse(
"Authorization code missing from redirect".to_string(),
))?;
Ok(code.to_string())
} else {
Err(MsalError::GeneralFailure(
"ProcessAuth Authorization request failed".to_string(),
))
}
}
async fn request_authorization_internal(
&self,
username: &str,
flow: &MFAAuthContinue,
selected_mfa_method: &MfaMethodInfo,
) -> Result<String, MsalError> {
let mfa_method = match selected_mfa_method.auth_method_id.as_str() {
"ConsolidatedTelephony" => "OneWaySMS".to_string(),
other => other.to_string(),
};
let params = [
("request", &flow.ctx),
("mfaAuthMethod", &mfa_method),
("login", &username.to_string()),
("flowToken", &flow.flow_token),
("canary", &flow.canary),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
match self
.auth_code_intercept_internal(&flow.url_post, payload)
.await
{
Ok(code) => Ok(code),
Err(MsalError::SkipMfaRegistration(url_skip_mfa_registration, sft, canary)) => {
let params = [
(
"flowtoken",
&sft.ok_or(MsalError::GeneralFailure("Missing flow token".to_string()))?,
),
("ctx", &flow.ctx),
("canary", &canary),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
self.auth_code_intercept_internal(&url_skip_mfa_registration, payload)
.await
}
Err(e) => Err(e),
}
}
async fn exchange_authorization_code_for_access_token_internal(
&self,
authorization_code: String,
resource: Option<&str>,
custom_redirect_uri: Option<&str>,
) -> Result<UserToken, MsalError> {
let redirect_uri = if let Some(custom_redirect_uri) = custom_redirect_uri {
custom_redirect_uri.to_string()
} else {
self.app.get_auth_redirect_uri(None, resource)
};
let params = [
("client_id", self.client_id()),
("grant_type", "authorization_code"),
("code", &authorization_code),
("redirect_uri", &redirect_uri),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>()
.join("&");
let resp = self
.client()
.post(format!("{}/oauth2/token", self.authority()?))
.header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let token: UserToken = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("Failed to parse UserToken: {}", e)))?;
Ok(token)
} else {
let text = resp.text().await.map_err(|e| {
MsalError::RequestFailed(format!("Failed to read response text: {}", e))
})?;
let json_resp: ErrorResponse = json_from_str(&text).map_err(|e| {
MsalError::InvalidJson(format!(
"Failed to parse ErrorResponse: {}. Raw response: {}",
e, text
))
})?;
error!(
"exchange_authorization_code_for_access_token_internal: {}",
json_resp.error_description
);
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
async fn exchange_accesspass_for_auth_code_internal(
&self,
username: &str,
accesspass: &str,
flow: &mut MFAAuthContinue,
) -> Result<String, MsalError> {
let mut params = vec![
("login", username),
("loginfmt", username),
("accesspass", accesspass),
("canary", &flow.canary),
("hpgrequestid", &flow.session_id),
("flowToken", &flow.flow_token),
];
if flow.url_post.contains("ProcessAuth") {
params.push(("request", &flow.ctx));
} else {
params.push(("ctx", &flow.ctx));
}
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
self.auth_code_intercept_internal(&flow.url_post, payload)
.await
}
async fn exchange_fido_assertion_for_auth_code_internal(
&self,
assertion: &str,
flow: &mut MFAAuthContinue,
) -> Result<String, MsalError> {
let cross_domain_canary = flow.cross_domain_canary.clone().ok_or(MsalError::Missing(
"sCrossDomainCanary missing from response".to_string(),
))?;
let params = [
("type", "23"),
("ps", "23"),
("assertion", assertion),
("lmcCanary", &cross_domain_canary),
("hpgrequestid", &flow.session_id),
("ctx", &flow.ctx),
("canary", &flow.canary),
("flowToken", &flow.flow_token),
];
let payload = serde_urlencoded::to_string(params).map_err(|e| {
MsalError::GeneralFailure(format!("Failed to encode FIDO assertion payload: {}", e))
})?;
let mut resp = self
.client()
.post(&flow.url_post)
.header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
let text;
(text, resp) = self.await_working(resp).await?;
debug!(
"exchange_fido_assertion_for_auth_code_internal: status={}, body length={}",
resp.status(),
text.len()
);
if resp.status().is_redirection() {
if let Some(location) = resp.headers().get("location") {
if let Ok(redirect) = location.to_str() {
if let Ok(url) = Url::parse(redirect) {
let params = url.query_pairs().collect::<Vec<_>>();
if let Some((_, error_description)) =
params.iter().find(|(k, _)| k == "error_description")
{
return Err(MsalError::GeneralFailure(format!(
"Error in FIDO redirect: {}",
error_description
)));
}
if let Some((_, code)) = params.iter().find(|(k, _)| k == "code") {
return Ok(code.to_string());
}
}
}
}
let document = Html::parse_document(&text);
let selector = Selector::parse("a[href]").map_err(|_| {
MsalError::InvalidParse("Failed parsing auth code response".to_string())
})?;
if let Some(element) = document.select(&selector).next() {
if let Some(href_encoded) = element.value().attr("href") {
let href = percent_decode_str(href_encoded)
.decode_utf8()
.map_err(|e| {
MsalError::URLFormatFailed(format!("Failed decoding url: {:?}", e))
})?;
if let Ok(url) = Url::parse(&href) {
return url
.query_pairs()
.find_map(|(key, value)| {
if key == "code" {
Some(value.into_owned())
} else {
None
}
})
.ok_or(MsalError::GeneralFailure(format!(
"Authorization code not found in FIDO redirect. Body: {}",
text
)));
}
}
}
Err(MsalError::GeneralFailure(format!(
"Authorization code not found in FIDO redirect. Body: {}",
text
)))
} else if resp.status().is_success() {
let re = Regex::new(r#"document\.location\.replace\("([^"]+)"\)"#)
.map_err(|e| MsalError::InvalidRegex(format!("{}", e)))?;
if let Some(m) = re.captures(&text) {
if let Some(redirect) = m.get(1) {
let redirect_decoded = Url::parse(&redirect.as_str().replace(r#"\u0026"#, "&"))
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
for (k, v) in redirect_decoded.query_pairs().collect::<Vec<_>>() {
if k == "code" {
return Ok(v.to_string());
}
if k == "error_description" {
return Err(MsalError::GeneralFailure(v.to_string()));
}
}
}
}
let document = Html::parse_document(&text);
let selector = Selector::parse("a[href]").map_err(|_| {
MsalError::InvalidParse(format!("Failed parsing error response: {}", text))
})?;
if let Some(element) = document.select(&selector).next() {
if let Some(href_encoded) = element.value().attr("href") {
let href = percent_decode_str(href_encoded)
.decode_utf8()
.map_err(|e| {
MsalError::URLFormatFailed(format!("Failed decoding url: {:?}", e))
})?;
if let Ok(url) = Url::parse(&href) {
for (key, value) in url.query_pairs() {
if key == "code" {
return Ok(value.to_string());
}
if key == "error_description" {
return Err(MsalError::GeneralFailure(format!(
"error_description in FIDO response: {}",
value
)));
}
}
}
}
}
match self.parse_auth_config(&text, false, false) {
#[cfg(feature = "changepassword")]
Err(MsalError::ChangePassword) => return Err(MsalError::ChangePassword),
Err(MsalError::AADSTSError(e)) => return Err(MsalError::AADSTSError(e)),
Err(MsalError::ConsentRequested(e)) => return Err(MsalError::ConsentRequested(e)),
Ok(auth_config) if auth_config.pgid.as_deref() == Some("ConvergedTFA") => {
return Err(MsalError::MFARequired);
}
_ => {}
}
Err(MsalError::GeneralFailure(format!(
"Authorization code not found in FIDO response. Body: {}",
text
)))
} else {
Err(MsalError::GeneralFailure(format!(
"FIDO assertion request failed with status {}. Body: {}",
resp.status(),
text
)))
}
}
pub async fn acquire_token_by_mfa_flow(
&self,
username: &str,
auth_data: Option<&str>,
poll_attempt: Option<u32>,
flow: &mut MFAAuthContinue,
) -> Result<UserToken, MsalError> {
if let Some(auth_code) = flow.auth_code.take() {
return self
.exchange_authorization_code_for_access_token_internal(
auth_code,
flow.resource.as_deref(),
None,
)
.await;
}
if let Some(dag_flow) = &flow.dag {
if dag_flow.verification_uri.contains("www.microsoft.com/link") {
return match self
.acquire_token_by_personal_device_flow(dag_flow.clone())
.await
{
Ok(token) => {
if token.spn()?.to_lowercase() != username.to_lowercase() {
return Err(MsalError::GeneralFailure(
"The authenticating user did not match".to_string(),
));
}
if let Some(resource) = &flow.resource {
if !resource.contains("enrollment.manage.microsoft.com") {
let scope = format!("{}/.default", resource);
return self
.acquire_token_by_refresh_token(
&token.refresh_token,
vec![&scope],
)
.await;
}
}
Ok(token)
}
Err(MsalError::AcquireTokenFailed(ref resp)) => {
if resp.error_codes.contains(&AUTH_PENDING) {
info!("Polling for acquire_token_by_personal_device_flow");
return Err(MsalError::MFAPollContinue);
}
error!(
"acquire_token_by_mfa_flow_internal: {}",
resp.error_description
);
Err(MsalError::AcquireTokenFailed(resp.clone()))
}
Err(e) => Err(e),
};
} else {
return match self.acquire_token_by_device_flow(dag_flow.clone()).await {
Ok(token) => {
if token.spn()?.to_lowercase() != username.to_lowercase() {
return Err(MsalError::GeneralFailure(
"The authenticating user did not match".to_string(),
));
}
let token = if let Some(resource) = &flow.resource {
let scope = format!("{}/.default", resource);
self.acquire_token_by_refresh_token(&token.refresh_token, vec![&scope])
.await?
} else {
token
};
Ok(token)
}
Err(MsalError::AcquireTokenFailed(ref resp)) => {
if resp.error_codes.contains(&AUTH_PENDING) {
info!("Polling for acquire_token_by_device_flow");
return Err(MsalError::MFAPollContinue);
}
error!(
"acquire_token_by_mfa_flow_internal: {}",
resp.error_description
);
Err(MsalError::AcquireTokenFailed(resp.clone()))
}
Err(e) => Err(e),
};
}
}
let mfa_method = flow.selected_mfa_method_id.as_deref();
if let Some(method) = mfa_method {
if !flow.has_mfa_method(method) {
return Err(MsalError::GeneralFailure(format!(
"Stored MFA method '{}' is not available. Available methods: {:?}",
method,
flow.get_available_mfa_methods().join(", ")
)));
}
}
let selected_mfa_method = match mfa_method {
Some(method) => flow.get_mfa_method_by_id(method),
None => flow.get_default_mfa_method_details(),
};
let selected_mfa_method = match selected_mfa_method {
Some(value) => value,
None => {
let method_desc = mfa_method.unwrap_or("default");
return Err(MsalError::GeneralFailure(format!(
"Unable to determine MFA method details - selected method was: {}",
method_desc
)));
}
};
match auth_data {
Some(auth_data) => {
if selected_mfa_method.auth_method_id == "FidoKey" {
let auth_code = self
.exchange_fido_assertion_for_auth_code_internal(auth_data, flow)
.await?;
self.exchange_authorization_code_for_access_token_internal(
auth_code,
flow.resource.as_deref(),
None,
)
.await
} else if selected_mfa_method.auth_method_id == "AccessPass" {
let auth_code = self
.exchange_accesspass_for_auth_code_internal(username, auth_data, flow)
.await?;
self.exchange_authorization_code_for_access_token_internal(
auth_code,
flow.resource.as_deref(),
None,
)
.await
} else {
let payload = json!({
"AdditionalAuthData": auth_data.trim(),
"AuthMethodId": &selected_mfa_method.auth_method_id,
"SessionId": &flow.session_id,
"FlowToken": &flow.flow_token,
"Ctx": &flow.ctx,
"Method": "EndAuth",
});
let url_end_auth = match &flow.url_end_auth {
Some(url_end_auth) => url_end_auth,
None => {
return Err(MsalError::GeneralFailure(
"urlEndAuth is missing".to_string(),
))
}
};
let resp = self
.client()
.post(url_end_auth)
.header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.header("canary", &flow.canary)
.json(&payload)
.send()
.await
.map_err(|e| match MsalError::request_failed(&e) {
MsalError::RequestFailed(msg) => MsalError::RequestFailed(format!(
"Request to {} failed: {}",
url_end_auth, msg
)),
other => other,
})?;
if resp.status().is_success() {
let text = resp.text().await.map_err(|e| {
MsalError::GeneralFailure(format!("Response decoding failed: {}", e))
})?;
if let Ok(auth_config) = self.parse_auth_config(&text, false, false) {
if let Some(service_exception_msg) = auth_config.service_exception_msg {
return Err(MsalError::GeneralFailure(
format!("Service exception during acquire_token_by_mfa_flow_internal(): {}", service_exception_msg),
));
}
}
let auth_response: AuthResponse = json_from_str(&text)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
if auth_response.success {
flow.ctx = auth_response.ctx;
flow.flow_token = auth_response.flow_token;
let auth_code = self
.request_authorization_internal(
username,
flow,
&selected_mfa_method,
)
.await?;
self.exchange_authorization_code_for_access_token_internal(
auth_code,
flow.resource.as_deref(),
None,
)
.await
} else if let Some(msg) = auth_response.message {
Err(MsalError::MFAInvalidCode(msg))
} else {
Err(MsalError::GeneralFailure("EndAuth failed".to_string()))
}
} else {
Err(MsalError::GeneralFailure(
"EndAuth Authentication request failed".to_string(),
))
}
}
}
None => {
let resp = if let Some(url_end_auth) = &flow.url_end_auth {
let url = Url::parse_with_params(
url_end_auth,
[
("authMethodId", &selected_mfa_method.auth_method_id),
(
"pollCount",
&format!(
"{}",
poll_attempt.ok_or(MsalError::GeneralFailure(
"Poll attempt required".to_string()
))?
),
),
],
)
.map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
self.client()
.get(url)
.header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
.header("x-ms-sessionId", &flow.session_id)
.header("x-ms-flowToken", &flow.flow_token)
.header("x-ms-ctx", &flow.ctx)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?
} else if let Some(url_session_state) = &flow.url_session_state {
let url =
Url::parse_with_params(url_session_state, [("code", &flow.session_id)])
.map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
let payload = json!({
"DeviceCode": &flow.session_id,
});
self.client()
.post(url)
.header(header::USER_AGENT, env!("CARGO_PKG_NAME"))
.header(header::CONTENT_TYPE, "application/json")
.header("canary", &flow.canary)
.json(&payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?
} else {
return Err(MsalError::GeneralFailure("Request invalid".to_string()));
};
if resp.status().is_success() {
let text = resp.text().await.map_err(|e| {
MsalError::GeneralFailure(format!("Response decoding failed: {}", e))
})?;
if flow.url_end_auth.is_some() {
if let Ok(auth_config) = self.parse_auth_config(&text, false, false) {
if let Some(service_exception_msg) = auth_config.service_exception_msg {
return Err(MsalError::GeneralFailure(format!(
"Service exception: {}",
service_exception_msg
)));
}
}
let auth_response: AuthResponse = json_from_str(&text)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
if auth_response.success {
flow.ctx = auth_response.ctx;
flow.flow_token = auth_response.flow_token;
let auth_code = self
.request_authorization_internal(
username,
flow,
&selected_mfa_method,
)
.await?;
return self
.exchange_authorization_code_for_access_token_internal(
auth_code,
flow.resource.as_deref(),
None,
)
.await;
} else if !auth_response.retry.ok_or(MsalError::GeneralFailure(
"Auth response Retry missing".to_string(),
))? {
return Err(MsalError::AuthorizationDenied);
}
Err(MsalError::MFAPollContinue)
} else {
let status: DeviceCodeStatus = json_from_str(&text)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
if status.authorization_state == 0 {
Err(MsalError::MFAPollContinue)
} else if status.authorization_state == 1 {
Err(MsalError::AuthorizationDenied)
} else if status.authorization_state == 2 {
let auth_code = self
.request_authorization_passwordless_internal(username, flow)
.await?;
return self
.exchange_authorization_code_for_access_token_internal(
auth_code,
flow.resource.as_deref(),
None,
)
.await;
} else {
Err(MsalError::GeneralFailure(format!(
"Unexpected authorization_state in DeviceCodeStatus {}: {}",
status.authorization_state, text
)))
}
}
} else {
Err(MsalError::GeneralFailure(
"EndAuth Authentication request failed".to_string(),
))
}
}
}
}
fn get_auth_redirect_uri(&self, client_id: Option<&str>, resource: Option<&str>) -> String {
self.app.get_auth_redirect_uri(client_id, resource)
}
}
struct EnrollmentKeyWrapper {
key: RS256Key,
cert: Certificate,
}
#[cfg(feature = "broker")]
#[derive(Clone, Serialize, Deserialize)]
pub enum P2PPrivateKey {
ExistingDevice(LoadableMsDeviceEnrolmentKey),
GeneratedUser(LoadableRS256Key),
}
#[cfg(feature = "broker")]
#[derive(Clone, Serialize, Deserialize)]
pub struct P2PCertificate {
pub certificate_der: Vec<u8>,
pub ca_certificate_der: Vec<u8>,
pub ca_certificate_pem: Option<String>,
pub subject: String,
pub issuer: String,
pub thumbprint_sha1: String,
pub dns_names: Vec<String>,
pub not_after_unix: i64,
pub private_key: P2PPrivateKey,
}
#[cfg(feature = "broker")]
impl fmt::Debug for P2PCertificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("P2PCertificate")
.field("subject", &self.subject)
.field("issuer", &self.issuer)
.field("thumbprint_sha1", &self.thumbprint_sha1)
.field("dns_names", &self.dns_names)
.field("not_after_unix", &self.not_after_unix)
.field("private_key", &"<redacted>")
.finish()
}
}
#[cfg(feature = "broker")]
#[derive(Clone, Debug, PartialEq, Eq, Sequence)]
struct P2PCertReqInfo {
pub version: x509_cert::request::Version,
pub subject: Name,
pub public_key: SubjectPublicKeyInfoOwned,
#[asn1(context_specific = "0", tag_mode = "IMPLICIT")]
pub attributes: SetOfVec<Attribute>,
}
#[cfg(feature = "broker")]
pub struct BrokerClientApplication {
app: PublicClientApplication,
transport_key: Option<LoadableMsOapxbcRsaKey>,
cert_key: Option<LoadableMsDeviceEnrolmentKey>,
on_behalf_of_client_id: Option<String>,
}
#[cfg(feature = "broker")]
impl BrokerClientApplication {
pub fn new(
authority: Option<&str>,
client_id: Option<&str>,
transport_key: Option<LoadableMsOapxbcRsaKey>,
cert_key: Option<LoadableMsDeviceEnrolmentKey>,
#[cfg(feature = "set_timeout")] timeout: Duration,
#[cfg(feature = "ipvers")] ip_version: &[IpVersion],
) -> Result<Self, MsalError> {
Ok(BrokerClientApplication {
app: PublicClientApplication::new(
BROKER_APP_ID,
authority,
#[cfg(feature = "set_timeout")]
timeout,
#[cfg(feature = "ipvers")]
ip_version,
)?,
transport_key,
cert_key,
on_behalf_of_client_id: client_id.map(|s| s.to_string()),
})
}
fn client(&self) -> &Client {
self.app.client()
}
pub fn clear_cookies(&self) {
self.app.clear_cookies()
}
fn authority(&self) -> Result<String, MsalError> {
self.app.authority()
}
pub fn set_authority(&self, new_authority: &str) -> Result<(), MsalError> {
self.app.set_authority(new_authority)
}
fn transport_key(
&self,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<MsOapxbcRsaKey, MsalError> {
let transport_key = &self.transport_key.as_ref()
.ok_or_else(||
MsalError::ConfigError("The transport key was not found. Please provide the transport key during initialize of the BrokerClientApplication, or enroll the device.".to_string())
)?;
tpm.msoapxbc_rsa_key_load(storage_key, transport_key)
.map_err(|e| MsalError::TPMFail(format!("Failed to load Msoapxbc: {:?}", e)))
}
pub fn set_transport_key(&mut self, transport_key: Option<LoadableMsOapxbcRsaKey>) {
self.transport_key = transport_key;
}
fn cert_key(
&self,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<EnrollmentKeyWrapper, MsalError> {
let cert_key = self.cert_key.clone()
.ok_or_else(||
MsalError::ConfigError("The certificate key was not found. Please provide the certificate key during initialize of the BrokerClientApplication, or enroll the device.".to_string())
)?;
let (key, cert) = tpm
.ms_device_enrolment_key_load(storage_key, cert_key)
.map_err(|e| MsalError::TPMFail(format!("Failed to load IdentityKey: {:?}", e)))?;
Ok(EnrollmentKeyWrapper { key, cert })
}
pub fn set_cert_key(&mut self, cert_key: Option<LoadableMsDeviceEnrolmentKey>) {
self.cert_key = cert_key;
}
pub async fn acquire_device_p2p_certificate(
&self,
tenant_id: &str,
device_name: &str,
dns_names: Option<&[&str]>,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<P2PCertificate, MsalError> {
debug!("Acquiring a device P2P certificate");
let loadable_cert_key = self.cert_key.clone().ok_or_else(|| {
MsalError::ConfigError(
"The certificate key was not found. Please provide the certificate key during initialize of the BrokerClientApplication, or enroll the device.".to_string(),
)
})?;
let cert_key = self.cert_key(tpm, storage_key)?;
let cert_der = cert_key.cert.to_der().map_err(|e| {
MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
})?;
let request_tenant_id =
Self::device_certificate_tenant_id(&cert_der)?.unwrap_or_else(|| tenant_id.to_string());
if request_tenant_id != tenant_id {
warn!(
"P2P device certificate tenant {} differs from requested tenant {}; using certificate tenant",
request_tenant_id, tenant_id
);
}
let token_endpoint = self.tenant_token_endpoint(&request_tenant_id)?;
let nonce = self.request_nonce_from_endpoint(&token_endpoint).await?;
let cert = X509::from_der(&cert_der).map_err(|e| {
MsalError::CryptoFail(format!("Failed to create X509 from DER: {:?}", e))
})?;
let subject = Name::from_der(
cert.subject_name()
.to_der()
.map_err(|e| MsalError::CryptoFail(format!("Failed encoding subject: {}", e)))?
.as_slice(),
)
.map_err(|e| MsalError::CryptoFail(format!("Failed parsing subject: {:?}", e)))?;
let (csr_der, public_key_der) = Self::create_p2p_csr(tpm, &cert_key.key, subject)?;
let csr = STANDARD.encode(&csr_der);
let dns_names_vec = dns_names
.map(|names| names.iter().map(|name| name.to_string()).collect())
.unwrap_or_else(|| vec![device_name.to_string()]);
let payload = P2PDeviceCertificatePayload::new(&nonce, &csr, device_name, &dns_names_vec);
if let Ok(pretty) = to_string_pretty(&payload) {
debug!("P2P Device Certificate Payload: {}", pretty);
}
let signed_jwt = Self::sign_p2p_device_jwt(&payload, &cert_der, tpm, &cert_key.key)?;
let response = self
.post_p2p_certificate_request(&token_endpoint, &signed_jwt, "2.0")
.await?;
Self::p2p_certificate_from_response(
&response,
P2PPrivateKey::ExistingDevice(loadable_cert_key),
&public_key_der,
)
}
pub async fn acquire_user_p2p_certificate(
&self,
sealed_prt: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<P2PCertificate, MsalError> {
debug!("Acquiring a user P2P certificate");
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
let tenant_id = if !prt.id_token.tid.is_empty() {
prt.id_token.tid.clone()
} else if let Some(utid) = prt.client_info.utid {
utid.to_string()
} else {
return Err(MsalError::GeneralFailure(
"No tenant id available for P2P user certificate request".to_string(),
));
};
let token_endpoint = self.tenant_token_endpoint(&tenant_id)?;
let nonce = self.request_nonce_from_endpoint(&token_endpoint).await?;
let session_key = prt.session_key()?;
let loadable_user_key = tpm
.rs256_create(storage_key)
.map_err(|e| MsalError::TPMFail(format!("Failed creating P2P user key: {:?}", e)))?;
let user_key = tpm
.rs256_load(storage_key, &loadable_user_key)
.map_err(|e| MsalError::TPMFail(format!("Failed loading P2P user key: {:?}", e)))?;
let subject = Name::from_str("CN=")
.map_err(|e| MsalError::CryptoFail(format!("Failed parsing subject: {:?}", e)))?;
let (csr_der, public_key_der) = Self::create_p2p_csr(tpm, &user_key, subject)?;
let csr = STANDARD.encode(&csr_der);
let payload = P2PUserCertificatePayload::new(&prt, &nonce, &csr);
if let Ok(pretty) = payload.redacted().and_then(|redacted| {
to_string_pretty(&redacted).map_err(|e| MsalError::InvalidJson(format!("{}", e)))
}) {
debug!("P2P User Certificate Payload: {}", pretty);
}
let signed_jwt =
self.sign_p2p_user_jwt(&payload, tpm, storage_key, &transport_key, &session_key)?;
let response = self
.post_p2p_certificate_request(&token_endpoint, &signed_jwt, "1.0")
.await?;
Self::p2p_certificate_from_response(
&response,
P2PPrivateKey::GeneratedUser(loadable_user_key),
&public_key_der,
)
}
pub async fn enroll_device(
&mut self,
refresh_token: &str,
attrs: EnrollAttrs,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<(LoadableMsOapxbcRsaKey, LoadableMsDeviceEnrolmentKey, String), MsalError> {
let token = self
.acquire_token_by_refresh_token_for_device_enrollment(refresh_token)
.await?;
let (in_progess_enrolment, csr) = tpm
.ms_device_enrolment_begin(storage_key, "7E980AD9-B86D-4306-9425-9AC066FB014A")
.map_err(|e| MsalError::TPMFail(format!("Failed creating certificate key: {:?}", e)))?;
let csr_der = csr.to_der().map_err(|_|
MsalError::GeneralFailure("Unable to convert X509 Request to DER".into()))?;
let loadable_transport_key = tpm
.msoapxbc_rsa_key_create(storage_key)
.map_err(|e| MsalError::TPMFail(format!("Failed creating transport key: {:?}", e)))?;
self.transport_key = Some(loadable_transport_key.clone());
let transport_key = match tpm.msoapxbc_rsa_key_load(storage_key, &loadable_transport_key) {
Ok(transport_key) => transport_key,
Err(e) => {
return Err(MsalError::TPMFail(format!(
"Failed loading id key: {:?}",
e
)))
}
};
let transport_key_der = tpm
.msoapxbc_rsa_public_as_der(&transport_key)
.map_err(|err| {
MsalError::TPMFail(format!("Failed getting transport key as der: {:?}", err))
})?;
let transport_key_rsa = Rsa::public_key_from_der(&transport_key_der)
.map_err(|e| MsalError::TPMFail(format!("{}", e)))?;
let (cert, device_id) = match &token.access_token {
Some(access_token) => {
self.enroll_device_internal(access_token, attrs, &transport_key_rsa, &csr_der)
.await?
}
None => {
return Err(MsalError::GeneralFailure(
"Access token not found".to_string(),
))
}
};
let cert_der = cert.to_der().map_err(|_|
MsalError::GeneralFailure("Unable to convert X509 to DER".into()))?;
let new_loadable_cert_key = tpm
.ms_device_enrolment_finalise(storage_key, in_progess_enrolment, &cert_der)
.map_err(|err| {
MsalError::TPMFail(format!("Failed creating loadable identity key: {:?}", err))
})?;
self.cert_key = Some(new_loadable_cert_key.clone());
Ok((
loadable_transport_key,
new_loadable_cert_key,
device_id.to_string(),
))
}
async fn enroll_device_internal(
&self,
access_token: &str,
attrs: EnrollAttrs,
transport_key: &Rsa<Public>,
csr_der: &Vec<u8>,
) -> Result<(X509, String), MsalError> {
let services = Services::new(
access_token,
&attrs.target_domain,
#[cfg(feature = "set_timeout")]
self.app.app.timeout,
#[cfg(feature = "ipvers")]
&self.app.app.ip_version,
)
.await?;
services
.enroll_device(access_token, attrs, transport_key, csr_der)
.await
}
pub async fn acquire_token_by_username_password(
&self,
username: &str,
password: &str,
scopes: Vec<&str>,
request_resource: Option<String>,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<UserToken, MsalError> {
let v2_endpoint = !scopes.is_empty();
if !scopes.is_empty() && request_resource.is_some() {
return Err(MsalError::GeneralFailure(
"Scopes cannot be specified with a request_resource".to_string(),
));
}
let prt = self
.acquire_user_prt_by_username_password_internal(username, password, tpm, storage_key)
.await?;
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let session_key = prt.session_key()?;
let mut token = self
.exchange_prt_for_access_token_internal(
&prt,
scopes.clone(),
v2_endpoint,
tpm,
storage_key,
&session_key,
request_resource,
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
#[cfg(feature = "redirect_uri")]
None,
#[cfg(feature = "pop_support")]
None,
false,
)
.await?;
token.client_info = prt.client_info.clone();
token.prt = Some(self.seal_user_prt(&prt, tpm, prt_storage_key)?);
Ok(token)
}
pub async fn acquire_token_by_refresh_token(
&self,
refresh_token: &str,
scopes: Vec<&str>,
request_resource: Option<String>,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<UserToken, MsalError> {
self.acquire_token_by_refresh_token_internal(
refresh_token,
scopes,
request_resource,
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
false,
tpm,
storage_key,
)
.await
}
async fn acquire_token_by_refresh_token_internal(
&self,
refresh_token: &str,
scopes: Vec<&str>,
request_resource: Option<String>,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
demand_mfa: bool,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<UserToken, MsalError> {
let prt = self
.acquire_user_prt_by_refresh_token_internal(refresh_token, tpm, storage_key)
.await?;
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let session_key = prt.session_key()?;
let v2_endpoint = !scopes.is_empty();
if !scopes.is_empty() && request_resource.is_some() {
return Err(MsalError::GeneralFailure(
"Scopes cannot be specified with a request_resource".to_string(),
));
}
let mut token = self
.exchange_prt_for_access_token_internal(
&prt,
scopes.clone(),
v2_endpoint,
tpm,
storage_key,
&session_key,
request_resource,
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
#[cfg(feature = "redirect_uri")]
None,
#[cfg(feature = "pop_support")]
None,
demand_mfa,
)
.await?;
token.client_info = prt.client_info.clone();
token.prt = Some(self.seal_user_prt(&prt, tpm, prt_storage_key)?);
Ok(token)
}
pub fn initiate_authorization_code_pkce_flow(
&self,
scopes: Vec<&str>,
redirect_uri: &str,
) -> Result<AuthorizationCodePkceFlow, MsalError> {
self.app
.initiate_authorization_code_pkce_flow(scopes, redirect_uri)
}
pub async fn acquire_token_by_authorization_code_pkce_flow(
&self,
flow: &AuthorizationCodePkceFlow,
redirect_url: &str,
) -> Result<UserToken, MsalError> {
self.app
.acquire_token_by_authorization_code_pkce_flow(flow, redirect_url)
.await
}
pub async fn acquire_token_by_username_password_for_device_enrollment(
&self,
username: &str,
password: &str,
) -> Result<UserToken, MsalError> {
let drs_scope = "https://enrollment.manage.microsoft.com/.default";
self.app
.acquire_token_by_username_password(username, password, vec![drs_scope])
.await
}
async fn acquire_token_by_refresh_token_for_device_enrollment(
&self,
refresh_token: &str,
) -> Result<UserToken, MsalError> {
let drs_scope = format!("{}/.default", DRS_APP_ID);
self.app
.acquire_token_by_refresh_token(refresh_token, vec![&drs_scope])
.await
}
pub async fn initiate_device_flow_for_device_enrollment(
&self,
#[cfg(feature = "optional_mfa")] options: &[AuthOption],
) -> Result<DeviceAuthorizationResponse, MsalError> {
#[cfg(feature = "optional_mfa")]
let portal_scope = if options.contains(&AuthOption::ForceMFA) {
format!("{}/.default", AZURE_PORTAL_APP_ID)
} else {
format!("{}/.default", DRS_APP_ID)
};
#[cfg(not(feature = "optional_mfa"))]
let portal_scope = format!("{}/.default", AZURE_PORTAL_APP_ID);
self.app.initiate_device_flow(vec![&portal_scope]).await
}
pub async fn acquire_token_by_device_flow(
&self,
flow: DeviceAuthorizationResponse,
) -> Result<UserToken, MsalError> {
let portal_token = self.app.acquire_token_by_device_flow(flow).await?;
let drs_scope = "https://enrollment.manage.microsoft.com/.default";
self.app
.acquire_token_by_refresh_token(&portal_token.refresh_token, vec![&drs_scope])
.await
}
pub async fn check_user_exists(
&self,
username: &str,
options: &[AuthOption],
) -> Result<AuthInit, MsalError> {
let intune_resource = "0000000a-0000-0000-c000-000000000000";
self.app
.check_user_exists(username, Some(intune_resource), options)
.await
}
pub async fn initiate_acquire_token_by_mfa_flow_for_device_enrollment(
&self,
username: &str,
password: Option<&str>,
options: &[AuthOption],
auth_init: Option<AuthInit>,
#[cfg(feature = "mfa_method_selection")] selected_method: Option<&str>,
) -> Result<MFAAuthContinue, MsalError> {
let intune_resource = "0000000a-0000-0000-c000-000000000000";
self.app
.initiate_acquire_token_by_mfa_flow(
username,
password,
vec![],
Some(intune_resource),
options,
auth_init,
#[cfg(feature = "mfa_method_selection")]
selected_method,
)
.await
}
pub async fn initiate_acquire_token_by_mfa_flow(
&self,
username: &str,
password: Option<&str>,
options: &[AuthOption],
auth_init: Option<AuthInit>,
#[cfg(feature = "mfa_method_selection")] selected_method: Option<&str>,
) -> Result<MFAAuthContinue, MsalError> {
self.app
.initiate_acquire_token_by_mfa_flow(
username,
password,
vec![],
None,
options,
auth_init,
#[cfg(feature = "mfa_method_selection")]
selected_method,
)
.await
}
pub async fn acquire_token_by_mfa_flow(
&self,
username: &str,
auth_data: Option<&str>,
poll_attempt: Option<u32>,
flow: &mut MFAAuthContinue,
) -> Result<UserToken, MsalError> {
self.app
.acquire_token_by_mfa_flow(username, auth_data, poll_attempt, flow)
.await
}
async fn request_nonce(&self) -> Result<String, MsalError> {
let token_endpoint = format!("{}/oauth2/token", self.authority()?);
self.request_nonce_from_endpoint(&token_endpoint).await
}
fn tenant_token_endpoint(&self, tenant_id: &str) -> Result<String, MsalError> {
if tenant_id.is_empty() || tenant_id.contains('/') {
return Err(MsalError::ConfigError(
"Invalid tenant id for P2P certificate request".to_string(),
));
}
let mut authority = Url::parse(&self.authority()?)
.map_err(|e| MsalError::URLFormatFailed(format!("{}", e)))?;
authority.set_path(&format!("{}/oauth2/token", tenant_id));
authority.set_query(None);
authority.set_fragment(None);
Ok(authority.to_string())
}
async fn request_nonce_from_endpoint(&self, token_endpoint: &str) -> Result<String, MsalError> {
let resp = self
.client()
.post(token_endpoint)
.body("grant_type=srv_challenge")
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let json_resp: Nonce = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(json_resp.nonce)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
fn device_certificate_tenant_id(cert_der: &[u8]) -> Result<Option<String>, MsalError> {
const TENANT_ID_OID: &str = "1.2.840.113556.1.5.284.5";
let cert = x509_cert::Certificate::from_der(cert_der).map_err(|e| {
MsalError::CryptoFail(format!("Failed parsing device certificate DER: {:?}", e))
})?;
let extensions = match cert.tbs_certificate.extensions.as_ref() {
Some(extensions) => extensions,
None => return Ok(None),
};
let raw = extensions
.iter()
.find(|ext| ext.extn_id.to_string() == TENANT_ID_OID)
.map(|ext| ext.extn_value.as_bytes());
let raw = match raw {
Some(raw) => raw,
None => return Ok(None),
};
match Self::tenant_id_from_device_certificate_oid(raw) {
Ok(tenant_id) => Ok(Some(tenant_id)),
Err(e) => {
warn!("Ignoring device certificate tenant OID: {:?}", e);
Ok(None)
}
}
}
fn tenant_id_from_device_certificate_oid(raw: &[u8]) -> Result<String, MsalError> {
let tenant_bytes = Self::device_certificate_oid_guid(raw).ok_or_else(|| {
MsalError::CryptoFail("Invalid device certificate tenant OID length".to_string())
})?;
Ok(Uuid::from_bytes_le(tenant_bytes).to_string())
}
fn device_certificate_oid_guid(raw: &[u8]) -> Option<[u8; 16]> {
if let Ok(octet_string) = OctetString::from_der(raw) {
if let Ok(guid) = octet_string.as_bytes().try_into() {
return Some(guid);
}
}
if let [0x04, 0x81, len, guid @ ..] = raw {
if usize::from(*len) == guid.len() {
if let Ok(guid) = guid.try_into() {
return Some(guid);
}
}
}
raw.try_into().ok()
}
fn p2p_compact_signing_input<T: Serialize, U: Serialize>(
header: &T,
payload: &U,
) -> Result<String, MsalError> {
let header_json = json_to_vec(header).map_err(|e| {
MsalError::InvalidJson(format!("Failed serializing P2P JWT header: {}", e))
})?;
let payload_json = json_to_vec(payload).map_err(|e| {
MsalError::InvalidJson(format!("Failed serializing P2P JWT payload: {}", e))
})?;
Ok(format!(
"{}.{}",
URL_SAFE_NO_PAD.encode(header_json),
URL_SAFE_NO_PAD.encode(payload_json)
))
}
fn sign_p2p_device_jwt(
payload: &P2PDeviceCertificatePayload,
cert_der: &[u8],
tpm: &mut BoxedDynTpm,
signing_key: &RS256Key,
) -> Result<String, MsalError> {
let x5c = STANDARD.encode(cert_der);
let header = P2PDeviceCertificateHeader {
alg: "RS256",
typ: "JWT",
x5c: &x5c,
};
debug!(
"P2P Device Certificate Header: {}",
json!({
"alg": header.alg,
"typ": header.typ,
"x5c": "<base64 DER certificate>"
})
);
let signing_input = Self::p2p_compact_signing_input(&header, payload)?;
let signature = tpm
.rs256_sign(signing_key, signing_input.as_bytes())
.map_err(|e| MsalError::TPMFail(format!("Failed signing P2P device JWT: {:?}", e)))?;
let signature_bytes: Box<[u8]> = signature.into();
Ok(format!(
"{}.{}",
signing_input,
URL_SAFE_NO_PAD.encode(&signature_bytes)
))
}
fn sign_p2p_user_jwt_with_key(
payload: &P2PUserCertificatePayload,
ctx: &[u8],
hmac_key: &[u8],
) -> Result<String, MsalError> {
let ctx = STANDARD.encode(ctx);
let header = P2PUserCertificateHeader {
alg: "HS256",
typ: "JWT",
ctx: &ctx,
};
debug!(
"P2P User Certificate Header: {}",
json!({
"alg": header.alg,
"typ": header.typ,
"ctx": "<base64 context>"
})
);
let signing_input = Self::p2p_compact_signing_input(&header, payload)?;
let signature = Self::hmac_sha256(hmac_key, signing_input.as_bytes())?;
Ok(format!(
"{}.{}",
signing_input,
URL_SAFE_NO_PAD.encode(signature)
))
}
fn hmac_sha256(key: &[u8], data: &[u8]) -> Result<Vec<u8>, MsalError> {
let key = PKey::hmac(key)
.map_err(|e| MsalError::CryptoFail(format!("Failed creating HMAC key: {}", e)))?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)
.map_err(|e| MsalError::CryptoFail(format!("Failed creating HMAC signer: {}", e)))?;
signer
.update(data)
.map_err(|e| MsalError::CryptoFail(format!("Failed updating HMAC signer: {}", e)))?;
signer
.sign_to_vec()
.map_err(|e| MsalError::CryptoFail(format!("Failed signing HMAC: {}", e)))
}
fn sign_p2p_user_jwt(
&self,
payload: &P2PUserCertificatePayload,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
transport_key: &MsOapxbcRsaKey,
session_key: &SessionKey,
) -> Result<String, MsalError> {
const P2P_CTX_LEN: usize = 24;
const AAD_KDF_LABEL: &[u8; 26] = b"AzureAD-SecureConversation";
let mut ctx = [0u8; P2P_CTX_LEN];
rand_bytes(&mut ctx)
.map_err(|e| MsalError::CryptoFail(format!("Failed creating P2P context: {}", e)))?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let session_key = MsOapxbcSessionKey::complete_tpm_rsa_oaep_key_agreement(
tpm,
prt_storage_key,
transport_key,
&session_key.session_key_jwe,
)
.map_err(|e| MsalError::CryptoFail(format!("Unable to decipher session_key_jwe: {}", e)))?;
let MsOapxbcSessionKey::A256GCM { sealed_session_key } = session_key;
let aes_key = tpm
.unseal_data(prt_storage_key, &sealed_session_key)
.map_err(|e| {
MsalError::TPMFail(format!("Failed unsealing PRT session key: {:?}", e))
})?;
let derived_key = crypto_glue::nist_sp800_108_kdf_hmac_sha256::derive_key_aes256(
&aes_key,
AAD_KDF_LABEL,
&ctx,
)
.ok_or_else(|| {
MsalError::CryptoFail("Failed deriving P2P user JWT signing key".to_string())
})?;
Self::sign_p2p_user_jwt_with_key(payload, &ctx, &derived_key)
}
fn create_p2p_csr(
tpm: &mut BoxedDynTpm,
signing_key: &RS256Key,
subject: Name,
) -> Result<(Vec<u8>, Vec<u8>), MsalError> {
let public_key = tpm
.rs256_public(signing_key)
.map_err(|e| MsalError::TPMFail(format!("Failed getting public key: {:?}", e)))?;
let public_key_der = public_key
.to_public_key_der()
.map_err(|e| MsalError::CryptoFail(format!("Failed encoding public key: {:?}", e)))?;
let spki = SubjectPublicKeyInfoOwned::try_from(public_key_der.as_bytes())
.map_err(|e| MsalError::CryptoFail(format!("Failed parsing SPKI: {:?}", e)))?;
let cert_req_info = P2PCertReqInfo {
version: x509_cert::request::Version::V1,
subject,
public_key: spki,
attributes: SetOfVec::new(),
};
let tbs_der = cert_req_info
.to_der()
.map_err(|e| MsalError::CryptoFail(format!("Failed encoding CSR info: {:?}", e)))?;
let signature = tpm
.rs256_sign(signing_key, &tbs_der)
.map_err(|e| MsalError::TPMFail(format!("Failed signing CSR: {:?}", e)))?;
let signature_bytes: Box<[u8]> = signature.into();
let signature_algorithm = AlgorithmIdentifierOwned {
oid: rfc5912::SHA_256_WITH_RSA_ENCRYPTION,
parameters: Some(der::asn1::AnyRef::from(der::asn1::Null).into()),
};
#[derive(Sequence)]
struct P2PCertReq {
info: P2PCertReqInfo,
algorithm: AlgorithmIdentifierOwned,
signature: BitString,
}
let cert_req = P2PCertReq {
info: cert_req_info,
algorithm: signature_algorithm,
signature: BitString::from_bytes(&signature_bytes).map_err(|e| {
MsalError::CryptoFail(format!("Failed creating CSR signature: {:?}", e))
})?,
};
let csr_der = cert_req
.to_der()
.map_err(|e| MsalError::CryptoFail(format!("Failed encoding CSR: {:?}", e)))?;
Ok((csr_der, public_key_der.to_vec()))
}
fn x509_name_to_string(name: &openssl::x509::X509NameRef) -> String {
name.entries()
.map(|entry| {
let key = entry.object().nid().short_name().unwrap_or("OID");
let value = entry
.data()
.to_string()
.map(|value| value.to_string())
.unwrap_or_else(|_| "<non-utf8>".to_string());
format!("{}={}", key, value)
})
.collect::<Vec<String>>()
.join(", ")
}
fn p2p_certificate_from_response(
response: &P2PCertificateResponse,
private_key: P2PPrivateKey,
expected_public_key_der: &[u8],
) -> Result<P2PCertificate, MsalError> {
let certificate_der = STANDARD
.decode(&response.x5c)
.map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?;
let cert = X509::from_der(&certificate_der)
.map_err(|e| MsalError::CryptoFail(format!("Failed parsing P2P certificate: {}", e)))?;
let cert_public_key_der = cert
.public_key()
.and_then(|key| key.public_key_to_der())
.map_err(|e| {
MsalError::CryptoFail(format!("Failed reading P2P certificate public key: {}", e))
})?;
if cert_public_key_der != expected_public_key_der {
return Err(MsalError::CryptoFail(
"P2P certificate public key does not match CSR key".to_string(),
));
}
let thumbprint_sha1 = hash(MessageDigest::sha1(), &certificate_der)
.map_err(|e| MsalError::CryptoFail(format!("{}", e)))?
.iter()
.map(|byte| format!("{:02X}", byte))
.collect::<String>();
let dns_names = cert
.subject_alt_names()
.map(|names| {
names
.iter()
.filter_map(|name| name.dnsname().map(|dns| dns.to_string()))
.collect()
})
.unwrap_or_default();
let not_after_unix = Self::asn1_time_to_unix(cert.not_after())?;
let ca_certificate_der = STANDARD
.decode(&response.x5c_ca)
.map_err(|e| MsalError::InvalidBase64(format!("{}", e)))?;
let ca_certificate_pem = match X509::from_der(&ca_certificate_der) {
Ok(_) => Some(format!(
"-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
response.x5c_ca
)),
Err(e) => {
warn!("Failed parsing the P2P issuing CA certificate: {}", e);
None
}
};
Ok(P2PCertificate {
certificate_der,
ca_certificate_der,
ca_certificate_pem,
subject: Self::x509_name_to_string(cert.subject_name()),
issuer: Self::x509_name_to_string(cert.issuer_name()),
thumbprint_sha1,
dns_names,
not_after_unix,
private_key,
})
}
fn asn1_time_to_unix(time: &Asn1TimeRef) -> Result<i64, MsalError> {
let epoch = Asn1Time::from_unix(0)
.map_err(|e| MsalError::CryptoFail(format!("Failed creating unix epoch: {}", e)))?;
let diff = epoch.diff(time).map_err(|e| {
MsalError::CryptoFail(format!("Failed comparing certificate time: {}", e))
})?;
Ok(i64::from(diff.days) * 86400 + i64::from(diff.secs))
}
async fn post_p2p_certificate_request(
&self,
token_endpoint: &str,
signed_jwt: &str,
windows_api_version: &str,
) -> Result<P2PCertificateResponse, MsalError> {
let params = [
("windows_api_version", windows_api_version),
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
("request", signed_jwt),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let mut debug_payload = params;
debug_payload[2] = ("request", "**********");
if let Ok(pretty) = to_string_pretty(&debug_payload) {
debug!("POST {}: {}", token_endpoint, pretty);
}
let resp = self
.client()
.post(token_endpoint)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
resp.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
async fn build_jwt_by_username_password(
&self,
username: &str,
password: &str,
cert: Option<&X509>,
) -> Result<Jws, MsalError> {
let nonce = self.request_nonce().await?;
let mut builder = JwsBuilder::from(
serde_json::to_vec(&UsernamePasswordAuthenticationPayload::new(
username, password, &nonce,
))
.map_err(|e| {
MsalError::InvalidJson(format!("Failed serializing UsernamePassword JWT: {}", e))
})?,
)
.set_typ(Some("JWT"));
if let Some(cert) = cert {
builder = builder.set_x5c(Some(vec![cert
.to_der()
.map_err(|e| MsalError::CryptoFail(format!("{}", e)))?]));
}
let jwt = builder.build();
if let Ok(mut debug_jwt) = jwt.from_json::<Value>() {
debug_jwt["password"] = "**********".into();
if let Ok(pretty) = to_string_pretty(&debug_jwt) {
debug!("Username/Password JWT: {}", pretty);
}
}
Ok(jwt)
}
pub async fn acquire_user_prt_by_username_password(
&self,
username: &str,
password: &str,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<SealedData, MsalError> {
let prt = self
.acquire_user_prt_by_username_password_internal(username, password, tpm, storage_key)
.await?;
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
self.seal_user_prt(&prt, tpm, prt_storage_key)
}
async fn acquire_user_prt_by_username_password_internal(
&self,
username: &str,
password: &str,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<PrimaryRefreshToken, MsalError> {
debug!("Acquiring User PRT via Username/Password");
let cert_key = self.cert_key(tpm, storage_key)?;
let cert_der = cert_key.cert.to_der().map_err(|e| {
MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
})?;
let cert = X509::from_der(&cert_der).map_err(|e| {
MsalError::CryptoFail(format!("Failed to create X509 from DER: {:?}", e))
})?;
let jwt = self
.build_jwt_by_username_password(username, password, Some(&cert))
.await?;
let signed_jwt = self.sign_jwt(&jwt, tpm, storage_key).await?;
self.acquire_user_prt_jwt(&signed_jwt).await
}
async fn build_jwt_by_refresh_token(
&self,
refresh_token: &str,
cert: Option<&X509>,
) -> Result<Jws, MsalError> {
let nonce = self.request_nonce().await?;
let mut builder = JwsBuilder::from(
serde_json::to_vec(&RefreshTokenAuthenticationPayload::new(
refresh_token,
&nonce,
))
.map_err(|e| {
MsalError::InvalidJson(format!("Failed serializing RefreshToken JWT: {}", e))
})?,
)
.set_typ(Some("JWT"));
if let Some(cert) = cert {
builder = builder.set_x5c(Some(vec![cert
.to_der()
.map_err(|e| MsalError::CryptoFail(format!("{}", e)))?]));
}
let jwt = builder.build();
if let Ok(mut debug_jwt) = jwt.from_json::<Value>() {
debug_jwt["refresh_token"] = "**********".into();
if let Ok(pretty) = to_string_pretty(&debug_jwt) {
debug!("Refresh Token JWT: {}", pretty);
}
}
Ok(jwt)
}
pub async fn acquire_user_prt_by_refresh_token(
&self,
refresh_token: &str,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<SealedData, MsalError> {
let prt = self
.acquire_user_prt_by_refresh_token_internal(refresh_token, tpm, storage_key)
.await?;
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
self.seal_user_prt(&prt, tpm, prt_storage_key)
}
async fn acquire_user_prt_by_refresh_token_internal(
&self,
refresh_token: &str,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<PrimaryRefreshToken, MsalError> {
debug!("Acquiring User PRT via Refresh Token");
let cert_key = self.cert_key(tpm, storage_key)?;
let cert_der = cert_key.cert.to_der().map_err(|e| {
MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
})?;
let cert = X509::from_der(&cert_der).map_err(|e| {
MsalError::CryptoFail(format!("Failed to create X509 from DER: {:?}", e))
})?;
let jwt = self
.build_jwt_by_refresh_token(refresh_token, Some(&cert))
.await?;
let signed_jwt = self.sign_jwt(&jwt, tpm, storage_key).await?;
self.acquire_user_prt_jwt(&signed_jwt).await
}
async fn sign_jwt(
&self,
jwt: &Jws,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<String, MsalError> {
let cert_key = self.cert_key(tpm, storage_key)?;
let mut jws_tpm_signer = match JwsTpmRs256Signer::new(tpm, &cert_key.key) {
Ok(jws_tpm_signer) => jws_tpm_signer,
Err(e) => {
return Err(MsalError::TPMFail(format!(
"Failed loading tpm signer: {}",
e
)))
}
};
let signed_jwt = match jws_tpm_signer.sign(jwt) {
Ok(signed_jwt) => signed_jwt,
Err(e) => return Err(MsalError::TPMFail(format!("Failed signing jwk: {}", e))),
};
Ok(format!("{}", signed_jwt))
}
async fn acquire_user_prt_jwt(
&self,
signed_jwt: &str,
) -> Result<PrimaryRefreshToken, MsalError> {
debug!("Acquiring User PRT via JWT");
let params = [
("windows_api_version", "2.0"),
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
("request", signed_jwt),
("client_info", "1"),
("tgt", "true"),
];
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>()
.join("&");
let url = format!("{}/oauth2/token", self.authority()?);
let mut debug_payload = params;
debug_payload[2] = ("request", "**********");
if let Ok(pretty) = to_string_pretty(&debug_payload) {
debug!("POST {}: {}", url, pretty);
}
let resp = self
.client()
.post(url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let json_resp: PrimaryRefreshToken = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(json_resp)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
async fn sign_session_key_jwt(
&self,
jwt: &Jws,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
session_key: &SessionKey,
) -> Result<String, MsalError> {
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let signed_jwt = session_key.sign(tpm, &transport_key, prt_storage_key, jwt)?;
Ok(format!("{}", signed_jwt))
}
pub async fn exchange_prt_for_ssh_certificate(
&self,
sealed_prt: &SealedData,
openssh_public_key: &str,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<EntraSshCertificate, MsalError> {
let jwk = ssh_rsa_public_key_to_jwk(openssh_public_key)?;
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
let session_key = prt.session_key()?;
let redirect_uri = self.app.get_auth_redirect_uri(Some(AZURE_CLI_APP_ID), None);
let bearer_token = self
.exchange_prt_for_refresh_token_jwt_bearer(
&prt,
tpm,
storage_key,
&session_key,
AZURE_CLI_APP_ID,
&redirect_uri,
None,
)
.await?;
self.acquire_ssh_certificate_with_refresh_token(&bearer_token.refresh_token, &jwk)
.await
}
async fn acquire_ssh_certificate_with_refresh_token(
&self,
refresh_token: &str,
jwk: &SshRsaJwk,
) -> Result<EntraSshCertificate, MsalError> {
let payload = build_ssh_certificate_request_form(refresh_token, jwk)?;
let url = format!("{}/oauth2/v2.0/token", self.authority()?);
debug!("POST SSH certificate request {} for key {}", url, jwk.kid);
let response = self
.client()
.post(&url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::ACCEPT, "application/json")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if response.status().is_success() {
let response: SshCertificateTokenResponse = response
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
parse_ssh_certificate_response(response, jwk)
} else {
let response: ErrorResponse = response
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(response))
}
}
#[allow(clippy::too_many_arguments)]
pub async fn exchange_prt_for_access_token(
&self,
sealed_prt: &SealedData,
scope: Vec<&str>,
request_resource: Option<String>,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
#[cfg(feature = "redirect_uri")] redirect_uri: Option<&str>,
#[cfg(feature = "pop_support")] req_cnf: Option<&str>,
) -> Result<UserToken, MsalError> {
#[cfg(not(feature = "pop_support"))]
let _req_cnf: Option<&str> = None;
let v2_endpoint = !scope.is_empty();
if !scope.is_empty() && request_resource.is_some() {
return Err(MsalError::GeneralFailure(
"Scopes cannot be specified with a request_resource".to_string(),
));
}
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
let session_key = prt.session_key()?;
let mut token = self
.exchange_prt_for_access_token_internal(
&prt,
scope,
v2_endpoint,
tpm,
storage_key,
&session_key,
request_resource,
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
#[cfg(feature = "redirect_uri")]
redirect_uri,
#[cfg(feature = "pop_support")]
req_cnf,
false,
)
.await?;
token.client_info = prt.client_info.clone();
Ok(token)
}
#[allow(clippy::too_many_arguments)]
async fn exchange_prt_for_access_token_internal(
&self,
prt: &PrimaryRefreshToken,
scope: Vec<&str>,
v2_endpoint: bool,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
session_key: &SessionKey,
request_resource: Option<String>,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
#[cfg(feature = "redirect_uri")] redirect_uri: Option<&str>,
#[cfg(feature = "pop_support")] req_cnf: Option<&str>,
demand_mfa: bool,
) -> Result<UserToken, MsalError> {
debug!("Exchanging a PRT for an Access Token");
#[cfg(not(feature = "pop_support"))]
let req_cnf: Option<&str> = None;
if let Some(req_cnf_val) = req_cnf {
return self
.exchange_prt_for_access_token_jwt_bearer(
prt,
scope,
v2_endpoint,
tpm,
storage_key,
session_key,
request_resource,
req_cnf_val,
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
#[cfg(feature = "redirect_uri")]
redirect_uri,
)
.await;
}
let request_id = Uuid::new_v4().to_string();
let auth_code = self
.exchange_prt_for_auth_code(
prt,
scope.clone(),
&request_id,
request_resource.as_deref(),
v2_endpoint,
session_key,
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
tpm,
storage_key,
#[cfg(feature = "redirect_uri")]
redirect_uri,
req_cnf,
demand_mfa,
)
.await?;
self.exchange_auth_code_for_access_token_internal(
scope,
&request_id,
v2_endpoint,
auth_code,
request_resource.as_deref(),
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
#[cfg(feature = "redirect_uri")]
redirect_uri,
req_cnf,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn exchange_prt_for_refresh_token_jwt_bearer(
&self,
prt: &PrimaryRefreshToken,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
session_key: &SessionKey,
client_id: &str,
redirect_uri: &str,
request_resource: Option<&str>,
) -> Result<UserToken, MsalError> {
let nonce = self.request_nonce().await?;
let jwt_payload = ExchangePRTForATPayload::new(
prt,
&nonce,
"openid profile offline_access",
client_id,
redirect_uri,
request_resource,
)?;
let jwt = JwsBuilder::from(serde_json::to_vec(&jwt_payload).map_err(|e| {
MsalError::InvalidJson(format!("Failed serializing ExchangePRTForAT JWT: {}", e))
})?)
.set_typ(Some("JWT"))
.build();
if let Ok(mut payload) = jwt.from_json::<Value>() {
payload["refresh_token"] = "**********".into();
if let Ok(pretty) = to_string_pretty(&payload) {
debug!("Exchange PRT for refresh token payload: {}", pretty);
}
}
let signed_jwt = self
.sign_session_key_jwt(&jwt, tpm, storage_key, session_key)
.await?;
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
("windows_api_version", "2.2"),
("request", signed_jwt.as_str()),
("client_id", client_id),
("redirect_uri", redirect_uri),
("client_info", "1"),
];
let payload = params
.iter()
.map(|(key, value)| format!("{}={}", key, value))
.collect::<Vec<String>>()
.join("&");
let url = format!("{}/oauth2/token", self.authority()?);
let mut debug_params = params;
debug_params[2] = ("request", "**********");
if let Ok(pretty) = to_string_pretty(&debug_params) {
debug!("POST PRT refresh-token exchange {}: {}", url, pretty);
}
let response = self
.client()
.post(&url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
let token: UserToken = if response.status().is_success() {
let response_text = response
.text()
.await
.map_err(|e| MsalError::GeneralFailure(format!("{}", e)))?;
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
if let Ok(jwe) = JweCompact::from_str(&response_text) {
let decrypted =
session_key.decipher_prt_v2(tpm, &transport_key, prt_storage_key, &jwe)?;
json_from_str(
std::str::from_utf8(decrypted.payload())
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?
} else {
json_from_str(&response_text)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?
}
} else {
let response: ErrorResponse = response
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
return Err(MsalError::AcquireTokenFailed(response));
};
debug!(
"PRT refresh-token exchange succeeded, refresh_token present={}",
!token.refresh_token.is_empty()
);
Ok(token)
}
#[allow(clippy::too_many_arguments)]
async fn exchange_prt_for_access_token_jwt_bearer(
&self,
prt: &PrimaryRefreshToken,
scope: Vec<&str>,
v2_endpoint: bool,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
session_key: &SessionKey,
request_resource: Option<String>,
req_cnf: &str,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
#[cfg(feature = "redirect_uri")] redirect_uri_override: Option<&str>,
) -> Result<UserToken, MsalError> {
debug!("Exchanging a PRT for an Access Token via JWT bearer (PoP)");
#[cfg(not(feature = "on_behalf_of"))]
let on_behalf_of_client_id: Option<&str> = None;
#[cfg(not(feature = "redirect_uri"))]
let redirect_uri_override: Option<&str> = None;
let (client_id, redirect_uri) = if let Some(uri) = redirect_uri_override {
let cid = if v2_endpoint {
if let Some(obo) = on_behalf_of_client_id {
obo.to_string()
} else if let Some(obo) = &self.on_behalf_of_client_id {
obo.clone()
} else {
LINUX_BROKER_APP_ID.to_string()
}
} else {
self.app.client_id().to_string()
};
(cid, uri.to_string())
} else if v2_endpoint {
if let Some(obo) = on_behalf_of_client_id {
(
obo.to_string(),
self.app
.get_auth_redirect_uri(Some(obo), request_resource.as_deref()),
)
} else if let Some(obo) = &self.on_behalf_of_client_id {
(obo.clone(), HIMMELBLAU_REDIRECT_URI.to_string())
} else {
(
LINUX_BROKER_APP_ID.to_string(),
self.app.get_auth_redirect_uri(
Some(LINUX_BROKER_APP_ID),
request_resource.as_deref(),
),
)
}
} else {
(
self.app.client_id().to_string(),
self.app
.get_auth_redirect_uri(None, request_resource.as_deref()),
)
};
let bearer_token = self
.exchange_prt_for_refresh_token_jwt_bearer(
prt,
tpm,
storage_key,
session_key,
&client_id,
&redirect_uri,
request_resource.as_deref(),
)
.await?;
let target_scopes = if v2_endpoint {
format!("openid profile offline_access {}", scope.join(" "))
} else {
"openid".to_string()
};
let mut step2_params = vec![
("client_id", client_id.as_str()),
("scope", target_scopes.as_str()),
("grant_type", "refresh_token"),
("refresh_token", &bearer_token.refresh_token),
("client_info", "1"),
("token_type", "pop"),
("req_cnf", req_cnf),
];
if !v2_endpoint {
if let Some(resource) = request_resource.as_deref() {
step2_params.push(("resource", resource));
}
}
let step2_payload = step2_params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let step2_url = if v2_endpoint {
format!("{}/oauth2/v2.0/token", self.authority()?)
} else {
format!("{}/oauth2/token", self.authority()?)
};
let mut debug_params2 = step2_params.clone();
debug_params2[3] = ("refresh_token", "**********");
if let Ok(pretty) = to_string_pretty(&debug_params2) {
debug!("Step 2 POST {}: {}", step2_url, pretty);
}
let resp2 = self
.client()
.post(&step2_url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(header::ACCEPT, "application/json")
.body(step2_payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp2.status().is_success() {
let token: UserToken = resp2
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(token)
} else {
let json_resp: ErrorResponse = resp2
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
pub async fn exchange_prt_for_prt(
&self,
sealed_prt: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
request_tgt: bool,
) -> Result<SealedData, MsalError> {
debug!("Exchanging a PRT for a new PRT");
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
let session_key = prt.session_key()?;
let nonce = self.request_nonce().await?;
let jwt = JwsBuilder::from(
serde_json::to_vec(&ExchangePRTPayload::new(&prt, &nonce, None, true)?).map_err(
|e| MsalError::InvalidJson(format!("Failed serializing ExchangePRT JWT: {}", e)),
)?,
)
.set_typ(Some("JWT"))
.build();
if let Ok(mut payload) = jwt.from_json::<Value>() {
payload["refresh_token"] = "**********".into();
if let Ok(pretty) = to_string_pretty(&payload) {
debug!("Exchange PRT Payload JWT: {}", pretty);
}
}
let signed_jwt = self
.sign_session_key_jwt(&jwt, tpm, storage_key, &session_key)
.await?;
let mut params = vec![
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
("windows_api_version", "2.2"),
("request", &signed_jwt),
("client_info", "1"),
];
if request_tgt {
params.push(("tgt", "true"));
}
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>()
.join("&");
let url = format!("{}/oauth2/token", self.authority()?);
let mut debug_payload = params.clone();
debug_payload[2] = ("request", "**********");
if let Ok(pretty) = to_string_pretty(&debug_payload) {
debug!("POST {}: {}", url, pretty);
}
let resp = self
.client()
.post(url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let enc = resp
.text()
.await
.map_err(|e| MsalError::GeneralFailure(format!("{}", e)))?;
let jwe = JweCompact::from_str(&enc)
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
let mut new_prt: PrimaryRefreshToken = json_from_str(
std::str::from_utf8(
session_key
.decipher_prt_v2(tpm, &transport_key, prt_storage_key, &jwe)?
.payload(),
)
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?,
)
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
prt.clone_session_key(&mut new_prt);
self.seal_user_prt(&new_prt, tpm, prt_storage_key)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
pub async fn provision_hello_for_business_key(
&self,
token: &UserToken,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
pin: &str,
) -> Result<LoadableMsHelloKey, MsalError> {
debug!("Provisioning a Hello for Business Key");
if !token.amr_ngcmfa()? && !token.amr_mfa()? {
error!("Key provisioning is impossible without an ngcmfa amr!");
return Err(MsalError::GeneralFailure(
"Token is missing an ngcmfa amr".to_string(),
));
}
let pin = PinValue::new(pin)
.map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
let access_token = match &token.access_token {
Some(access_token) => access_token.clone(),
None => {
return Err(MsalError::GeneralFailure(
"Access token missing".to_string(),
))
}
};
let services = Services::new(
&access_token,
&token.tenant_id()?,
#[cfg(feature = "set_timeout")]
self.app.app.timeout,
#[cfg(feature = "ipvers")]
&self.app.app.ip_version,
)
.await?;
let resource_id = services.key_provisioning_resource_id();
let token = self
.acquire_token_by_refresh_token_internal(
&token.refresh_token,
vec![],
Some(resource_id),
#[cfg(feature = "on_behalf_of")]
None,
true, tpm,
storage_key,
)
.await?;
let loadable_win_hello_key = tpm.ms_hello_key_create(storage_key, &pin).map_err(|e| {
MsalError::TPMFail(format!("Failed creating Windows Hello Key: {:?}", e))
})?;
let (win_hello_key, _win_hello_storage_key) = tpm
.ms_hello_key_load(storage_key, &loadable_win_hello_key, &pin)
.map_err(|e| {
MsalError::TPMFail(format!("Failed loading Windows Hello Key: {:?}", e))
})?;
let win_hello_pub_der = tpm
.ms_hello_rsa_public_as_der(&win_hello_key)
.map_err(|e| {
MsalError::TPMFail(format!("Failed getting Windows Hello Key as der: {:?}", e))
})?;
let win_hello_rsa = Rsa::public_key_from_der(&win_hello_pub_der)
.map_err(|e| MsalError::TPMFail(format!("{}", e)))?;
let access_token = match &token.access_token {
Some(access_token) => access_token.clone(),
None => {
return Err(MsalError::GeneralFailure(
"Access token missing".to_string(),
))
}
};
match services.provision_key(&access_token, &win_hello_rsa).await {
Ok(()) => Ok(loadable_win_hello_key.clone()),
Err(_) => Err(MsalError::GeneralFailure(
"Failed registering Windows Hello Key".to_string(),
)),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn acquire_token_by_hello_for_business_key(
&self,
username: &str,
key: &LoadableMsHelloKey,
scopes: Vec<&str>,
request_resource: Option<String>,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
pin: &str,
) -> Result<UserToken, MsalError> {
let v2_endpoint = !scopes.is_empty();
if !scopes.is_empty() && request_resource.is_some() {
return Err(MsalError::GeneralFailure(
"Scopes cannot be specified with a request_resource".to_string(),
));
}
let pin = PinValue::new(pin)
.map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
let prt = self
.acquire_user_prt_by_hello_for_business_key_internal(
username,
key,
tpm,
storage_key,
&pin,
)
.await?;
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let session_key = prt.session_key()?;
let mut token = self
.exchange_prt_for_access_token_internal(
&prt,
scopes.clone(),
v2_endpoint,
tpm,
storage_key,
&session_key,
request_resource,
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
#[cfg(feature = "redirect_uri")]
None,
#[cfg(feature = "pop_support")]
None,
false,
)
.await?;
token.client_info = prt.client_info.clone();
token.prt = Some(self.seal_user_prt(&prt, tpm, prt_storage_key)?);
Ok(token)
}
async fn build_jwt_by_hello_for_business_key(
&self,
username: &str,
loadable_key: &LoadableMsHelloKey,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
pin: &PinValue,
) -> Result<Jws, MsalError> {
debug!("Building a Hello for Business JWT");
let mut nonce = self.request_nonce().await?;
let (key, _win_hello_storage_key) = tpm
.ms_hello_key_load(storage_key, loadable_key, pin)
.map_err(|e| MsalError::TPMFail(format!("{:?}", e)))?;
let win_hello_pub_der = tpm.ms_hello_rsa_public_as_der(&key).map_err(|e| {
MsalError::TPMFail(format!("Failed getting Windows Hello Key as der: {:?}", e))
})?;
let win_hello_rsa = Rsa::public_key_from_der(&win_hello_pub_der)
.map_err(|e| MsalError::TPMFail(format!("{}", e)))?;
let win_hello_blob: Vec<u8> = BcryptRsaKeyBlob::new(
2048,
&win_hello_rsa.e().to_vec(),
&win_hello_rsa.n().to_vec(),
)
.try_into()?;
let kid = STANDARD.encode(
hash(MessageDigest::sha256(), &win_hello_blob)
.map_err(|e| MsalError::CryptoFail(format!("{}", e)))?,
);
let assertion_jwt = JwsBuilder::from(
serde_json::to_vec(
&HelloForBusinessAssertion::new(username, &nonce)
.map_err(|e| MsalError::GeneralFailure(format!("{:?}", e)))?,
)
.map_err(|e| {
MsalError::InvalidJson(format!(
"Failed serializing Hello for Business Assertion JWT: {}",
e
))
})?,
)
.set_typ(Some("JWT"))
.set_use(Some("ngc"))
.set_kid(Some(&kid))
.build();
if let Ok(payload) = assertion_jwt.from_json::<Value>() {
if let Ok(pretty) = to_string_pretty(&payload) {
debug!("Hello for Business Assertion: {}", pretty);
}
}
let mut jws_tpm_signer = match JwsTpmRs256Signer::new(tpm, &key) {
Ok(jws_tpm_signer) => jws_tpm_signer,
Err(e) => {
return Err(MsalError::TPMFail(format!(
"Failed loading tpm signer: {}",
e
)))
}
};
let signed_assertion = match jws_tpm_signer.sign(&assertion_jwt) {
Ok(signed_jwt) => signed_jwt,
Err(e) => return Err(MsalError::TPMFail(format!("Failed signing jwk: {}", e))),
};
let assertion = format!("{}", signed_assertion);
nonce = self.request_nonce().await?;
let cert_key = self.cert_key(tpm, storage_key)?;
let cert_der = cert_key.cert.to_der().map_err(|e| {
MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
})?;
let jwt = JwsBuilder::from(
serde_json::to_vec(&HelloForBusinessPayload::new(username, &assertion, &nonce))
.map_err(|e| {
MsalError::InvalidJson(format!(
"Failed serializing Hello for Business JWT: {}",
e
))
})?,
)
.set_typ(Some("JWT"))
.set_x5c(Some(vec![cert_der]))
.build();
if let Ok(mut jwt_debug) = jwt.from_json::<Value>() {
jwt_debug["assertion"] = "**********".into();
if let Ok(pretty) = to_string_pretty(&jwt_debug) {
debug!("Hello for Business Payload: {}", pretty);
}
}
Ok(jwt)
}
async fn acquire_user_prt_by_hello_for_business_key_internal(
&self,
username: &str,
key: &LoadableMsHelloKey,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
pin: &PinValue,
) -> Result<PrimaryRefreshToken, MsalError> {
debug!("Acquiring a User PRT via a Hello for Business Key");
let jwt = self
.build_jwt_by_hello_for_business_key(username, key, tpm, storage_key, pin)
.await?;
let signed_jwt = self.sign_jwt(&jwt, tpm, storage_key).await?;
self.acquire_user_prt_jwt(&signed_jwt).await
}
pub async fn acquire_user_prt_by_hello_for_business_key(
&self,
username: &str,
key: &LoadableMsHelloKey,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
pin: &str,
) -> Result<SealedData, MsalError> {
let pin = PinValue::new(pin)
.map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
let prt = self
.acquire_user_prt_by_hello_for_business_key_internal(
username,
key,
tpm,
storage_key,
&pin,
)
.await?;
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
self.seal_user_prt(&prt, tpm, prt_storage_key)
}
#[allow(clippy::too_many_arguments)]
async fn exchange_prt_for_auth_code_internal(
&self,
scope: Vec<&str>,
request_id: &str,
resource: Option<&str>,
v2_endpoint: bool,
signed_prt_payload: Option<String>,
signed_device_payload: Option<String>,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
#[cfg(feature = "redirect_uri")] redirect_uri_override: Option<&str>,
_req_cnf: Option<&str>,
demand_mfa: bool,
) -> Result<String, MsalError> {
#[cfg(not(feature = "on_behalf_of"))]
let on_behalf_of_client_id: Option<&str> = None;
#[cfg(not(feature = "redirect_uri"))]
let redirect_uri_override: Option<&str> = None;
let scope = format!("openid profile {}", scope.join(" "));
let (client_id, redirect_uri) = if let Some(uri) = redirect_uri_override {
let cid = if v2_endpoint {
if let Some(obo) = on_behalf_of_client_id {
obo.to_string()
} else if let Some(obo) = &self.on_behalf_of_client_id {
obo.clone()
} else {
LINUX_BROKER_APP_ID.to_string()
}
} else {
self.app.client_id().to_string()
};
(cid, uri.to_string())
} else if v2_endpoint {
if let Some(on_behalf_of_client_id) = on_behalf_of_client_id {
(
on_behalf_of_client_id.to_string(),
self.app
.get_auth_redirect_uri(Some(on_behalf_of_client_id), resource),
)
} else if let Some(on_behalf_of_client_id) = &self.on_behalf_of_client_id {
(
on_behalf_of_client_id.clone(),
HIMMELBLAU_REDIRECT_URI.to_string(),
)
} else {
(
LINUX_BROKER_APP_ID.to_string(),
self.app
.get_auth_redirect_uri(Some(LINUX_BROKER_APP_ID), resource),
)
}
} else {
(
self.app.client_id().to_string(),
self.app.get_auth_redirect_uri(None, resource),
)
};
let mut params = vec![
("client_id", client_id.as_str()),
("response_type", "code"),
("redirect_uri", redirect_uri.as_str()),
("client-request-id", request_id),
];
if v2_endpoint {
params.push(("scope", &scope));
} else if let Some(resource) = resource {
params.push(("resource", resource));
} else {
params.push(("resource", "https://graph.microsoft.com"));
}
if !v2_endpoint && demand_mfa {
params.push(("amr_values", "ngcmfa"));
}
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, url_encode(v)))
.collect::<Vec<String>>()
.join("&");
let url = if v2_endpoint {
format!("{}/oAuth2/v2.0/authorize?{}", self.authority()?, payload)
} else {
format!("{}/oauth2/authorize?{}", self.authority()?, payload)
};
debug!("GET {}", url);
let mut req = self.client().get(url).header(header::USER_AGENT, "");
if let Some(signed_prt_payload) = signed_prt_payload {
req = req.header("x-ms-RefreshTokenCredential", signed_prt_payload);
}
if let Some(signed_device_payload) = signed_device_payload {
req = req.header("x-ms-DeviceCredential", signed_device_payload);
}
let mut resp = req
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
let text;
(text, resp) = self.app.await_working(resp).await?;
if resp.status().is_redirection() {
let document = Html::parse_document(&text);
let selector = Selector::parse("a[href]").map_err(|_| {
MsalError::InvalidParse("Failed parsing auth code response".to_string())
})?;
if let Some(element) = document.select(&selector).next() {
if let Some(href_encoded) = element.value().attr("href") {
let href = percent_decode_str(href_encoded)
.decode_utf8()
.map_err(|e| {
MsalError::URLFormatFailed(format!("Failed decoding url: {:?}", e))
})?;
if let Ok(url) = Url::parse(&href) {
return url
.query_pairs()
.find_map(|(key, value)| {
if key == "code" {
Some(value.into_owned())
} else {
None
}
})
.ok_or(MsalError::GeneralFailure(
"Authorization code not found".to_string(),
));
}
}
}
match self.app.parse_auth_config(&text, false, false) {
#[cfg(feature = "changepassword")]
Err(MsalError::ChangePassword) => return Err(MsalError::ChangePassword),
Err(MsalError::AADSTSError(e)) => return Err(MsalError::AADSTSError(e)),
Err(MsalError::ConsentRequested(e)) => return Err(MsalError::ConsentRequested(e)),
Ok(auth_config) if auth_config.pgid.as_deref() == Some("ConvergedTFA") => {
return Err(MsalError::MFARequired);
}
_ => {}
}
Err(MsalError::GeneralFailure(format!(
"Authorization code not found in: {}",
text
)))
} else if resp.status().is_success() {
let re = Regex::new(r#"document\.location\.replace\("([^"]+)"\)"#)
.map_err(|e| MsalError::InvalidRegex(format!("{}", e)))?;
if let Some(m) = re.captures(&text) {
if let Some(redirect) = m.get(1) {
let redirect_decoded = Url::parse(&redirect.as_str().replace(r#"\u0026"#, "&"))
.map_err(|e| MsalError::InvalidParse(format!("{}", e)))?;
for (k, v) in redirect_decoded.query_pairs().collect::<Vec<_>>() {
if k == "code" {
return Ok(v.to_string());
}
if k == "error_description" {
return Err(MsalError::GeneralFailure(v.to_string()));
}
}
}
}
match self.app.parse_auth_config(&text, false, false) {
#[cfg(feature = "changepassword")]
Err(MsalError::ChangePassword) => return Err(MsalError::ChangePassword),
Err(MsalError::AADSTSError(e)) => return Err(MsalError::AADSTSError(e)),
Err(MsalError::ConsentRequested(e)) => return Err(MsalError::ConsentRequested(e)),
Ok(auth_config) if auth_config.pgid.as_deref() == Some("ConvergedTFA") => {
return Err(MsalError::MFARequired);
}
_ => {}
}
Err(MsalError::GeneralFailure(format!(
"Authorization code not found in: {}",
text
)))
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
pub async fn acquire_prt_sso_cookie(
&self,
prt: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<String, MsalError> {
self.acquire_prt_sso_cookie_with_nonce(prt, None, tpm, storage_key)
.await
}
pub async fn acquire_prt_sso_cookie_with_nonce(
&self,
prt: &SealedData,
sso_nonce: Option<&str>,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<String, MsalError> {
debug!("Creating a prt sso cookie");
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(prt, tpm, prt_storage_key)?;
let session_key = prt.session_key()?;
let jwt = JwsBuilder::from(
serde_json::to_vec(&RefreshTokenCredentialPayload::new(&prt, sso_nonce)?).map_err(
|e| MsalError::InvalidJson(format!("Failed serializing Authorization JWT: {}", e)),
)?,
)
.set_typ(Some("JWT"))
.build();
self.sign_session_key_jwt(&jwt, tpm, storage_key, &session_key)
.await
}
#[allow(clippy::too_many_arguments)]
async fn exchange_prt_for_auth_code(
&self,
prt: &PrimaryRefreshToken,
scope: Vec<&str>,
request_id: &str,
resource: Option<&str>,
v2_endpoint: bool,
session_key: &SessionKey,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
#[cfg(feature = "redirect_uri")] redirect_uri: Option<&str>,
req_cnf: Option<&str>,
demand_mfa: bool,
) -> Result<String, MsalError> {
debug!("Exchanging a PRT for an Authorization Code");
let nonce = self.request_nonce().await?;
let jwt = JwsBuilder::from(
serde_json::to_vec(&RefreshTokenCredentialPayload::new(prt, Some(&nonce))?).map_err(
|e| MsalError::InvalidJson(format!("Failed serializing Authorization JWT: {}", e)),
)?,
)
.set_typ(Some("JWT"))
.build();
let signed_prt_payload = self
.sign_session_key_jwt(&jwt, tpm, storage_key, session_key)
.await?;
if let Ok(mut payload) = jwt.from_json::<Value>() {
payload["refresh_token"] = "**********".into();
if let Ok(pretty) = to_string_pretty(&payload) {
debug!("Refresh Token Credential Payload: {}", pretty);
}
}
let cert_key = self.cert_key(tpm, storage_key)?;
let cert_der = cert_key.cert.to_der().map_err(|e| {
MsalError::CryptoFail(format!("Failed to convert certificate to DER: {:?}", e))
})?;
let jwt = JwsBuilder::from(
serde_json::to_vec(&DeviceCredentialPayload::new(&nonce)?).map_err(|e| {
MsalError::InvalidJson(format!("Failed serializing Authorization JWT: {}", e))
})?,
)
.set_typ(Some("JWT"))
.set_x5c(Some(vec![cert_der]))
.build();
let signed_device_payload = self.sign_jwt(&jwt, tpm, storage_key).await?;
if let Ok(payload) = jwt.from_json::<Value>() {
if let Ok(pretty) = to_string_pretty(&payload) {
debug!("Device Credential Payload: {}", pretty);
}
}
let result = self
.exchange_prt_for_auth_code_internal(
scope.clone(),
request_id,
resource,
v2_endpoint,
Some(signed_prt_payload.clone()),
Some(signed_device_payload.clone()),
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
#[cfg(feature = "redirect_uri")]
redirect_uri,
req_cnf,
demand_mfa,
)
.await;
match result {
Err(MsalError::AADSTSError(ref e)) if e.code == 16000 => {
warn!("PRT exchange failed with AADSTS16000, clearing cookies and retrying");
self.clear_cookies();
self.exchange_prt_for_auth_code_internal(
scope,
request_id,
resource,
v2_endpoint,
Some(signed_prt_payload),
Some(signed_device_payload),
#[cfg(feature = "on_behalf_of")]
on_behalf_of_client_id,
#[cfg(feature = "redirect_uri")]
redirect_uri,
req_cnf,
demand_mfa,
)
.await
}
other => other,
}
}
#[allow(clippy::too_many_arguments)]
async fn exchange_auth_code_for_access_token_internal(
&self,
scope: Vec<&str>,
request_id: &str,
v2_endpoint: bool,
authorization_code: String,
request_resource: Option<&str>,
#[cfg(feature = "on_behalf_of")] on_behalf_of_client_id: Option<&str>,
#[cfg(feature = "redirect_uri")] redirect_uri_override: Option<&str>,
req_cnf: Option<&str>,
) -> Result<UserToken, MsalError> {
debug!("Exchanging an Authorization Code for an Access Token");
#[cfg(not(feature = "on_behalf_of"))]
let on_behalf_of_client_id: Option<&str> = None;
#[cfg(not(feature = "redirect_uri"))]
let redirect_uri_override: Option<&str> = None;
let scopes_str = format!("openid profile offline_access {}", scope.join(" "));
let (client_id, redirect_uri) = if let Some(uri) = redirect_uri_override {
let cid = if v2_endpoint {
if let Some(obo) = on_behalf_of_client_id {
obo.to_string()
} else if let Some(obo) = &self.on_behalf_of_client_id {
obo.clone()
} else {
LINUX_BROKER_APP_ID.to_string()
}
} else {
self.app.client_id().to_string()
};
(cid, uri.to_string())
} else if v2_endpoint {
if let Some(on_behalf_of_client_id) = on_behalf_of_client_id {
(
on_behalf_of_client_id.to_string(),
self.app
.get_auth_redirect_uri(Some(on_behalf_of_client_id), request_resource),
)
} else if let Some(on_behalf_of_client_id) = &self.on_behalf_of_client_id {
(
on_behalf_of_client_id.clone(),
HIMMELBLAU_REDIRECT_URI.to_string(),
)
} else {
(
LINUX_BROKER_APP_ID.to_string(),
self.app
.get_auth_redirect_uri(Some(LINUX_BROKER_APP_ID), request_resource),
)
}
} else {
(
self.app.client_id().to_string(),
self.app.get_auth_redirect_uri(None, request_resource),
)
};
let mut params = vec![
("client_id", client_id.as_str()),
("grant_type", "authorization_code"),
("code", &authorization_code),
("redirect_uri", &redirect_uri),
("client-request-id", request_id),
];
if v2_endpoint {
params.push(("scope", &scopes_str));
} else if let Some(request_resource) = request_resource {
params.push(("resource", request_resource));
} else {
params.push(("resource", "https://graph.microsoft.com"));
}
if let Some(req_cnf) = req_cnf {
params.push(("req_cnf", req_cnf));
}
let payload = params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>()
.join("&");
let url = if v2_endpoint {
format!("{}/oAuth2/v2.0/token", self.authority()?)
} else {
format!("{}/oauth2/token", self.authority()?)
};
let mut debug_payload = params;
debug_payload[2] = ("code", "**********");
if let Ok(pretty) = to_string_pretty(&debug_payload) {
debug!("POST {}: {}", url, pretty);
}
let resp = self
.client()
.post(url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(payload)
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let token: UserToken = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Ok(token)
} else {
let json_resp: ErrorResponse = resp
.json()
.await
.map_err(|e| MsalError::InvalidJson(format!("{}", e)))?;
Err(MsalError::AcquireTokenFailed(json_resp))
}
}
#[cfg(feature = "changepassword")]
pub async fn handle_password_change(
&self,
username: &str,
password: &str,
new_password: &str,
) -> Result<(), MsalError> {
self.app
.handle_password_change(username, password, new_password)
.await
}
pub fn name_from_prt(
&self,
sealed_data: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<String, MsalError> {
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_data, tpm, prt_storage_key)?;
Ok(prt.name())
}
pub fn spn_from_prt(
&self,
sealed_data: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<String, MsalError> {
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_data, tpm, prt_storage_key)?;
prt.spn()
}
pub fn uuid_from_prt(
&self,
sealed_data: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<Uuid, MsalError> {
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_data, tpm, prt_storage_key)?;
prt.uuid()
}
pub fn store_cloud_tgt(
&self,
_sealed_prt: &SealedData,
_filename: &str,
_tpm: &mut BoxedDynTpm,
_storage_key: &StorageKey,
) -> Result<(), MsalError> {
Err(MsalError::NotImplemented)
}
pub fn store_ad_tgt(
&self,
_sealed_prt: &SealedData,
_filename: &str,
_tpm: &mut BoxedDynTpm,
_storage_key: &StorageKey,
) -> Result<(), MsalError> {
Err(MsalError::NotImplemented)
}
fn kerberos_credentials_to_ccache_bytes(
credentials: &KerberosCredentials,
) -> Result<Vec<u8>, MsalError> {
let temp_path = format!("/tmp/himmelblau_ccache_{}", Uuid::new_v4().as_hyphenated());
let ccache_name = format!("FILE:{}", temp_path);
let mut ccache = ccache_resolve(Some(&ccache_name)).map_err(|e| {
error!("Failed to resolve ccache: {:?}", e);
MsalError::CryptoFail("Failed to resolve ccache".to_string())
})?;
ccache.init(credentials.name(), None).map_err(|e| {
error!("Failed to init ccache: {:?}", e);
MsalError::CryptoFail("Failed to init ccache".to_string())
})?;
ccache.store(credentials).map_err(|e| {
error!("Failed to store credentials in ccache: {:?}", e);
MsalError::CryptoFail("Failed to store credentials in ccache".to_string())
})?;
let mut file = fs::File::open(&temp_path).map_err(|e| {
error!("Failed to open ccache file: {:?}", e);
MsalError::CryptoFail("Failed to open ccache file".to_string())
})?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes).map_err(|e| {
error!("Failed to read ccache file: {:?}", e);
MsalError::CryptoFail("Failed to read ccache file".to_string())
})?;
let _ = fs::remove_file(&temp_path);
Ok(bytes)
}
pub fn fetch_cloud_ccache(
&self,
sealed_prt: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<Vec<u8>, MsalError> {
let credentials = self.fetch_cloud_tgt(sealed_prt, tpm, storage_key)?;
Self::kerberos_credentials_to_ccache_bytes(&credentials)
}
pub fn fetch_ad_ccache(
&self,
sealed_prt: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<Vec<u8>, MsalError> {
let credentials = self.fetch_ad_tgt(sealed_prt, tpm, storage_key)?;
Self::kerberos_credentials_to_ccache_bytes(&credentials)
}
pub fn fetch_cloud_tgt(
&self,
sealed_prt: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<Box<KerberosCredentials>, MsalError> {
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
if let Some(error) = &prt.tgt_cloud.error {
return Err(MsalError::Missing(error.to_string()));
}
let session_key = prt.session_key()?;
let client_key =
prt.tgt_cloud
.derived_key(tpm, &transport_key, storage_key, &session_key)?;
let as_rep = prt.tgt_cloud.as_rep()?;
let kdc_reply = as_rep
.enc_part
.decrypt_enc_kdc_rep(&client_key)
.map_err(|e| {
let msg = format!("Failed to decrypt KDC reply part from AS reply: {:?}", e);
MsalError::CryptoFail(msg)
})?;
let creds = KerberosCredentials::new(as_rep.name, as_rep.ticket, kdc_reply);
Ok(Box::new(creds))
}
pub fn fetch_ad_tgt(
&self,
sealed_prt: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<Box<KerberosCredentials>, MsalError> {
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
let session_key = prt.session_key()?;
let (client_key, as_rep) = match &prt.tgt_on_prem {
OnPremTgt::Structured { tgt_ad } => {
if let Some(error) = tgt_ad.error.as_ref() {
return Err(MsalError::Missing(error.clone()));
}
let client_key =
tgt_ad.derived_key(tpm, &transport_key, storage_key, &session_key)?;
let as_rep = tgt_ad.as_rep()?;
(client_key, as_rep)
}
OnPremTgt::Raw(raw_tgt) => {
let client_key =
raw_tgt.derived_key(tpm, &transport_key, storage_key, &session_key)?;
let as_rep = raw_tgt.as_rep()?;
(client_key, as_rep)
}
OnPremTgt::Absent => {
return Err(MsalError::Missing(
"No on-prem partial ticket bundled".to_string(),
))
}
};
let kdc_reply = as_rep
.enc_part
.decrypt_enc_kdc_rep(&client_key)
.map_err(|e| {
let msg = format!("Failed to decrypt KDC reply part from AS reply: {:?}", e);
MsalError::CryptoFail(msg)
})?;
let creds = KerberosCredentials::new(as_rep.name, as_rep.ticket, kdc_reply);
Ok(Box::new(creds))
}
pub fn unseal_prt_kerberos_top_level_names(
&self,
sealed_prt: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<String, MsalError> {
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(sealed_prt, tpm, prt_storage_key)?;
let kerberos_top_level_names =
prt.kerberos_top_level_names
.clone()
.ok_or(MsalError::Missing(
"kerberos_top_level_names missing from PRT".to_string(),
))?;
Ok(kerberos_top_level_names.clone())
}
fn seal_user_prt(
&self,
prt: &PrimaryRefreshToken,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<SealedData, MsalError> {
let prt_data = json_to_vec(prt)
.map(Zeroizing::new)
.map_err(|e| MsalError::InvalidJson(format!("Failed serializing PRT {:?}", e)))?;
tpm.seal_data(storage_key, prt_data)
.map_err(|e| MsalError::TPMFail(format!("Failed sealing PRT {:?}", e)))
}
fn unseal_user_prt(
&self,
sealed_data: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<PrimaryRefreshToken, MsalError> {
let prt_data = tpm
.unseal_data(storage_key, sealed_data)
.map_err(|e| MsalError::TPMFail(format!("Failed unsealing PRT {:?}", e)))?;
json_from_slice(&prt_data)
.map_err(|e| MsalError::InvalidJson(format!("Failed deserializing PRT {:?}", e)))
}
pub fn seal_user_prt_with_hello_key(
&self,
prt: &SealedData,
hello_key: &LoadableMsHelloKey,
pin: &str,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<SealedData, MsalError> {
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(prt, tpm, prt_storage_key)?;
let prt_data = json_to_vec(&prt)
.map(Zeroizing::new)
.map_err(|e| MsalError::InvalidJson(format!("Failed serializing PRT {:?}", e)))?;
let pin = PinValue::new(pin)
.map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
let (_key, win_hello_storage_key) = tpm
.ms_hello_key_load(storage_key, hello_key, &pin)
.map_err(|e| MsalError::TPMFail(format!("{:?}", e)))?;
tpm.seal_data(&win_hello_storage_key, prt_data)
.map_err(|e| MsalError::TPMFail(format!("Failed sealing PRT {:?}", e)))
}
pub fn unseal_user_prt_with_hello_key(
&self,
sealed_data: &SealedData,
hello_key: &LoadableMsHelloKey,
pin: &str,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<SealedData, MsalError> {
let pin = PinValue::new(pin)
.map_err(|e| MsalError::TPMFail(format!("Failed setting pin value: {:?}", e)))?;
let (_key, win_hello_storage_key) = tpm
.ms_hello_key_load(storage_key, hello_key, &pin)
.map_err(|e| MsalError::TPMFail(format!("{:?}", e)))?;
let prt_data = tpm
.unseal_data(&win_hello_storage_key, sealed_data)
.map_err(|e| MsalError::TPMFail(format!("Failed unsealing PRT {:?}", e)))?;
let prt = json_from_slice(&prt_data)
.map_err(|e| MsalError::InvalidJson(format!("Failed deserializing PRT {:?}", e)))?;
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
self.seal_user_prt(&prt, tpm, prt_storage_key)
}
pub async fn resolve_nametosid(
&self,
username: &str,
tpm: &mut BoxedDynTpm,
machine_key: &StorageKey,
) -> Result<SidToName, MsalError> {
let nonce = self.request_nonce().await?;
let os_release = match OsRelease::new() {
Ok(os_release) => Some(format!(
"{} {}",
os_release.pretty_name, os_release.version_id
)),
Err(_) => None,
};
let jwt_body = json!({
"win_ver": os_release,
"version": "1.0",
"nonce": nonce,
"username": username,
});
let jwt = JwsBuilder::from(
serde_json::to_vec(&jwt_body)
.map_err(|e| MsalError::InvalidJson(format!("Failed to serialize JWT: {}", e)))?,
)
.set_typ(Some("JWT"))
.build();
let signed_jwt = self.sign_jwt(&jwt, tpm, machine_key).await?;
let form_body = serde_urlencoded::to_string([
("windows_api_version", "2.2"),
("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
("signedRequest", &signed_jwt),
])
.map_err(|e| MsalError::InvalidJson(format!("Failed to encode form: {}", e)))?;
let url = format!("{}/sidtoname", self.authority()?);
let resp = self
.client()
.get(url)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(Body::from(form_body))
.send()
.await
.map_err(|e| MsalError::request_failed(&e))?;
if resp.status().is_success() {
let json_resp: SidToName = resp
.json()
.await
.map_err(|e| MsalError::RequestFailed(format!("{:?}", e)))?;
Ok(json_resp)
} else {
Err(MsalError::RequestFailed(format!("{}", resp.status())))
}
}
pub fn is_prt_expired(
&self,
prt: &SealedData,
tpm: &mut BoxedDynTpm,
storage_key: &StorageKey,
) -> Result<bool, MsalError> {
let transport_key = self.transport_key(tpm, storage_key)?;
let maybe_transport_storage_key = tpm.rs256_yield_cek(&transport_key);
let prt_storage_key = maybe_transport_storage_key.as_ref().unwrap_or(storage_key);
let prt = self.unseal_user_prt(prt, tpm, prt_storage_key)?;
Ok(prt.is_expired())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[cfg(feature = "broker")]
use kanidm_hsm_crypto::{provider::SoftTpm, AuthValue};
#[cfg(feature = "broker")]
use openssl::{
asn1::{Asn1Object, Asn1OctetString, Asn1Time},
bn::BigNum,
pkey::{PKey, Private},
sign::Verifier,
x509::{X509Builder, X509Extension, X509NameBuilder},
};
fn test_cred_type(if_exists_result: i32, throttle_status: u8) -> CredType {
serde_json::from_value(json!({
"Credentials": {
"PrefCredential": 1,
"HasPassword": true
},
"ThrottleStatus": throttle_status,
"IfExistsResult": if_exists_result
}))
.expect("test credential type should deserialize")
}
#[test]
fn adfs_url_detection_requires_a_secure_complete_path_segment() {
for value in [
"https://fs.example.com/adfs/ls/?wa=wsignin1.0",
"https://fs.example.com/ADFS/LS/",
"https://fs.example.com:8443/prefix/adfs/ls",
] {
assert!(parse_adfs_federation_url(value).unwrap().is_some());
}
for value in [
"http://fs.example.com/adfs/ls/",
"https://user:secret@fs.example.com/adfs/ls/",
"https://adfs.example.com/login/",
"https://fs.example.com/notadfs/ls/",
"https://fs.example.com/login/?next=/adfs/ls/",
"https://fs.example.com/adfs/ls/#fragment",
] {
assert!(parse_adfs_federation_url(value).unwrap().is_none());
}
assert!(parse_adfs_federation_url("not a URL").is_err());
}
#[test]
fn ws_fed_form_parser_selects_complete_form_and_decodes_entities() {
let html = r#"
<html><body>
<form><input name="UserName" value="ignored"></form>
<form action="https://untrusted.example/collect">
<input type="hidden" name="WCTX" value="ctx&value">
<input type="hidden" name="wresult" value="<Assertion>ok</Assertion>">
<input type="hidden" name="WA" value="wsignin1.0">
<input type="hidden" name="Password" value="must-not-be-forwarded">
</form>
</body></html>
"#;
let form = parse_ws_fed_form(html).unwrap();
assert_eq!(form.wa, "wsignin1.0");
assert_eq!(form.wctx, "ctx&value");
assert_eq!(form.wresult, "<Assertion>ok</Assertion>");
}
#[test]
fn ws_fed_form_parser_rejects_login_and_incomplete_forms() {
for html in [
r#"<form><input name="UserName"><input name="Password"></form>"#,
r#"<form><input name="wa" value="wsignin1.0"><input name="wctx" value="ctx"></form>"#,
r#"<form><input name="wa" value="wrong"><input name="wctx" value="ctx"><input name="wresult" value="assertion"></form>"#,
r#"<form><input name="wa" value="wsignin1.0"><input name="wctx" value=""><input name="wresult" value="assertion"></form>"#,
] {
assert!(parse_ws_fed_form(html).is_err());
}
}
#[test]
fn login_srf_is_derived_from_authority_origin() {
assert_eq!(
entra_login_srf_url("https://login.microsoftonline.com/common/?ignored=true")
.unwrap()
.as_str(),
"https://login.microsoftonline.com/login.srf"
);
assert_eq!(
entra_login_srf_url("https://login.microsoftonline.us:8443/tenant")
.unwrap()
.as_str(),
"https://login.microsoftonline.us:8443/login.srf"
);
assert!(entra_login_srf_url("http://login.example.com/common").is_err());
}
#[test]
fn adfs_redirects_stay_on_https_origin_and_control_password_replay() {
let origin = Url::parse("https://fs.example.com/adfs/ls/").unwrap();
let (next, method) = resolve_adfs_redirect(
&origin,
&origin,
"../continue",
302,
AdfsRequestMethod::PostCredentials,
)
.unwrap();
assert_eq!(next.as_str(), "https://fs.example.com/adfs/continue");
assert_eq!(method, AdfsRequestMethod::Get);
let (_, method) = resolve_adfs_redirect(
&origin,
&origin,
"/adfs/resubmit",
307,
AdfsRequestMethod::PostCredentials,
)
.unwrap();
assert_eq!(method, AdfsRequestMethod::PostCredentials);
for location in [
"http://fs.example.com/adfs/continue",
"https://other.example.com/adfs/continue",
"https://user:secret@fs.example.com/adfs/continue",
"/adfs/continue#fragment",
] {
assert!(resolve_adfs_redirect(
&origin,
&origin,
location,
302,
AdfsRequestMethod::PostCredentials,
)
.is_err());
}
}
#[test]
fn usable_account_result_is_not_rejected_by_backend_throttle_status() {
for throttle_status in [0, 1, 2, u8::MAX] {
let cred_type = test_cred_type(0, throttle_status);
assert!(cred_type.account_exists().unwrap());
}
}
#[test]
fn throttled_account_lookup_remains_retryable_error() {
let cred_type = test_cred_type(2, 0);
assert!(matches!(
cred_type.account_exists(),
Err(MsalError::AADSTSError(err)) if err.code == 90055
));
}
#[test]
fn retryable_account_lookup_failures_keep_existing_error() {
for if_exists_result in [-1, 4] {
let cred_type = test_cred_type(if_exists_result, 0);
assert!(matches!(
cred_type.account_exists(),
Err(MsalError::AADSTSError(err)) if err.code == 90006
));
}
}
fn build_access_token(payload_json: &str) -> String {
let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"none"}"#.as_bytes());
let payload = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
format!("{}.{}.", header, payload)
}
#[cfg(feature = "broker")]
const TEST_TENANT_GUID: &str = "6973bb37-65f8-440e-b39e-dd57da64f6cf";
#[cfg(feature = "broker")]
const TEST_TENANT_BYTES_LE: [u8; 16] = [
0x37, 0xbb, 0x73, 0x69, 0xf8, 0x65, 0x0e, 0x44, 0xb3, 0x9e, 0xdd, 0x57, 0xda, 0x64, 0xf6,
0xcf,
];
#[cfg(feature = "broker")]
const TENANT_ID_OID: &str = "1.2.840.113556.1.5.284.5";
#[cfg(feature = "broker")]
fn test_tpm() -> (BoxedDynTpm, StorageKey) {
let mut tpm = BoxedDynTpm::new(SoftTpm::new());
let auth_str = AuthValue::generate().unwrap();
let auth_value = AuthValue::from_str(&auth_str).unwrap();
let loadable_machine_key = tpm.root_storage_key_create(&auth_value).unwrap();
let machine_key = tpm
.root_storage_key_load(&auth_value, &loadable_machine_key)
.unwrap();
(tpm, machine_key)
}
#[cfg(feature = "broker")]
fn test_prt(refresh_token: &str) -> PrimaryRefreshToken {
PrimaryRefreshToken {
token_type: "Bearer".to_string(),
expires_in: "3600".to_string(),
ext_expires_in: "3600".to_string(),
expires_on: "9999999999".to_string(),
refresh_token: refresh_token.to_string(),
refresh_token_expires_in: 3600,
session_key_jwe: None,
id_token: IdToken::default(),
client_info: ClientInfo::default(),
device_tenant_id: None,
tgt_on_prem: OnPremTgt::Absent,
tgt_cloud: StructuredTgt::default(),
kerberos_top_level_names: None,
}
}
#[cfg(feature = "broker")]
fn self_signed_cert(pkey: &PKey<Private>, extensions: &[X509Extension]) -> Vec<u8> {
let mut name = X509NameBuilder::new().unwrap();
name.append_entry_by_text("CN", "p2p-test").unwrap();
let name = name.build();
let mut builder = X509Builder::new().unwrap();
builder.set_version(2).unwrap();
let serial = BigNum::from_u32(1).unwrap().to_asn1_integer().unwrap();
builder.set_serial_number(&serial).unwrap();
builder.set_subject_name(&name).unwrap();
builder.set_issuer_name(&name).unwrap();
builder.set_pubkey(pkey).unwrap();
builder
.set_not_before(Asn1Time::days_from_now(0).unwrap().as_ref())
.unwrap();
builder
.set_not_after(Asn1Time::days_from_now(1).unwrap().as_ref())
.unwrap();
for extension in extensions {
builder.append_extension2(extension).unwrap();
}
builder.sign(pkey, MessageDigest::sha256()).unwrap();
builder.build().to_der().unwrap()
}
#[cfg(feature = "broker")]
fn tenant_oid_extension(value: &[u8]) -> X509Extension {
let oid = Asn1Object::from_str(TENANT_ID_OID).unwrap();
let value = Asn1OctetString::new_from_bytes(value).unwrap();
X509Extension::new_from_der(&oid, false, &value).unwrap()
}
#[cfg(feature = "broker")]
fn test_broker_app(authority: &str) -> BrokerClientApplication {
BrokerClientApplication::new(
Some(authority),
None,
None,
None,
#[cfg(feature = "set_timeout")]
Duration::from_secs(30),
#[cfg(feature = "ipvers")]
&[],
)
.unwrap()
}
#[cfg(feature = "broker")]
fn decode_jwt_header(jwt: &str) -> Value {
let header = jwt.split('.').next().unwrap();
let header = URL_SAFE_NO_PAD.decode(header).unwrap();
json_from_slice(&header).unwrap()
}
fn build_user_token(access_token: String) -> UserToken {
UserToken {
token_type: "Bearer".to_string(),
scope: None,
expires_in: 3600,
ext_expires_in: 3600,
access_token: Some(access_token),
refresh_token: "refresh-token".to_string(),
id_token: IdToken::default(),
client_info: ClientInfo::default(),
#[cfg(feature = "broker")]
prt: None,
}
}
#[test]
fn pkce_challenge_matches_rfc7636_vector() {
assert_eq!(
pkce_code_challenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"),
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
);
}
#[test]
fn initiate_authorization_code_pkce_flow_builds_expected_url() {
let app = PublicClientApplication::new(
"client-id",
Some("https://login.microsoftonline.com/tenant"),
#[cfg(feature = "set_timeout")]
Duration::from_secs(3),
#[cfg(feature = "ipvers")]
&[],
)
.unwrap();
let flow = app
.initiate_authorization_code_pkce_flow(
vec!["https://graph.microsoft.com/User.Read"],
"http://localhost/callback",
)
.unwrap();
let url = Url::parse(&flow.auth_url).unwrap();
let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
assert_eq!(
url.as_str().split('?').next().unwrap(),
"https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize"
);
assert_eq!(params["client_id"], "client-id");
assert_eq!(params["response_type"], "code");
assert_eq!(params["redirect_uri"], "http://localhost/callback");
assert_eq!(params["response_mode"], "query");
assert_eq!(
params["scope"],
"openid profile offline_access https://graph.microsoft.com/User.Read"
);
assert_eq!(params["state"], flow.state);
assert_eq!(
params["code_challenge"],
pkce_code_challenge(&flow.code_verifier)
);
assert_eq!(params["code_challenge_method"], "S256");
assert!(flow.code_verifier.len() >= 43);
}
#[tokio::test]
async fn acquire_token_by_authorization_code_pkce_flow_rejects_wrong_state() {
let app = PublicClientApplication::new(
"client-id",
Some("https://login.microsoftonline.com/tenant"),
#[cfg(feature = "set_timeout")]
Duration::from_secs(3),
#[cfg(feature = "ipvers")]
&[],
)
.unwrap();
let flow = app
.initiate_authorization_code_pkce_flow(vec!["User.Read"], "http://localhost/callback")
.unwrap();
let result = app
.acquire_token_by_authorization_code_pkce_flow(
&flow,
"http://localhost/callback?code=abc&state=wrong",
)
.await;
assert!(matches!(result, Err(MsalError::InvalidParse(_))));
}
#[tokio::test]
async fn acquire_token_by_authorization_code_pkce_flow_maps_redirect_error() {
let app = PublicClientApplication::new(
"client-id",
Some("https://login.microsoftonline.com/tenant"),
#[cfg(feature = "set_timeout")]
Duration::from_secs(3),
#[cfg(feature = "ipvers")]
&[],
)
.unwrap();
let flow = app
.initiate_authorization_code_pkce_flow(vec!["User.Read"], "http://localhost/callback")
.unwrap();
let redirect = format!(
"http://localhost/callback?error=access_denied&error_description=nope&state={}",
flow.state
);
let result = app
.acquire_token_by_authorization_code_pkce_flow(&flow, &redirect)
.await;
assert!(matches!(result, Err(MsalError::AcquireTokenFailed(_))));
if let Err(MsalError::AcquireTokenFailed(error)) = result {
assert_eq!(error.error, "access_denied");
assert_eq!(error.error_description, "nope");
}
}
#[test]
fn authorization_code_pkce_token_form_contains_expected_fields() {
let app = PublicClientApplication::new(
"client-id",
Some("https://login.microsoftonline.com/tenant"),
#[cfg(feature = "set_timeout")]
Duration::from_secs(3),
#[cfg(feature = "ipvers")]
&[],
)
.unwrap();
let flow = app
.initiate_authorization_code_pkce_flow(vec!["User.Read"], "http://localhost/callback")
.unwrap();
let form: HashMap<&str, &str> = app
.authorization_code_pkce_token_form(&flow, "auth-code")
.into_iter()
.collect();
assert_eq!(form["client_id"], "client-id");
assert_eq!(form["grant_type"], "authorization_code");
assert_eq!(form["code"], "auth-code");
assert_eq!(form["redirect_uri"], "http://localhost/callback");
assert_eq!(form["scope"], "openid profile offline_access User.Read");
assert_eq!(form["code_verifier"], flow.code_verifier);
assert_eq!(form["client_info"], "1");
}
#[cfg(feature = "broker")]
#[test]
fn p2p_device_payload_serializes_expected_fields() {
let payload = P2PDeviceCertificatePayload::new(
"nonce",
"csr",
"host1",
&["host1.example.com".to_string()],
);
let value: Value = serde_json::to_value(payload).unwrap();
assert_eq!(value["client_id"], BROKER_CLIENT_IDENT);
assert_eq!(value["request_nonce"], "nonce");
assert_eq!(value["grant_type"], "device_auth");
assert_eq!(value["cert_token_use"], "device_cert");
assert_eq!(
value["csr_type"],
"http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10"
);
assert_eq!(value["csr"], "csr");
assert_eq!(value["netbios_name"], "host1");
assert_eq!(value["dns_names"][0], "host1.example.com");
}
#[cfg(feature = "broker")]
#[test]
fn p2p_device_jwt_header_matches_broker_shape() {
let (mut tpm, machine_key) = test_tpm();
let loadable_key = tpm.rs256_create(&machine_key).unwrap();
let key = tpm.rs256_load(&machine_key, &loadable_key).unwrap();
let cert_der = vec![0x30, 0x03, 0x02, 0x01, 0x00];
let payload =
P2PDeviceCertificatePayload::new("nonce", "csr", "host1", &["host1".to_string()]);
let jwt = BrokerClientApplication::sign_p2p_device_jwt(&payload, &cert_der, &mut tpm, &key)
.unwrap();
let header = decode_jwt_header(&jwt);
let header = header.as_object().unwrap();
assert_eq!(header.len(), 3);
assert_eq!(header["alg"], "RS256");
assert_eq!(header["typ"], "JWT");
assert_eq!(header["x5c"], STANDARD.encode(cert_der));
assert!(!header.contains_key("kid"));
}
#[cfg(feature = "broker")]
#[test]
fn p2p_device_jwt_signature_verifies() {
let (mut tpm, machine_key) = test_tpm();
let loadable_key = tpm.rs256_create(&machine_key).unwrap();
let key = tpm.rs256_load(&machine_key, &loadable_key).unwrap();
let payload =
P2PDeviceCertificatePayload::new("nonce", "csr", "host1", &["host1".to_string()]);
let jwt =
BrokerClientApplication::sign_p2p_device_jwt(&payload, &[0x30, 0x00], &mut tpm, &key)
.unwrap();
let (signing_input, signature) = jwt.rsplit_once('.').unwrap();
let signature = URL_SAFE_NO_PAD.decode(signature).unwrap();
let public_key = PKey::public_key_from_der(&tpm.rs256_public_der(&key).unwrap()).unwrap();
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key).unwrap();
verifier.update(signing_input.as_bytes()).unwrap();
assert!(verifier.verify(&signature).unwrap());
}
#[cfg(feature = "broker")]
#[test]
fn p2p_user_payload_serializes_expected_fields() {
let prt = test_prt("prt-refresh-token");
let payload = P2PUserCertificatePayload::new(&prt, "nonce", "csr");
let value: Value = serde_json::to_value(payload).unwrap();
assert_eq!(value["iss"], "aad:brokerplugin");
assert_eq!(value["aud"], "login.microsoftonline.com");
assert_eq!(value["grant_type"], "refresh_token");
assert_eq!(value["scope"], "openid aza ugs");
assert_eq!(value["refresh_token"], "prt-refresh-token");
assert_eq!(value["client_id"], BROKER_CLIENT_IDENT);
assert_eq!(value["cert_token_use"], "user_cert");
assert_eq!(value["csr"], "csr");
}
#[cfg(feature = "broker")]
#[test]
fn p2p_user_payload_redacts_refresh_token() {
let prt = test_prt("prt-refresh-token");
let payload = P2PUserCertificatePayload::new(&prt, "nonce", "csr");
let redacted = payload.redacted().unwrap();
assert_eq!(redacted["refresh_token"], "**********");
assert_eq!(redacted["csr"], "csr");
assert_eq!(
serde_json::to_value(&payload).unwrap()["refresh_token"],
"prt-refresh-token"
);
}
#[cfg(feature = "broker")]
#[test]
fn p2p_user_jwt_header_matches_broker_shape() {
let prt = test_prt("prt-refresh-token");
let payload = P2PUserCertificatePayload::new(&prt, "nonce", "csr");
let ctx = [0xA5; 24];
let hmac_key = [0x5A; 32];
let jwt =
BrokerClientApplication::sign_p2p_user_jwt_with_key(&payload, &ctx, &hmac_key).unwrap();
let header = decode_jwt_header(&jwt);
let header = header.as_object().unwrap();
assert_eq!(header.len(), 3);
assert_eq!(header["alg"], "HS256");
assert_eq!(header["typ"], "JWT");
assert_eq!(
STANDARD
.decode(header["ctx"].as_str().unwrap())
.unwrap()
.len(),
24
);
assert!(!header.contains_key("kid"));
}
#[cfg(feature = "broker")]
#[test]
fn p2p_device_tenant_oid_uses_little_endian_guid() {
let raw = [
0x04, 0x81, 0x10, 0x37, 0xbb, 0x73, 0x69, 0xf8, 0x65, 0x0e, 0x44, 0xb3, 0x9e, 0xdd,
0x57, 0xda, 0x64, 0xf6, 0xcf,
];
assert_eq!(
BrokerClientApplication::tenant_id_from_device_certificate_oid(&raw).unwrap(),
TEST_TENANT_GUID
);
}
#[cfg(feature = "broker")]
#[test]
fn p2p_tenant_oid_accepts_all_encodings() {
let mut long_form = vec![0x04, 0x81, 0x10];
long_form.extend_from_slice(&TEST_TENANT_BYTES_LE);
let mut short_form = vec![0x04, 0x10];
short_form.extend_from_slice(&TEST_TENANT_BYTES_LE);
for raw in [long_form, short_form, TEST_TENANT_BYTES_LE.to_vec()] {
assert_eq!(
BrokerClientApplication::tenant_id_from_device_certificate_oid(&raw).unwrap(),
TEST_TENANT_GUID
);
}
}
#[cfg(feature = "broker")]
#[test]
fn p2p_device_tenant_id_absent_extension() {
let rsa = Rsa::generate(2048).unwrap();
let pkey = PKey::from_rsa(rsa).unwrap();
let cert_der = self_signed_cert(&pkey, &[]);
assert_eq!(
BrokerClientApplication::device_certificate_tenant_id(&cert_der).unwrap(),
None
);
}
#[cfg(feature = "broker")]
#[test]
fn p2p_device_tenant_id_extracts_from_extension() {
let rsa = Rsa::generate(2048).unwrap();
let pkey = PKey::from_rsa(rsa).unwrap();
let tenant_value = OctetString::new(TEST_TENANT_BYTES_LE)
.unwrap()
.to_der()
.unwrap();
let cert_der = self_signed_cert(&pkey, &[tenant_oid_extension(&tenant_value)]);
assert_eq!(
BrokerClientApplication::device_certificate_tenant_id(&cert_der).unwrap(),
Some(TEST_TENANT_GUID.to_string())
);
}
#[cfg(feature = "broker")]
#[test]
fn p2p_device_tenant_id_ignores_malformed_oid() {
let rsa = Rsa::generate(2048).unwrap();
let pkey = PKey::from_rsa(rsa).unwrap();
let cert_der = self_signed_cert(&pkey, &[tenant_oid_extension(b"contoso.onmicrosoft.com")]);
assert_eq!(
BrokerClientApplication::device_certificate_tenant_id(&cert_der).unwrap(),
None
);
}
#[cfg(feature = "broker")]
#[test]
fn p2p_tenant_endpoint_uses_current_authority_host() {
let app = test_broker_app("https://login.microsoftonline.com/common");
assert_eq!(
app.tenant_token_endpoint("11111111-1111-1111-1111-111111111111")
.unwrap(),
"https://login.microsoftonline.com/11111111-1111-1111-1111-111111111111/oauth2/token"
);
let app = test_broker_app("https://login.microsoftonline.us/organizations");
assert_eq!(
app.tenant_token_endpoint("contoso.com").unwrap(),
"https://login.microsoftonline.us/contoso.com/oauth2/token"
);
assert!(app.tenant_token_endpoint("../bad").is_err());
}
#[cfg(feature = "broker")]
#[test]
fn p2p_tenant_endpoint_rejects_empty_tenant() {
let app = test_broker_app("https://login.microsoftonline.com/common");
assert!(app.tenant_token_endpoint("").is_err());
}
#[cfg(feature = "broker")]
#[test]
fn p2p_csr_is_der_and_matches_key() {
let (mut tpm, machine_key) = test_tpm();
let loadable_key = tpm.rs256_create(&machine_key).unwrap();
let key = tpm.rs256_load(&machine_key, &loadable_key).unwrap();
let subject = Name::from_str("CN=test-device").unwrap();
let (csr_der, public_key_der) =
BrokerClientApplication::create_p2p_csr(&mut tpm, &key, subject).unwrap();
assert!(!csr_der.is_empty());
assert_eq!(public_key_der, tpm.rs256_public_der(&key).unwrap());
}
#[cfg(feature = "broker")]
#[test]
fn p2p_user_csr_accepts_blank_common_name() {
let (mut tpm, machine_key) = test_tpm();
let loadable_key = tpm.rs256_create(&machine_key).unwrap();
let key = tpm.rs256_load(&machine_key, &loadable_key).unwrap();
let subject = Name::from_str("CN=").unwrap();
let (csr_der, public_key_der) =
BrokerClientApplication::create_p2p_csr(&mut tpm, &key, subject).unwrap();
assert!(!csr_der.is_empty());
assert_eq!(public_key_der, tpm.rs256_public_der(&key).unwrap());
}
#[cfg(feature = "broker")]
fn test_private_key() -> P2PPrivateKey {
P2PPrivateKey::GeneratedUser(LoadableRS256Key::Soft2048V2 {
key: vec![],
tag: [0; 16],
iv: [0; 16],
})
}
#[cfg(feature = "broker")]
#[test]
fn p2p_response_parses_certificate_metadata() {
let rsa = Rsa::generate(2048).unwrap();
let pkey = PKey::from_rsa(rsa).unwrap();
let public_key_der = pkey.public_key_to_der().unwrap();
let cert_der = self_signed_cert(&pkey, &[]);
let cert = BrokerClientApplication::p2p_certificate_from_response(
&P2PCertificateResponse {
x5c: STANDARD.encode(&cert_der),
x5c_ca: STANDARD.encode(&cert_der),
},
test_private_key(),
&public_key_der,
)
.unwrap();
assert_eq!(cert.certificate_der, cert_der);
assert_eq!(cert.subject, "CN=p2p-test");
assert_eq!(cert.issuer, "CN=p2p-test");
assert_eq!(cert.thumbprint_sha1.len(), 40);
assert!(cert.not_after_unix > 0);
}
#[cfg(feature = "broker")]
#[test]
fn p2p_response_parses_ca_certificate() {
let rsa = Rsa::generate(2048).unwrap();
let pkey = PKey::from_rsa(rsa).unwrap();
let public_key_der = pkey.public_key_to_der().unwrap();
let cert_der = self_signed_cert(&pkey, &[]);
let ca_der = self_signed_cert(&pkey, &[]);
let cert = BrokerClientApplication::p2p_certificate_from_response(
&P2PCertificateResponse {
x5c: STANDARD.encode(&cert_der),
x5c_ca: STANDARD.encode(&ca_der),
},
test_private_key(),
&public_key_der,
)
.unwrap();
assert_eq!(cert.ca_certificate_der, ca_der);
assert_eq!(
cert.ca_certificate_pem,
Some(format!(
"-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
STANDARD.encode(&ca_der)
))
);
let cert = BrokerClientApplication::p2p_certificate_from_response(
&P2PCertificateResponse {
x5c: STANDARD.encode(&cert_der),
x5c_ca: "AQID".to_string(),
},
test_private_key(),
&public_key_der,
)
.unwrap();
assert_eq!(cert.ca_certificate_der, vec![0x01, 0x02, 0x03]);
assert_eq!(cert.ca_certificate_pem, None);
}
#[cfg(feature = "broker")]
#[test]
fn p2p_response_rejects_public_key_mismatch() {
let pkey = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
let other = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
let cert_der = self_signed_cert(&pkey, &[]);
assert!(BrokerClientApplication::p2p_certificate_from_response(
&P2PCertificateResponse {
x5c: STANDARD.encode(&cert_der),
x5c_ca: STANDARD.encode(&cert_der),
},
test_private_key(),
&other.public_key_to_der().unwrap(),
)
.is_err());
}
#[test]
fn user_token_spn_uses_upn_when_present() {
let access_token = build_access_token(
r#"{"amr":["pwd"],"tid":"11111111-1111-1111-1111-111111111111","upn":"user@example.com"}"#,
);
let token = build_user_token(access_token);
assert_eq!(token.spn().unwrap_or_default(), "user@example.com");
}
#[test]
fn user_token_spn_falls_back_to_unique_name() {
let access_token = build_access_token(
r#"{"amr":["pwd"],"tid":"11111111-1111-1111-1111-111111111111","unique_name":"alias@example.com"}"#,
);
let token = build_user_token(access_token);
assert_eq!(token.spn().unwrap_or_default(), "alias@example.com");
}
#[test]
fn user_token_spn_prefers_upn_when_both_fields_present() {
let access_token = build_access_token(
r#"{"amr":["pwd"],"tid":"11111111-1111-1111-1111-111111111111","upn":"primary@example.com","unique_name":"alias@example.com"}"#,
);
let token = build_user_token(access_token);
assert_eq!(token.spn().unwrap_or_default(), "primary@example.com");
}
fn build_mfa_auth_continue(
methods: Vec<MfaMethodInfo>,
skip_fido_for_mfa: bool,
) -> MFAAuthContinue {
let mfa_methods = methods.iter().map(|m| m.auth_method_id.clone()).collect();
MFAAuthContinue {
mfa_method_details: methods,
mfa_methods,
skip_fido_for_mfa,
..Default::default()
}
}
#[test]
fn mfa_prefers_sms_when_default_and_fido_is_cross_device_passkey() {
let methods = vec![
MfaMethodInfo {
auth_method_id: "OneWaySMS".to_string(),
display: "+X XXXXXXXX90".to_string(),
is_default: true,
},
MfaMethodInfo {
auth_method_id: "FidoKey".to_string(),
display: "MS Authenticator passkey".to_string(),
is_default: false,
},
MfaMethodInfo {
auth_method_id: "PhoneAppNotification".to_string(),
display: "Microsoft Authenticator".to_string(),
is_default: false,
},
];
let mfa = build_mfa_auth_continue(methods, true);
assert_eq!(mfa.mfa_method(), "OneWaySMS");
let default = mfa.get_default_mfa_method_details();
assert!(default.is_some(), "default MFA method should exist");
if let Some(default) = default {
assert_eq!(default.auth_method_id, "OneWaySMS");
assert!(default.is_default);
}
assert!(mfa.has_mfa_method("FidoKey"));
let fido = mfa.get_mfa_method_by_id("FidoKey");
assert!(fido.is_some(), "FidoKey method should exist");
if let Some(fido) = fido {
assert!(mfa.should_skip_fido_method(&fido));
}
assert_eq!(mfa.mfa_method_count(), 3);
}
#[test]
fn passwordless_fido_skipped_when_cross_device_passkey_and_sms_preferred() {
let options = vec![AuthOption::PasswordlessFido, AuthOption::Fido];
let result = should_attempt_passwordless_security_key(
&options, true, );
assert!(
result,
"Should attempt security key when FIDO params present and legacy flag enabled"
);
}
#[test]
fn security_key_skipped_when_no_fido_params() {
let options = vec![AuthOption::PasswordlessSecurityKey];
let result = should_attempt_passwordless_security_key(&options, false);
assert!(
!result,
"Should not attempt security key without FIDO params"
);
}
#[test]
fn security_key_skipped_when_not_enabled() {
let options = vec![AuthOption::Fido];
let result = should_attempt_passwordless_security_key(&options, true);
assert!(
!result,
"Should not attempt security key when not enabled in config"
);
}
#[test]
fn qr_bluetooth_attempted_when_cross_device_passkey() {
let options = vec![AuthOption::PasswordlessQrBluetooth];
let result = should_attempt_passwordless_qr_bluetooth(&options, true, true);
assert!(
result,
"Should attempt QR/Bluetooth when user has cross-device passkey"
);
}
#[test]
fn qr_bluetooth_skipped_when_no_cross_device_passkey() {
let options = vec![AuthOption::PasswordlessQrBluetooth];
let result = should_attempt_passwordless_qr_bluetooth(&options, true, false);
assert!(
!result,
"Should not attempt QR/Bluetooth when user has no cross-device passkey"
);
}
#[test]
fn qr_bluetooth_skipped_when_not_enabled() {
let options = vec![AuthOption::PasswordlessFido];
let result = should_attempt_passwordless_qr_bluetooth(&options, true, true);
assert!(
!result,
"Legacy PasswordlessFido should not enable QR/Bluetooth"
);
}
#[test]
fn both_flows_when_fido_params_and_cross_device() {
let options = vec![
AuthOption::PasswordlessSecurityKey,
AuthOption::PasswordlessQrBluetooth,
];
let security_key = should_attempt_passwordless_security_key(&options, true);
let qr_bluetooth = should_attempt_passwordless_qr_bluetooth(&options, true, true);
assert!(security_key, "Should attempt security key");
assert!(qr_bluetooth, "Should attempt QR/Bluetooth");
}
#[test]
fn prt_serialize_deserialize() {
let prt = PrimaryRefreshToken {
token_type: "Bearer".to_string(),
expires_in: "3600".to_string(),
ext_expires_in: "3600".to_string(),
expires_on: "9999999999".to_string(),
refresh_token: "refresh_token".to_string(),
refresh_token_expires_in: 3600,
session_key_jwe: None,
id_token: IdToken::default(),
client_info: ClientInfo::default(),
device_tenant_id: None,
tgt_on_prem: OnPremTgt::Absent {},
tgt_cloud: StructuredTgt::default(),
kerberos_top_level_names: None,
};
let prt_json = r#"
{
"token_type":"Bearer",
"expires_in":"3600",
"ext_expires_in":"3600",
"expires_on":"9999999999",
"refresh_token":"refresh_token",
"refresh_token_expires_in":3600,
"session_key_jwe":null,
"id_token":{
"name":"",
"oid":"",
"preferred_username":null,
"puid":null,
"tenant_region_scope":null,
"tid":""
},
"client_info":{
"uid":null,
"utid":null
},
"device_tenant_id":null,
"tgt_cloud":{
"clientKey":null,
"keyType":0,
"error":null,
"messageBuffer":null,
"realm":null,
"sn":null,
"cn":null,
"sessionKeyType":0,
"accountType":0
},
"kerberos_top_level_names":null
}
"#
.to_string()
.replace("\n", "")
.replace(" ", "");
let se = serde_json::to_string(&prt).expect("Failed to serialize");
assert_eq!(prt_json, se);
let de: PrimaryRefreshToken = serde_json::from_str(&se).expect("Falied to deserialize");
assert_eq!(prt, de);
}
#[test]
fn prt_serialize_deserialize_tgt_ad() {
let prt = PrimaryRefreshToken {
token_type: "Bearer".to_string(),
expires_in: "3600".to_string(),
ext_expires_in: "3600".to_string(),
expires_on: "9999999999".to_string(),
refresh_token: "refresh_token".to_string(),
refresh_token_expires_in: 3600,
session_key_jwe: None,
id_token: IdToken::default(),
client_info: ClientInfo::default(),
device_tenant_id: None,
tgt_on_prem: OnPremTgt::Structured {
tgt_ad: StructuredTgt {
client_key: Some("a".to_string()),
key_type: 18,
error: None,
message_buffer: Some("b".to_string()),
realm: Some("c".to_string()),
sn: Some("d".to_string()),
cn: Some("e".to_string()),
session_key_type: 18,
account_type: 1,
},
},
tgt_cloud: StructuredTgt::default(),
kerberos_top_level_names: None,
};
let prt_json = r#"
{
"token_type":"Bearer",
"expires_in":"3600",
"ext_expires_in":"3600",
"expires_on":"9999999999",
"refresh_token":"refresh_token",
"refresh_token_expires_in":3600,
"session_key_jwe":null,
"id_token":{
"name":"",
"oid":"",
"preferred_username":null,
"puid":null,
"tenant_region_scope":null,
"tid":""
},
"client_info":{
"uid":null,
"utid":null
},
"device_tenant_id":null,
"tgt_ad":{
"clientKey":"a",
"keyType":18,
"error":null,
"messageBuffer":"b",
"realm":"c",
"sn":"d",
"cn":"e",
"sessionKeyType":18,
"accountType":1
},
"tgt_cloud":{
"clientKey":null,
"keyType":0,
"error":null,
"messageBuffer":null,
"realm":null,
"sn":null,
"cn":null,
"sessionKeyType":0,
"accountType":0
},
"kerberos_top_level_names":null
}
"#
.to_string()
.replace("\n", "")
.replace(" ", "");
let se = serde_json::to_string(&prt).expect("Failed to serialize");
assert_eq!(prt_json, se);
let de: PrimaryRefreshToken = serde_json::from_str(&se).expect("Falied to deserialize");
assert_eq!(prt, de);
}
#[test]
fn prt_serialize_deserialize_tgt_message_buffer() {
let prt = PrimaryRefreshToken {
token_type: "Bearer".to_string(),
expires_in: "3600".to_string(),
ext_expires_in: "3600".to_string(),
expires_on: "9999999999".to_string(),
refresh_token: "refresh_token".to_string(),
refresh_token_expires_in: 3600,
session_key_jwe: None,
id_token: IdToken::default(),
client_info: ClientInfo::default(),
device_tenant_id: None,
tgt_on_prem: OnPremTgt::Raw(RawTgt {
tgt_message_buffer: "a".to_string(),
tgt_client_key: "b".to_string(),
tgt_key_type: 18,
}),
tgt_cloud: StructuredTgt::default(),
kerberos_top_level_names: None,
};
let prt_json = r#"
{
"token_type":"Bearer",
"expires_in":"3600",
"ext_expires_in":"3600",
"expires_on":"9999999999",
"refresh_token":"refresh_token",
"refresh_token_expires_in":3600,
"session_key_jwe":null,
"id_token":{
"name":"",
"oid":"",
"preferred_username":null,
"puid":null,
"tenant_region_scope":null,
"tid":""
},
"client_info":{
"uid":null,
"utid":null
},
"device_tenant_id":null,
"tgt_message_buffer":"a",
"tgt_client_key":"b",
"tgt_key_type":18,
"tgt_cloud":{
"clientKey":null,
"keyType":0,
"error":null,
"messageBuffer":null,
"realm":null,
"sn":null,
"cn":null,
"sessionKeyType":0,
"accountType":0
},
"kerberos_top_level_names":null
}
"#
.to_string()
.replace("\n", "")
.replace(" ", "");
let se = serde_json::to_string(&prt).expect("Failed to serialize");
assert_eq!(prt_json, se);
let de: PrimaryRefreshToken = serde_json::from_str(&se).expect("Falied to deserialize");
assert_eq!(prt, de);
}
#[test]
fn prt_deserialize_invalid() {
let se = r#"
{
"token_type":"Bearer",
"expires_in":"3600",
"ext_expires_in":"3600",
"expires_on":"9999999999",
"refresh_token":"refresh_token",
"refresh_token_expires_in":3600,
"session_key_jwe":null,
"id_token":{
"name":"",
"oid":"",
"preferred_username":null,
"puid":null,
"tenant_region_scope":null,
"tid":""
},
"client_info":{
"uid":null,
"utid":null
},
"device_tenant_id":null,
"tgt_message_buffer":"a",
"tgt_client_key":"b",
"tgt_key_type":18,
"tgt_ad":{
"clientKey":"a",
"keyType":18,
"error":null,
"messageBuffer":"b",
"realm":"c",
"sn":"d",
"cn":"e",
"sessionKeyType":18,
"accountType":1
},
"tgt_cloud":{
"clientKey":null,
"keyType":0,
"error":null,
"messageBuffer":null,
"realm":null,
"sn":null,
"cn":null,
"sessionKeyType":0,
"accountType":0
},
"kerberos_top_level_names":null
}
"#
.to_string()
.replace("\n", "")
.replace(" ", "");
let de = serde_json::from_str::<PrimaryRefreshToken>(&se);
assert!(de.is_err());
}
}