Search code examples
c#.netexchange-server

How can i load a user token required for outlook POP/IMAP using Microsoft.Identity.Client


I do have a windows service which reads email (IMAP) without any user interaction. Microsoft is deprecating POP/IMAP basic authentication for exchange online.

Based on the documentation from Microsoft, the MSAL client library should be used for getting a user token. enter image description here

I try to get the user token using

result = await app.AcquireTokenByUsernamePassword(scopes, USERNAME, securePassword).ExecuteAsync();

But this only returns AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'

Questsion(s)

Im not exactly sure where i am wrong.

  • Do i need to provide a client_secret or is this something which can be configured on the user in Azure AD?
  • Is the ClientID an Application i need to create on Azure or is this the ClientID of my exchange online?
    • If it is the ID of exchange online, where can i get this ID?
  • Is there a good example for loading a token for an exchange online user?
  • Is it even possible to get a token without programming?
    • e.g. via exchange online website

Full working example:

using Microsoft.Identity.Client;
using System;
using System.Linq;
using System.Security;
using System.Threading.Tasks;

namespace MSALTest
{
    /// <summary>
    /// Sample application for loading a token for an outlook account
    /// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Username-Password-Authentication
    /// </summary>
    internal class Program
    {
        //     URL of the security token service (STS) from which MSAL.NET will acquire the
        //     tokens. Usual authorities endpoints for the Azure public Cloud are:
        //     • https://login.microsoftonline.com/tenant/ where tenant is the tenant ID of
        //     the Azure AD tenant or a domain associated with this Azure AD tenant, in order
        //     to sign-in users of a specific organization only
        //     • https://login.microsoftonline.com/common/ to sign-in users with any work and
        //     school accounts or Microsoft personal account
        //     • https://login.microsoftonline.com/organizations/ to sign-in users with any
        //     work and school accounts
        //     • https://login.microsoftonline.com/consumers/ to sign-in users with only personal
        //     Microsoft accounts (live)
        //     Note that this setting needs to be consistent with what is declared in the application
        //     registration portal
        const string AUTHORITY = "https://login.microsoftonline.com/<my tenantn>/";

        //     Client ID (also known as App ID) of the application as registered in the application
        //     registration portal (https://aka.ms/msal-net-register-app)/.
        const string CLIENTID = "<my clientid>";

        //     Identifier of the user application requests token on behalf. Generally in UserPrincipalName
        //     (UPN) format, e.g. [email protected]
        const string USERNAME = "<my user>";


        static async Task GetTokenTest()
        {
            string[] scopes = new string[] { "user.read" };
            IPublicClientApplication app;
            app = PublicClientApplicationBuilder.Create(CLIENTID)
                                              .WithAuthority(AUTHORITY)
                                              .Build();
            var accounts = await app.GetAccountsAsync();

            AuthenticationResult result = null;
            if (accounts.Any())
            {
                result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync();
            }
            else
            {
                var securePassword = GetPassword();
                result = await app.AcquireTokenByUsernamePassword(scopes, USERNAME, securePassword).ExecuteAsync();
            }
            Console.WriteLine(result.Account.Username);
        }


