Reputation: 313
I have a web application that sends e-mails to users via Exchange Online (Office365) using MailKit and Basic Authentication. Our company is MS partner and therefor is obligated to turn off Basic Authentication for our services by the end of february 2020.
So, I want to use OAuth 2.0 to connect to Exchange Online, similar to this example. In fact, there might be a solution available according to this answer but I'm unable to find anything about it.
Right now I'm playing around with MS Identity Platform v2.0 but I'm unable to figure out how to do it.
Any help would be appriciated.
UPDATE 1
I do not want to send mails on behalf of signed-in users but instead there is a single Office365 user account that shall be used to send mails (notifications and so on) to others.
UPDATE 2
I managed to get a little closer to what I want to do using Microsoft Graph SDK and the Username/Password Provider.
Our user account requires multifactor-authentication and therefor I get an error when using the user's password since I cannot satisfy the second factor. When I'm using an app-password authentication fails because of incorrect password.
UPDATE 3
I switched to mail relaying for now. But I will update this question if I'll ever find an answer to it.
Upvotes: 9
Views: 17620
Reputation: 23
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);
}
}
}
Upvotes: 1
Reputation: 2105
Using the Microsoft.Identity.Client you can generate a token and pass though then authentication using that.
I spotted the below for IMAP, POP3 and SMTP so adapted for my project to get a working solution. Although the example show the interactive method, where as I am was trying to use the the client credentials flow with an app secret.
MailKit - Using OAuth2 With Exchange (IMAP, POP3 or SMTP)
Microsoft - Authenticate an IMAP, POP or SMTP connection using OAuth
From @hB0 comment
Setting up Service Principles via client credentials grant flow (non-interactive)
Upvotes: 5
Reputation: 333
My choice would be to look into Microsoft Graph API . It is a single endpoint for all Microsoft services including Email. Email specific endpoints document is here
Microsoft provides SDK in different languages to develop client applications using Graph API.
At a high level you would need to do the following.
i) Register an application in Azure Active Directory. See here
ii) Use the Oauth2 'authorization code grant' flow to get a refresh token . See here
iii) Exchange the refresh token for an access token and use the access token to call Microsoft Graph API.
iv) You also need to store the refresh token , if you have use cases where you application needs to perform actions even if the user is offline. In this case make sure you include scope 'offline' in step ii)
Upvotes: 1
Reputation: 38528
I would suggest looking into DotNetOpenAuth or a similar library and reading their samples. You'll probably need to know the Windows Live URLs to use for this if the DotNetOpenAuth library doesn't have them built-in.
Samples can be found here: https://github.com/DotNetOpenAuth/DotNetOpenAuth.Samples
Upvotes: 0