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