        static async Task Main(string[] args)
        {
            try
            {
                await GetTokenTest();
            }
            catch (MsalUiRequiredException ex) when (ex.Message.Contains("AADSTS65001"))
            {
                // Here are the kind of error messages you could have, and possible mitigations

                // ------------------------------------------------------------------------
                // MsalUiRequiredException: AADSTS65001: The user or administrator has not consented to use the application
                // with ID '{appId}' named '{appName}'. Send an interactive authorization request for this user and resource.

                // Mitigation: you need to get user consent first. This can be done either statically (through the portal), 
                /// or dynamically (but this requires an interaction with Azure AD, which is not possible with 
                // the username/password flow)
                // Statically: in the portal by doing the following in the "API permissions" tab of the application registration:
                // 1. Click "Add a permission" and add all the delegated permissions corresponding to the scopes you want (for instance
                // User.Read and User.ReadBasic.All)
                // 2. Click "Grant/revoke admin consent for <tenant>") and click "yes".
                // Dynamically, if you are not using .NET Core (which does not have any Web UI) by 
                // calling (once only) AcquireTokenInteractive.
                // remember that Username/password is for public client applications that is desktop/mobile applications.
                // If you are using .NET core or don't want to call AcquireTokenInteractive, you might want to:
                // - use device code flow (See https://aka.ms/msal-net-device-code-flow)
                // - or suggest the user to navigate to a URL to consent: https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&scope=user.read
                // ------------------------------------------------------------------------

                // ------------------------------------------------------------------------
                // ErrorCode: invalid_grant
                // SubError: basic_action
                // MsalUiRequiredException: AADSTS50079: The user is required to use multi-factor authentication.
                // The tenant admin for your organization has chosen to oblige users to perform multi-factor authentication.
                // Mitigation: none for this flow
                // Your application cannot use the Username/Password grant.
                // Like in the previous case, you might want to use an interactive flow (AcquireTokenInteractive()), 
                // or Device Code Flow instead.
                // Note this is one of the reason why using username/password is not recommended;
                // ------------------------------------------------------------------------

                // ------------------------------------------------------------------------
                // ex.ErrorCode: invalid_grant
                // subError: null
                // Message = "AADSTS70002: Error validating credentials.
                // AADSTS50126: Invalid username or password
                // In the case of a managed user (user from an Azure AD tenant opposed to a
                // federated user, which would be owned
                // in another IdP through ADFS), the user has entered the wrong password
                // Mitigation: ask the user to re-enter the password
                // ------------------------------------------------------------------------

                // ------------------------------------------------------------------------
                // ex.ErrorCode: invalid_grant
                // subError: null
                // MsalServiceException: ADSTS50034: To sign into this application the account must be added to 
                // the {domainName} directory.
                // or The user account does not exist in the {domainName} directory. To sign into this application, 
                // the account must be added to the directory.
                // The user was not found in the directory
                // Explanation: wrong username
                // Mitigation: ask the user to re-enter the username.
                // ------------------------------------------------------------------------$
                Console.WriteLine($"Exception: invalid_grant. {ex.Message}");
            }
            catch (MsalServiceException ex) when (ex.ErrorCode == "invalid_request")
            {
                // ------------------------------------------------------------------------
                // AADSTS90010: The grant type is not supported over the /common or /consumers endpoints. 
                // Please use the /organizations or tenant-specific endpoint.
                // you used common.
                // Mitigation: as explained in the message from Azure AD, the authority you use in the application needs 
                // to be tenanted or otherwise "organizations". change the
                // "Tenant": property in the appsettings.json to be a GUID (tenant Id), or domain name (contoso.com) 
                // if such a domain is registered with your tenant
                // or "organizations", if you want this application to sign-in users in any Work and School accounts.
                // ------------------------------------------------------------------------
                Console.WriteLine($"Exception: invalid_request. {ex.Message}");
            }
            catch (MsalServiceException ex) when (ex.ErrorCode == "unauthorized_client")
            {
                // ------------------------------------------------------------------------
                // AADSTS700016: Application with identifier '{clientId}' was not found in the directory '{domain}'.
                // This can happen if the application has not been installed by the administrator of the tenant or consented 
                // to by any user in the tenant.
                // You may have sent your authentication request to the wrong tenant
                // Cause: The clientId in the appsettings.json might be wrong
                // Mitigation: check the clientId and the app registration
                // ------------------------------------------------------------------------
                Console.WriteLine($"Exception: unauthorized_client. {ex.Message}");
            }
            catch (MsalServiceException ex) when (ex.ErrorCode == "invalid_client")
            {
                // ------------------------------------------------------------------------
                // AADSTS70002: The request body must contain the following parameter: 'client_secret or client_assertion'.
                // Explanation: this can happen if your application was not registered as a public client application in Azure AD
                // Mitigation: in the Azure portal, edit the manifest for your application and set the `allowPublicClient` to `true`
                // ------------------------------------------------------------------------
                Console.WriteLine($"Exception: invalid_client. {ex.Message}");
            }
            catch (MsalServiceException ex)
            {
                Console.WriteLine($"Exception: MsalServiceException. {ex.Message}");
            }
            catch (MsalClientException ex) when (ex.ErrorCode == "unknown_user_type")
            {
                // Message = "Unsupported User Type 'Unknown'. Please see https://aka.ms/msal-net-up"
                // The user is not recognized as a managed user, or a federated user. Azure AD was not
                // able to identify the IdP that needs to process the user
                Console.WriteLine($"U/P: Wrong username. {ex.Message}");
            }
            catch (MsalClientException ex) when (ex.ErrorCode == "user_realm_discovery_failed")
            {
                // The user is not recognized as a managed user, or a federated user. Azure AD was not
                // able to identify the IdP that needs to process the user. That's for instance the case
                // if you use a phone number
                Console.WriteLine($"user_realm_discovery_failed. {ex.Message}");
            }
            catch (MsalClientException ex) when (ex.ErrorCode == "unknown_user")
            {
                // the username was probably empty
                // ex.Message = "Could not identify the user logged into the OS. See http://aka.ms/msal-net-iwa for details."
                Console.WriteLine($"unknown_user. {ex.Message}");
            }
            catch (MsalClientException ex) when (ex.ErrorCode == "parsing_wstrust_response_failed")
            {
                // ------------------------------------------------------------------------
                // In the case of a Federated user (that is owned by a federated IdP, as opposed to a managed user owned in an Azure AD tenant)
                // ID3242: The security token could not be authenticated or authorized.
                // The user does not exist or has entered the wrong password
                // ------------------------------------------------------------------------
                Console.WriteLine($"parsing_wstrust_response_failed. {ex.Message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Exception: {ex.Message}");
                if (ex.InnerException != null)
                {
                    Console.WriteLine($"InnerException: {ex.InnerException.Message}");
                }



            }
            Console.ReadLine();
        }

        /// <summary>
        /// Asks the user for the password
        /// The password is never show in the console and the password is only saved as SecureString
        /// </summary>
        /// <returns>The entered password as a secure string</returns>
        public static SecureString GetPassword()
        {
            Console.WriteLine("Enter your password. Confirm by pressing enter. (The password is neither displayed nor saved)");
            var pwd = new SecureString();
            while (true)
            {
                ConsoleKeyInfo i = Console.ReadKey(true);
                if (i.Key == ConsoleKey.Enter)
                {
                    break;
                }
                else if (i.Key == ConsoleKey.Backspace)
                {
                    if (pwd.Length > 0)
                    {
                        pwd.RemoveAt(pwd.Length - 1);
                        Console.Write("\b \b");
                    }
                }
                else if (i.KeyChar != '\u0000') // KeyChar == '\u0000' if the key pressed does not correspond to a printable character, e.g. F1, Pause-Break, etc
                {
                    pwd.AppendChar(i.KeyChar);
                    Console.Write("*");
                }
            }
            Console.WriteLine();
            return pwd;
        }
    }
}

Solution

  • The AcquireTokenByUsernamePassword() method uses the ROPC flow to authenticate your users and acquire an accessToken. This flow has some drawbacks (no MFA, might not work with federated users, etc.), so review the previous doc carefully.

    To make this flow work without a client_secret or client_assertion, you need to "allow public client flows" on your app registration. As below:

    App Registration Config