Skip to content

msal net 4.1

jennyf19 edited this page Jun 28, 2019 · 35 revisions

MSAL.NET 4.1 to be released

We are excited to release an incremental update of MSAL.NET addressing some of the issues you raised, and bringing:

Improved experience with the system web browser (on .NET Core)

From MSAL.NET 4.0.0, you have been able to use the interactive token acquisition with .NET Core, by delegating the sign-in and consent part to the system Web browser on your machine. MSAL.NET 4.1, brings improvements to this experience by helping you run a specific browser if you wish, and by giving you ways to decide what to display to the user in case of a successful authentication, and in case of failure

MSAL.NET 4.1 adds a new class named SystemWebViewOptions which enables you to specify:

  • the URI to navigate to (BrowserRedirectError), or the HTML fragment to display (HtmlMessageError) in case of sign-in / consent errors in the System web browser
  • the URI to navivate to (BrowserRedirectSuccess), or the HTML fragment to display (HtmlMessageSuccess) in case of successful sign-in / consent.
  • the action to run to start the system browser. For this you cnan provide your own implementation by setting the OpenBrowserAsync delegate. The class also provides a default implementation for two browsers: OpenWithEdgeBrowserAsync and OpenWithChromeEdgeBrowserAsync, respectively for Microsoft Edge and Edge on Chromium.
public class SystemWebViewOptions
{
 public SystemWebViewOptions();
 public Uri BrowserRedirectError { get; set; }
 public Uri BrowserRedirectSuccess { get; set; }
 public string HtmlMessageError { get; set; }
 public string HtmlMessageSuccess { get; set; }
 public Func<Uri, Task> OpenBrowserAsync { get; set; }
 public static Task OpenWithChromeEdgeBrowserAsync(Uri uri);
 public static Task OpenWithEdgeBrowserAsync(Uri uri);
}

To use this structure you can write something like the following:

IPublicClientApplication app;
...

options = new SystemWebViewOptions
{
 HtmlMessageError = "<b>Sign-in failed. You can close this tab ...</b>",
 BrowserRedirectSuccess = "https://contoso.com/help-for-my-awesome-commandline-tool.html"
};

var result = app.AcquireTokenInteractive(scopes)
                .WithEmbeddedWebView(false)       // The default in .NET Core
                .WithSystemWebViewOptions(options)
                .Build();

New Classification property on MsalUiRequiredException enables you to provide a better User experience in your apps

What was the problem?

One of common status codes returned from MSAL.NET when calling AcquireTokenSilent() in MsalError.InvalidGrantError. This status code means that the application should call the authentication library again, but in interactive mode (AcquireTokenInteractive or AcquireTokenByDeviceCodeFlow for public client applications, and do a challenge in Web apps). This is because additional user interaction is required before authentication token can be issued.

The interaction aims at having the user do an action. Some of those conditions are easy for users to resolve (e.g. accept Terms of Use with a single click), and some cannot be resolved with the current configuration (e.g. the machine in question needs to connect to a specific corporate network). Some help the user setting-up Multi-factor authentication, or install authenticator on their device.

Depending on how complicated and involved required user interaction is, apps may want to show different user experience for different levels of difficulty of user interaction. For example, if the app is trying to show multiple resources to the user at the same time (e.g. items in a collection returned by a search result), the app may choose to not display specific results for which authentication flow to resolve InvalidGrant condition is too intrusive, but may choose to enable resolution of InvalidGrant condition if resolution is quick and simple. In the same idea, they might choose to batch the resolution.

Until MSAL.NET 4.1, applications did not have a way to distinguish between different classes of conditions that cause InvalidGrant, and therefore could only have a very generic way to handle this state, which leads to end user confusion and user experience dead ends in some of application flows.

For full details see Issue #1148

The Classification property

