How to connect to IMAP server in Azure using OAuth for a MFA account in .NET C#?

102 Views Asked by At

When Microsoft revoked the Basic Authentication for IMAP protocol (past year), I implemented a way to get the token to use in the connection. I'm using Aspose.Email and I followed that article: https://docs.aspose.com/email/net/access-mail-services-using-oauth/#implementation-of-custom-itokenprovider-for-office-365

But that way doesn't support MFA (Multi-Factor Authentication) accounts, and I'm trying to find the "better" way (for my app) to implement the full flow, and think at the same time, with the SMTP protocol, that I use to send mails in batch processes.

The question summarized is: Is there any way to connect to Azure IMAP (or SMTP) with OAuth and MFA without user interaction?

I've crawled in internet, and I've found that solution, but I don't like it (must assign permissions to all mailboxes, manually), anyway I must try it, when I can test with the Azure admin: https://learn.microsoft.com/en-us/answers/questions/1112032/outlookoffice365-imap-how-to-get-access-token(api)#answers

If the solution requires user interaction (I understand that it would be the way), how could I refresh the token when it will expire? Some kind of cookie?

I've separated applications for the front (ASPNET MVC) and back (.NET WCF Services), and the connection to IMAP must be established in back.

I've published a question in Aspose.Email forum too: https://forum.aspose.com/t/aspose-mail-connect-to-azure-imap-oauth-via-ropc-in-a-mfa-account/276114

1

There are 1 best solutions below

0
On BEST ANSWER

Finally I've decided to implement the validation flow through Azure SSO web, because I haven't found any easy way to implement it without user interaction. So I've implemented two mockup apps (ASPNET MVC Web App, and Console App) to simulate the same environment of my app:

ASPNET MVC Web App:

  • Create a MSAL client, IConfidentialClientApplication, through ConfidentialClientApplicationBuilder to validate the account sending the user to the AuthorizationUrl.
  • After validation, connect again to the Azure the get the Token through AcquireTokenByAuthorizationCode method, and serialize the Token(Cache) to a file (in real app I would persist on database).

Console App:

  • Create a new MSAL client, and deserialize the tokencache from the file stored in web app.
  • Use the token recovered from the file to create ImapClient (or SMTPClient).

Web mockup code (Controller):

public ActionResult ValidateAccount()
{
    IConfidentialClientApplication _msalClient = Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.Create(Constants.CLIENT_ID)
      .WithClientSecret(Constants.SECRET)
      .WithAuthority(new Uri(Constants.AUTHORITY))
      .WithRedirectUri(Constants.APP_URL) // StoreToken
      .Build();
    var authUrl = _msalClient.GetAuthorizationRequestUrl(Constants.SCOPES).ExecuteAsync().Result;
    return Redirect(authUrl.ToString());
}


public async Task<ActionResult> StoreToken(string code)
{
    IConfidentialClientApplication _msalClient = Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.Create(Constants.CLIENT_ID)
      .WithClientSecret(Constants.SECRET)
      .WithAuthority(new Uri(Constants.AUTHORITY))
      .WithRedirectUri(Constants.APP_URL)
      .Build();
    var authUrl = _msalClient.GetAuthorizationRequestUrl(Constants.SCOPES).ExecuteAsync().Result;
    string cacheFilePath = ControllerContext.HttpContext.Server.MapPath(@"~/token.json");
    FileBasedTokenCache fileBasedTokenCache1 = new FileBasedTokenCache(cacheFilePath, _msalClient.UserTokenCache);
    var result = await _msalClient.AcquireTokenByAuthorizationCode(new[] { "https://outlook.office.com/IMAP.AccessAsUser.All" }, code).ExecuteAsync();

    // Show the token on view ONLY for debug 
    ViewBag.Message = result.AccessToken;
    return View();
}

Console mockup code:

protected async Task<string> GetAuthToken()
{
    var _msalClient = ConfidentialClientApplicationBuilder.Create(Constants.CLIENT_ID)
        .WithClientSecret(Constants.SECRET)
        .WithAuthority(new Uri(Constants.AUTHORITY))
        .WithRedirectUri("https://localhost")
        .Build();

    ITokenCache tokenCache = _msalClient.UserTokenCache;
    string cacheFilePath = @"..\..\..\Web\WebOAuth2\token.json";
    FileBasedTokenCache fileBasedTokenCache = new FileBasedTokenCache(cacheFilePath, tokenCache);

    var accounts = await _msalClient.GetAccountsAsync(this.AccountId);

    IAccount account = accounts.FirstOrDefault();
    string[] SCOPES = new string[] { "https://outlook.office.com/IMAP.AccessAsUser.All"};

    var result = await _msalClient.AcquireTokenSilent(SCOPES, account).ExecuteAsync();
    return result.AccessToken;
}

And the FileBasedTokenCache shared class:

public class FileBasedTokenCache
{
    private readonly string cacheFilePath;
    private ITokenCache tokenCache;

    public FileBasedTokenCache(string filePath, ITokenCache cache)
    {
        cacheFilePath = filePath;
        tokenCache = cache;
        tokenCache.SetBeforeAccess(BeforeAccessNotification);
        tokenCache.SetAfterAccess(AfterAccessNotification);
    }

    private void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        if (File.Exists(cacheFilePath))
        {
            byte[] cachedData = File.ReadAllBytes(cacheFilePath);
            args.TokenCache.DeserializeMsalV3(cachedData);
        }
    }

    private void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        // Only write back if the cache has changed
        if (args.HasStateChanged)
        {
            byte[] data = args.TokenCache.SerializeMsalV3();
            File.WriteAllBytes(cacheFilePath, data);
        }
    }
}