I know this is an old post but with Microsoft progressively rolling modern authentication on all Office 365 tenants. Here's what I cobbled together.
I haven't worked with MFA setups.
I use it to fetch attachments via POP3 from automated mails coming in a mailbox of our tenant, the app runs from a scheduled task so it needs to be able to run without interaction.
First, you need to get the TenantID and ClientID the tenant admin gets when registering an app on the tenant. Credits to @jstedfast for the bootstrap doc to use those informations elegantly.
Then, setup a cache for the authentication token (following this article and the wiki page linked to it).
Then handle the logic whether to use interactive or silent authentication and avoid prompting for sign-in everytime. (straight copy/paste from documentation, but rather than leaving a link that might break...)
I wrapped it all together in a function that I call later to handle the authentication.
private static async Task<AuthenticationResult> GetMSALTokenAsync()
{
var scopes = new string[] {
"email",
"offline_access",
"https://outlook.office.com/POP.AccessAsUser.All"
};
var options = new PublicClientApplicationOptions
{
ClientId = Settings.Default.MSALClientId,
TenantId = Settings.Default.MSALTenantId,
RedirectUri = Settings.Default.MSALRedirectURI
};
var storageProperties = new StorageCreationPropertiesBuilder(
Settings.Default.MSALTokenCache,
MsalCacheHelper.UserRootDirectory)
.Build();
var publicClientApplication = PublicClientApplicationBuilder
.CreateWithApplicationOptions(options)
.Build();
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties);
cacheHelper.RegisterCache(publicClientApplication.UserTokenCache);
var accounts = await publicClientApplication.GetAccountsAsync();
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
AuthenticationResult authToken;
try
{
authToken = await publicClientApplication.AcquireTokenSilent(scopes, accounts.First(o => o.Username == Settings.Default.LoginPop)).ExecuteAsync();
return authToken;
}
catch (MsalUiRequiredException ex) when (ex.ErrorCode == MsalError.InvalidGrantError)
{
switch (ex.Classification)
{
case UiRequiredExceptionClassification.None:
break;
case UiRequiredExceptionClassification.MessageOnly:
// You might want to call AcquireTokenInteractive(). Azure AD will show a message
// that explains the condition. AcquireTokenInteractively() will return UserCanceled error
// after the user reads the message and closes the window. The calling application may choose
// to hide features or data that result in message_only if the user is unlikely to benefit
// from the message
try
{
authToken = await publicClientApplication.AcquireTokenInteractive(scopes).ExecuteAsync(token);
return authToken;
}
catch (MsalClientException ex2) when (ex2.ErrorCode == MsalError.AuthenticationCanceledError)
{
// Do nothing. The user has seen the message
}
break;
case UiRequiredExceptionClassification.BasicAction:
// Call AcquireTokenInteractive() so that the user can, for instance accept terms
// and conditions
case UiRequiredExceptionClassification.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 UiRequiredExceptionClassification.ConsentRequired:
// Call AcquireTokenInteractive() for user to give consent.
case UiRequiredExceptionClassification.UserPasswordExpired:
// Call AcquireTokenInteractive() so that user can reset their password
case UiRequiredExceptionClassification.PromptNeverFailed:
// You used WithPrompt(Prompt.Never) and this failed
case UiRequiredExceptionClassification.AcquireTokenSilentFailed:
default:
// May be resolved by user interaction during the interactive authentication flow.
authToken = await publicClientApplication.AcquireTokenInteractive(scopes).ExecuteAsync(token);
return authToken;
}
}
catch (InvalidOperationException)
{
authToken = await publicClientApplication.AcquireTokenInteractive(scopes).ExecuteAsync(token);
return authToken;
}
log.Error("Authentication failed.");
return null;
}
Then you can just roll on with the actual logic to do your stuff with the Exchange server.
private static async Task PopDownloadAsync()
{
using (var client = new Pop3Client())
{
try
{
await client.ConnectAsync(Settings.Default.SrvPop, 995, SecureSocketOptions.SslOnConnect);
}
catch (Pop3CommandException ex)
{
// do stuff
return;
}
catch (Pop3ProtocolException ex)
{
// do stuff
return;
}
try
{
var result = await GetMSALTokenAsync();
if (result != null)
{
var oauth2 = new SaslMechanismOAuth2(result.Account.Username, result.AccessToken);
await client.AuthenticateAsync(oauth2);
}
else
{
throw new AuthenticationException("Something went wrong during authentication...");
}
}
catch (AuthenticationException ex)
{
// do stuff
return;
}
catch (Pop3CommandException ex)
{
// do stuff
return;
}
catch (Pop3ProtocolException ex)
{
// do stuff
return;
}
if (client.Capabilities.HasFlag(Pop3Capabilities.UIDL))
{
try
{
// do stuff
}
catch (Pop3CommandException ex)
{
// do stuff
}
catch (Pop3ProtocolException ex)
{
// do stuff
if (!client.IsConnected)
return;
}
catch (Exception e)
{
// do stuff
return;
}
}
if (client.IsConnected)
{
await client.DisconnectAsync(true);
}
}
}