@microsoft/teamsfx-core 3.0.5-alpha.e092ea0a6.0 → 3.0.5-alpha.e2e0fdf1c.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/client/graphClient.d.ts +8 -4
- package/build/client/graphClient.d.ts.map +1 -1
- package/build/client/graphClient.js +35 -8
- package/build/client/graphClient.js.map +1 -1
- package/build/client/interfaces/GetGroupResponse.d.ts +6 -0
- package/build/client/interfaces/GetGroupResponse.d.ts.map +1 -0
- package/build/client/interfaces/GetGroupResponse.js +5 -0
- package/build/client/interfaces/GetGroupResponse.js.map +1 -0
- package/build/client/interfaces/GetUserResponse.d.ts +7 -0
- package/build/client/interfaces/GetUserResponse.d.ts.map +1 -0
- package/build/client/interfaces/GetUserResponse.js +5 -0
- package/build/client/interfaces/GetUserResponse.js.map +1 -0
- package/build/common/constants.d.ts +1 -0
- package/build/common/constants.d.ts.map +1 -1
- package/build/common/constants.js +2 -1
- package/build/common/constants.js.map +1 -1
- package/build/common/permissionInterface.d.ts +6 -0
- package/build/common/permissionInterface.d.ts.map +1 -1
- package/build/common/templates-config.json +1 -1
- package/build/component/deps-checker/internal/testToolChecker.js +1 -1
- package/build/component/deps-checker/internal/testToolChecker.js.map +1 -1
- package/build/component/driver/teamsApp/utils/PluginManifestUtils.d.ts +1 -1
- package/build/component/driver/teamsApp/utils/PluginManifestUtils.d.ts.map +1 -1
- package/build/component/driver/teamsApp/utils/PluginManifestUtils.js +5 -2
- package/build/component/driver/teamsApp/utils/PluginManifestUtils.js.map +1 -1
- package/build/component/feature/collaboration.d.ts +7 -1
- package/build/component/feature/collaboration.d.ts.map +1 -1
- package/build/component/feature/collaboration.js +84 -1
- package/build/component/feature/collaboration.js.map +1 -1
- package/build/component/generator/combinedProject/generator.d.ts.map +1 -1
- package/build/component/generator/combinedProject/generator.js +2 -1
- package/build/component/generator/combinedProject/generator.js.map +1 -1
- package/build/component/generator/declarativeAgent/generator.d.ts.map +1 -1
- package/build/component/generator/declarativeAgent/generator.js +2 -1
- package/build/component/generator/declarativeAgent/generator.js.map +1 -1
- package/build/component/generator/generator.d.ts +1 -1
- package/build/component/generator/generator.d.ts.map +1 -1
- package/build/component/generator/generator.js +2 -1
- package/build/component/generator/generator.js.map +1 -1
- package/build/component/generator/openApiSpec/common.d.ts.map +1 -1
- package/build/component/generator/openApiSpec/common.js +2 -1
- package/build/component/generator/openApiSpec/common.js.map +1 -1
- package/build/component/generator/other/ssrTabGenerator.d.ts.map +1 -1
- package/build/component/generator/other/ssrTabGenerator.js +2 -1
- package/build/component/generator/other/ssrTabGenerator.js.map +1 -1
- package/build/component/generator/other/tdpGenerator.d.ts.map +1 -1
- package/build/component/generator/other/tdpGenerator.js +2 -1
- package/build/component/generator/other/tdpGenerator.js.map +1 -1
- package/build/component/generator/templates/templateReplaceMap.d.ts.map +1 -1
- package/build/component/generator/templates/templateReplaceMap.js +8 -6
- package/build/component/generator/templates/templateReplaceMap.js.map +1 -1
- package/build/component/m365/packageService.d.ts +6 -1
- package/build/component/m365/packageService.d.ts.map +1 -1
- package/build/component/m365/packageService.js +28 -5
- package/build/component/m365/packageService.js.map +1 -1
- package/build/core/FxCore.d.ts.map +1 -1
- package/build/core/FxCore.js +1 -29
- package/build/core/FxCore.js.map +1 -1
- package/build/core/collaborator.d.ts +1 -0
- package/build/core/collaborator.d.ts.map +1 -1
- package/build/core/collaborator.js +55 -29
- package/build/core/collaborator.js.map +1 -1
- package/build/core/share.d.ts.map +1 -1
- package/build/core/share.js +16 -2
- package/build/core/share.js.map +1 -1
- package/build/question/collaborator.d.ts +6 -0
- package/build/question/collaborator.d.ts.map +1 -0
- package/build/question/collaborator.js +166 -0
- package/build/question/collaborator.js.map +1 -0
- package/build/question/index.d.ts.map +1 -1
- package/build/question/index.js +3 -2
- package/build/question/index.js.map +1 -1
- package/build/question/inputs/ShareInputs.d.ts +2 -2
- package/build/question/inputs/ShareInputs.d.ts.map +1 -1
- package/build/question/options/ShareOptions.js +1 -1
- package/build/question/options/ShareOptions.js.map +1 -1
- package/build/question/other.d.ts +1 -4
- package/build/question/other.d.ts.map +1 -1
- package/build/question/other.js +2 -145
- package/build/question/other.js.map +1 -1
- package/build/question/questionNames.d.ts +1 -0
- package/build/question/questionNames.d.ts.map +1 -1
- package/build/question/questionNames.js +1 -0
- package/build/question/questionNames.js.map +1 -1
- package/build/question/share.d.ts +1 -3
- package/build/question/share.d.ts.map +1 -1
- package/build/question/share.js +2 -14
- package/build/question/share.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/resource/package.nls.json +7 -4
- package/templates/fallback/common.zip +0 -0
- package/templates/fallback/csharp.zip +0 -0
- package/templates/fallback/js.zip +0 -0
- package/templates/fallback/python.zip +0 -0
- package/templates/fallback/ts.zip +0 -0
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/BotAuthenticationOptions.cs +44 -0
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/IIdentityClientAdapter.cs +21 -0
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/ITeamsInfo.cs +24 -0
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/IdentityClientAdapter.cs +30 -0
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/SsoDialog.cs +12 -9
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/SsoOperations.cs +2 -2
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsBotSsoPrompt.cs +423 -0
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsBotSsoPromptSettings.cs +41 -0
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsBotSsoPromptTokenResponse.cs +21 -0
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsInfoWrapper.cs +20 -0
- package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsSsoBot.cs +6 -5
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
using Antlr4.Runtime.Misc;
|
|
5
|
+
using Azure.Core;
|
|
6
|
+
using Microsoft.Agents.Builder;
|
|
7
|
+
using Microsoft.Agents.Builder.Dialogs;
|
|
8
|
+
using Microsoft.Agents.Builder.Dialogs.Prompts;
|
|
9
|
+
using Microsoft.Agents.Core.Models;
|
|
10
|
+
using Microsoft.Agents.Extensions.Teams.Models;
|
|
11
|
+
using Microsoft.Identity.Client;
|
|
12
|
+
using System.Net;
|
|
13
|
+
using System.Text.RegularExpressions;
|
|
14
|
+
using System.IdentityModel.Tokens.Jwt;
|
|
15
|
+
using {{YOUR_NAMESPACE}}.Configuration;
|
|
16
|
+
using {{YOUR_NAMESPACE}}.SSO;
|
|
17
|
+
using System.Text.Json;
|
|
18
|
+
using Json.More;
|
|
19
|
+
|
|
20
|
+
namespace {{YOUR_NAMESPACE}};
|
|
21
|
+
|
|
22
|
+
/// <summary>
|
|
23
|
+
/// Creates a new prompt that leverage Teams Single Sign On (SSO) support for bot to automatically sign in user and
|
|
24
|
+
/// help receive oauth token, asks the user to consent if needed.
|
|
25
|
+
/// </summary>
|
|
26
|
+
/// <remarks>
|
|
27
|
+
/// The prompt will attempt to retrieve the user's current token of the desired scopes.
|
|
28
|
+
/// User will be automatically signed in leveraging Teams support of Bot Single Sign On(SSO):
|
|
29
|
+
/// https://docs.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/auth-aad-sso-bots
|
|
30
|
+
/// </remarks>
|
|
31
|
+
///
|
|
32
|
+
/// <example>
|
|
33
|
+
/// ## Prompt Usage
|
|
34
|
+
///
|
|
35
|
+
/// When used with your bot's <see cref="DialogSet"/> you can simply add a new instance of the prompt as a named
|
|
36
|
+
/// dialog using <see cref="DialogSet.Add(Dialog)"/>. You can then start the prompt from a waterfall step using either
|
|
37
|
+
/// <see cref="DialogContext.BeginDialogAsync(string, object, CancellationToken)"/> or
|
|
38
|
+
/// <see cref="DialogContext.PromptAsync(string, PromptOptions, CancellationToken)"/>. The user
|
|
39
|
+
/// will be prompted to signin as needed and their access token will be passed as an argument to
|
|
40
|
+
/// the caller's next waterfall step.
|
|
41
|
+
///
|
|
42
|
+
/// <code>
|
|
43
|
+
/// var convoState = new ConversationState(new MemoryStorage());
|
|
44
|
+
/// var dialogState = convoState.CreateProperty<DialogState>("dialogState");
|
|
45
|
+
/// var dialogs = new DialogSet(dialogState);
|
|
46
|
+
/// var botAuthOptions = new BotAuthenticationOptions {
|
|
47
|
+
/// ClientId = "{client_id_guid_value}",
|
|
48
|
+
/// ClientSecret = "{client_secret_value}",
|
|
49
|
+
/// TenantId = "{tenant_id_guid_value}",
|
|
50
|
+
/// ApplicationIdUri = "{application_id_uri_value}",
|
|
51
|
+
/// OAuthAuthority = "https://login.microsoftonline.com/{tenant_id_guid_value}",
|
|
52
|
+
/// LoginStartPageEndpoint = "https://{bot_web_app_domain}/bot-auth-start"
|
|
53
|
+
/// };
|
|
54
|
+
///
|
|
55
|
+
/// var scopes = new string[] { "User.Read" };
|
|
56
|
+
/// var teamsBotSsoPromptSettings = new TeamsBotSsoPromptSettings(botAuthOptions, scopes);
|
|
57
|
+
///
|
|
58
|
+
/// dialogs.Add(new TeamsBotSsoPrompt("{unique_id_for_the_prompt}", teamsBotSsoPromptSettings));
|
|
59
|
+
/// dialogs.Add(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
|
|
60
|
+
/// {
|
|
61
|
+
/// async(WaterfallStepContext stepContext, CancellationToken cancellationToken) => {
|
|
62
|
+
/// return await stepContext.BeginDialogAsync(nameof(TeamsBotSsoPrompt), null, cancellationToken);
|
|
63
|
+
/// },
|
|
64
|
+
/// async(WaterfallStepContext stepContext, CancellationToken cancellationToken) => {
|
|
65
|
+
/// var tokenResponse = (TeamsBotSsoPromptTokenResponse)stepContext.Result;
|
|
66
|
+
/// if (tokenResponse?.Token != null)
|
|
67
|
+
/// {
|
|
68
|
+
/// // ... continue with task needing access token ...
|
|
69
|
+
/// }
|
|
70
|
+
/// else
|
|
71
|
+
/// {
|
|
72
|
+
/// await stepContext.Context.SendActivityAsync(MessageFactory.Text("Login was not successful please try again."), cancellationToken);
|
|
73
|
+
/// }
|
|
74
|
+
/// return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
|
|
75
|
+
/// }
|
|
76
|
+
/// }));
|
|
77
|
+
/// </code>
|
|
78
|
+
///
|
|
79
|
+
/// </example>
|
|
80
|
+
public class TeamsBotSsoPrompt : Dialog
|
|
81
|
+
{
|
|
82
|
+
private readonly TeamsBotSsoPromptSettings _settings;
|
|
83
|
+
private const string PersistedExpires = "expires";
|
|
84
|
+
internal IIdentityClientAdapter _identityClientAdapter { private get; set; }
|
|
85
|
+
internal ITeamsInfo _teamsInfo { private get; set; }
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/// <summary>
|
|
89
|
+
/// Initializes a new instance of the <see cref="TeamsBotSsoPrompt"/> class.
|
|
90
|
+
/// </summary>
|
|
91
|
+
/// <param name="dialogId">The ID to assign to this prompt.</param>
|
|
92
|
+
/// <param name="settings">Additional OAuth settings to use with this instance of the prompt.</param>
|
|
93
|
+
/// <remarks>The value of <paramref name="dialogId"/> must be unique within the
|
|
94
|
+
/// <see cref="DialogSet"/> or <see cref="ComponentDialog"/> to which the prompt is added.</remarks>
|
|
95
|
+
/// <exception cref="ExceptionCode.InvalidParameter">When input parameters is null.</exception>
|
|
96
|
+
public TeamsBotSsoPrompt(string dialogId, TeamsBotSsoPromptSettings settings) : base(dialogId)
|
|
97
|
+
{
|
|
98
|
+
if (string.IsNullOrWhiteSpace(dialogId))
|
|
99
|
+
{
|
|
100
|
+
throw new Exception($"Parameter {nameof(dialogId)} is null or empty.");
|
|
101
|
+
}
|
|
102
|
+
_settings = settings ?? throw new Exception($"Parameter {nameof(settings)} is null or empty.");
|
|
103
|
+
|
|
104
|
+
var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(_settings.BotAuthOptions.ClientId)
|
|
105
|
+
.WithClientSecret(_settings.BotAuthOptions.ClientSecret)
|
|
106
|
+
.WithAuthority(_settings.BotAuthOptions.OAuthAuthority)
|
|
107
|
+
.Build();
|
|
108
|
+
_identityClientAdapter = new IdentityClientAdapter(confidentialClientApplication);
|
|
109
|
+
_teamsInfo = new TeamsInfoWrapper();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// <summary>
|
|
113
|
+
/// Called when the dialog is started and pushed onto the dialog stack.
|
|
114
|
+
/// </summary>
|
|
115
|
+
/// <param name="dialogContext">The Microsoft.Bot.Builder.Dialogs.DialogContext for the current turn of conversation.</param>
|
|
116
|
+
/// <param name="options">Optional, initial information to pass to the dialog.</param>
|
|
117
|
+
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
|
118
|
+
/// <returns> A System.Threading.Tasks.Task representing the asynchronous operation.</returns>
|
|
119
|
+
/// <exception cref="ExceptionCode.InvalidParameter">if dialog context argument is null</exception>
|
|
120
|
+
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dialogContext, object options = null, CancellationToken cancellationToken = default)
|
|
121
|
+
{
|
|
122
|
+
if (dialogContext == null)
|
|
123
|
+
{
|
|
124
|
+
throw new Exception($"Parameter {nameof(dialogContext)} is null or empty.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
EnsureMsTeamsChannel(dialogContext);
|
|
128
|
+
|
|
129
|
+
var state = dialogContext.ActiveDialog?.State;
|
|
130
|
+
state[PersistedExpires] = DateTime.UtcNow.AddMilliseconds(_settings.Timeout);
|
|
131
|
+
|
|
132
|
+
// Send OAuthCard that tells Teams to obtain an authentication token for the bot application.
|
|
133
|
+
await SendOAuthCardToObtainTokenAsync(dialogContext.Context, cancellationToken).ConfigureAwait(false);
|
|
134
|
+
return EndOfTurn;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// <summary>
|
|
138
|
+
/// Called when a prompt dialog is the active dialog and the user replied with a new activity.
|
|
139
|
+
/// </summary>
|
|
140
|
+
/// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
|
|
141
|
+
/// <param name="cancellationToken">A cancellation token that can be used by other objects
|
|
142
|
+
/// or threads to receive notice of cancellation.</param>
|
|
143
|
+
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
|
144
|
+
/// <remarks>If the task is successful, the result indicates whether the dialog is still
|
|
145
|
+
/// active after the turn has been processed by the dialog.
|
|
146
|
+
/// <para>The prompt generally ends on invalid message from user's reply.</para></remarks>
|
|
147
|
+
/// <exception cref="ExceptionCode.InternalError">When failed to login with unknown error.</exception>
|
|
148
|
+
/// <exception cref="ExceptionCode.ServiceError">When failed to get access token from identity server(AAD).</exception>
|
|
149
|
+
public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
|
|
150
|
+
{
|
|
151
|
+
EnsureMsTeamsChannel(dc);
|
|
152
|
+
|
|
153
|
+
// Check for timeout
|
|
154
|
+
var state = dc.ActiveDialog?.State;
|
|
155
|
+
bool isMessage = (dc.Context.Activity.Type == ActivityTypes.Message);
|
|
156
|
+
bool isTimeoutActivityType =
|
|
157
|
+
isMessage ||
|
|
158
|
+
IsTeamsVerificationInvoke(dc.Context) ||
|
|
159
|
+
IsTokenExchangeRequestInvoke(dc.Context);
|
|
160
|
+
|
|
161
|
+
// If the incoming Activity is a message, or an Activity Type normally handled by TeamsBotSsoPrompt,
|
|
162
|
+
// check to see if this TeamsBotSsoPrompt Expiration has elapsed, and end the dialog if so.
|
|
163
|
+
bool hasTimedOut = isTimeoutActivityType && DateTime.Compare(DateTime.UtcNow, (DateTime)state[PersistedExpires]) > 0;
|
|
164
|
+
if (hasTimedOut)
|
|
165
|
+
{
|
|
166
|
+
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
167
|
+
}
|
|
168
|
+
else
|
|
169
|
+
{
|
|
170
|
+
if (IsTeamsVerificationInvoke(dc.Context) || IsTokenExchangeRequestInvoke(dc.Context))
|
|
171
|
+
{
|
|
172
|
+
// Recognize token
|
|
173
|
+
PromptRecognizerResult<TeamsBotSsoPromptTokenResponse> recognized = await RecognizeTokenAsync(dc, cancellationToken).ConfigureAwait(false);
|
|
174
|
+
|
|
175
|
+
if (recognized.Succeeded)
|
|
176
|
+
{
|
|
177
|
+
return await dc.EndDialogAsync(recognized.Value, cancellationToken).ConfigureAwait(false);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (isMessage)
|
|
181
|
+
{
|
|
182
|
+
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return EndOfTurn;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// <summary>
|
|
190
|
+
/// This is intended for internal use.
|
|
191
|
+
/// </summary>
|
|
192
|
+
/// <param name="dc">DialogContext.</param>
|
|
193
|
+
/// <param name="cancellationToken">CancellationToken.</param>
|
|
194
|
+
/// <returns>PromptRecognizerResult.</returns>
|
|
195
|
+
/// <exception cref="ExceptionCode.InternalError">When failed to login with unknown error.</exception>
|
|
196
|
+
/// <exception cref="ExceptionCode.ServiceError">When failed to get access token from identity server(AAD).</exception>
|
|
197
|
+
private async Task<PromptRecognizerResult<TeamsBotSsoPromptTokenResponse>> RecognizeTokenAsync(DialogContext dc, CancellationToken cancellationToken)
|
|
198
|
+
{
|
|
199
|
+
|
|
200
|
+
ITurnContext context = dc.Context;
|
|
201
|
+
var result = new PromptRecognizerResult<TeamsBotSsoPromptTokenResponse>();
|
|
202
|
+
TeamsBotSsoPromptTokenResponse tokenResponse = null;
|
|
203
|
+
|
|
204
|
+
if (IsTokenExchangeRequestInvoke(context))
|
|
205
|
+
{
|
|
206
|
+
var tokenResponseObject = context.Activity.Value.ToJsonDocument();
|
|
207
|
+
string ssoToken = tokenResponseObject.RootElement.GetProperty("token").ToString();
|
|
208
|
+
// Received activity is not a token exchange request
|
|
209
|
+
if (String.IsNullOrEmpty(ssoToken))
|
|
210
|
+
{
|
|
211
|
+
var warningMsg =
|
|
212
|
+
"The bot received an InvokeActivity that is missing a TokenExchangeInvokeRequest value. This is required to be sent with the InvokeActivity.";
|
|
213
|
+
await SendInvokeResponseAsync(context, HttpStatusCode.BadRequest, warningMsg, cancellationToken).ConfigureAwait(false);
|
|
214
|
+
}
|
|
215
|
+
else
|
|
216
|
+
{
|
|
217
|
+
try
|
|
218
|
+
{
|
|
219
|
+
var exchangedToken = await GetToken(ssoToken, _settings.Scopes).ConfigureAwait(false);
|
|
220
|
+
|
|
221
|
+
var ssoTokenObj = ParseJwt(ssoToken);
|
|
222
|
+
var ssoExpiration = DateTimeOffset.FromUnixTimeSeconds(long.Parse(ssoTokenObj.Payload["exp"].ToString()));
|
|
223
|
+
tokenResponse = new TeamsBotSsoPromptTokenResponse
|
|
224
|
+
{
|
|
225
|
+
SsoToken = ssoToken,
|
|
226
|
+
SsoTokenExpiration = ssoExpiration.ToString(),
|
|
227
|
+
Token = exchangedToken.Token,
|
|
228
|
+
Expiration = exchangedToken.ExpiresOn.ToString(),
|
|
229
|
+
ConnectionName = "fakeConnectionName"
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
await SendInvokeResponseAsync(context, HttpStatusCode.OK, null, cancellationToken).ConfigureAwait(false);
|
|
233
|
+
}
|
|
234
|
+
catch (MsalUiRequiredException) // Need user interaction
|
|
235
|
+
{
|
|
236
|
+
var warningMsg = "The bot is unable to exchange token. Ask for user consent first.";
|
|
237
|
+
await SendInvokeResponseAsync(context, HttpStatusCode.PreconditionFailed, new TokenExchangeInvokeResponse
|
|
238
|
+
{
|
|
239
|
+
Id = context.Activity.Id,
|
|
240
|
+
FailureDetail = warningMsg,
|
|
241
|
+
}, cancellationToken).ConfigureAwait(false);
|
|
242
|
+
}
|
|
243
|
+
catch (MsalServiceException ex) // Errors that returned from AAD service
|
|
244
|
+
{
|
|
245
|
+
throw new Exception($"Failed to get access token from OAuth identity server with error: {ex.ResponseBody}");
|
|
246
|
+
}
|
|
247
|
+
catch (MsalClientException ex) // Exceptions that are local to the MSAL library
|
|
248
|
+
{
|
|
249
|
+
throw new Exception($"Failed to get access token with error: {ex.Message}");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else if (IsTeamsVerificationInvoke(context))
|
|
255
|
+
{
|
|
256
|
+
await SendOAuthCardToObtainTokenAsync(context, cancellationToken).ConfigureAwait(false);
|
|
257
|
+
await SendInvokeResponseAsync(context, HttpStatusCode.OK, null, cancellationToken).ConfigureAwait(false);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (tokenResponse != null)
|
|
261
|
+
{
|
|
262
|
+
result.Succeeded = true;
|
|
263
|
+
result.Value = tokenResponse;
|
|
264
|
+
}
|
|
265
|
+
else
|
|
266
|
+
{
|
|
267
|
+
result.Succeeded = false;
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async Task<AccessToken> GetToken(string ssoToken, string[] scopes)
|
|
273
|
+
{
|
|
274
|
+
AccessToken result;
|
|
275
|
+
var ssoTokenObj = ParseJwt(ssoToken);
|
|
276
|
+
var ssoTokenExpiration = DateTimeOffset.FromUnixTimeSeconds(long.Parse(ssoTokenObj.Payload["exp"].ToString()));
|
|
277
|
+
|
|
278
|
+
// Get sso token
|
|
279
|
+
if (scopes.Length == 0)
|
|
280
|
+
{
|
|
281
|
+
if (DateTimeOffset.Compare(DateTimeOffset.UtcNow, ssoTokenExpiration) > 0)
|
|
282
|
+
{
|
|
283
|
+
throw new Exception("SSO token has already expired.");
|
|
284
|
+
}
|
|
285
|
+
result = new AccessToken(ssoToken, ssoTokenExpiration);
|
|
286
|
+
}
|
|
287
|
+
else
|
|
288
|
+
{
|
|
289
|
+
var authenticationResult = await _identityClientAdapter.GetAccessToken(ssoToken, scopes).ConfigureAwait(false);
|
|
290
|
+
result = new AccessToken(authenticationResult.AccessToken, authenticationResult.ExpiresOn);
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private static async Task SendInvokeResponseAsync(ITurnContext turnContext, HttpStatusCode statusCode, object body, CancellationToken cancellationToken)
|
|
296
|
+
{
|
|
297
|
+
await turnContext.SendActivityAsync(
|
|
298
|
+
new Activity
|
|
299
|
+
{
|
|
300
|
+
Type = ActivityTypes.InvokeResponse,
|
|
301
|
+
Value = new InvokeResponse
|
|
302
|
+
{
|
|
303
|
+
Status = (int)statusCode,
|
|
304
|
+
Body = body,
|
|
305
|
+
},
|
|
306
|
+
}, cancellationToken).ConfigureAwait(false);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private bool IsTeamsVerificationInvoke(ITurnContext context)
|
|
310
|
+
{
|
|
311
|
+
return (context.Activity.Type == ActivityTypes.Invoke) && (context.Activity.Name == SignInConstants.VerifyStateOperationName);
|
|
312
|
+
}
|
|
313
|
+
private bool IsTokenExchangeRequestInvoke(ITurnContext context)
|
|
314
|
+
{
|
|
315
|
+
return (context.Activity.Type == ActivityTypes.Invoke) && (context.Activity.Name == SignInConstants.TokenExchangeOperationName);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// <summary>
|
|
319
|
+
/// Send OAuthCard that tells Teams to obtain an authentication token for the bot application.
|
|
320
|
+
/// For details see https://docs.microsoft.com/en-us/microsoftteams/platform/bots/how-to/authentication/auth-aad-sso-bots.
|
|
321
|
+
/// </summary>
|
|
322
|
+
/// <param name="context">ITurnContext</param>
|
|
323
|
+
/// <param name="cancellationToken">CancellationToken.</param>
|
|
324
|
+
/// <returns>The task to await.</returns>
|
|
325
|
+
private async Task SendOAuthCardToObtainTokenAsync(ITurnContext context, CancellationToken cancellationToken)
|
|
326
|
+
{
|
|
327
|
+
TeamsChannelAccount account = await _teamsInfo.GetTeamsMemberAsync(context, context.Activity.From.Id, cancellationToken).ConfigureAwait(false);
|
|
328
|
+
|
|
329
|
+
string loginHint = account.UserPrincipalName ?? "";
|
|
330
|
+
if (String.IsNullOrEmpty(account.TenantId))
|
|
331
|
+
{
|
|
332
|
+
throw new Exception("Failed to get tenant id through bot framework.");
|
|
333
|
+
}
|
|
334
|
+
string tenantId = account.TenantId ?? "";
|
|
335
|
+
SignInResource signInResource = GetSignInResource(loginHint, tenantId);
|
|
336
|
+
|
|
337
|
+
// Ensure prompt initialized
|
|
338
|
+
IActivity prompt = Activity.CreateMessageActivity();
|
|
339
|
+
prompt.Attachments = new List<Attachment>();
|
|
340
|
+
prompt.Attachments.Add(new Attachment
|
|
341
|
+
{
|
|
342
|
+
ContentType = OAuthCard.ContentType,
|
|
343
|
+
Content = new OAuthCard
|
|
344
|
+
{
|
|
345
|
+
Text = "Sign In",
|
|
346
|
+
ConnectionName = "fakeConnectionName",
|
|
347
|
+
Buttons = new[]
|
|
348
|
+
{
|
|
349
|
+
new CardAction
|
|
350
|
+
{
|
|
351
|
+
Title = "Teams SSO Sign In",
|
|
352
|
+
Value = signInResource.SignInLink,
|
|
353
|
+
Type = ActionTypes.Signin,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
TokenExchangeResource = signInResource.TokenExchangeResource,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
// Send prompt
|
|
360
|
+
await context.SendActivityAsync(prompt, cancellationToken).ConfigureAwait(false);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
/// <summary>
|
|
365
|
+
/// Get sign in authentication configuration
|
|
366
|
+
/// </summary>
|
|
367
|
+
/// <param name="loginHint">login hint</param>
|
|
368
|
+
/// <param name="tenantId">tenant id</param>
|
|
369
|
+
/// <returns>sign in resource</returns>
|
|
370
|
+
private SignInResource GetSignInResource(string loginHint, string tenantId)
|
|
371
|
+
{
|
|
372
|
+
string signInLink = $"{_settings.BotAuthOptions.InitiateLoginEndpoint}?scope={Uri.EscapeDataString(string.Join(" ", _settings.Scopes))}&clientId={_settings.BotAuthOptions.ClientId}&tenantId={tenantId}&loginHint={loginHint}";
|
|
373
|
+
|
|
374
|
+
SignInResource signInResource = new SignInResource
|
|
375
|
+
{
|
|
376
|
+
SignInLink = signInLink,
|
|
377
|
+
TokenExchangeResource = new TokenExchangeResource
|
|
378
|
+
{
|
|
379
|
+
Id = Guid.NewGuid().ToString(),
|
|
380
|
+
Uri = Regex.Replace(_settings.BotAuthOptions.ApplicationIdUri, @"/\/$/", "") + "/access_as_user"
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
return signInResource;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// <summary>
|
|
388
|
+
/// Ensure bot is running in MS Teams since TeamsBotSsoPrompt is only supported in MS Teams channel.
|
|
389
|
+
/// </summary>
|
|
390
|
+
/// <param name="dc">dialog context</param>
|
|
391
|
+
/// <exception cref="ExceptionCode.ChannelNotSupported"> if bot channel is not MS Teams </exception>
|
|
392
|
+
private void EnsureMsTeamsChannel(DialogContext dc)
|
|
393
|
+
{
|
|
394
|
+
if (dc.Context.Activity.ChannelId != Channels.Msteams)
|
|
395
|
+
{
|
|
396
|
+
var errorMessage = "Teams Bot SSO Prompt is only supported in MS Teams Channel";
|
|
397
|
+
throw new Exception(errorMessage);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private static JwtSecurityToken ParseJwt(string token)
|
|
402
|
+
{
|
|
403
|
+
if (string.IsNullOrEmpty(token))
|
|
404
|
+
{
|
|
405
|
+
throw new Exception("SSO token is null or empty.");
|
|
406
|
+
}
|
|
407
|
+
var handler = new JwtSecurityTokenHandler();
|
|
408
|
+
try
|
|
409
|
+
{
|
|
410
|
+
var jsonToken = handler.ReadToken(token);
|
|
411
|
+
if (jsonToken is not JwtSecurityToken tokenS || string.IsNullOrEmpty(tokenS.Payload["exp"].ToString()))
|
|
412
|
+
{
|
|
413
|
+
throw new Exception("Decoded token is null or exp claim does not exists.");
|
|
414
|
+
}
|
|
415
|
+
return tokenS;
|
|
416
|
+
}
|
|
417
|
+
catch (ArgumentException e)
|
|
418
|
+
{
|
|
419
|
+
var errorMessage = $"Parse jwt token failed with error: {e.Message}";
|
|
420
|
+
throw new Exception(errorMessage);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
using {{YOUR_NAMESPACE}}.Configuration;
|
|
5
|
+
|
|
6
|
+
namespace {{YOUR_NAMESPACE}}.SSO;
|
|
7
|
+
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Contains settings for an <see cref="TeamsBotSsoPrompt"/>.
|
|
10
|
+
/// </summary>
|
|
11
|
+
public class TeamsBotSsoPromptSettings
|
|
12
|
+
{
|
|
13
|
+
|
|
14
|
+
/// <summary>
|
|
15
|
+
/// Constructor of TeamsBotSsoPromptSettings
|
|
16
|
+
/// </summary>
|
|
17
|
+
public TeamsBotSsoPromptSettings(BotAuthenticationOptions botAuthOptions, string[] scopes, int timeout = 900000)
|
|
18
|
+
{
|
|
19
|
+
BotAuthOptions = botAuthOptions;
|
|
20
|
+
Scopes = scopes;
|
|
21
|
+
Timeout = timeout;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// <summary>
|
|
25
|
+
/// Gets or sets the array of strings that declare the desired permissions and the resources requested.
|
|
26
|
+
/// </summary>
|
|
27
|
+
/// <value>The array of strings that declare the desired permissions and the resources requested.</value>
|
|
28
|
+
public string[] Scopes { get; set; }
|
|
29
|
+
|
|
30
|
+
/// <summary>
|
|
31
|
+
/// Gets or sets the number of milliseconds the prompt waits for the user to authenticate.
|
|
32
|
+
/// Default is 900,000 (15 minutes).
|
|
33
|
+
/// </summary>
|
|
34
|
+
/// <value>The number of milliseconds the prompt waits for the user to authenticate.</value>
|
|
35
|
+
public int Timeout { get; set; }
|
|
36
|
+
|
|
37
|
+
/// <summary>
|
|
38
|
+
/// Gets or sets bot related authentication options.
|
|
39
|
+
/// </summary>
|
|
40
|
+
public BotAuthenticationOptions BotAuthOptions { get; set; }
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
using Microsoft.Agents.Core.Models;
|
|
5
|
+
namespace {{YOUR_NAMESPACE}}.SSO;
|
|
6
|
+
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Token response provided by Teams Bot SSO prompt
|
|
9
|
+
/// </summary>
|
|
10
|
+
public class TeamsBotSsoPromptTokenResponse : TokenResponse
|
|
11
|
+
{
|
|
12
|
+
/// <summary>
|
|
13
|
+
/// SSO token for user
|
|
14
|
+
/// </summary>
|
|
15
|
+
public string SsoToken { get; set; }
|
|
16
|
+
|
|
17
|
+
/// <summary>
|
|
18
|
+
/// Expiration time of SSO token
|
|
19
|
+
/// </summary>
|
|
20
|
+
public string SsoTokenExpiration { get; set; }
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
using Microsoft.Agents.Builder;
|
|
5
|
+
using Microsoft.Agents.Extensions.Teams.Connector;
|
|
6
|
+
using Microsoft.Agents.Extensions.Teams.Models;
|
|
7
|
+
|
|
8
|
+
namespace {{YOUR_NAMESPACE}}.SSO
|
|
9
|
+
{
|
|
10
|
+
/// <summary>
|
|
11
|
+
/// Helper class used to wrap static method and simplify unit test.
|
|
12
|
+
/// </summary>
|
|
13
|
+
internal class TeamsInfoWrapper : ITeamsInfo
|
|
14
|
+
{
|
|
15
|
+
public Task<TeamsChannelAccount> GetTeamsMemberAsync(ITurnContext context, string userId, CancellationToken cancellationToken = default)
|
|
16
|
+
{
|
|
17
|
+
return TeamsInfo.GetMemberAsync(context, userId, cancellationToken);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
using Microsoft.
|
|
2
|
-
using Microsoft.
|
|
3
|
-
using Microsoft.
|
|
4
|
-
using Microsoft.
|
|
1
|
+
using Microsoft.Agents.Extensions.Teams.Compat;
|
|
2
|
+
using Microsoft.Agents.Builder.Dialogs;
|
|
3
|
+
using Microsoft.Agents.Builder.State;
|
|
4
|
+
using Microsoft.Agents.Builder;
|
|
5
|
+
using Microsoft.Agents.Core.Models;
|
|
5
6
|
|
|
6
7
|
namespace {{YOUR_NAMESPACE}}.SSO;
|
|
7
8
|
|
|
8
9
|
public class TeamsSsoBot<T> : TeamsActivityHandler where T : Dialog
|
|
9
10
|
{
|
|
10
11
|
private readonly ILogger<TeamsSsoBot<T>> _logger;
|
|
11
|
-
private readonly
|
|
12
|
+
private readonly AgentState _conversationState;
|
|
12
13
|
private readonly Dialog _dialog;
|
|
13
14
|
private readonly IStatePropertyAccessor<DialogState> _dialogState;
|
|
14
15
|
|