@microsoft/teamsfx-core 3.0.5-alpha.cd5b2565b.0 → 3.0.5-alpha.d4a083ca4.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.
Files changed (48) hide show
  1. package/build/common/templates-config.json +1 -1
  2. package/build/component/coordinator/index.d.ts.map +1 -1
  3. package/build/component/coordinator/index.js +1 -1
  4. package/build/component/coordinator/index.js.map +1 -1
  5. package/build/component/generator/combinedProject/generator.d.ts.map +1 -1
  6. package/build/component/generator/combinedProject/generator.js +2 -1
  7. package/build/component/generator/combinedProject/generator.js.map +1 -1
  8. package/build/component/generator/declarativeAgent/generator.d.ts.map +1 -1
  9. package/build/component/generator/declarativeAgent/generator.js +2 -1
  10. package/build/component/generator/declarativeAgent/generator.js.map +1 -1
  11. package/build/component/generator/generator.d.ts +1 -1
  12. package/build/component/generator/generator.d.ts.map +1 -1
  13. package/build/component/generator/generator.js +2 -1
  14. package/build/component/generator/generator.js.map +1 -1
  15. package/build/component/generator/openApiSpec/common.d.ts.map +1 -1
  16. package/build/component/generator/openApiSpec/common.js +2 -1
  17. package/build/component/generator/openApiSpec/common.js.map +1 -1
  18. package/build/component/generator/other/ssrTabGenerator.d.ts.map +1 -1
  19. package/build/component/generator/other/ssrTabGenerator.js +2 -1
  20. package/build/component/generator/other/ssrTabGenerator.js.map +1 -1
  21. package/build/component/generator/other/tdpGenerator.d.ts.map +1 -1
  22. package/build/component/generator/other/tdpGenerator.js +2 -1
  23. package/build/component/generator/other/tdpGenerator.js.map +1 -1
  24. package/build/component/generator/templates/templateReplaceMap.d.ts.map +1 -1
  25. package/build/component/generator/templates/templateReplaceMap.js +8 -6
  26. package/build/component/generator/templates/templateReplaceMap.js.map +1 -1
  27. package/build/question/questionNames.d.ts +1 -0
  28. package/build/question/questionNames.d.ts.map +1 -1
  29. package/build/question/questionNames.js +1 -0
  30. package/build/question/questionNames.js.map +1 -1
  31. package/build/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +4 -4
  33. package/templates/fallback/common.zip +0 -0
  34. package/templates/fallback/csharp.zip +0 -0
  35. package/templates/fallback/js.zip +0 -0
  36. package/templates/fallback/python.zip +0 -0
  37. package/templates/fallback/ts.zip +0 -0
  38. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/BotAuthenticationOptions.cs +44 -0
  39. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/IIdentityClientAdapter.cs +21 -0
  40. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/ITeamsInfo.cs +24 -0
  41. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/IdentityClientAdapter.cs +30 -0
  42. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/SsoDialog.cs +12 -9
  43. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/SsoOperations.cs +2 -2
  44. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsBotSsoPrompt.cs +423 -0
  45. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsBotSsoPromptSettings.cs +41 -0
  46. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsBotSsoPromptTokenResponse.cs +21 -0
  47. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsInfoWrapper.cs +20 -0
  48. package/templates/plugins/resource/aad/auth/V3/Bot/SSO/TeamsSsoBot.cs +6 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/teamsfx-core",
3
- "version": "3.0.5-alpha.cd5b2565b.0",
3
+ "version": "3.0.5-alpha.d4a083ca4.0",
4
4
  "main": "build/index.js",
5
5
  "types": "build/index.d.ts",
6
6
  "license": "MIT",
@@ -107,8 +107,8 @@
107
107
  "@microsoft/dev-tunnels-contracts": "1.1.9",
108
108
  "@microsoft/dev-tunnels-management": "1.1.9",
109
109
  "@microsoft/kiota": "1.26.1",
110
- "@microsoft/m365-spec-parser": "^0.2.9-alpha.cd5b2565b.0",
111
- "@microsoft/teamsfx-api": "0.23.9-alpha.cd5b2565b.0",
110
+ "@microsoft/m365-spec-parser": "^0.2.9-alpha.d4a083ca4.0",
111
+ "@microsoft/teamsfx-api": "0.23.9-alpha.d4a083ca4.0",
112
112
  "adm-zip": "^0.5.10",
