Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] AcquireTokenSilent silently discards MFA claim? #4908

Open
S-dn-Y opened this issue Aug 26, 2024 · 7 comments
Open

[Bug] AcquireTokenSilent silently discards MFA claim? #4908

S-dn-Y opened this issue Aug 26, 2024 · 7 comments

Comments

@S-dn-Y
Copy link

S-dn-Y commented Aug 26, 2024

Library version used

4.61.3.0

.NET version

4.8.04161

Scenario

PublicClient - desktop app

Is this a new or an existing app?

This is a new app or experiment

Issue description and reproduction steps

Hi everyone,

I'm integrating the MSAL library into our PowerShell code and I've observed that AcquireTokenSilent seems to silently discard MFA claims.
I'm adding the extra query parameters to the request, which are applied as shown in the logs. The application is new, also as shown in the logs.
The token is provided without issues, but if I check the actual token using https://jwt.ms/, the amr claim is not in there, only the pwd and rsa values for amr.
Using the token results in MFA issues of course.
The doesn't seem to comply with what the docs are saying:

#Exceptions
MsalUiRequiredException
will be thrown in the case where an interaction is required with the end user of the application, for instance, if no refresh token was in the cache, or the user needs to consent, or re-sign-in (for instance if the password expired), or the user needs to perform two factor authentication

Since the recommended pattern is to first try to achieve a token silently from cache before starting an interactive flow, shouldn't AcquireTokenSilent throw an exception of type Microsoft.Identity.Client.MsalUiRequiredException allowing the user to complete the interactive flow?

Relevant log entries:
###> New application nothing found in cache yet

  • [GetAccounts] Found 0 RTs and 0 accounts in MSAL cache.

###> Query parameters are added to the request

  • [2024-08-26 18:38:46Z] [MSAL:0005] INFO ModifyAndValidateAuthParameters:219 Additional query parameter added successfully. Key: 'claims' Value: '{"access_token" : {"amr": { "values": ["mfa"] }}}'

Is this expected behaviour?
I've added some of the code I'm using the reproduce it.
Note that the same code is used in the interactive flow and the user is challenged for MFA and the claim is given.
Now I don't expect that the silent flow is able to complete the MFA challenge, but if MSAL silently discards the claim, how would I know that a cached token has the claim? Especially since decoding the tokens on the client isn't best practice (if I recall reading it correctly).

Hoping someone can help! Any effort is appreciated.

Best regards,
Sidney

Relevant code snippets

Import-Module -Name Az.Accounts -MinimumVersion 3.0.3 # Loads mentioned MSAL.NET
[String]$ClientId = '<clientId>' # ClientId of App Registration with some graph permissions applied
[String]$RedirectUri = 'http://localhost'
[String]$Authority = 'https://login.microsoftonline.com/<tentantId>'
[Bool]$ValidateAuthority = $false
[System.Collections.Arraylist]$Scopes = [System.Collections.Arraylist]::new(@('https://graph.microsoft.com/.default'))

# Create PublicClientApplicationBuilder object and add requested parameters
[Microsoft.Identity.Client.PublicClientApplicationBuilder]$appBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId)
$appBuilder.WithRedirectUri($RedirectUri) | Out-Null
$appBuilder.WithAuthority($Authority, $ValidateAuthority) | Out-Null

# Add the WAM broker (recommended for interactive authentication on Windows platforms)
[Microsoft.Identity.Client.BrokerOptions]$brokerOptions = [Microsoft.Identity.Client.BrokerOptions]::new("Windows")
[Microsoft.Identity.Client.Broker.BrokerExtension]::WithBroker($appBuilder, $brokerOptions) | Out-Null
# Now build the application
[Microsoft.Identity.Client.IPublicClientApplication]$app = $appBuilder.Build()
Write-Verbose "Done." -Verbose

# Add requested scopes to a generic list
[Collections.Generic.List[string]]$scopeList = [Collections.Generic.List[string]]::new()
foreach ($scope in $Scopes) {
   $scopeList.Add($scope) | Out-Null
}

# If the application was already logged into we'll use the account from cache
[Microsoft.Identity.Client.IAccount]$loginAccount = $app.GetAccountsAsync().GetAwaiter().GetResult() | Select-Object -First 1

# And if the account wasn't found we'll use the currently logged in user
if (-not($loginAccount)) {

    # [Microsoft.Identity.Client.PublicClientApplication]::OperatingSystemAccount represent the currently logged in account
    $loginAccount = [Microsoft.Identity.Client.PublicClientApplication]::OperatingSystemAccount
    Write-Verbose "Account wasn't found in cache, using currently logged in user." -Verbose
}
else {
                            
    Write-Verbose "Found account in cache." -Verbose
}

