毋献仪 发表于 2025-7-22 14:26:48

翻译:基于 OAuth2 在 Web 服务中使用 MailKit

MailKit 基于 OAuth2 操作 Exchange (IMAP, POP3 和 SMTP)

原文地址:https://github.com/jstedfast/MailKit/blob/master/ExchangeOAuth2.md#using-oauth2-with-exchange-imap-pop3-or-smtp
1. 注册应用

不管你是准备在桌面应用、移动应用,还是在 Web 服务中基于 MailKit 来访问 Exchange,你需要做的第一件事,就是在微软的 Identity Platform 中注册你的应用。
你需要按照 快速入门 中的要求来完成这一步。
注册之后,你可以得到你所注册的应用 Id, 或者在 ids 中被称为 client id.

在上图中,client id 为:11111111-1111-1111-1111-11111111, 同时,还可以看到相应的 Object ID: 00000000-0000-0000-0000-00000000
2. 为应用配置正确的 API 权限

基于你的应用希望支持的邮件协议,有许多种不同的 API 权限需要配置。见:为你的 Extra AD 应用准备权限:POP, IMAP, SMTP

[*]在 Azure 门户种,通过 Microsoft Extra appliation 的管理视图,选择 API Permissions 权限
[*]选择我的组织使用的 API 权限 (APSs my organization uses),然后,选择 "Office 365 Exchange Online"
[*]点击应用权限 (Application permissions)
[*]对于 POP 访问协议,选择 POP.AccessAsApp 权限
[*]对于 IMAP 访问协议,选择 IMAP.AccessAsApp
[*]对于 SMTP 访问协议,选择 SMTP.SendAsApp

[*]选择正确的协议之后,点击增加权限 Add permissions

现在,你应该在你的 Entra application 权限种添加了 SMTP, POP 或者 IMAP 权限。
3. 基于 OAuth2 在 Web 服务中使用 MailKit

3.1 为您的 Web 服务注册服务主体

Web 应用代理用户访问 Exchange 需要先进行授权的,这需要到 Exchange 服务器上进行授权。
您的 Web 服务注册完成后,租户管理员需要注册您的服务主体。这是在 Exchange 中配置 Web 服务的访问权限。
要使用 New-ServicePrincipal 命令,请打开 Azure Powershell 终端,安装 ExchangeOnlineManagement 并连接到您的租户,如下所示:
Install-Module -Name ExchangeOnlineManagement -allowprerelease
Import-module ExchangeOnlineManagement
Connect-ExchangeOnline -Organization <tenantId>然后,为你的 Web 服务注册服务主体, 这需要使用 New-ServicePrincipal 命令。
此 cmdlet 仅适用于基于云的服务。
使用 New-ServicePrincipal cmdlet 可在基于云的组织中创建服务主体。
Microsoft Entra ID 中的服务主体用于定义应用程序可以执行的操作、谁可以访问应用程序以及应用程序可以访问的资源。在 Exchange Online 中,服务主体是对 Microsoft Entra ID 中服务主体的引用。要将 Exchange Online 基于角色的访问控制 (RBAC) 角色分配给 Microsoft Entra ID 中的服务主体,请使用 Exchange Online 中的服务主体引用。Exchange Online PowerShell 中的 *-ServicePrincipal cmdlet 允许您查看、创建和删除这些服务主体引用。
有关更多信息,请参阅 Microsoft Entra ID 中的应用程序和服务主体对象。
您需要先获得权限才能运行此 cmdlet。虽然本主题列出了此 cmdlet 的所有参数,但如果某些参数未包含在分配给您的权限中,您可能无法访问它们。要查找在您的组织中运行任何 cmdlet 或参数所需的权限,请参阅查找运行任何 Exchange cmdlet 所需的权限。
ObjectId 参数指定服务主体的唯一 ObjectId GUID 值。例如,7c7599b2-23af-45e3-99ff-0025d148e929。
此参数的有效值可在以下位置获取:

[*]Microsoft Graph PowerShell 中 Get-MgServicePrincipal cmdlet 输出中的 Id 属性。
[*]Microsoft Entra 管理中心中企业应用程序中的 Object ID 属性
New-ServicePrincipal -AppId-ObjectId <OBJECT_ID> [-Organization <ORGANIZATION_ID>]3.2 为你的 Web 服务进行授权

为了授予您的 Web 服务访问 Office365 和/或 Exchange 帐户的权限,您需要首先使用以下命令获取上一步中注册的服务主体 ID:
Get-ServicePrincipal | fl一旦你的 Web 服务拥有了服务主体标识 Service Principal ID,使用下面的命令,来为你 Web 服务种使用的邮箱账号添加完整的邮箱权限。
Add-MailboxPermission -Identity "john.smith@example.com" -User <SERVICE_PRINCIPAL_ID> -AccessRights FullAccess3.3 使用 OAuth2 认证 Web 服务