Classification Meaning Recommended handling
basic_action Condition can be resolved by user interaction during the interactive authentication flow. Call AcquireTokenInteractively().
additional_action Condition can be resolved by additional remedial interaction with the system, outside of the interactive authentication flow. Call AcquireTokenInteractively() to show a message that explains the remedial action. Calling application may choose to hide flows that require additional_action if the user is unlikely to complete the remedial action.
message_only Condition cannot be resolved at this time. Launching interactive authentication flow will show a message explaining the condition. Call AcquireTokenInteractively() to show a message that explains the condition. AcquireTokenInteractively() will return UserCanceled error after the user reads the message and closes the window. Calling application may choose to hide flows that result in message_only if the user is unlikely to benefit from the message.
consent_required User consent is missing, or has been revoked. Call AcquireTokenInteractively() for user to give consent.
user_password_expired User's password has expired. Call AcquireTokenInteractively() so that user can reset their password.
[empty string] Condition may be resolved by user interaction during the interactive authentication flow. Call AcquireTokenInteractively().

Code snippet using the Classification property

AuthenticationResult res;
try
{
 res = await application.AcquireTokenSilent(scopes, account)
                        .ExecuteAsync();
}
catch (MsalUiRequiredException ex) when (ex.ErrorCode == MsalError.InvalidGrantError)
{
switch (ex.Classification)
{
 case MsalUiRequiredException.MessageOnly:
 // You might want to call AcquireTokenInteractive() to show a message that explains the condition. 
 // AcquireTokenInteractive() will return UserCanceled error after the user reads the message 
 // and closes the window. 
 // Calling application may choose to hide flows that result in message_only if the user is unlikely to benefit from the message
 try
 {
  res = await application.AcquireTokenInteractive(scopes)
                         .ExecuteAsync();
 }
 catch (MsalClientException ex2) when (ex2.ErrorCode == MsalError.AuthenticationCanceledError)
 {
  // Do nothing. The user has seen the message
 }
 break;

 case MsalUiRequiredException.AdditionalAction:
 // You might want to call AcquireTokenInteractive() to show a message that explains the remedial action. 
 // The calling application may choose to hide flows that require additional_action if the user 
 // is unlikely to complete the remedial action (even if this means a degraded experience)

 case MsalUiRequiredException.BasicAction:
 // Call AcquireTokenInteractive()

 case MsalUiRequiredException.ConsentRequired:
 // Call AcquireTokenInteractive() for user to give consent.

 case MsalUiRequiredException.UserPasswordExpired:
 // Call AcquireTokenInteractive() so that user can reset their password

 case "":
 // Condition may be resolved by user interaction during the interactive authentication flow.

 res = await application.AcquireTokenInteractive(scopes)
                                                .ExecuteAsync();
 break;

 default:
 break;
}

Confidential client applications now support client assertions

New capabilities for confidential client applications to prove their identity

In order to prove their identity, confidential client applications exchange a secret with Azure AD. This can be a:

  • a client secret (application password),
  • a certificate, which is really used to build a signed assertion containing standard claims. This can also be a signed assertion directly.

MSAL.NET 4.1 adds a new capabilities for this advanced scenario: in addition to .WithClientSecret() and .WithCertificate(), it now provides three new methods: .WithSignedAssertion(), .WithClientClaims() and .WithClientAdditionalClaims()

WithSignedAssertion

The first method takes a signed client assertion. It's expected to be a Base64 encoding of a JWT token which needs to contain mandatory claims. To use it:

string signedClientAssertion = ComputeAssertion();
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
                                          .WithClientAssertion(signedClientAssertion)
                                          .Build();

The claims expected by Azure AD are:

Claim type Value Description
aud https://login.microsoftonline.com/{tenantId}/v2.0 The "aud" (audience) claim identifies the recipients that the JWT is intended for (here Azure AD) See [RFC 7519, Section 4.1.3]
exp Thu Jun 27 2019 15:04:17 GMT+0200 (Romance Daylight Time) The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. See [RFC 7519, Section 4.1.4]
iss {ClientID} The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The "iss" value is a case-sensitive string containing a StringOrURI value. [RFC 7519, Section 4.1.1]
jti (a Guid) The "jti" (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The "jti" claim can be used to prevent the JWT from being replayed. The "jti" value is a case-sensitive string. [RFC 7519, Section 4.1.7]
nbf Thu Jun 27 2019 14:54:17 GMT+0200 (Romance Daylight Time) The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. [RFC 7519, Section 4.1.5]
sub {ClientID} The "sub" (subject) claim identifies the subject of the JWT. The claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The See [RFC 7519, Section 4.1.2]

WithAdditionalClientClaims

WithAdditionalClientClaims(X509Certificate2 certificate, IDictionary<string, string> additionalClaims) will produce for you signed assertion containing the claims expected by Azure AD plus additional client claims that you want to send. Here is a code snippet on how to do that.

string ipAddress = "192.168.1.2";
X509Certificate2 certificate = ReadCertificate(config.CertificateName);
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
                                          .WithAuthority(new Uri(config.Authority))
                                          .WithAdditionalClientClaims(certificate, 
                                                                      new Dictionary<string, string> { { "client_ip", ipAddress } })
                                          .Build();

If one of the claims name in the dictionnary is the same as one of the mandatory claims, the additional claims's value won't be taken into account (it's overriden by the claims computed by MSAL.NET)

WithClientClaims

If you want to provide yourself claims, including the mandatory claims expected by Azure AD, you can use the last method WithClientClaims. This time you are on your own and you'll need to provide all the values for aud, exp, iss, jti, nbf and sub,

X509Certificate2 certificate = ReadCertificate(config.CertificateName);
app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
                                          .WithAuthority(new Uri(config.Authority))
                                          .WithClientClaims(certificate, 
                                                            dictionaryOfClaimsToSign)
                                          .Build();

Better control over Correlation IDs

Correlation IDs are used in logging, and help troubleshooting issues. When a transaction is starting at the front door of a service (Gateway), it gets a correlation ID, this ID is preserved through the calls to MSAL, Azure AD, down inside it sub components, and back. Each of the components knows the correlation ID coming into it, preserves it in their logs and then passes it on when it delegates to other components.

MSAL.NET was so far generating a correlation ID. But if the application developer wanted to create their own Correlation ID, and pass it to MSAL.NET, this was not yet possible. This is, in particular, imlportant in Web APIs calling downstream APIS. Then we did not surface the Correlation ID other than in the logs, so if the application wanted to access it it had basically to parse a log line.

Starting from MSAL.NET 4.1:

  • AuthenticationResult now has a property named CorrelationId. It's a Guid returning the correlation ID used for the authentication request. This fullfil the need for apps developer to know the correlation ID.

    AuthenticationResult result = await ....
    Guid usedCorrelationId = result.CorrelationId
  • There is now an API (WithCorrelationId(Guid)), applicable on any AcquireTokenXXX(), to set the correlation ID. This fullfils the need for service developers who themselve receive a CorrelationID and want the token acquisition to be part of the same transaction.

    result = await AcquireTokenXX(scopes)
                   .WithCorrelationId(correlationId)
                   .ExecuteAsync();

Bug fixes

  • When using the ConfidentialClientApplicationOptions and including, for example Instance = "https://login.microsoftonline.com/", MSAL.NET was concatenating the double-slash. MSAL.NET will now check for a trailing slash and remove it. There is no action needed on the part of the developer. See [#1196] for details.
  • When using ADFS 2019, if no login-hint was included in the call, a null ref was thrown. See [#1214] for details.
  • On iOS, for certain older auth libraries, sharing the cache with MSAL.NET, there was an issue with null handling in json. The json serializer in MSAL.NET no longer writes values to json for which the values are null, this is especially important for foci_id. See [#1189] and [#1176] for details.
  • When using .WithCertificate() and /common/ as the authority in a confidential client flow, MSAL.NET was creating the aud claim of the client assertion as "https://login.microsoftonline.com/{tenantid}/v2.0". Now, MSAL.NET will honor both a tenant specific authority and common or organizations when creating the aud claim. [#891]

Getting started with MSAL.NET

Acquiring tokens

Desktop/Mobile apps

Web Apps / Web APIs / daemon apps

Advanced topics

News

FAQ

Other resources

Clone this wiki locally