# Now construct parameter builder
[Microsoft.Identity.Client.AcquireTokenSilentParameterBuilder]$tokenRequest = $app.AcquireTokenSilent($scopeList, $loginAccount)

# Add MFA claim
[System.Collections.Generic.Dictionary[[string],[string]]]$extraQueryParams = [System.Collections.Generic.Dictionary[[string],[string]]]::new()
$extraQueryParams.Add('claims','{"access_token" : {"amr": { "values": ["mfa"] }}}')
$tokenRequest.WithExtraQueryParameters($extraQueryParams) | Out-Null

Write-Verbose "Trying to acquire a token silently." -Verbose
Write-Verbose "Request will time out after 20 seconds." -Verbose
[System.Threading.CancellationTokenSource]$cancellationTokenSourceSilent = [System.Threading.CancellationTokenSource]::new(20000) # TimeOut in milliseconds
[System.Threading.CancellationToken]$cancellationTokenSilent = $cancellationTokenSourceSilent.Token
$cancellationTokenSilent.ThrowIfCancellationRequested()
[Microsoft.Identity.Client.AuthenticationResult]$authenticationResult = $null
$authenticationResult = $tokenRequest.ExecuteAsync($cancellationTokenSilent).GetAwaiter().GetResult()
Write-Verbose "Succesfully retrieved token." -Verbose

Expected behavior

I would expect that AcquireTokenSilent throws an MsalUiRequiredException, instead of requesting the token without the MFA claim in the result.

Identity provider

Microsoft Entra ID (Work and School accounts and Personal Microsoft accounts)

Regression

No response

Solution and workarounds

I'm now removing the application from the cache if any error happens while requesting a token, so I know that every application in cache that has the MFA claim will have the MFA claim after silently acquiring it from the cache.

@S-dn-Y S-dn-Y added needs attention Delete label after triage untriaged Do not delete. Needed for Automation labels Aug 26, 2024
@bgavrilMS bgavrilMS added requires more info and removed untriaged Do not delete. Needed for Automation needs attention Delete label after triage labels Aug 28, 2024
@bgavrilMS
Copy link
Member

Does this code work if you don't use WAM?

I am not familiar with adding a claims challenge for forcing MFA. Is this documented anywhere? Afaik, the STS is responsible for enforcing MFA through Conditional Access policies.

@S-dn-Y
Copy link
Author

S-dn-Y commented Aug 28, 2024

Hi @bgavrilMS,

Thanks for the reply.
Interesting view, honestly I'm not sure, I found this method in several modules on the PowerShell Gallery and online resources, but no MS owned ones as far as I know (for example, search for ExtraQueryParameters shows the usage):

As for without the WAM broker, good point.
I'm now using the code in production without the WAM broker (yet) and the silent method fails regardless if the account is not cached yet, as there's nothing to silently acquire.
With WAM this is different as WAM is able to silently sign in the user, without having a cached application.
Expanded my test a bit to cache the application (using the interactive flow) first without the ExtraQueryParameters added and then try to silently acquire it, with the ExtraQueryParameters as sepicfied in the code added. All without WAM involved.

Same behaviour though, the token is granted through the interactive flow, without being challenged with MFA, then the application and token is cached and the interactive flow (with the MFA claim added in the ExtraQueryParameters) acquires the token without asking for anything, but ignores that MFA claim.

Hopefully this helps. Please let me know if there's anything I can help you with.

Best regards,
Sidney

@bgavrilMS
Copy link
Member

Well MSAL does have a "WithClaims" API, which is probably better used here as it will affect the communication with both /authorization and /token endpoint. But it'll also bypass the cache, so it should not be used after the first login.

@S-dn-Y
Copy link
Author

S-dn-Y commented Aug 28, 2024

Thank you @bgavrilMS.
Reading in on the method, also tried converting my ExtraQueryParameters to a WithClaims method:
$tokenRequestSilent.WithClaims('{"access_token" : {"amr": { "values": ["mfa"] }}}') | Out-Null
also tried
$tokenRequestSilent.WithClaims('{"amr": { "values": ["mfa"] }}') | Out-Null
Doesn't seem to do the trick though, still ignored.
The docs point out that I would want to add any claims that are returned with the error to this method while calling AcquireTokenInteractive (eg $tokenRequestInteractive.WithClaims($exception.Claims)), but I'm not getting an error as MFA is not enforced on my account but enforced on specific endpoints/resources I sign in with the token.
I may be using the WithClaims string incorrectly but wasn't able to find any examples as of now on how it should be formatted.

Thanks again!!
Best regards,
Sidney

@xinyuxu1026
Copy link