现在,已经有了 Client ID 和 Tenant ID,需要把他们使用到你的应用中。
在下面的示例代码中,使用 Microsoft.Identity.Client NuGet 包来获得访问令牌 Access token, MailKit 需要将它传递给 Exchange Server.
// protocal 是准备使用的邮件访问协议,例如 "SMTP"
// 不同的协议,需要使用的 scope 不同
static async Task GetConfidentialClientOAuth2CredentialsAsync (string protocol, CancellationToken cancellationToken = default)
{
    var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create (clientId)
      .WithAuthority ($"https://login.microsoftonline.com/{tenantId}/v2.0")
      .WithCertificate (certificate) // or .WithClientSecret (clientSecret)
      .Build ();

    string[] scopes;

    if (protocol.Equals ("SMTP", StringComparison.OrdinalIgnoreCase)) {
      scopes = new string[] {
            // For SMTP, use the following scope
            "https://outlook.office365.com/.default"
      };
    } else {
      scopes = new string[] {
            // For IMAP and POP3, use the following scope
            "https://ps.outlook.com/.default"
      };
    }

    return await confidentialClientApplication.AcquireTokenForClient (scopes).ExecuteAsync (cancellationToken);
}3.3.1 SMTP

使用通过 GetConfidentialClientOAuth2CredentialsAsync() 方法获得的令牌访问邮箱。
var result = await GetConfidentialClientOAuth2CredentialsAsync ("SMTP");
var oauth2 = new SaslMechanismOAuth2 ("username@outlook.com", result.AccessToken);

using (var client = new SmtpClient ()) {
    await client.ConnectAsync ("smtp.office365.com", 587, SecureSocketOptions.StartTls);
    await client.AuthenticateAsync (oauth2);
    await client.DisconnectAsync (true);
}4. 桌面和移动应用

现在您已获得客户端 ID 和租户 ID 字符串,您需要将这些值插入到您的应用程序中。
以下示例代码使用 Microsoft.Identity.Client Nuget 包来获取访问令牌,MailKit 需要该令牌才能将其传递给 Exchange 服务器。
// 获得指定用户的访问令牌,protocol 为邮件访问协议,emailAddress 为用户邮箱
static async Task GetPublicClientOAuth2CredentialsAsync (string protocol, string emailAddress, CancellationToken cancellationToken = default)
{
    var options = new PublicClientApplicationOptions {
      ClientId = "Application (client) ID",
      TenantId = "Directory (tenant) ID",

      // Use "https://login.microsoftonline.com/common/oauth2/nativeclient" for apps using
      // embedded browsers or "http://localhost" for apps that use system browsers.
      RedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"
    };

    var publicClientApplication = PublicClientApplicationBuilder
      .CreateWithApplicationOptions (options)
      .Build ();

    string[] scopes;

    if (protocol.Equals ("IMAP", StringComparison.OrdinalIgnoreCase)) {
      scopes = new string[] {
            "email",
            "offline_access",
            "https://outlook.office.com/IMAP.AccessAsUser.All"
      };
    } else if (protocol.Equals ("POP", StringComparison.OrdinalIgnoreCase)) {
      scopes = new string[] {
            "email",
            "offline_access",
            "https://outlook.office.com/POP.AccessAsUser.All"
      };
    } else {
      scopes = new string[] {
            "email",
            "offline_access",
            "https://outlook.office.com/SMTP.Send"
      };
    }

    try {
      // First, check the cache for an auth token.
      return await publicClientApplication.AcquireTokenSilent (scopes, emailAddress).ExecuteAsync (cancellationToken);
    } catch (MsalUiRequiredException) {
      // If that fails, then try getting an auth token interactively.
      return await publicClientApplication.AcquireTokenInteractive (scopes).WithLoginHint (emailAddress).ExecuteAsync (cancellationToken);
    }
}4.1 简单邮件发送协议 SMTP

var result = await GetPublicClientOAuth2CredentialsAsync ("SMTP", "username@outlook.com");

// Note: We always use result.Account.Username instead of `Username` because the user may have selected an alternative account.
var oauth2 = new SaslMechanismOAuth2 (result.Account.Username, result.AccessToken);

using (var client = new SmtpClient ()) {
    await client.ConnectAsync ("smtp.office365.com", 587, SecureSocketOptions.StartTls);
    await client.AuthenticateAsync (oauth2);
    await client.DisconnectAsync (true);
}注意:一旦使用上述交互式方法获取了身份验证令牌,您就可以避免提示用户,如果您缓存结果。帐户信息,然后使用以下代码在将来静默地重新获取身份验证令牌:
var result = await publicClientApplication.AcquireTokenSilent(scopes, account).ExecuteAsync(cancellationToken);注意:有关缓存令牌的信息,请参阅 Microsoft 有关如何实现 跨平台令牌缓存 的文档。
4.2 IMAP

var result = await GetPublicClientOAuth2CredentialsAsync ("IMAP", "username@outlook.com");

