添加代码以在机器人应用中启用 SSO
在添加代码以启用单一登录 (SSO) 之前,请确保已在 entra 管理中心Microsoft配置应用和机器人资源。
需要配置应用代码,以便从 Entra ID Microsoft获取访问令牌。 访问令牌是代表机器人应用颁发的。
注意
如果已使用 Microsoft Teams 工具包生成 Teams 应用,则可以按照工具和 SDK 模块中的说明为应用启用 SSO。 有关详细信息,请参阅 向 Teams 应用添加单一登录。 Teams 工具包支持 Visual Studio Code 中的 JavaScript 和 TypeScript 应用的 SSO,在适用于 C# 应用的 Teams 工具包 17.4 预览版 3 中支持 SSO。
本节介绍:
更新开发环境变量
你已在 Entra ID Microsoft中为应用配置了客户端密码和 OAuth 连接设置。 必须使用这些值配置代码。
若要更新开发环境变量,请执行以下操作:
打开机器人应用项目。
打开项目的环境文件。
更新以下变量:
- 对于
MicrosoftAppId
,请从 Microsoft Entra ID 更新机器人 ID。 - 对于
MicrosoftAppPassword
,请更新客户端密码。 - 对于
ConnectionName
,请更新在 Entra ID Microsoft配置的 OAuth 连接的名称。 - 对于
MicrosoftAppTenantId
,请更新租户 ID。
- 对于
保存文件。
现在,你已为机器人应用和 SSO 配置了所需的环境变量。 接下来,添加用于处理机器人令牌的代码。
添加代码以处理访问令牌
获取令牌的请求是使用现有消息架构的 POST 消息请求。 它包含在 OAuthCard 的附件中。 OAuthCard 类的架构在 Microsoft Bot Schema 4.0 中定义。 如果卡上填充了 属性, TokenExchangeResource
Teams 将刷新令牌。 对于 Teams 频道,仅接受唯一标识令牌请求的 Id
属性。
注意
SSO 身份验证支持 Microsoft Bot Framework OAuthPrompt
或 MultiProviderAuthDialog
。
若要更新应用的代码,请执行以下操作:
为
TeamsSSOTokenExchangeMiddleware
添加代码片段。将以下代码片段添加到
AdapterWithErrorHandler.cs
(或应用的代码) 中的等效类:base.Use(new TeamsSSOTokenExchangeMiddleware(storage, configuration["ConnectionName"]));
注意
如果用户有多个活动终结点,则可能会收到给定请求的多个响应。 必须使用 令牌消除所有重复或冗余响应。 有关 signin/tokenExchange 的详细信息,请参阅 TeamsSSOTokenExchangeMiddleware 类。
使用以下代码片段请求令牌。
添加 后,
AdapterWithErrorHandler.cs
必须显示以下代码:public class AdapterWithErrorHandler : CloudAdapter { public AdapterWithErrorHandler( IConfiguration configuration, IHttpClientFactory httpClientFactory, ILogger<IBotFrameworkHttpAdapter> logger, IStorage storage, ConversationState conversationState) : base(configuration, httpClientFactory, logger) { base.Use(new TeamsSSOTokenExchangeMiddleware(storage, configuration["ConnectionName"])); OnTurnError = async (turnContext, exception) => { // Log any leaked exception from the application. // NOTE: In production environment, you must consider logging this to // Azure Application Insights. Visit https://learn.microsoft.com/azure/bot-service/bot-builder-telemetry?view=azure-bot-service-4.0&tabs=csharp to see how // to add telemetry capture to your bot. logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); // Send a message to the user. await turnContext.SendActivityAsync("The bot encountered an error or bug."); await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code."); if (conversationState != null) { try { // Delete the conversationState for the current conversation to prevent the // bot from getting stuck in a error-loop caused by being in a bad state. // conversationState must be thought of as similar to "cookie-state" in a Web pages. await conversationState.DeleteAsync(turnContext); } catch (Exception e) { logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}"); } } // Send a trace activity, which is displayed in the Bot Framework Emulator. await turnContext.TraceActivityAsync( "OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); }; } }
获取访问令牌的许可对话框
如果应用用户首次使用该应用程序,并且需要用户同意,则会出现以下对话框:
当用户选择“ 继续”时,会发生以下事件之一:
如果机器人 UI 具有登录按钮,则会激活机器人的登录流。 可以确定需要应用用户同意的权限。 如果应用需要 Graph 权限,则
openid
使用此方法。如果机器人在 OAuth 卡上没有登录按钮,则需要应用用户同意才能获得最小权限集。 此令牌可用于基本身份验证和获取应用用户的电子邮件地址。
显示的同意对话框适用于在 Entra ID Microsoft 中定义的开放 id 范围。 应用用户必须只提供一次同意。 同意后,应用用户可以访问机器人应用并将其用于授予的权限和范围。
注意
应用用户同意后,不需要他们再次同意任何其他权限。 如果修改了Microsoft Entra 范围中定义的权限,则应用用户可能需要再次同意。 但是,如果同意提示无法允许应用用户访问,机器人应用将回退到登录卡。
重要
不需要同意对话的方案:
- 如果租户管理员已代表租户授予同意,则无需提示应用用户同意。 这意味着应用用户看不到同意对话框,并且可以无缝访问应用。
- 如果你的 Microsoft Entra 应用注册在 Teams 中请求身份验证的同一租户中,则不能要求应用用户同意,并且会立即获得访问令牌。 仅当 Microsoft Entra 应用在不同租户中注册时,应用用户才同意这些权限。
如果遇到任何错误,请参阅 排查 Teams 中的 SSO 身份验证问题。
添加代码以接收令牌
带有令牌的响应通过调用活动发送,该调用活动的架构与机器人今天接收的其他调用活动相同。 唯一的区别是调用名称、sign in/tokenExchange 和 值 字段。 “值”字段包含 ID、获取令牌的初始请求的字符串和令牌字段(包括令牌的字符串值)。
使用以下代码片段调用响应:
public MainDialog(IConfiguration configuration, ILogger<MainDialog> logger)
: base(nameof(MainDialog), configuration["ConnectionName"])
{
AddDialog(new OAuthPrompt(
nameof(OAuthPrompt),
new OAuthPromptSettings
{
ConnectionName = ConnectionName,
Text = "Please Sign In",
Title = "Sign In",
Timeout = 300000, // User has 5 minutes to login (1000 * 60 * 5)
EndOnInvalidMessage = true
}));
AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
PromptStepAsync,
LoginStepAsync,
}));
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
}
private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var tokenResponse = (TokenResponse)stepContext.Result;
if (tokenResponse?.Token != null)
{
var token = tokenResponse.Token;
// On successful login, the token contains sign in token.
}
else
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken);
}
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
注意
代码片段使用瀑布对话。 有关详细信息,请参阅 关于组件和瀑布对话。
验证访问令牌
服务器上的 Web API 必须解码访问令牌,并验证它是否从客户端发送。
注意
如果使用 Bot Framework,它将处理访问令牌验证。 如果不使用 Bot Framework,请遵循本部分中提供的准则。
有关验证访问令牌的详细信息,请参阅 验证令牌。
有许多库可用于处理 JWT 验证。 基本验证包括:
- 检查令牌的格式是否正确。
- 检查令牌是否由预期颁发机构颁发。
- 检查令牌是否面向 Web API。
验证令牌时,请牢记以下准则:
- 有效的 SSO 令牌由 Microsoft Entra ID 颁发。
iss
令牌中的声明必须以此值开头。 - 令牌的参数
aud1
设置为在Microsoft Entra 应用注册期间生成的应用 ID。 - 令牌的
scp
参数设置为access_as_user
。
示例访问令牌
以下代码片段是访问令牌的典型解码有效负载:
{
aud: "2c3caa80-93f9-425e-8b85-0745f50c0d24",
iss: "https://login.microsoftonline.com/fec4f964-8bc9-4fac-b972-1c1da35adbcd/v2.0",
iat: 1521143967,
nbf: 1521143967,
exp: 1521147867,
aio: "ATQAy/8GAAAA0agfnU4DTJUlEqGLisMtBk5q6z+6DB+sgiRjB/Ni73q83y0B86yBHU/WFJnlMQJ8",
azp: "e4590ed6-62b3-5102-beff-bad2292ab01c",
azpacr: "0",
e_exp: 262800,
name: "Mila Nikolova",
oid: "6467882c-fdfd-4354-a1ed-4e13f064be25",
preferred_username: "milan@contoso.com",
scp: "access_as_user",
sub: "XkjgWjdmaZ-_xDmhgN1BMP2vL2YOfeVxfPT_o8GRWaw",
tid: "fec4f964-8bc9-4fac-b972-1c1da35adbcd",
uti: "MICAQyhrH02ov54bCtIDAA",
ver: "2.0"
}
处理应用用户注销
使用以下代码片段在应用用户注销时处理访问令牌:
private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc,
CancellationToken cancellationToken = default(CancellationToken))
{
if (innerDc.Context.Activity.Type == ActivityTypes.Message)
{
var text = innerDc.Context.Activity.Text.ToLowerInvariant();
// Allow logout anywhere in the command.
if (text.IndexOf("logout") >= 0)
{
// The UserTokenClient encapsulates the authentication processes.
var userTokenClient = innerDc.Context.TurnState.Get<UserTokenClient>();
await userTokenClient.SignOutUserAsync(
innerDc.Context.Activity.From.Id,
ConnectionName,
innerDc.Context.Activity.ChannelId,
cancellationToken
).ConfigureAwait(false);
await innerDc.Context.SendActivityAsync(MessageFactory.Text("You have been signed out."), cancellationToken);
return await innerDc.CancelAllDialogsAsync(cancellationToken);
}
}
return null;
}
代码示例
示例名称 | 说明 | C# | Node.js |
---|---|---|---|
机器人对话 SSO 快速入门 | 此示例代码演示如何在适用于 Microsoft Teams 的机器人中开始使用 SSO。 | View | View |
注意
OnTeamsMessagingExtensionQueryAsync
和 OnTeamsAppBasedLinkQueryAsync
文件中 TeamsMessagingExtensionsSearchAuthConfigBot.cs
的唯一支持 SSO 处理程序。 不支持其他 SSO 处理程序。
本节介绍:
更新开发环境变量
你已在 Entra ID Microsoft中为应用配置了客户端密码和 OAuth 连接设置。 必须使用这些变量配置应用代码。
若要更新开发环境变量,请执行以下操作:
打开应用项目。
./env
打开项目的 文件。更新以下变量:
- 对于
MicrosoftAppId
,请从 Microsoft Entra ID 更新机器人注册 ID。 - 对于
MicrosoftAppPassword
,请更新机器人注册客户端密码。 - 对于
ConnectionName
,请更新在 Entra ID Microsoft配置的 OAuth 连接的名称。 - 对于
MicrosoftAppTenantId
,请更新租户 ID。
- 对于
保存文件。
现在,你已为机器人应用和 SSO 配置了所需的环境变量。 接下来,添加用于处理令牌的代码。
添加代码以请求令牌
获取令牌的请求是使用现有消息架构的 POST 消息请求。 它包含在 OAuthCard 的附件中。 OAuthCard 类的架构在 Microsoft Bot Schema 4.0 中定义。 如果卡上填充了 属性, TokenExchangeResource
Teams 将刷新令牌。 对于 Teams 频道,仅接受唯一标识令牌请求的 Id
属性。
注意
SSO 身份验证支持 Microsoft Bot Framework OAuthPrompt
或 MultiProviderAuthDialog
。
若要更新应用的代码,请执行以下操作:
为
TeamsSSOTokenExchangeMiddleware
添加代码片段。将以下代码片段添加到
AdapterWithErrorHandler.cs
(或应用的代码) 中的等效类:base.Use(new TeamsSSOTokenExchangeMiddleware(storage, configuration["ConnectionName"]));
注意
如果用户有多个活动终结点,则可能会收到给定请求的多个响应。 必须使用 令牌消除所有重复或冗余响应。 有关 signin/tokenExchange 的详细信息,请参阅 TeamsSSOTokenExchangeMiddleware 类。
使用以下代码片段请求令牌。
添加 后,
AdapterWithErrorHandler.cs
必须显示以下代码:public class AdapterWithErrorHandler : CloudAdapter { public AdapterWithErrorHandler( IConfiguration configuration, IHttpClientFactory httpClientFactory, ILogger<IBotFrameworkHttpAdapter> logger, IStorage storage, ConversationState conversationState) : base(configuration, httpClientFactory, logger) { base.Use(new TeamsSSOTokenExchangeMiddleware(storage, configuration["ConnectionName"])); OnTurnError = async (turnContext, exception) => { // Log any leaked exception from the application. // NOTE: In production environment, you must consider logging this to // Azure Application Insights. Visit https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-telemetry?view=azure-bot-service-4.0&tabs=csharp to see how // to add telemetry capture to your bot. logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); // Send a message to the user. await turnContext.SendActivityAsync("The bot encountered an error or bug."); await turnContext.SendActivityAsync("To continue to run this bot, please fix the bot source code."); if (conversationState != null) { try { // Delete the conversationState for the current conversation to prevent the // bot from getting stuck in an error-loop caused by being in a bad state. // ConversationState must be thought of as similar to "cookie-state" in a Web pages. await conversationState.DeleteAsync(turnContext); } catch (Exception e) { logger.LogError(e, $"Exception caught on attempting to Delete ConversationState : {e.Message}"); } } // Send a trace activity, which will be displayed in the Bot Framework Emulator. await turnContext.TraceActivityAsync( "OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); }; } }
获取访问令牌的许可对话框
如果应用用户首次使用你的应用,则需要同意 SSO 身份验证。
当应用用户选择用户名时,将授予该权限,并且他们可以使用该应用。
显示的同意对话框适用于在 Entra ID Microsoft 中定义的开放 id 范围。 应用用户必须只提供一次同意。 同意后,应用用户可以访问和使用消息扩展应用来获取已授予的权限和范围。
重要
不需要同意对话的方案:
- 如果租户管理员已代表租户授予同意,则无需提示应用用户同意。 这意味着应用用户看不到同意对话框,并且可以无缝访问应用。
如果遇到任何错误,请参阅 排查 Teams 中的 SSO 身份验证问题。
添加代码以接收令牌
带有令牌的响应通过调用活动发送,该调用活动的架构与机器人今天接收的其他调用活动相同。 唯一的区别是调用名称、sign in/tokenExchange 和 值 字段。 “值”字段包含 ID、获取令牌的初始请求的字符串和令牌字段(包括令牌的字符串值)。
使用以下代码片段示例调用响应:
public MainDialog(IConfiguration configuration, ILogger<MainDialog> logger)
: base(nameof(MainDialog), configuration["ConnectionName"])
{
AddDialog(new OAuthPrompt(
nameof(OAuthPrompt),
new OAuthPromptSettings
{
ConnectionName = ConnectionName,
Text = "Please Sign In",
Title = "Sign In",
Timeout = 300000, // User has 5 minutes to login (1000 * 60 * 5)
EndOnInvalidMessage = true
}));
AddDialog(new ConfirmPrompt(nameof(ConfirmPrompt)));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
PromptStepAsync,
LoginStepAsync,
}));
// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);
}
private async Task<DialogTurnResult> PromptStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
}
private async Task<DialogTurnResult> LoginStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var tokenResponse = (TokenResponse)stepContext.Result;
if (tokenResponse?.Token != null)
{
var token = tokenResponse.Token;
// On successful login, the token contains sign in token.
}
else
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken);
}
return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
注意
代码片段使用瀑布对话机器人。 有关瀑布对话的详细信息,请参阅 关于组件和瀑布对话。
在有效负载或 中的 处理程序turnContext.Activity.Value
中OnTeamsAppBasedLinkQueryAsync
接收令牌OnTeamsMessagingExtensionQueryAsync
,具体取决于要为其启用 SSO 的方案。
JObject valueObject=JObject.FromObject(turnContext.Activity.Value);
if(valueObject["authentication"] !=null)
{
JObject authenticationObject=JObject.FromObject(valueObject["authentication"]);
if(authenticationObject["token"] !=null)
}
验证访问令牌
服务器上的 Web API 必须解码访问令牌,并验证它是否从客户端发送。
注意
如果使用 Bot Framework,它将处理访问令牌验证。 如果不使用 Bot Framework,请遵循本部分中的准则。
有关验证访问令牌的详细信息,请参阅 验证令牌。
有许多库可用于处理 JWT 验证。 基本验证包括:
- 检查令牌的格式是否正确。
- 检查令牌是否由预期颁发机构颁发。
- 检查令牌是否面向 Web API。
验证令牌时,请牢记以下准则:
- 有效的 SSO 令牌由 Microsoft Entra ID 颁发。
iss
令牌中的声明必须以此值开头。 - 令牌的参数
aud1
设置为在Microsoft Entra 应用注册期间生成的应用 ID。 - 令牌的
scp
参数设置为access_as_user
。
示例访问令牌
以下代码片段是访问令牌的典型解码有效负载:
{
aud: "2c3caa80-93f9-425e-8b85-0745f50c0d24",
iss: "https://login.microsoftonline.com/fec4f964-8bc9-4fac-b972-1c1da35adbcd/v2.0",
iat: 1521143967,
nbf: 1521143967,
exp: 1521147867,
aio: "ATQAy/8GAAAA0agfnU4DTJUlEqGLisMtBk5q6z+6DB+sgiRjB/Ni73q83y0B86yBHU/WFJnlMQJ8",
azp: "e4590ed6-62b3-5102-beff-bad2292ab01c",
azpacr: "0",
e_exp: 262800,
name: "Mila Nikolova",
oid: "6467882c-fdfd-4354-a1ed-4e13f064be25",
preferred_username: "milan@contoso.com",
scp: "access_as_user",
sub: "XkjgWjdmaZ-_xDmhgN1BMP2vL2YOfeVxfPT_o8GRWaw",
tid: "fec4f964-8bc9-4fac-b972-1c1da35adbcd",
uti: "MICAQyhrH02ov54bCtIDAA",
ver: "2.0"
}
将令牌添加到 Bot Framework 令牌存储
如果使用 OAuth 连接,则必须在 Bot Framework 令牌存储中更新或添加令牌。 将以下代码片段示例添加到 TeamsMessagingExtensionsSearchAuthConfigBot.cs
(或应用代码中的等效文件,) 用于更新或添加存储中的令牌:
注意
可以在 Tab、机器人和消息扩展中找到示例TeamsMessagingExtensionsSearchAuthConfigBot.cs
, (ME) SSO。
protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
JObject valueObject = JObject.FromObject(turnContext.Activity.Value);
if (valueObject["authentication"] != null)
{
JObject authenticationObject = JObject.FromObject(valueObject["authentication"]);
if (authenticationObject["token"] != null)
{
//If the token is NOT exchangeable, then return 412 to require user consent.
if (await TokenIsExchangeable(turnContext, cancellationToken))
{
return await base.OnInvokeActivityAsync(turnContext, cancellationToken).ConfigureAwait(false);
}
else
{
var response = new InvokeResponse();
response.Status = 412;
return response;
}
}
}
return await base.OnInvokeActivityAsync(turnContext, cancellationToken).ConfigureAwait(false);
}
private async Task<bool> TokenIsExchangeable(ITurnContext turnContext, CancellationToken cancellationToken)
{
TokenResponse tokenExchangeResponse = null;
try
{
JObject valueObject = JObject.FromObject(turnContext.Activity.Value);
var tokenExchangeRequest =
((JObject)valueObject["authentication"])?.ToObject<TokenExchangeInvokeRequest>();
var userTokenClient = turnContext.TurnState.Get<UserTokenClient>();
tokenExchangeResponse = await userTokenClient.ExchangeTokenAsync(
turnContext.Activity.From.Id,
_connectionName,
turnContext.Activity.ChannelId,
new TokenExchangeRequest
{
Token = tokenExchangeRequest.Token,
},
cancellationToken).ConfigureAwait(false);
}
#pragma warning disable CA1031 //Do not catch general exception types (ignoring, see comment below)
catch
#pragma warning restore CA1031 //Do not catch general exception types
{
//ignore exceptions.
//if token exchange failed for any reason, tokenExchangeResponse above remains null, and a failure invoke response is sent to the caller.
//This ensures the caller knows that the invoke has failed.
}
if (tokenExchangeResponse == null || string.IsNullOrEmpty(tokenExchangeResponse.Token))
{
return false;
}
return true;
}
处理应用用户注销
使用以下代码片段在应用用户注销时处理访问令牌:
private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc,
CancellationToken cancellationToken = default(CancellationToken))
{
if (innerDc.Context.Activity.Type == ActivityTypes.Message)
{
var text = innerDc.Context.Activity.Text.ToLowerInvariant();
// Allow logout anywhere in the command.
if (text.IndexOf("logout") >= 0)
{
// The UserTokenClient encapsulates the authentication processes.
var userTokenClient = innerDc.Context.TurnState.Get<UserTokenClient>();
await userTokenClient.SignOutUserAsync(
innerDc.Context.Activity.From.Id,
ConnectionName,
innerDc.Context.Activity.ChannelId,
cancellationToken
).ConfigureAwait(false);
await innerDc.Context.SendActivityAsync(MessageFactory.Text("You have been signed out."), cancellationToken);
return await innerDc.CancelAllDialogsAsync(cancellationToken);
}
}
return null;
}
代码示例
本部分提供机器人身份验证 v3 SDK 示例。
示例名称 | 说明 | .NET | Node.js | Python | 清单 |
---|---|---|---|---|---|
机器人身份验证 | 此示例演示如何在 Teams 机器人中开始进行身份验证。 | View | View | View | View |
选项卡、机器人和消息扩展 (ME) SSO | 此示例演示选项卡、机器人和消息扩展的 SSO - 搜索、操作、链接展开。 | View | View | 不适用 | View |
选项卡、机器人和消息扩展 | 此示例演示如何使用 SSO 在机器人、选项卡和消息扩展中检查身份验证 | View | View | 不适用 | View |