diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/BotAuthenticationBaseTests.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/BotAuthenticationBaseTests.cs index 4349378ec..72c4f1ba0 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/BotAuthenticationBaseTests.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/BotAuthenticationBaseTests.cs @@ -1,7 +1,6 @@ using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Bot.Schema; -using Microsoft.Teams.AI.Application.Authentication.Bot; using Microsoft.Teams.AI.Exceptions; using Microsoft.Teams.AI.State; using Microsoft.Teams.AI.Tests.TestUtils; diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/MessageExtensions/MessageExtensionsAuthenticationBaseTests.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/MessageExtensions/MessageExtensionsAuthenticationBaseTests.cs index 36b15ccbc..c77b28da7 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/MessageExtensions/MessageExtensionsAuthenticationBaseTests.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/MessageExtensions/MessageExtensionsAuthenticationBaseTests.cs @@ -22,7 +22,7 @@ public override Task GetSignInLink(ITurnContext context) return Task.FromResult("mocked link"); } - public override Task HandlerUserSignIn(ITurnContext context, string magicCode) + public override Task HandleUserSignIn(ITurnContext context, string magicCode) { if (_signInResponse == null) { diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/MockedAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/MockedAuthentication.cs index 0f16db096..6ad486509 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/MockedAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/MockedAuthentication.cs @@ -1,10 +1,11 @@ using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.Exceptions; using Microsoft.Teams.AI.State; namespace Microsoft.Teams.AI.Tests.Application.Authentication { public class MockedAuthentication : IAuthentication - where TState : TurnState + where TState : TurnState, new() { private string _mockedToken; private SignInStatus _mockedStatus; @@ -17,11 +18,26 @@ public MockedAuthentication(SignInStatus mockedStatus = SignInStatus.Complete, s _validActivity = validActivity; } + public void Initialize(Application app, string name, IStorage? storage = null) + { + return; + } + public Task IsValidActivity(ITurnContext context) { return Task.FromResult(_validActivity); } + public IAuthentication OnUserSignInFailure(Func handler) + { + return this; + } + + public IAuthentication OnUserSignInSuccess(Func handler) + { + return this; + } + public Task SignInUser(ITurnContext context, TState state) { var result = new SignInResponse(_mockedStatus); diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs index e47fecace..af9e5ecde 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs @@ -66,8 +66,25 @@ public Application(ApplicationOptions options) MessageExtensions = new MessageExtensions(this); TaskModules = new TaskModules(this); + // Validate long running messages configuration + if (Options.LongRunningMessages && (Options.Adapter == null || Options.BotAppId == null)) + { + throw new ArgumentException("The ApplicationOptions.LongRunningMessages property is unavailable because no adapter or botAppId was configured."); + } + + _routes = new ConcurrentQueue>(); + _invokeRoutes = new ConcurrentQueue>(); + _beforeTurn = new ConcurrentQueue>(); + _afterTurn = new ConcurrentQueue>(); + if (options.Authentication != null) { + // Initialize the authentication classes + foreach (KeyValuePair> pair in options.Authentication.Authentications) + { + pair.Value.Initialize(this, pair.Key, options.Storage); + } + Authentication = new AuthenticationManager(options.Authentication); if (options.Authentication.AutoSignIn != null) { @@ -78,17 +95,6 @@ public Application(ApplicationOptions options) _startSignIn = (context, cancellationToken) => Task.FromResult(true); } } - - // Validate long running messages configuration - if (Options.LongRunningMessages && (Options.Adapter == null || Options.BotAppId == null)) - { - throw new ArgumentException("The ApplicationOptions.LongRunningMessages property is unavailable because no adapter or botAppId was configured."); - } - - _routes = new ConcurrentQueue>(); - _invokeRoutes = new ConcurrentQueue>(); - _beforeTurn = new ConcurrentQueue>(); - _afterTurn = new ConcurrentQueue>(); } /// diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/ApplicationOptions.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/ApplicationOptions.cs index 1c31e14f1..a88e8437f 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/ApplicationOptions.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/ApplicationOptions.cs @@ -10,7 +10,7 @@ namespace Microsoft.Teams.AI /// /// Type of the turn state. public class ApplicationOptions - where TState : TurnState + where TState : TurnState, new() { /// /// Optional. Bot adapter being used. diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/AdaptiveCardsAuthenticationBase.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/AdaptiveCardsAuthenticationBase.cs index e7d39770e..c562a9c21 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/AdaptiveCardsAuthenticationBase.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/AdaptiveCardsAuthenticationBase.cs @@ -1,7 +1,7 @@ using Microsoft.Bot.Builder; using Microsoft.Bot.Schema; -namespace Microsoft.Teams.AI.Application.Authentication.AdaptiveCards +namespace Microsoft.Teams.AI { /// /// Base class for adaptive card authentication that handles common logic diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/OAuthAdaptiveCardsAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/OAuthAdaptiveCardsAuthentication.cs index e8edbad5b..0c3fcfcff 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/OAuthAdaptiveCardsAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/OAuthAdaptiveCardsAuthentication.cs @@ -1,4 +1,4 @@ -namespace Microsoft.Teams.AI.Application.Authentication.AdaptiveCards +namespace Microsoft.Teams.AI { /// /// Handles authentication for Adaptive Cards in Teams using OAuth Connection. diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/TeamsSsoAdaptiveCardsAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/TeamsSsoAdaptiveCardsAuthentication.cs index f0605b632..44c5c401e 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/TeamsSsoAdaptiveCardsAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AdaptiveCards/TeamsSsoAdaptiveCardsAuthentication.cs @@ -1,4 +1,4 @@ -namespace Microsoft.Teams.AI.Application.Authentication.AdaptiveCards +namespace Microsoft.Teams.AI { /// /// Handles authentication for Adaptive Cards in Teams based on Teams SSO. diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthUtilities.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthUtilities.cs index 67d36c67a..e6357d779 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthUtilities.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthUtilities.cs @@ -5,7 +5,7 @@ namespace Microsoft.Teams.AI /// /// Authentication utilities /// - public class AuthUtilities + internal class AuthUtilities { /// /// Set token in state diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationManager.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationManager.cs index 59c6ccafa..328a32909 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationManager.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationManager.cs @@ -90,7 +90,13 @@ public async Task IsValidActivity(ITurnContext context, string? handlerNam return await auth.IsValidActivity(context); } - private IAuthentication Get(string name) + /// + /// Get an authentication class via name + /// + /// The name of authentication class + /// The authentication class + /// When cannot find the class with given name + public IAuthentication Get(string name) { if (_authentications.ContainsKey(name)) { diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationOptions.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationOptions.cs index e5edd0088..15a5653a1 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationOptions.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationOptions.cs @@ -17,7 +17,7 @@ namespace Microsoft.Teams.AI /// Options for authentication. /// public class AuthenticationOptions - where TState : TurnState + where TState : TurnState, new() { /// /// The authentication classes to sign-in and sign-out users. diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/BotAuthenticationBase.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/BotAuthenticationBase.cs index ff461af3f..118dbad76 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/BotAuthenticationBase.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/BotAuthenticationBase.cs @@ -4,7 +4,7 @@ using Microsoft.Teams.AI.Exceptions; using Microsoft.Teams.AI.State; -namespace Microsoft.Teams.AI.Application.Authentication.Bot +namespace Microsoft.Teams.AI { /// /// Base class for bot authentication that handles common logic @@ -107,7 +107,7 @@ public async Task AuthenticateAsync(ITurnContext context, TState /// /// The turn context /// True if valid. Otherwise, false. - public bool IsValidActivity(ITurnContext context) + public virtual bool IsValidActivity(ITurnContext context) { return context.Activity.Type == ActivityTypes.Message && !string.IsNullOrEmpty(context.Activity.Text); @@ -182,22 +182,18 @@ public async Task HandleSignInActivity(ITurnContext context, TState state, Cance /// The handler function is called when the user has successfully signed in /// /// The handler function to call when the user has successfully signed in - /// The class itself for chaining purpose - public BotAuthenticationBase OnUserSignInSuccess(Func handler) + public void OnUserSignInSuccess(Func handler) { _userSignInSuccessHandler = handler; - return this; } /// /// The handler function is called when the user sign in flow fails /// /// The handler function to call when the user failed to signed in - /// The class itself for chaining purpose - public BotAuthenticationBase OnUserSignInFailure(Func handler) + public void OnUserSignInFailure(Func handler) { _userSignInFailureHandler = handler; - return this; } /// @@ -206,7 +202,7 @@ public BotAuthenticationBase OnUserSignInFailure(FuncThe turn context /// The cancellation token /// True if the activity should be handled by current authentication hanlder. Otherwise, false. - protected Task VerifyStateRouteSelector(ITurnContext context, CancellationToken cancellationToken) + protected virtual Task VerifyStateRouteSelector(ITurnContext context, CancellationToken cancellationToken) { return Task.FromResult(context.Activity.Type == ActivityTypes.Invoke && context.Activity.Name == SignInConstants.VerifyStateOperationName); @@ -218,7 +214,7 @@ protected Task VerifyStateRouteSelector(ITurnContext context, Cancellation /// The turn context /// The cancellation token /// True if the activity should be handled by current authentication hanlder. Otherwise, false. - protected Task TokenExchangeRouteSelector(ITurnContext context, CancellationToken cancellationToken) + protected virtual Task TokenExchangeRouteSelector(ITurnContext context, CancellationToken cancellationToken) { return Task.FromResult(context.Activity.Type == ActivityTypes.Invoke && context.Activity.Name == SignInConstants.TokenExchangeOperationName); diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/OAuthBotAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/OAuthBotAuthentication.cs index 65766adbf..eea83d8d7 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/OAuthBotAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/OAuthBotAuthentication.cs @@ -2,12 +2,12 @@ using Microsoft.Bot.Builder.Dialogs; using Microsoft.Teams.AI.State; -namespace Microsoft.Teams.AI.Application.Authentication.Bot +namespace Microsoft.Teams.AI { /// /// Handles authentication for bot in Teams using OAuth Connection. /// - public class OAuthBotAuthentication : BotAuthenticationBase + internal class OAuthBotAuthentication : BotAuthenticationBase where TState : TurnState, new() { /// diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/TeamsSsoBotAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/TeamsSsoBotAuthentication.cs index 14bd91e1d..039a0e39f 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/TeamsSsoBotAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/TeamsSsoBotAuthentication.cs @@ -1,23 +1,40 @@ using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; using Microsoft.Teams.AI.State; +using Microsoft.Teams.AI.Exceptions; +using Newtonsoft.Json.Linq; +using System.Text.RegularExpressions; -namespace Microsoft.Teams.AI.Application.Authentication.Bot +namespace Microsoft.Teams.AI { /// /// Handles authentication for bot in Teams using Teams SSO. /// - public class TeamsSsoBotAuthentication : BotAuthenticationBase + internal class TeamsSsoBotAuthentication : BotAuthenticationBase where TState : TurnState, new() { + private const string SSO_DIALOG_ID = "_TeamsSsoDialog"; + private Regex _tokenExchangeIdRegex; + private TeamsSsoPrompt _prompt; + /// /// Initializes the class /// /// The application instance /// The name of current authentication handler + /// The authentication settings /// The storage to save turn state - public TeamsSsoBotAuthentication(Application app, string name, IStorage? storage = null) : base(app, name, storage) + public TeamsSsoBotAuthentication(Application app, string name, TeamsSsoSettings settings, IStorage? storage = null) : base(app, name, storage) { + _tokenExchangeIdRegex = new Regex("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-" + name); + _prompt = new TeamsSsoPrompt("TeamsSsoPrompt", name, settings); + + // Do not save state for duplicate token exchange events to avoid eTag conflicts + app.OnAfterTurn((context, state, cancellationToken) => + { + return Task.FromResult(state.Temp.DuplicateTokenExchange != true); + }); } /// @@ -27,9 +44,10 @@ public TeamsSsoBotAuthentication(Application app, string name, IStorage? /// The turn state /// The property name for storing dialog state. /// Dialog turn result that contains token if sign in success - public override Task ContinueDialog(ITurnContext context, TState state, string dialogStateProperty) + public override async Task ContinueDialog(ITurnContext context, TState state, string dialogStateProperty) { - throw new NotImplementedException(); + DialogContext dialogContext = await CreateSsoDialogContext(context, state, dialogStateProperty); + return await dialogContext.ContinueDialogAsync(); } /// @@ -39,9 +57,115 @@ public override Task ContinueDialog(ITurnContext context, TSta /// The turn state /// The property name for storing dialog state. /// Dialog turn result that contains token if sign in success - public override Task RunDialog(ITurnContext context, TState state, string dialogStateProperty) + public override async Task RunDialog(ITurnContext context, TState state, string dialogStateProperty) + { + DialogContext dialogContext = await CreateSsoDialogContext(context, state, dialogStateProperty); + DialogTurnResult result = await dialogContext.ContinueDialogAsync(); + if (result.Status == DialogTurnStatus.Empty) + { + result = await dialogContext.BeginDialogAsync(SSO_DIALOG_ID); + } + return result; + } + + /// + /// The route selector for signin/tokenExchange activity + /// + /// The turn context + /// The cancellation token + /// True if the activity should be handled by current authentication hanlder. Otherwise, false. + protected override async Task TokenExchangeRouteSelector(ITurnContext context, CancellationToken cancellationToken) + { + JObject value = JObject.FromObject(context.Activity.Value); + JToken? id = value["id"]; + string idStr = id?.ToString() ?? ""; + return await base.TokenExchangeRouteSelector(context, cancellationToken) + && this._tokenExchangeIdRegex.IsMatch(idStr); + } + + private async Task CreateSsoDialogContext(ITurnContext context, TState state, string dialogStateProperty) + { + TurnStateProperty accessor = new(state, "conversation", dialogStateProperty); + DialogSet dialogSet = new(accessor); + WaterfallDialog ssoDialog = new(SSO_DIALOG_ID); + dialogSet.Add(this._prompt); + dialogSet.Add(new WaterfallDialog(SSO_DIALOG_ID, new WaterfallStep[] + { + async (step, cancellationToken) => + { + return await step.BeginDialogAsync(this._prompt.Id); + }, + async (step, cancellationToken) => + { + TokenResponse? tokenResponse = step.Result as TokenResponse; + if (tokenResponse != null && await ShouldDedup(context)) + { + state.Temp.DuplicateTokenExchange = true; + return Dialog.EndOfTurn; + } + return await step.EndDialogAsync(step.Result); + } + })); + return await dialogSet.CreateContextAsync(context); + } + + private async Task ShouldDedup(ITurnContext context) + { + string key = GetStorageKey(context); + string id = (context.Activity.Value as JObject)?.Value("id")!; // The id exists if GetStorageKey success + IStoreItem storeItem = new TokenStoreItem(id); + Dictionary storesItems = new() + { + {key, storeItem} + }; + + try + { + await this._storage.WriteAsync(storesItems); + } + catch (Exception ex) + { + if (ex.Message.StartsWith("Etag conflict", StringComparison.OrdinalIgnoreCase) || ex.Message.Contains("pre-condition is not met")) + { + return true; + } + throw; + } + return false; + } + + private string GetStorageKey(ITurnContext context) + { + if (context == null || context.Activity == null + || context.Activity.Conversation == null) + { + throw new TeamsAIAuthException("Invalid context, can not get storage key!"); + } + + Activity activity = context.Activity; + string channelId = activity.ChannelId; + string conversationId = activity.Conversation.Id; + if (activity.Type != ActivityTypes.Invoke || activity.Name != SignInConstants.TokenExchangeOperationName) + { + throw new TeamsAIAuthException("TokenExchangeState can only be used with Invokes of signin/tokenExchange."); + } + JObject value = JObject.FromObject(activity.Value); + JToken? id = value["id"]; + if (id == null) + { + throw new TeamsAIAuthException("Invalid signin/tokenExchange. Missing activity.value.id."); + } + return $"{channelId}/{conversationId}/{id}"; + } + } + + internal class TokenStoreItem : IStoreItem + { + public string ETag { get; set; } + + public TokenStoreItem(string etag) { - throw new NotImplementedException(); + ETag = etag; } } } diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/TeamsSsoPrompt.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/TeamsSsoPrompt.cs new file mode 100644 index 000000000..8b9de21fa --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/TeamsSsoPrompt.cs @@ -0,0 +1,232 @@ +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Dialogs; +using Microsoft.Bot.Schema; +using System.Net; +using Newtonsoft.Json.Linq; +using Microsoft.Identity.Client; +using Microsoft.Teams.AI.Exceptions; + +namespace Microsoft.Teams.AI +{ + internal class TeamsSsoPrompt : Dialog + { + private const string _expiresKey = "expires"; + private string _name; + private TeamsSsoSettings _settings; + + public TeamsSsoPrompt(string dialogId, string name, TeamsSsoSettings settings) + : base(dialogId) + { + this._name = name; + this._settings = settings; + } + + public override async Task BeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken) + { + int timeout = _settings.Timeout; + + IDictionary state = dc.ActiveDialog.State; + state[_expiresKey] = DateTime.Now.AddMilliseconds(timeout); + + AuthenticationResult? token = await this.TryGetUserToken(dc.Context); + if (token != null) + { + TokenResponse tokenResponse = new() + { + ConnectionName = "", // No connection name is available in this implementation + Token = token.AccessToken, + Expiration = token.ExpiresOn.ToString("o") + }; + return await dc.EndDialogAsync(tokenResponse); + } + + // Cannot get token from cache, send OAuth card to get SSO token + await this.SendOAuthCardToObtainTokenAsync(dc.Context, cancellationToken); + return EndOfTurn; + } + + public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default) + { + // Check for timeout + IDictionary state = dc.ActiveDialog.State; + bool isMessage = (dc.Context.Activity.Type == ActivityTypes.Message); + bool isTimeoutActivityType = + isMessage || + IsTeamsVerificationInvoke(dc.Context) || + IsTokenExchangeRequestInvoke(dc.Context); + + // If the incoming Activity is a message, or an Activity Type normally handled by TeamsBotSsoPrompt, + // check to see if this TeamsBotSsoPrompt Expiration has elapsed, and end the dialog if so. + bool hasTimedOut = isTimeoutActivityType && DateTime.Compare(DateTime.UtcNow, (DateTime)state[_expiresKey]) > 0; + if (hasTimedOut) + { + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + else + { + if (IsTeamsVerificationInvoke(dc.Context) || IsTokenExchangeRequestInvoke(dc.Context)) + { + // Recognize token + PromptRecognizerResult recognized = await RecognizeTokenAsync(dc, cancellationToken).ConfigureAwait(false); + + if (recognized.Succeeded) + { + return await dc.EndDialogAsync(recognized.Value, cancellationToken).ConfigureAwait(false); + } + } + else if (isMessage && _settings.EndOnInvalidMessage) + { + return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + + return EndOfTurn; + } + } + + private async Task> RecognizeTokenAsync(DialogContext dc, CancellationToken cancellationToken) + { + + ITurnContext context = dc.Context; + PromptRecognizerResult result = new(); + TokenResponse? tokenResponse = null; + + if (IsTokenExchangeRequestInvoke(context)) + { + JObject? tokenResponseObject = context.Activity.Value as JObject; + string? ssoToken = tokenResponseObject?.ToObject()?.Token; + // Received activity is not a token exchange request + if (string.IsNullOrEmpty(ssoToken)) + { + string warningMsg = + "The bot received an InvokeActivity that is missing a TokenExchangeInvokeRequest value. This is required to be sent with the InvokeActivity."; + await SendInvokeResponseAsync(context, HttpStatusCode.BadRequest, warningMsg, cancellationToken).ConfigureAwait(false); + } + else + { + try + { + AuthenticationResult exchangedToken = await _settings.MSAL.AcquireTokenOnBehalfOf(_settings.Scopes, new UserAssertion(ssoToken)).ExecuteAsync(); + + tokenResponse = new TokenResponse + { + Token = exchangedToken.AccessToken, + Expiration = exchangedToken.ExpiresOn.ToString("o") + }; + + await SendInvokeResponseAsync(context, HttpStatusCode.OK, null, cancellationToken).ConfigureAwait(false); + } + catch (MsalUiRequiredException) // Need user interaction + { + string warningMsg = "The bot is unable to exchange token. Ask for user consent first."; + await SendInvokeResponseAsync(context, HttpStatusCode.PreconditionFailed, new TokenExchangeInvokeResponse + { + Id = context.Activity.Id, + FailureDetail = warningMsg, + }, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + string message = $"Failed to get access token with error: {ex.Message}"; + throw new TeamsAIAuthException(message); + } + } + } + else if (IsTeamsVerificationInvoke(context)) + { + await SendOAuthCardToObtainTokenAsync(context, cancellationToken).ConfigureAwait(false); + await SendInvokeResponseAsync(context, HttpStatusCode.OK, null, cancellationToken).ConfigureAwait(false); + } + + if (tokenResponse != null) + { + result.Succeeded = true; + result.Value = tokenResponse; + } + else + { + result.Succeeded = false; + } + return result; + } + + private async Task SendOAuthCardToObtainTokenAsync(ITurnContext context, CancellationToken cancellationToken) + { + SignInResource signInResource = GetSignInResource(); + + // Ensure prompt initialized + IMessageActivity prompt = Activity.CreateMessageActivity(); + prompt.Attachments = new List(); + prompt.Attachments.Add(new Attachment + { + ContentType = OAuthCard.ContentType, + Content = new OAuthCard + { + Text = "Sign In", + Buttons = new[] + { + new CardAction + { + Title = "Teams SSO Sign In", + Value = signInResource.SignInLink, + Type = ActionTypes.Signin, + }, + }, + TokenExchangeResource = signInResource.TokenExchangeResource, + }, + }); + // Send prompt + await context.SendActivityAsync(prompt, cancellationToken).ConfigureAwait(false); + } + + private SignInResource GetSignInResource() + { + string signInLink = $"{_settings.SignInLink}?scope={Uri.EscapeDataString(string.Join(" ", _settings.Scopes))}&clientId={_settings.MSAL.AppConfig.ClientId}&tenantId={_settings.MSAL.AppConfig.TenantId}"; + + SignInResource signInResource = new() + { + SignInLink = signInLink, + TokenExchangeResource = new TokenExchangeResource + { + Id = $"{Guid.NewGuid()}-{_name}" + } + }; + + return signInResource; + } + + private async Task TryGetUserToken(ITurnContext context) + { + string homeAccountId = $"{context.Activity.From.AadObjectId}.{context.Activity.Conversation.TenantId}"; + IAccount account = await this._settings.MSAL.GetAccountAsync(homeAccountId); + if (account != null) + { + AuthenticationResult result = await this._settings.MSAL.AcquireTokenSilent(this._settings.Scopes, account).ExecuteAsync(); + return result; + } + return null; // Return empty indication no token found in cache + } + + private bool IsTeamsVerificationInvoke(ITurnContext context) + { + return (context.Activity.Type == ActivityTypes.Invoke) && (context.Activity.Name == SignInConstants.VerifyStateOperationName); + } + private bool IsTokenExchangeRequestInvoke(ITurnContext context) + { + return (context.Activity.Type == ActivityTypes.Invoke) && (context.Activity.Name == SignInConstants.TokenExchangeOperationName); + } + + private static async Task SendInvokeResponseAsync(ITurnContext turnContext, HttpStatusCode statusCode, object body, CancellationToken cancellationToken) + { + await turnContext.SendActivityAsync( + new Activity + { + Type = ActivityTypesEx.InvokeResponse, + Value = new InvokeResponse + { + Status = (int)statusCode, + Body = body, + }, + }, cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/IAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/IAuthentication.cs index eb9c5c0e1..ebcf9786b 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/IAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/IAuthentication.cs @@ -1,4 +1,5 @@ using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.Exceptions; using Microsoft.Teams.AI.State; namespace Microsoft.Teams.AI @@ -47,7 +48,7 @@ public SignInResponse(SignInStatus status) /// Handles user sign-in and sign-out. /// public interface IAuthentication - where TState : TurnState + where TState : TurnState, new() { /// /// Signs in a user. @@ -71,5 +72,28 @@ public interface IAuthentication /// Current turn context. /// True if current activity supports authentication. Otherwise, false. Task IsValidActivity(ITurnContext context); + + /// + /// Initialize the authentication class + /// + /// The application object + /// The name of the authentication handler + /// The storage to save turn state + /// + void Initialize(Application app, string name, IStorage? storage = null); + + /// + /// The handler function is called when the user has successfully signed in + /// + /// The handler function to call when the user has successfully signed in + /// The class itself for chaining purpose + IAuthentication OnUserSignInSuccess(Func handler); + + /// + /// The handler function is called when the user sign in flow fails + /// + /// The handler function to call when the user failed to signed in + /// The class itself for chaining purpose + IAuthentication OnUserSignInFailure(Func handler); } } diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/MessageExtensionsAuthenticationBase.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/MessageExtensionsAuthenticationBase.cs index ea564976a..58cf8c7de 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/MessageExtensionsAuthenticationBase.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/MessageExtensionsAuthenticationBase.cs @@ -65,7 +65,7 @@ public async Task AuthenticateAsync(ITurnContext context) { try { - TokenResponse response = await HandlerUserSignIn(context, state.ToString()); + TokenResponse response = await HandleUserSignIn(context, state.ToString()); if (!string.IsNullOrEmpty(response.Token)) { return new SignInResponse(SignInStatus.Complete) @@ -96,8 +96,7 @@ public async Task AuthenticateAsync(ITurnContext context) { Actions = new List { - new CardAction - { + new() { Type = ActionTypes.OpenUrl, Value = signInLink, Title = "Bot Service OAuth", @@ -117,13 +116,13 @@ public async Task AuthenticateAsync(ITurnContext context) /// /// The turn context /// True if valid. Otherwise, false. - public bool IsValidActivity(ITurnContext context) + public virtual bool IsValidActivity(ITurnContext context) { return context.Activity.Type == ActivityTypes.Invoke && (context.Activity.Name == MessageExtensionsInvokeNames.QUERY_INVOKE_NAME - && context.Activity.Name == MessageExtensionsInvokeNames.FETCH_TASK_INVOKE_NAME - && context.Activity.Name == MessageExtensionsInvokeNames.QUERY_LINK_INVOKE_NAME - && context.Activity.Name == MessageExtensionsInvokeNames.ANONYMOUS_QUERY_LINK_INVOKE_NAME); + || context.Activity.Name == MessageExtensionsInvokeNames.FETCH_TASK_INVOKE_NAME + || context.Activity.Name == MessageExtensionsInvokeNames.QUERY_LINK_INVOKE_NAME + || context.Activity.Name == MessageExtensionsInvokeNames.ANONYMOUS_QUERY_LINK_INVOKE_NAME); } /// @@ -139,7 +138,7 @@ public bool IsValidActivity(ITurnContext context) /// The turn context /// The magic code from user sign-in. /// The token response if successfully verified the magic code - public abstract Task HandlerUserSignIn(ITurnContext context, string magicCode); + public abstract Task HandleUserSignIn(ITurnContext context, string magicCode); /// /// Gets the sign in link for the user. diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/OAuthMessageExtensionsAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/OAuthMessageExtensionsAuthentication.cs index ee61a4670..8f4995131 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/OAuthMessageExtensionsAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/OAuthMessageExtensionsAuthentication.cs @@ -1,12 +1,12 @@ using Microsoft.Bot.Builder; using Microsoft.Bot.Schema; -namespace Microsoft.Teams.AI.Application.Authentication.MessageExtensions +namespace Microsoft.Teams.AI { /// /// Handles authentication for Message Extensions in Teams using OAuth Connection. /// - public class OAuthMessageExtensionsAuthentication : MessageExtensionsAuthenticationBase + internal class OAuthMessageExtensionsAuthentication : MessageExtensionsAuthenticationBase { /// /// Gets the sign in link for the user. @@ -24,7 +24,7 @@ public override Task GetSignInLink(ITurnContext context) /// The turn context /// The magic code from user sign-in. /// The token response if successfully verified the magic code - public override Task HandlerUserSignIn(ITurnContext context, string magicCode) + public override Task HandleUserSignIn(ITurnContext context, string magicCode) { throw new NotImplementedException(); } diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/TeamsSsoMessageExtensionsAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/TeamsSsoMessageExtensionsAuthentication.cs index ef1ba4120..44b745ee6 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/TeamsSsoMessageExtensionsAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/MessageExtensions/TeamsSsoMessageExtensionsAuthentication.cs @@ -1,13 +1,24 @@ using Microsoft.Bot.Builder; using Microsoft.Bot.Schema; +using Microsoft.Identity.Client; +using Microsoft.Teams.AI.Exceptions; +using Newtonsoft.Json.Linq; -namespace Microsoft.Teams.AI.Application.Authentication.MessageExtensions +namespace Microsoft.Teams.AI { /// /// Handles authentication for Message Extensions in Teams using Teams SSO. /// - public class TeamsSsoMessageExtensionsAuthentication : MessageExtensionsAuthenticationBase + internal class TeamsSsoMessageExtensionsAuthentication : MessageExtensionsAuthenticationBase { + private TeamsSsoSettings _settings; + + public TeamsSsoMessageExtensionsAuthentication(TeamsSsoSettings settings) + { + _settings = settings; + } + + /// /// Gets the sign in link for the user. /// @@ -15,7 +26,9 @@ public class TeamsSsoMessageExtensionsAuthentication : MessageExtensionsAuthenti /// The sign in link public override Task GetSignInLink(ITurnContext context) { - throw new NotImplementedException(); + string signInLink = $"{_settings.SignInLink}?scope={Uri.EscapeDataString(string.Join(" ", _settings.Scopes))}&clientId={_settings.MSAL.AppConfig.ClientId}&tenantId={_settings.MSAL.AppConfig.TenantId}"; + + return Task.FromResult(signInLink); } /// @@ -24,9 +37,10 @@ public override Task GetSignInLink(ITurnContext context) /// The turn context /// The magic code from user sign-in. /// The token response if successfully verified the magic code - public override Task HandlerUserSignIn(ITurnContext context, string magicCode) + public override Task HandleUserSignIn(ITurnContext context, string magicCode) { - throw new NotImplementedException(); + // Return empty token response to tirgger silentAuth again + return Task.FromResult(new TokenResponse()); } /// @@ -34,9 +48,43 @@ public override Task HandlerUserSignIn(ITurnContext context, stri /// /// The turn context /// The token response if token exchange success - public override Task HandleSsoTokenExchange(ITurnContext context) + public override async Task HandleSsoTokenExchange(ITurnContext context) + { + JObject value = JObject.FromObject(context.Activity.Value); + JToken? tokenExchangeRequest = value["authentication"]; + JToken? token = tokenExchangeRequest?["token"]; + if (token != null) + { + try + { + AuthenticationResult result = await _settings.MSAL.AcquireTokenOnBehalfOf( + _settings.Scopes, + new UserAssertion(token.ToString()) + ).ExecuteAsync(); + return new TokenResponse() + { + Token = result.AccessToken, + Expiration = result.ExpiresOn.ToString("O") + }; + } + catch (MsalUiRequiredException) + { + // Requires user consent, ignore this exception + } + catch (Exception ex) + { + string message = $"Failed to exchange access token with error: {ex.Message}"; + throw new TeamsAIAuthException(message); + } + } + + return new TokenResponse(); + } + + public override bool IsValidActivity(ITurnContext context) { - throw new NotImplementedException(); + return base.IsValidActivity(context) + && context.Activity.Name == MessageExtensionsInvokeNames.QUERY_INVOKE_NAME; } } } diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/OAuthAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/OAuthAuthentication.cs index 135276f32..77d459a78 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/OAuthAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/OAuthAuthentication.cs @@ -1,7 +1,8 @@ using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.Exceptions; using Microsoft.Teams.AI.State; -namespace Microsoft.Teams.AI.Application.Authentication +namespace Microsoft.Teams.AI { /// /// Handles authentication using OAuth Connection. @@ -9,6 +10,17 @@ namespace Microsoft.Teams.AI.Application.Authentication public class OAuthAuthentication : IAuthentication where TState : TurnState, new() { + /// + /// Initialize the authentication class + /// + /// The application object + /// The name of the authentication handler + /// The storage to save turn state + public void Initialize(Application app, string name, IStorage? storage = null) + { + throw new NotImplementedException(); + } + /// /// Whether the current activity is a valid activity that supports authentication /// @@ -19,6 +31,26 @@ public Task IsValidActivity(ITurnContext context) throw new NotImplementedException(); } + /// + /// The handler function is called when the user sign in flow fails + /// + /// The handler function to call when the user failed to signed in + /// The class itself for chaining purpose + public IAuthentication OnUserSignInFailure(Func handler) + { + throw new NotImplementedException(); + } + + /// + /// The handler function is called when the user has successfully signed in + /// + /// The handler function to call when the user has successfully signed in + /// The class itself for chaining purpose + public IAuthentication OnUserSignInSuccess(Func handler) + { + throw new NotImplementedException(); + } + /// /// Sign in current user /// diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TeamsSsoAuthentication.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TeamsSsoAuthentication.cs index ba268c774..ca7459bad 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TeamsSsoAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TeamsSsoAuthentication.cs @@ -1,12 +1,9 @@ using Microsoft.Bot.Builder; using Microsoft.Identity.Client; -using Microsoft.Teams.AI.Application.Authentication.AdaptiveCards; -using Microsoft.Teams.AI.Application.Authentication.Bot; -using Microsoft.Teams.AI.Application.Authentication.MessageExtensions; using Microsoft.Teams.AI.Exceptions; using Microsoft.Teams.AI.State; -namespace Microsoft.Teams.AI.Application.Authentication +namespace Microsoft.Teams.AI { /// /// Handles authentication based on Teams SSO. @@ -14,26 +11,30 @@ namespace Microsoft.Teams.AI.Application.Authentication public class TeamsSsoAuthentication : IAuthentication where TState : TurnState, new() { - private readonly string[] _scopes; - private readonly TeamsSsoBotAuthentication _botAuth; - private readonly TeamsSsoMessageExtensionsAuthentication _messageExtensionsAuth; - private readonly TeamsSsoAdaptiveCardsAuthentication _adaptiveCardsAuth; - private readonly IConfidentialClientApplication _msal; + private TeamsSsoBotAuthentication _botAuth; + private TeamsSsoMessageExtensionsAuthentication _messageExtensionsAuth; + private TeamsSsoAdaptiveCardsAuthentication _adaptiveCardsAuth; + private TeamsSsoSettings _settings; /// /// Initialize instance for current class /// - /// The application instance + /// The settings to initialize the class + public TeamsSsoAuthentication(TeamsSsoSettings settings) + { + _settings = settings; + } + + /// + /// Initialize the authentication class + /// + /// The application object /// The name of the authentication handler - /// The MSAL instance /// The storage to save turn state - /// The - public TeamsSsoAuthentication(Application app, string name, string[] scopes, IConfidentialClientApplication msal, IStorage? storage = null) + public void Initialize(Application app, string name, IStorage? storage = null) { - _scopes = scopes; - _msal = msal; - _botAuth = new TeamsSsoBotAuthentication(app, name, storage); - _messageExtensionsAuth = new TeamsSsoMessageExtensionsAuthentication(); + _botAuth = new TeamsSsoBotAuthentication(app, name, _settings, storage); + _messageExtensionsAuth = new TeamsSsoMessageExtensionsAuthentication(_settings); _adaptiveCardsAuth = new TeamsSsoAdaptiveCardsAuthentication(); } @@ -92,20 +93,42 @@ public async Task SignInUser(ITurnContext context, TState state) public async Task SignOutUser(ITurnContext context, TState state) { string homeAccountId = $"{context.Activity.From.AadObjectId}.{context.Activity.Conversation.TenantId}"; - IAccount account = await _msal.GetAccountAsync(homeAccountId); + IAccount account = await _settings.MSAL.GetAccountAsync(homeAccountId); if (account != null) { - await _msal.RemoveAsync(account); + await _settings.MSAL.RemoveAsync(account); } } + /// + /// The handler function is called when the user has successfully signed in + /// + /// The handler function to call when the user has successfully signed in + /// The class itself for chaining purpose + public IAuthentication OnUserSignInSuccess(Func handler) + { + _botAuth.OnUserSignInSuccess(handler); + return this; + } + + /// + /// The handler function is called when the user sign in flow fails + /// + /// The handler function to call when the user failed to signed in + /// The class itself for chaining purpose + public IAuthentication OnUserSignInFailure(Func handler) + { + _botAuth.OnUserSignInFailure(handler); + return this; + } + private async Task TryGetUserToken(ITurnContext context) { string homeAccountId = $"{context.Activity.From.AadObjectId}.{context.Activity.Conversation.TenantId}"; - IAccount account = await _msal.GetAccountAsync(homeAccountId); + IAccount account = await _settings.MSAL.GetAccountAsync(homeAccountId); if (account != null) { - AuthenticationResult result = await _msal.AcquireTokenSilent(_scopes, account).ExecuteAsync(); + AuthenticationResult result = await _settings.MSAL.AcquireTokenSilent(_settings.Scopes, account).ExecuteAsync(); return result.AccessToken; } return ""; // Return empty indication no token found in cache diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TeamsSsoSettings.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TeamsSsoSettings.cs new file mode 100644 index 000000000..18cbfa5fa --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TeamsSsoSettings.cs @@ -0,0 +1,58 @@ +using Microsoft.Identity.Client; + +namespace Microsoft.Teams.AI +{ + /// + /// Settings to initialize TeamsSsoAuthentication class + /// + public class TeamsSsoSettings + { + /// + /// The AAD scopes for authentication. Only one resource is allowed in the scopes. + /// + public string[] Scopes { get; set; } + + /// + /// The instance of ConfidentialClientApplication from Microsoft Authentication Library + /// + public IConfidentialClientApplication MSAL { get; set; } + + /// + /// The sign in link for authentication. + /// The library will pass `scope`, `clientId`, and `tenantId` to the link as query parameters. + /// Your sign in page can leverage these parameters to compose the AAD sign-in URL. + /// + public string SignInLink { get; set; } + + /// + /// Number of milliseconds to wait for the user to authenticate. + /// Defaults to a value `900,000` (15 minutes). + /// Only works in conversional bot scenario. + /// + public int Timeout { get; set; } + + /// + /// Value indicating whether the authentication should end upon receiving an invalid message. + /// Defaults to `true`. + /// Only works in conversional bot scenario. + /// + public bool EndOnInvalidMessage { get; set; } + + /// + /// Initializes the class + /// + /// The AAD scopes for authentication. + /// The sign in link for authentication. + /// The instance of ConfidentialClientApplication from Microsoft Authentication Library + /// Number of milliseconds to wait for the user to authenticate. + /// Value indicating whether the authentication should end upon receiving an invalid message. + public TeamsSsoSettings(string[] scopes, string signInLink, IConfidentialClientApplication msal, int timeout = 900000, bool endOnInvalidMessage = true) + { + this.Scopes = scopes; + this.MSAL = msal; + this.SignInLink = signInLink; + this.Timeout = timeout; + this.EndOnInvalidMessage = endOnInvalidMessage; + } + } +} diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TurnStateProperty.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TurnStateProperty.cs new file mode 100644 index 000000000..3b67fc7a7 --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TurnStateProperty.cs @@ -0,0 +1,64 @@ +using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.State; +using Microsoft.Teams.AI.Exceptions; + +namespace Microsoft.Teams.AI +{ + internal class TurnStateProperty : IStatePropertyAccessor + where TState : new() + { + private string _propertyName; + private TurnStateEntry _state; + + public TurnStateProperty(TurnState state, string scopeName, string propertyName) + { + _propertyName = propertyName; + + TurnStateEntry? scope = state.GetScope(scopeName); + if (scope == null) + { + throw new TeamsAIException($"TurnStateProperty: TurnState missing state scope named {scope}"); + } + + _state = scope; + } + + public string Name => throw new NotImplementedException(); + + public Task DeleteAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) + { + _state.Value?.Remove(_propertyName); + return Task.CompletedTask; + } + + public Task GetAsync(ITurnContext turnContext, Func defaultValueFactory = null, CancellationToken cancellationToken = default) + { + if (_state.Value != null) + { + if (!_state.Value.ContainsKey(_propertyName)) + { + _state.Value[_propertyName] = defaultValueFactory(); + } + + if (_state.Value.TryGetValue(_propertyName, out TState result)) + { + return Task.FromResult(result); + } + else + { + TState defaultValue = defaultValueFactory(); + _state.Value[_propertyName] = defaultValue; + return Task.FromResult(defaultValue); + } + } + + throw new TeamsAIException("No state value available"); + } + + public Task SetAsync(ITurnContext turnContext, TState value, CancellationToken cancellationToken = default) + { + this._state.Value?.Set(_propertyName, value); + return Task.CompletedTask; + } + } +} diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/State/TempState.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/State/TempState.cs index cab5afdea..6f94ba2d5 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/State/TempState.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/State/TempState.cs @@ -34,6 +34,12 @@ public class TempState : Record /// public const string AuthTokenKey = "authTokens"; + + /// + /// Name of the duplicate token exchange property + /// + public const string DuplicateTokenExchangeKey = "duplicateTokenExchange"; + /// /// Creates a new instance of the class. /// @@ -43,6 +49,8 @@ public TempState() : base() this[OutputKey] = string.Empty; this[HistoryKey] = string.Empty; this[ActionOutputsKey] = new Dictionary(); + this[AuthTokenKey] = new Dictionary(); + this[DuplicateTokenExchangeKey] = false; } /// @@ -91,5 +99,14 @@ public Dictionary AuthTokens get => Get>(AuthTokenKey)!; set => Set(AuthTokenKey, value); } + + /// + /// Whether current token exchange is a duplicate one + /// + public bool DuplicateTokenExchange + { + get => Get(DuplicateTokenExchangeKey)!; + set => Set(DuplicateTokenExchangeKey, value); + } } }