// Note: We always use result.Account.Username instead of `Username` because the user may have selected an alternative account.
var oauth2 = new SaslMechanismOAuth2 (result.Account.Username, result.AccessToken);

using (var client = new ImapClient ()) {
    await client.ConnectAsync ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect);
    await client.AuthenticateAsync (oauth2);
    await client.DisconnectAsync (true);
}5 Web 应用

独立用户通过微软认证之后,发送或者接收邮件。
5.1 使用 OAuth2 认证 Web 应用

如果你希望代理用户进行邮件收/发,请使用如下代码:
// Common Code
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Kiota.Abstractions.Authentication;

public static class OAuthMicrosoft
{
    public static readonly string[] RegistrationScopes = new string[] {
      "offline_access",
      "User.Read",
      "Mail.Send",
      "https://outlook.office.com/SMTP.Send",
      "https://outlook.office.com/IMAP.AccessAsUser.All",
    };

    public static readonly string[] SmtpScopes = new string[] {
      "email",
      "offline_access",
      "https://outlook.office.com/SMTP.Send"
    };

    public static readonly string[] ImapScopes = new string[] {
      "email",
      "offline_access",
      "https://outlook.office.com/IMAP.AccessAsUser.All",
    };

    // 获得访问令牌
    public static IConfidentialClientApplication CreateConfidentialClient ()
    {
      var clientId = "Application (client) ID";
      var tenantId = "common"; // common = anybody with microsoft account personal or organization; other options see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints
      var clientSecret = "client secret";

      var redirectURL = "https://example.com/oauth/microsoft/callback";

      var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create (clientId)
            .WithAuthority ($"https://login.microsoftonline.com/{tenantId}/v2.0")
            .WithClientSecret (clientSecret)
            .WithRedirectUri (redirectURL)
            .Build ();

      // You also need to configure an MSAL token cache. so that token are remembered.
      return confidentialClientApplication;
    }
}注册页面,将用户重定向到微软页面进行认证
// Registration page - redirect user to Microsoft to get authorization
// 注册页面,重定向用户到微软认证页面
public async Task<IActionResult> OnPostAsync ()
{
    var client = OAuthMicrosoft.CreateConfidentialClient ();

    // Note: When getting authorization, specify all of the scopes that your application will ever need (eg. SMTP /and/ IMAP).
    // Later, when requesting an access token, you will only ask for the specific scopes that you need (e.g. SMTP).
    var authurlbuilder = client.GetAuthorizationRequestUrl (OAuthMicrosoft.RegistrationScopes);
    var authurl = await authurlbuilder.ExecuteAsync ();

    return this.Redirect (authurl.ToString ());
}

// Callback page = https://example.com/oauth/microsoft/callback in this example
// 回调页面
public async Task<IActionResult> OnGet ( string code)
{
    var confidentialClientApplication = OAuthMicrosoft.CreateConfidentialClient ();
    var scopes = OAuthMicrosoft.SmtpScopes;

    var auth = await confidentialClientApplication.AcquireTokenByAuthorizationCode (scopes, code).ExecuteAsync (); //this saves the token in msal cache

    var ident = auth.Account.HomeAccountId.Identifier;
    // Note: you will need to persist the ident to refer to later.
}

// Use the credentials
// 发邮件
public async Task SendEmailAsync (string ident)
{
    var confidentialClientApplication = OAuthMicrosoft.CreateConfidentialClient ();
    var account = await confidentialClientApplication.GetAccountAsync (ident);
    var scopes = OAuthMicrosoft.SmtpScopes;

    try {
      var auth = await confidentialClientApplication.AcquireTokenSilent (scopes, account).ExecuteAsync ();

      using (var client = new SmtpClient ()) {
            await client.ConnectAsync ("smtp-mail.outlook.com", 587, SecureSocketOptions.StartTls);

            var oauth2 = new SaslMechanismOAuth2 (auth.Account.Username, auth.AccessToken);

            await client.AuthenticateAsync (oauth2);

            var serverfeedback = await client.SendAsync (message);
            await client.DisconnectAsync (true);
      }
    } catch (MsalUiRequiredException) {
      throw new Exception ("Need to get authorization again");
    }
}

public async Task TestImapAsync (string ident)
{
    var confidentialClientApplication = OAuthMicrosoft.CreateConfidentialClient ();
    var account = await confidentialClientApplication.GetAccountAsync (ident);
    var scopes = OAuthMicrosoft.ImapScopes;

    var auth = await confidentialClientApplication.AcquireTokenSilent (scopes, account).ExecuteAsync ();   

    var oauth2 = new SaslMechanismOAuth2 (auth.Account.Username, auth.AccessToken);

    using (var client = new ImapClient ()) {
      await client.ConnectAsync ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect);
      await client.AuthenticateAsync (oauth2);
      await client.DisconnectAsync (true);
    }
}参考资料


[*]How can I implement service-to-service authentication with Office365 using the OAuth2 SASL mechanism? #1126

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 翻译:基于 OAuth2 在 Web 服务中使用 MailKit