xinyuxu1026 commented Aug 29, 2024

Hi @S-dn-Y , have you configured the account with MFA? Here is the doc https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa?toc=%2Fentra%2Fidentity%2Fconditional-access%2Ftoc.json&bc=%2Fentra%2Fidentity%2Fconditional-access%2Fbreadcrumb%2Ftoc.json, could you help confirm and which resource you added the MFA policy?

Note that the same code is used in the interactive flow and the user is challenged for MFA and the claim is given.

so previously the interactive call is challenged with claims

Same behaviour though, the token is granted through the interactive flow, without being challenged with MFA,

but later the interactive flow doesn't challenge you MFA? Did you change anything?

I am trying to understand what you want to achieve, please correct me if I am wrong. Do you want both interactive and silent flow challenged with MFA with the specific resource? Could you provide the correlation id and timestamp so that we can check what's the issue? Thanks.

@S-dn-Y
Copy link
Author

S-dn-Y commented Aug 30, 2024

Hi @xinyuxu1026,

Thank you chipping in.
I may have left some information out on how I'm trying to use it exactly, let me try to elaborate :)

I'm trying to activate Eligible Microsoft Entra Privileged Identity Management (PIM) roles through code, PowerShell.
To do so, I've created some PowerShell functions around Get-AzRoleEligibilitySchedule and New-AzRoleAssignmentScheduleRequest (for Azure Resource roles) and created some custom functions around the Graph REST API (for Entra ID roles) because the Graph module needed for activating them loaded painfully slow.

I've created a Service Principal in Azure with delegated permissions needed to query and activate the roles.
I then use the Service Principal as Client Id in my MSAL code to request a token and thus get a token with the needed scopes added. Activated by either using 'https://graph.microsoft.com/.default' for Entra roles to get all Graph Scopes assigned to my Service principal or 'https://management.azure.com/user_impersonation' to get the needed rights for Azure Resources (but I only activate one per time since of course since combining the both resources is not supported).

So when I want to enable a Entra role the flow would look like this:

  1. Invoke function, Get-GPIMEntraIDRoleEligibleAssigment
  2. Function Get-GPIMEntraIDRoleEligibleAssigment invokes my MSAL function of which snippets were added to get a token using the mentioned Service Principal.
  3. Since retrieving eligible roles doesn't require MFA on the Entra ID side the token is requested without the ExtraQueryParameters for MFA added.
  4. The token is requested using the AcquireTokenSilent method and it succeeds cause the user is trying to sign in to the same tenant the device is Hybrid Joined into and the MSAL function uses WAM.
  5. The user gets a menu presented with eligible Entra roles and selects one which invokes another function: Enable-GPIMEntraIDRoleEligibleAssigment.
  6. Since I'm not sure if the role the user tries to activate requires an MFA challenge I request another MSAL token with the mentioned ExtraQueryParameters added.
  7. The MSAL function finds the application in cache and uses it, adds the ExtraQueryParameters to the AcquireTokenSilent method and invokes it.
  8. The AcquireTokenSilent method succeeds, the token is returned and used to activate the role against the Graph REST API. This fails however because the token doesn't include the MFA claim. Which https://jwt.ms/ acknowledges if I take a peek in the token.

Note that if I modify the flow and feed the ExtraQueryParameters to AcquireTokenInteractive (and thus skip the first AcquireTokenSilent) it prompts for credentials and challenges the user for MFA and the claim is added. The functions will then work without problems as the MFA claim is present.
Also subsequent call to AcquireTokenSilent will work because the MFA claim was added during my first call.
I would, however, expect AcquireTokenSilent to throw an error if it cannot silently provide a token that fulfils the extra query parameters.

I've just completed the flow as described.
Step 1-4 (quering roles without MFA ExtraQueryParameters): CorrelationId: 9c41d4cc-e154-4eaf-91dd-b89517239827 (the console with the log got truncated by screen resizing before I got to capture the timestamp, somewhat before the time mentioned below)
Step 5-8 (Activating role with MFA ExtraQueryParameters): CorrelationId: 7ecbf69a-fd3c-489e-bef1-69c6140d4a37 - Timestamp: 2024-08-30 08:05:05Z

Error triggered:
Could not activate the 'Application Administrator' PIM role: {"error":{"code":"RoleAssignmentRequestPolicyValidationFailed","message":"The following policy rules failed:
["MfaRule"]",...

Hope this helps and thanks again!
Best regards,
Sidney

@xinyuxu1026
Copy link

@S-dn-Y , for account not configured with MFA, the silent request discard MFA is by design.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

11 participants
@bgavrilMS @S-dn-Y @iulico-1 @xinyuxu1026 and others