我有一个 Web 应用程序,它使用 MailKit 和基本身份验证通过 Exchange Online (Office365) 向用户发送电子邮件。我们公司是 MS 合作伙伴,因此有义务在 2020 年 2 月底之前关闭我们服务的基本身份验证。
所以,我想使用 OAuth 2.0 连接到 Exchange Online,类似于这个示例。事实上,根据这个answer可能有一个可用的解决方案,但我找不到任何相关信息。
现在我正在使用 MS Identity Platform v2.0,但我不知道该怎么做。
如有任何帮助,我们将不胜感激。
更新1
我不想代表登录用户发送邮件,而是有一个 Office365 用户帐户用于向其他人发送邮件(通知等)。
更新2
我设法使用 Microsoft Graph SDK 和用户名/密码提供程序更接近我想要做的事情。
我们的用户帐户需要多因素身份验证,因此在使用用户密码时出现错误,因为我无法满足第二个因素。当我使用应用程序密码时,由于密码不正确,身份验证失败。
更新3
我现在切换到邮件转发。但如果我找到答案,我会更新这个问题。
使用 Microsoft.Identity.Client,您可以生成令牌并通过,然后使用该令牌进行身份验证。
我发现了以下 IMAP、POP3 和 SMTP 的内容,因此针对我的项目进行了调整,以获得一个可行的解决方案。尽管该示例显示了交互式方法,但我正在尝试将客户端凭据流与应用程序密钥一起使用。
MailKit - 将 OAuth2 与 Exchange(IMAP、POP3 或 SMTP)结合使用
Microsoft - 使用 OAuth 验证 IMAP、POP 或 SMTP 连接
来自@hB0评论
通过客户端凭据授予流程设置服务原则(非交互式)
我的选择是研究 Microsoft Graph API 。它是所有 Microsoft 服务(包括电子邮件)的单一端点。电子邮件特定端点文档位于此处
微软提供了不同语言的SDK,用于使用Graph API开发客户端应用程序。
在较高层面上,您需要执行以下操作。
i) 在 Azure Active Directory 中注册应用程序。参见这里
ii) 使用 Oauth2“授权代码授予”流程获取刷新令牌。请参阅此处
iii) 将刷新令牌交换为访问令牌,并使用访问令牌调用 Microsoft Graph API。
iv) 如果您的应用程序需要在用户离线时执行操作,那么您还需要存储刷新令牌。在这种情况下,请确保在步骤 ii) 中包含“离线”范围
我建议查看 DotNetOpenAuth 或类似的库并阅读他们的示例。如果 DotNetOpenAuth 库没有内置 Windows Live URL,您可能需要知道用于此目的的 Windows Live URL。
可以在这里找到示例:https://github.com/DotNetOpenAuth/DotNetOpenAuth.Samples
我知道这是一篇旧帖子,但随着 Microsoft 逐步在所有 Office 365 租户上推出现代身份验证。这是我拼凑出来的。
我没有使用过 MFA 设置。
我使用它通过 POP3 从我们租户邮箱中的自动邮件中获取附件,该应用程序从计划任务运行,因此它需要能够在没有交互的情况下运行。
首先,您需要获取租户管理员在租户上注册应用程序时获取的TenantID和ClientID。感谢 @jstedfast 的 bootstrap doc 来优雅地使用这些信息。 然后,为身份验证令牌设置缓存(遵循本文文章和链接到它的wiki页面)。 然后处理是使用交互式身份验证还是静默身份验证的逻辑,并避免每次都提示登录。 (直接从文档复制/粘贴,而不是留下可能会损坏的链接......) 我将它们全部包装在一个函数中,稍后调用该函数来处理身份验证。
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;
}
然后您就可以继续使用实际逻辑来使用 Exchange 服务器完成您的工作。
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);
}
}
}