113
113
  "ajv": "^8.5.0",
114
114
  "axios": "^1.8.3",
@@ -231,7 +231,7 @@
231
231
  "resource/**/*",
232
232
  "templates/**/*"
233
233
  ],
234
- "gitHead": "d1051be413a947b3b7c060462ef7c7309454d034",
234
+ "gitHead": "9304c1076c1c63568ed285a48e0cfd59e135b710",
235
235
  "publishConfig": {
236
236
  "access": "public"
237
237
  },
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,44 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+
4
+ using System.ComponentModel.DataAnnotations;
5
+
6
+ namespace {{YOUR_NAMESPACE}}.Configuration;
7
+
8
+ /// <summary>
9
+ /// Bot related authentication configuration.
10
+ /// </summary>
11
+ public class BotAuthenticationOptions
12
+ {
13
+ /// <summary>
14
+ /// The client (application) ID of an App Registration in the tenant.
15
+ /// </summary>
16
+ [Required(ErrorMessage = "Client id is required")]
17
+ [RegularExpression(@"^[0-9A-Fa-f\-]{36}$")]
18
+ public string ClientId { get; set; }
19
+
20
+ /// <summary>
21
+ /// The client (application) Secret of an App Registration in the tenant.
22
+ /// </summary>
23
+ [Required(ErrorMessage = "Client secret is required")]
24
+ public string ClientSecret { get; set; }
25
+
26
+ /// <summary>
27
+ /// Authority URL that is used in OAuth On-behalf-of flow.
28
+ /// </summary>
29
+ [Required(ErrorMessage = "OAuth authority is required")]
30
+ [RegularExpression(@"^http(s)?://[-a-zA-Z0-9@:%._\+~#=/]{1,100}$")]
31
+ public string OAuthAuthority { get; set; }
32
+
33
+ /// <summary>
34
+ /// Application ID URI.
35
+ /// </summary>
36
+ [Required(ErrorMessage = "Application id uri is required")]
37
+ public string ApplicationIdUri { get; set; }
38
+
39
+ /// <summary>
40
+ /// Login authentication start page endpoint.
41
+ /// </summary>
42
+ [Required(ErrorMessage = "Login authentication start page endpoint is required")]
43
+ public string InitiateLoginEndpoint { get; set; }
44
+ }
@@ -0,0 +1,21 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+
4
+ using Microsoft.Identity.Client;
5
+
6
+ namespace {{YOUR_NAMESPACE}}.SSO
7
+ {
8
+ /// <summary>
9
+ /// Adapter of IConfidentialClientApplication On-behalf-of flow.
10
+ /// </summary>
11
+ public interface IIdentityClientAdapter
12
+ {
13
+ /// <summary>
14
+ /// Use On-behalf-of flow to exchange access token.
15
+ /// </summary>
16
+ /// <param name="ssoToken">token from Teams client</param>
17
+ /// <param name="scopes">required scopes</param>
18
+ /// <returns></returns>
19
+ Task<AuthenticationResult> GetAccessToken(string ssoToken, IEnumerable<string> scopes);
20
+ }
21
+ }
@@ -0,0 +1,24 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+
4
+ using Microsoft.Agents.Builder;
5
+ using Microsoft.Agents.Extensions.Teams.Models;
6
+
7
+ namespace {{YOUR_NAMESPACE}}.SSO
8
+ {
9
+ /// <summary>
10
+ /// provides utility methods for interactions that occur within Microsoft Teams.
11
+ /// </summary>
12
+ public interface ITeamsInfo
13
+ {
14
+ /// <summary>
15
+ /// Gets the account of a single conversation member.
16
+ /// This works in one-on-one, group, and teams scoped conversations.
17
+ /// </summary>
18
+ /// <param name="context"> Turn context. </param>
19
+ /// <param name="userId"> ID of the user in question. </param>
20
+ /// <param name="cancellationToken"> cancellation token. </param>
21
+ /// <returns>Team Account Details.</returns>
22
+ Task<TeamsChannelAccount> GetTeamsMemberAsync(ITurnContext context, string userId, CancellationToken cancellationToken = default);
23
+ }
24
+ }
@@ -0,0 +1,30 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+
4
+ using Microsoft.Identity.Client;
5
+
6
+ namespace {{YOUR_NAMESPACE}}.SSO
7
+ {
8
+ /// <summary>
9
+ /// Helper class used to simplify unit test.
10
+ /// https://stackoverflow.com/questions/65334284/how-to-mock-moq-iconfidentialclientapplication-which-has-sealed-setup-abstract
11
+ /// </summary>
12
+ internal class IdentityClientAdapter : IIdentityClientAdapter
13
+ {
14
+ private readonly IConfidentialClientApplication _confidentialClientApplication;
15
+
16
+ public IdentityClientAdapter(IConfidentialClientApplication confidentialClientApplication)
17
+ {
18
+ _confidentialClientApplication = confidentialClientApplication;
19
+ }
20
+
21
+ public async Task<AuthenticationResult> GetAccessToken(string ssoToken, IEnumerable<string> scopes)
22
+ {
23
+ var userAssertion = new UserAssertion(ssoToken);
24
+ return await _confidentialClientApplication
25
+ .AcquireTokenOnBehalfOf(scopes, userAssertion)
26
+ .ExecuteAsync()
27
+ .ConfigureAwait(false);
28
+ }
29
+ }
30
+ }
@@ -1,9 +1,11 @@
1
- using Microsoft.Bot.Builder;
2
- using Microsoft.Bot.Builder.Dialogs;
3
- using Microsoft.Bot.Schema;
1
+ using {{YOUR_NAMESPACE}}.Configuration;
2
+ using Json.More;
3
+ using Microsoft.Agents.Builder;
4
+ using Microsoft.Agents.Builder.Dialogs;
5
+ using Microsoft.Agents.Builder.State;
6
+ using Microsoft.Agents.Core.Models;
7
+ using Microsoft.Agents.Storage;
4
8
  using Microsoft.Extensions.Options;
