为一次性密码发送事件配置自定义电子邮件提供程序(预览版)
适用于: 员工租户
外部租户(了解详细信息)
本文提供有关配置和设置一次性密码 (OTP) 发送事件类型的自定义电子邮件提供程序的指南。 激活 OTP 电子邮件时触发该事件,它允许通过调用 REST API 调用 REST API 来使用自己的电子邮件提供程序。
先决条件
- 熟悉和了解自定义身份验证扩展中介绍的概念。
- Azure 订阅。 如果没有现有的 Azure 帐户,请注册免费试用版,或者在创建帐户时使用 Visual Studio 订阅权益。
- 外部租户的 Microsoft Entra ID
- 对于 Azure 通信服务用户;
- Azure 通信服务资源。 如果没有,请使用新的或现有的资源组在快速入门:创建和管理通信服务资源中创建一个。
- 已使用预配的域创建并准备好了一个 Azure 电子邮件通信服务资源。 如果没有,请参阅快速入门:创建和管理电子邮件通信服务资源,并使用与 Azure 通信服务相同的资源组。
- 与电子邮件域连接的活动通信服务资源。 请参阅快速入门:如何连接已验证电子邮件域
- (可选)使用 Azure 通信服务发送电子邮件测试使用 Azure 通信服务向所需收件人发送电子邮件,同时验证应用程序发送电子邮件的配置。
- 对于 SendGrid 用户:
- 一个 SendGrid 帐户。 如果尚不拥有帐户,请从设置 SendGrid 帐户开始。 有关安装说明,请参阅如何在 Azure 中使用 SendGrid 发送电子邮件中的创建 SendGrid 帐户部分。
步骤 1:创建 Azure Function App
本部分介绍如何在 Azure 门户中设置 Azure 函数应用。 函数 API 是电子邮件提供商的网关。 创建一个 Azure 函数应用来托管 HTTP 触发器函数,并在函数中配置设置。
提示
本文中的步骤可能因开始使用的门户而略有不同。
在 Azure 门户菜单上或在门户主页中,选择“创建资源”。
搜索并选择“函数应用”,然后选择“创建”。
在“创建函数应用”页面上,依次选择“消耗”、“选择”。
在“创建函数应用(消耗)”页面的“基本信息”选项卡,使用下表指定的设置创建函数应用:
设置 建议值 描述 订阅 你的订阅 在其下创建此新函数应用的订阅。 资源组 myResourceGroup 选择用于将 Azure 通信服务和电子邮件通信服务资源设置为先决条件的一部分的资源组 函数应用名称 全局唯一名称 用于标识新函数应用的名称。 有效字符为 a-z
(不区分大小写)、0-9
和-
。部署代码或容器映像 代码 用于发布代码文件或 Docker 容器的选项。 对于本教程,请选择“代码”。 运行时堆栈 .NET 你的首选编程语言。 对于本教程,请选择“.NET”。 版本 8 (LTS) 进程内 .NET 运行时的版本。 进程内表示可以在门户中创建和修改函数,这是本指南建议的操作 区域 首选区域 选择与你靠近或者与函数可以访问的其他服务靠近的区域。 操作系统 Windows 系统会根据你选择的运行时堆栈预先选择操作系统。 选择“查看 + 创建”以查看应用配置选择,然后选择“创建”。 部署需要几分钟时间。
部署后,选择“转到资源”查看新函数应用。
1.1 创建 HTTP 触发器函数
创建 Azure 函数应用后,创建一个 HTTP 触发器函数。 借助 HTTP 触发器,可以使用 HTTP 请求调用函数。 此 HTTP 触发器将由 Microsoft Entra 自定义身份验证扩展引用。
- 在你的函数应用中,从菜单中选择“函数”。
- 选择“创建函数”。
- 在“创建函数”窗口中,在“选择模板”下,搜索并选择“HTTP 触发器”模板。 选择下一步。
- 在“模板详细信息”下,为“函数名称”属性输入 CustomAuthenticationExtensionsAPI。
- 对于“授权级别”,请选择“函数”。
- 选择创建。
1.2 编辑函数
该代码首先读取传入的 JSON 对象。 Microsoft Entra ID 将JSON 对象发送到 API。 在此示例中,它会读取电子邮件地址(标识符)和 OTP。 然后,代码将详细信息发送到通信服务,以使用动态模板发送电子邮件。
本操作指南演示了使用 Azure 通信服务和 SendGrid 的 OTP 发送事件。 使用选项卡选择实现。
在菜单中,选择“代码 + 测试”。
请将整个代码替换为以下代码片段。
using System.Dynamic; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Azure.Communication.Email; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; namespace Company.AuthEvents.OnOtpSend.CustomEmailACS { public class CustomEmailACS { private readonly ILogger<CustomEmailACS> _logger; public CustomEmailACS(ILogger<CustomEmailACS> logger) { _logger = logger; } [Function("OnOtpSend_CustomEmailACS")] public async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req) { _logger.LogInformation("C# HTTP trigger function processed a request."); // Get the request body string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); JsonNode jsonPayload = JsonNode.Parse(requestBody)!; // Get OTP and mail to string emailTo = jsonPayload["data"]!["otpContext"]!["identifier"]!.ToString(); string otp = jsonPayload["data"]!["otpContext"]!["onetimecode"]!.ToString(); // Send email await SendEmailAsync(emailTo, otp); // Prepare response ResponseObject responseData = new ResponseObject("microsoft.graph.OnOtpSendResponseData"); responseData.Data.Actions = new List<ResponseAction>() { new ResponseAction( "microsoft.graph.OtpSend.continueWithDefaultBehavior") }; return new OkObjectResult(responseData); } private async Task SendEmailAsync(string emailTo, string code) { // Get app settings var connectionString = Environment.GetEnvironmentVariable("mail_connectionString"); var sender = Environment.GetEnvironmentVariable("mail_sender"); var subject = Environment.GetEnvironmentVariable("mail_subject"); try { if (!string.IsNullOrEmpty(connectionString)) { var emailClient = new EmailClient(connectionString); var body = EmailTemplate.GenerateBody(code); _logger.LogInformation($"Sending OTP to {emailTo}"); EmailSendOperation emailSendOperation = await emailClient.SendAsync( Azure.WaitUntil.Started, sender, emailTo, subject, body); } } catch (System.Exception ex) { _logger.LogError(ex.Message); } } } public class ResponseObject { [JsonPropertyName("data")] public Data Data { get; set; } public ResponseObject(string dataType) { Data = new Data(dataType); } } public class Data { [JsonPropertyName("@odata.type")] public string DataType { get; set; } [JsonPropertyName("actions")] public List<ResponseAction> Actions { get; set; } public Data(string dataType) { DataType = dataType; } } public class ResponseAction { [JsonPropertyName("@odata.type")] public string DataType { get; set; } public ResponseAction(string dataType) { DataType = dataType; } } public class EmailTemplate { public static string GenerateBody(string oneTimeCode) { return @$"<html><body> <div style='background-color: #1F6402!important; padding: 15px'> <table> <tbody> <tr> <td colspan='2' style='padding: 0px;font-family: "Segoe UI Semibold", "Segoe UI Bold", "Segoe UI", "Helvetica Neue Medium", Arial, sans-serif;font-size: 17px;color: white;'>Woodgrove Groceries live demo</td> </tr> <tr> <td colspan='2' style='padding: 15px 0px 0px;font-family: "Segoe UI Light", "Segoe UI", "Helvetica Neue Medium", Arial, sans-serif;font-size: 35px;color: white;'>Your Woodgrove verification code</td> </tr> <tr> <td colspan='2' style='padding: 25px 0px 0px;font-family: "Segoe UI", Tahoma, Verdana, Arial, sans-serif;font-size: 14px;color: white;'> To access <span style='font-family: "Segoe UI Bold", "Segoe UI Semibold", "Segoe UI", "Helvetica Neue Medium", Arial, sans-serif; font-size: 14px; font-weight: bold; color: white;'>Woodgrove Groceries</span>'s app, please copy and enter the code below into the sign-up or sign-in page. This code is valid for 30 minutes. </td> </tr> <tr> <td colspan='2' style='padding: 25px 0px 0px;font-family: "Segoe UI", Tahoma, Verdana, Arial, sans-serif;font-size: 14px;color: white;'>Your account verification code:</td> </tr> <tr> <td style='padding: 0px;font-family: "Segoe UI Bold", "Segoe UI Semibold", "Segoe UI", "Helvetica Neue Medium", Arial, sans-serif;font-size: 25px;font-weight: bold;color: white;padding-top: 5px;'> {oneTimeCode}</td> <td rowspan='3' style='text-align: center;'> <img src='https://woodgrovedemo.com/custom-email/shopping.png' style='border-radius: 50%; width: 100px'> </td> </tr> <tr> <td style='padding: 25px 0px 0px;font-family: "Segoe UI", Tahoma, Verdana, Arial, sans-serif;font-size: 14px;color: white;'> If you didn't request a code, you can ignore this email. </td> </tr> <tr> <td style='padding: 25px 0px 0px;font-family: "Segoe UI", Tahoma, Verdana, Arial, sans-serif;font-size: 14px;color: white;'> Best regards, </td> </tr> <tr> <td> <img src='https://woodgrovedemo.com/Company-branding/headerlogo.png' height='20'> </td> <td style='font-family: "Segoe UI", Tahoma, Verdana, Arial, sans-serif;font-size: 14px;color: white; text-align: center;'> <a href='https://woodgrovedemo.com/Privacy' style='color: white; text-decoration: none;'>Privacy Statement</a> </td> </tr> </tbody> </table> </div> </body></html>"; } } }
选择“获取函数 URL”,并复制“函数密钥” URL,该 URL 自此使用并称为“
{Function_Url}
”。 关闭函数。
步骤 2:将连接字符串添加到 Azure 函数
通信服务 SDK 可以使用连接字符串连接到 Azure 并向其进行身份验证。 对于 Azure 通信服务和 SendGrid,需要将这些连接字符串作为环境变量添加到 Azure 函数应用。
2.1:从 Azure 通信服务资源中提取连接字符串和服务终结点
你可以从 Azure 门户或使用 Azure 资源管理器 API 以编程方式访问通信服务连接字符串和服务终结点。
在 Azure 门户的主页中,打开门户菜单,搜索并选择“所有资源”。
在左窗格中,选择“设置”下拉列表,然后选择“键”。
复制“终结点”,然后从“主密钥”复制“密钥”和“连接字符串”的值。
2.2:将连接字符串添加到 Azure 函数
导航回到在“创建 Azure Function 应用”中创建的 Azure 函数。
在函数应用的“概述”页的左侧菜单中,选择“设置”>“环境变量”添加以下应用设置。 添加所有设置后,选择“应用”,然后选择“确认”。
设置 值(示例) 描述 mail_connectionString https://ciamotpcommsrvc.unitedstates.communication.azure.com/:accesskey=A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u
Azure 通信服务终结点 mail_sender from.email@myemailprovider.com 发件人电子邮件地址。 mail_subject CIAM 演示 电子邮件主题。
步骤 3:注册自定义身份验证扩展
在此步骤中,你将配置自定义身份验证扩展,供 Microsoft Entra ID 用来调用你的 Azure 函数。 该自定义身份验证扩展包含有关 REST API 终结点、它从 REST API 分析的声明以及如何对 REST API 进行身份验证的信息。 使用 Azure 门户或 Microsoft Graph 注册应用程序,对 Azure 函数验证自定义身份验证扩展身份。
注册自定义身份验证扩展
搜索并选择“Microsoft Entra ID”,然后选择“企业应用程序”。
选择“自定义身份验证扩展”,然后选择“创建自定义扩展”。
在“基本信息”中选择“EmailOtpSend”事件类型,然后选择“下一步”。
在“终结点配置”选项卡中,填写以下属性,然后选择“下一步”继续。
- 名称 - 自定义身份验证扩展的名称。 例如,电子邮件 OTP 发送。
- 目标 URL - Azure 函数 URL 的 。 导航到 Azure 函数应用的“概述”页,然后选择创建的函数。 在函数“概述”页中,选择“获取函数 URL”,并使用复制图标复制 customauthenticationextension_extension(系统密钥)URL。
- 说明 - 自定义身份验证扩展的说明。
在“API 身份验证”选项卡中,选择“创建新的应用注册”选项以创建代表你的函数应用的应用注册。
为应用命名,例如“Azure Functions 身份验证事件 API”,并选择“下一步”。
在“应用程序”选项卡中,选择要与自定义身份验证扩展关联的应用程序。 选择下一步。 通过选中此框,可以选择在整个租户中应用它。 选择“下一步”继续。
在“查看”选项卡中,检查自定义身份验证扩展的详细信息是否正确。 请注意“API 身份验证”下的“应用 ID”,需要在 Azure 函数应用中“为 Azure 函数配置身份验证”。 选择创建。
授权管理员同意
创建自定义身份验证扩展后,在“应用注册”下从门户打开应用程序,并选择“API 权限”。
在“API 权限”页面,选择“为“YourTenant”授予权限”按钮,向已注册的应用授予管理员同意,从而允许自定义身份验证扩展对你的 API 进行身份验证。 自定义身份验证扩展使用client_credentials
和Receive custom authentication extension HTTP requests
权限对 Azure 函数应用进行身份验证。
以下屏幕截图显示了如何授予权限。
步骤 4:配置用于测试的 OpenID Connect 应用
若要获取令牌并测试自定义身份验证扩展,可以使用 https://jwt.ms 应用。 它是 Microsoft 拥有的一个 Web 应用程序,可显示令牌的解码内容(令牌内容永远不会离开你的浏览器)。
按照以下步骤注册 jwt.ms Web 应用程序:
4.1 注册测试 Web 应用
- 至少以应用程序管理员的身份登录到 Microsoft Entra 管理中心。
- 浏览到“标识”>“应用程序”>“应用程序注册”。
- 选择“新注册”。
- 输入应用程序的“名称”。 例如“我的测试应用程序”。
- 在“支持的帐户类型”下,选择“仅此组织目录中的帐户” 。
- 在“重定向 URI”中的“选择平台”下拉列表中选择“Web”,然后在“URL”文本框中输入
https://jwt.ms
。 - 选择“注册”以完成应用注册。
- 在应用注册中的“概述”下,复制“应用程序(客户端) ID”,稍后将使用它,并称为
{App_to_sendotp_ID}
。 在 Microsoft Graph 中,appId 属性引用了它。
以下屏幕截图显示如何注册“我的测试应用程序”。
4.1 获取应用程序 ID
在应用注册中的“概述”下,复制“应用程序(客户端) ID”。 在后续步骤中,该应用 ID 将引用为 {App_to_sendotp_ID}
。 在 Microsoft Graph 中,appId 属性引用了它。
4.2 启用隐式流
jwt.ms 测试应用程序使用隐式流。 在“我的测试应用程序”注册中启用隐式流:
重要说明
Microsoft 建议使用最安全的可用身份验证流。 在此过程中用于测试的身份验证流需要高度信任应用程序,并且存在其他流中不存在的风险。 此方法不应用于对生产应用进行用户认证(详细了解)。
- 在“管理”下,选择“身份验证”。
- 在“隐式授权和混合流”下,选中“ID 令牌(用于隐式流和混合流)”复选框。
- 选择“保存”。
步骤 5:保护 Azure 函数
Microsoft Entra 自定义身份验证扩展使用服务器到服务器的流来获取在 HTTP Authorization
标头中发送到 Azure 函数的访问令牌。 将函数发布到 Azure 时(尤其是在生产环境中),需要验证在授权标头中发送的令牌。
要保护 Azure 函数,请按照以下步骤集成 Microsoft Entra 身份验证,以便使用Azure Functions 身份验证事件 API应用程序注册来验证传入的令牌。
注意
如果 Azure 函数应用所在的 Azure 租户不同于注册自定义身份验证扩展的租户,请跳到使用 OpenID Connect 标识提供者步骤。
- 登录 Azure 门户。
- 导航到之前发布的函数应用并选择它。
- 在左侧菜单中选择“身份验证”。
- 选择“添加标识提供者”。
- 从下拉列表中选择 Microsoft 作为标识提供者。
- 在“应用注册”-“应用注册类型”下,选择“选择此目录中的现有应用注册”,并选择>在注册自定义声明提供程序时创建的“Azure Functions 身份验证事件 API”应用注册。
- 为应用添加客户端密码到期日期。
- 在“未经身份验证的请求”下,选择“HTTP 401 未授权”作为标识提供者。
- 取消选择“令牌存储”选项。
- 选择“添加”以将身份验证添加到 Azure 函数。
5.1 使用 OpenID Connect 标识提供者
如果你配置了 Microsoft 标识提供者,请跳过此步骤。 否则,如果 Azure 函数所在的租户不同于注册自定义身份验证扩展的租户,请执行以下步骤保护函数:
登录到 Azure 门户,导航到之前发布的函数应用并选择它。
选择左窗格的“身份验证”。
选择“添加标识提供者”。
选择“OpenID Connect”作为标识提供者。
提供名称,例如Contoso Microsoft Entra ID。
在“元数据条目”下的“文档 URL”中输入以下 URL。 将
{tenantId}
替换为 Microsoft Entra 租户 ID,并将{tenantname}
替换为租户的名称,而不使用“onmicrosoft.com”。https://{tenantname}.ciamlogin.com/{tenantId}/v2.0/.well-known/openid-configuration
在“应用注册”下,输入先前创建的“Azure Functions 身份验证事件 API”应用注册的应用程序 ID(客户端 ID)。
在 Microsoft Entra 管理中心:
- 选择先前创建的“Azure Functions 身份验证事件 API”应用注册。
- 选择“证书和机密”“客户端密码”>“新建客户端密码”>。
- 添加客户端机密的说明。
- 选择机密的过期时间,或指定自定义的生存期。
- 选择“添加” 。
- 请记下机密值,以便在客户端应用程序代码中使用。 退出此页面后,此机密值永不再显示。
返回 Azure 函数,在“应用注册”下输入“客户端机密”值。
取消选择“令牌存储”选项。
选择“添加”以添加 OpenID Connect 标识提供者。
步骤 6:测试应用程序
若要测试自定义电子邮件提供程序,请执行以下步骤:
打开新的隐私浏览器,并通过以下 URL 进行导航和登录。
https://{tenantname}.ciamlogin.com/{tenant-id}/oauth2/v2.0/authorize?client_id={App_to_sendotp_ID}&response_type=id_token&redirect_uri=https://jwt.ms&scope=openid&state=12345&nonce=12345
将
{tenant-id}
替换为你的租户 ID、租户名称或已验证的域名之一。 例如,contoso.onmicrosoft.com
。将
{tenantname}
替换为没有“onmicrosoft.com”的租户名称。确保使用电子邮件一次性密码帐户登录。 然后选择“发送代码”。 确保发送到已注册电子邮件地址的代码使用上面注册的自定义提供程序。
步骤 7:回退到 Microsoft 提供程序
如果扩展 API 中发生错误,默认情况下,Entra ID 不会向用户发送 OTP。 可以改为将错误上的行为设置为回退到 Microsoft 提供程序。
若要启用此功能,请运行以下请求。 将 {customListenerOjectId}
替换为前面记下的自定义身份验证侦听器 ID。
- 需要 EventListener.ReadWrite.All 委托权限。
PATCH https://graph.microsoft.com/beta/identity/authenticationEventListeners/{customListenerOjectId}
{
"@odata.type": "#microsoft.graph.onEmailOtpSendListener",
"handler": {
"@odata.type": "#microsoft.graph.onOtpSendCustomExtensionHandler",
"configuration": {
"behaviorOnError": {
"@odata.type": "#microsoft.graph.fallbackToMicrosoftProviderOnError"
}
}
}
}