如何使用 MailKit 中的 OAuth 2.0 连接到 Exchange Online?

问题描述 投票:0回答:4

我有一个 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

我现在切换到邮件转发。但如果我找到答案,我会更新这个问题。

oauth-2.0 microsoft-graph-api mailkit microsoft-identity-platform
4个回答
5
投票

使用 Microsoft.Identity.Client,您可以生成令牌并通过,然后使用该令牌进行身份验证。

我发现了以下 IMAP、POP3 和 SMTP 的内容,因此针对我的项目进行了调整,以获得一个可行的解决方案。尽管该示例显示了交互式方法,但我正在尝试将客户端凭据流与应用程序密钥一起使用。

MailKit - 将 OAuth2 与 Exchange(IMAP、POP3 或 SMTP)结合使用

Microsoft - 使用 OAuth 验证 IMAP、POP 或 SMTP 连接


来自@hB0评论

通过客户端凭据授予流程设置服务原则(非交互式)

https://techcommunity.microsoft.com/t5/exchange-team-blog/announcing-oauth-2-0-client-credentials-flow-support-for-pop-and/ba-p/3562963


1
投票

我的选择是研究 Microsoft Graph API 。它是所有 Microsoft 服务(包括电子邮件)的单一端点。电子邮件特定端点文档位于此处

微软提供了不同语言的SDK,用于使用Graph API开发客户端应用程序。

在较高层面上,您需要执行以下操作。

i) 在 Azure Active Directory 中注册应用程序。参见这里

ii) 使用 Oauth2“授权代码授予”流程获取刷新令牌。请参阅此处

iii) 将刷新令牌交换为访问令牌,并使用访问令牌调用 Microsoft Graph API。

iv) 如果您的应用程序需要在用户离线时执行操作,那么您还需要存储刷新令牌。在这种情况下,请确保在步骤 ii) 中包含“离线”范围


0
投票

我建议查看 DotNetOpenAuth 或类似的库并阅读他们的示例。如果 DotNetOpenAuth 库没有内置 Windows Live URL,您可能需要知道用于此目的的 Windows Live URL。

可以在这里找到示例:https://github.com/DotNetOpenAuth/DotNetOpenAuth.Samples


0
投票

我知道这是一篇旧帖子,但随着 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);
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.