5
- using Microsoft.TeamsFx.Bot;
6
- using Microsoft.TeamsFx.Configuration;
7
9
 
8
10
  namespace {{YOUR_NAMESPACE}}.SSO;
9
11
 
@@ -173,9 +175,10 @@ public class SsoDialog : ComponentDialog
173
175
 
174
176
  private async Task<bool> ShouldDedup(ITurnContext context)
175
177
  {
178
+ var tokenResponseObject = context.Activity.Value.ToJsonDocument();
176
179
  var storeItem = new StoreItem()
177
180
  {
178
- eTag = (context.Activity.Value as dynamic).id,
181
+ eTag = tokenResponseObject.RootElement.GetProperty("id").ToString(),
179
182
  };
180
183
  var key = GetStorageKey(context);
181
184
  var storeItems = new Dictionary<string, object>()
@@ -209,13 +212,13 @@ public class SsoDialog : ComponentDialog
209
212
  throw new Exception("TokenExchangeState can only be used with Invokes of signin/tokenExchange.");
210
213
  }
211
214
 
212
- var value = activity.Value;
213
- if (value == null || (value as dynamic).id == null)
215
+ var value = activity.Value.ToJsonDocument();
216
+ if (value == null || value.RootElement.GetProperty("id").ToString() == null)
214
217
  {
215
218
  throw new Exception("Invalid signin/tokenExchange. Missing activity.value.id.");
216
219
  }
217
220
 
218
- return $"{channelId}/{conversationId}/{(value as dynamic).id}";
221
+ return $"{channelId}/{conversationId}/{value.RootElement.GetProperty("id").ToString()}";
219
222
  }
220
223
 
221
224
  private string MatchCommands(string text)
@@ -1,7 +1,7 @@
1
- using Microsoft.Bot.Builder;
1
+ using {{YOUR_NAMESPACE}}.Configuration;
2
+ using Microsoft.Agents.Builder;
2
3
  using Microsoft.Graph;
3
4
  using Microsoft.Kiota.Abstractions.Authentication;
4
- using Microsoft.TeamsFx.Configuration;
5
5
 
6
6
  namespace {{YOUR_NAMESPACE}}.SSO;
7
7
 
@@ -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&lt;DialogState&gt;("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
+ }