diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/AuthUtilitiesTest.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/AuthUtilitiesTest.cs new file mode 100644 index 0000000000..98c830bb28 --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/AuthUtilitiesTest.cs @@ -0,0 +1,90 @@ +using Microsoft.Bot.Builder; +using Microsoft.Teams.AI.State; +using Microsoft.Teams.AI.Tests.TestUtils; + +namespace Microsoft.Teams.AI.Tests.Application.Authentication +{ + public class AuthUtilitiesTest + { + [Fact] + public async void Test_SetTokenInState() + { + // Arrange + TurnContext context = TurnStateConfig.CreateConfiguredTurnContext(); + TurnState state = await TurnStateConfig.GetTurnStateWithConversationStateAsync(context); + string settingName = "settingName"; + string token = "token"; + + // Act + AuthUtilities.SetTokenInState(state, settingName, token); + + // Assert + Assert.True(state.Temp.AuthTokens.ContainsKey(settingName)); + } + + [Fact] + public async void Test_DeleteTokenFromState() + { + // Arrange + TurnContext context = TurnStateConfig.CreateConfiguredTurnContext(); + TurnState state = await TurnStateConfig.GetTurnStateWithConversationStateAsync(context); + string settingName = "settingName"; + string token = "token"; + + // Act + state.Temp.AuthTokens[settingName] = token; + AuthUtilities.DeleteTokenFromState(state, settingName); + + // Assert + Assert.False(state.Temp.AuthTokens.ContainsKey(settingName)); + } + + [Fact] + public async void Test_UserInSignInFlow() + { + // Arrange + TurnContext context = TurnStateConfig.CreateConfiguredTurnContext(); + TurnState state = await TurnStateConfig.GetTurnStateWithConversationStateAsync(context); + string settingName = "settingName"; + + // Act + state.User.Set(AuthUtilities.IS_SIGNED_IN_KEY, settingName); + string? response = AuthUtilities.UserInSignInFlow(state); + + // Assert + Assert.True(response == settingName); + + } + + [Fact] + public async void Test_SetUserInSignInFlow() + { + // Arrange + TurnContext context = TurnStateConfig.CreateConfiguredTurnContext(); + TurnState state = await TurnStateConfig.GetTurnStateWithConversationStateAsync(context); + string settingName = "settingName"; + + // Act + AuthUtilities.SetUserInSignInFlow(state, settingName); + + // Assert + Assert.True(state.User.Get(AuthUtilities.IS_SIGNED_IN_KEY) == settingName); + } + + [Fact] + public async void Test_DeleteUserInSignInFlow() + { + // Arrange + TurnContext context = TurnStateConfig.CreateConfiguredTurnContext(); + TurnState state = await TurnStateConfig.GetTurnStateWithConversationStateAsync(context); + string settingName = "settingName"; + + // Act + state.User.Set(AuthUtilities.IS_SIGNED_IN_KEY, settingName); + AuthUtilities.DeleteUserInSignInFlow(state); + + // Assert + Assert.False(state.User.ContainsKey(AuthUtilities.IS_SIGNED_IN_KEY)); + } + } +} 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 e660ca7ae1..f820d229be 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 @@ -25,7 +25,7 @@ public override Task ContinueDialog(ITurnContext context, TSta { if (_throwExceptionWhenContinue) { - throw new TeamsAIAuthException("mocked error"); + throw new AuthException("mocked error"); } return Task.FromResult(_continueDialogResult); } @@ -161,7 +161,7 @@ public async void Test_HandleSignInActivity_Complete() messageText = context.Activity.Text; return Task.CompletedTask; }); - botAuth.OnUserSignInFailure((context, state, exception) => { throw new TeamsAIAuthException("sign in failure handler should not be called"); }); + botAuth.OnUserSignInFailure((context, state, exception) => { throw new AuthException("sign in failure handler should not be called"); }); // act await botAuth.HandleSignInActivity(context, state, new CancellationToken()); @@ -179,8 +179,8 @@ public async void Test_HandleSignInActivity_CompleteWithoutToken() var botAuth = new MockedBotAuthentication(app, "test", continueDialogResult: new DialogTurnResult(DialogTurnStatus.Complete)); var context = MockTurnContext(); var state = await TurnStateConfig.GetTurnStateWithConversationStateAsync(context); - TeamsAIAuthException? authException = null; - botAuth.OnUserSignInSuccess((context, state) => { throw new TeamsAIAuthException("sign in success handler should not be called"); }); + AuthException? authException = null; + botAuth.OnUserSignInSuccess((context, state) => { throw new AuthException("sign in success handler should not be called"); }); botAuth.OnUserSignInFailure((context, state, exception) => { authException = exception; return Task.CompletedTask; }); // act @@ -199,8 +199,8 @@ public async void Test_HandleSignInActivity_ThrowException() var botAuth = new MockedBotAuthentication(app, "test", throwExceptionWhenContinue: true); var context = MockTurnContext(); var state = await TurnStateConfig.GetTurnStateWithConversationStateAsync(context); - TeamsAIAuthException? authException = null; - botAuth.OnUserSignInSuccess((context, state) => { throw new TeamsAIAuthException("sign in success handler should not be called"); }); + AuthException? authException = null; + botAuth.OnUserSignInSuccess((context, state) => { throw new AuthException("sign in success handler should not be called"); }); botAuth.OnUserSignInFailure((context, state, exception) => { authException = exception; return Task.CompletedTask; }); // act 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 aa3fb5815a..7874e36394 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 @@ -7,7 +7,7 @@ namespace Microsoft.Teams.AI.Tests.Application.Authentication.MessageExtensions { - internal class MockedMessageExtensionsAuthentication : MessageExtensionsAuthenticationBase + internal sealed class MockedMessageExtensionsAuthentication : MessageExtensionsAuthenticationBase { private TokenResponse? _tokenExchangeResponse; private TokenResponse? _signInResponse; @@ -27,7 +27,7 @@ public override Task HandleUserSignIn(ITurnContext context, strin { if (_signInResponse == null) { - throw new TeamsAIAuthException("HandlerUserSignIn failed"); + throw new AuthException("HandlerUserSignIn failed"); } return Task.FromResult(_signInResponse); } @@ -36,18 +36,18 @@ public override Task HandleSsoTokenExchange(ITurnContext context) { if (_tokenExchangeResponse == null) { - throw new TeamsAIAuthException("HandleSsoTokenExchange failed"); + throw new AuthException("HandleSsoTokenExchange failed"); } return Task.FromResult(_tokenExchangeResponse); } } - internal class TokenExchangeRequest + internal sealed class TokenExchangeRequest { public Authentication? authentication { get; set; } } - internal class Authentication + internal sealed class Authentication { public string? token { get; set; } } 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 f8ba9d8568..6807a1c73e 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 @@ -23,12 +23,17 @@ public void Initialize(Application app, string name, IStorage? storage = return; } + public Task IsUserSignedInAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) + { + return Task.FromResult(null); + } + public Task IsValidActivityAsync(ITurnContext context) { return Task.FromResult(_validActivity); } - public IAuthentication OnUserSignInFailure(Func handler) + public IAuthentication OnUserSignInFailure(Func handler) { return this; } diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs index dd57ecf42f..8c757cfd34 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs @@ -4,6 +4,7 @@ using Microsoft.Bot.Schema; using Microsoft.Bot.Schema.Teams; using Microsoft.Teams.AI.AI; +using Microsoft.Teams.AI.Exceptions; using Microsoft.Teams.AI.State; using Microsoft.Teams.AI.Utilities; using System.Collections.Concurrent; @@ -30,6 +31,8 @@ public class Application : IBot private static readonly string CONFIG_SUBMIT_INVOKE_NAME = "config/submit"; private readonly AI? _ai; + private readonly AuthenticationManager? _authentication; + private readonly int _typingTimerDelay = 1000; private TypingTimer? _typingTimer; @@ -85,7 +88,7 @@ public Application(ApplicationOptions options) pair.Value.Initialize(this, pair.Key, options.Storage); } - Authentication = new AuthenticationManager(options.Authentication); + _authentication = new AuthenticationManager(options.Authentication); if (options.Authentication.AutoSignIn != null) { _startSignIn = options.Authentication.AutoSignIn; @@ -120,7 +123,19 @@ public Application(ApplicationOptions options) /// /// Accessing authentication specific features. /// - public AuthenticationManager? Authentication { get; } + public AuthenticationManager Authentication + { + + get + { + if (_authentication == null) + { + throw new ArgumentException("The Application.Authentication property is unavailable because no authentication options were configured."); + } + + return _authentication; + } + } /// /// Fluent interface for accessing AI specific features. @@ -842,21 +857,36 @@ private async Task _OnTurnAsync(ITurnContext turnContext, CancellationToken canc await turnState!.LoadStateAsync(storage, turnContext); + // If user is in sign in flow, return the authentication setting name + string? settingName = AuthUtilities.UserInSignInFlow(turnState); + bool shouldStartSignIn = _startSignIn != null && await _startSignIn(turnContext, cancellationToken); + // Sign the user in - if (Authentication != null && _startSignIn != null && await _startSignIn(turnContext, cancellationToken)) + if (this._authentication != null && (shouldStartSignIn || settingName != null)) { - // Should skip activity that does not support sign-in - if (await Authentication.IsValidActivityAsync(turnContext)) + if (settingName == null) { - SignInResponse response = await Authentication.SignUserInAsync(turnContext, turnState); - if (response.Status == SignInStatus.Pending) - { - // Requires user action, save state and stop processing current activity - await turnState.SaveStateAsync(turnContext, storage); - return; - } + settingName = this._authentication.Default; + } + + SignInResponse response = await this._authentication.SignUserInAsync(turnContext, turnState, settingName); + + if (response.Status == SignInStatus.Complete) + { + AuthUtilities.DeleteUserInSignInFlow(turnState); + } + + if (response.Status == SignInStatus.Pending) + { + // Requires user action, save state and stop processing current activity + await turnState.SaveStateAsync(turnContext, storage); + return; + } - // Sign-in success, continue processing current activity + if (response.Status == SignInStatus.Error && response.Cause != AuthExceptionReason.InvalidActivity) + { + AuthUtilities.DeleteUserInSignInFlow(turnState); + throw new TeamsAIException("An error occured when trying to sign in.", response.Error!); } } @@ -929,6 +959,66 @@ private async Task _OnTurnAsync(ITurnContext turnContext, CancellationToken canc } } + /// + /// If the user is signed in, get the access token. If not, triggers the sign in flow for the provided authentication setting name + /// and returns.In this case, the bot should end the turn until the sign in flow is completed. + /// + /// + /// Use this method to get the access token for a user that is signed in to the bot. + /// If the user isn't signed in, this method starts the sign-in flow. + /// The bot should end the turn in this case until the sign-in flow completes and the user is signed in. + /// + /// The turn context. + /// The turn state. + /// The name of the authentication setting. + /// The cancellation token. + /// The access token for the user if they are signed, otherwise null. + /// + public async Task GetTokenOrStartSignInAsync(ITurnContext turnContext, TState turnState, string settingName, CancellationToken cancellationToken = default) + { + string? token = await Authentication.Get(settingName).IsUserSignedInAsync(turnContext, cancellationToken); + + if (token != null) + { + AuthUtilities.SetTokenInState(turnState, settingName, token); + AuthUtilities.DeleteUserInSignInFlow(turnState); + return token; + } + + // User is currently not in sign in flow + if (AuthUtilities.UserInSignInFlow(turnState) == null) + { + AuthUtilities.SetUserInSignInFlow(turnState, settingName); + } + else + { + AuthUtilities.DeleteUserInSignInFlow(turnState); + throw new TeamsAIException("Invalid sign in flow state. Cannot start sign in when already started"); + } + + SignInResponse response = await Authentication.SignUserInAsync(turnContext, turnState, settingName); + + if (response.Status == SignInStatus.Error) + { + string message = response.Error!.ToString(); + if (response.Cause == AuthExceptionReason.InvalidActivity) + { + message = $"User is not signed in and cannot start sign in flow for this activity: {response.Error}"; + } + + throw new TeamsAIException($"Error occured while trying to authenticate user: {message}"); + } + + if (response.Status == SignInStatus.Complete) + { + AuthUtilities.DeleteUserInSignInFlow(turnState); + return turnState.Temp.AuthTokens[settingName]; + } + + // response.Status == SignInStatus.Pending + return null; + } + /// /// Convert original handler to proactive conversation. /// 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 e6357d7790..f0225642cb 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthUtilities.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthUtilities.cs @@ -7,6 +7,8 @@ namespace Microsoft.Teams.AI /// internal class AuthUtilities { + public const string IS_SIGNED_IN_KEY = "__InSignInFlow__"; + /// /// Set token in state /// @@ -36,5 +38,44 @@ internal class AuthUtilities state.Temp.AuthTokens.Remove(name); } } + + /// + /// Determines if the user is in the sign in flow. + /// + /// The turn state. + /// The turn state. + /// The connection setting name if the user is in sign in flow. Otherwise null. + public static string? UserInSignInFlow(TState state) where TState : TurnState, new() + { + string? value = state.User.Get(IS_SIGNED_IN_KEY); + + if (value == string.Empty || value == null) + { + return null; + } + + return value; + } + + /// + /// Update the turn state to indicate the user is in the sign in flow by providing the authentication setting name used. + /// + /// The turn state. + /// The turn state. + /// The connection setting name defined when configuring the authentication options within the application class. + public static void SetUserInSignInFlow(TState state, string settingName) where TState : TurnState, new() + { + state.User.Set(IS_SIGNED_IN_KEY, settingName); + } + + /// + /// Delete the user in sign in flow state from the turn state. + /// + /// The turn state. + /// The turn state. + public static void DeleteUserInSignInFlow(TState state) where TState : TurnState, new() + { + state.User.Remove(IS_SIGNED_IN_KEY); + } } } 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 7e9094c67b..cbb3882ccd 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationManager.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/AuthenticationManager.cs @@ -12,7 +12,11 @@ public class AuthenticationManager where TState : TurnState, new() { private Dictionary> _authentications { get; } - private string _default { get; set; } + + /// + /// The default authentication setting name. + /// + public string Default { get; } /// /// Creates a new instance of the class @@ -27,68 +31,68 @@ public AuthenticationManager(AuthenticationOptions options) } // If developer does not specify default authentication, set default to the first one in the options - _default = options.Default ?? options.Authentications.First().Key; + Default = options.Default ?? options.Authentications.First().Key; _authentications = options.Authentications; } /// - /// Sign in a user + /// Sign in a user. /// /// The turn context /// The turn state - /// Optional. The name of the authentication handler to use. If not specified, the default handler name is used. + /// Optional. The name of the authentication handler to use. If not specified, the default handler name is used. /// The cancellation token /// The sign in response - public async Task SignUserInAsync(ITurnContext context, TState state, string? handlerName = null, CancellationToken cancellationToken = default) + public async Task SignUserInAsync(ITurnContext context, TState state, string? settingName = null, CancellationToken cancellationToken = default) { - if (handlerName == null) + if (settingName == null) { - handlerName = _default; + settingName = Default; } - IAuthentication auth = Get(handlerName); + IAuthentication auth = Get(settingName); SignInResponse response = await auth.SignInUserAsync(context, state, cancellationToken); if (response.Status == SignInStatus.Complete) { - AuthUtilities.SetTokenInState(state, handlerName, response.Token!); + AuthUtilities.SetTokenInState(state, settingName, response.Token!); } return response; } /// - /// Signs out a user + /// Signs out a user. /// /// The turn context /// The turn state - /// Optional. The name of the authentication handler to use. If not specified, the default handler name is used. + /// Optional. The name of the authentication handler to use. If not specified, the default handler name is used. /// The cancellation token - public async Task SignOutUserAsync(ITurnContext context, TState state, string? handlerName = null, CancellationToken cancellationToken = default) + public async Task SignOutUserAsync(ITurnContext context, TState state, string? settingName = null, CancellationToken cancellationToken = default) { - if (handlerName == null) + if (settingName == null) { - handlerName = _default; + settingName = Default; } - IAuthentication auth = Get(handlerName); + IAuthentication auth = Get(settingName); await auth.SignOutUserAsync(context, state, cancellationToken); - AuthUtilities.DeleteTokenFromState(state, handlerName); + AuthUtilities.DeleteTokenFromState(state, settingName); } /// /// Check whether current activity supports authentication. /// /// Current turn context. - /// Optional. The name of the authentication handler to use. If not specified, the default handler name is used. + /// Optional. The name of the authentication handler to use. If not specified, the default handler name is used. /// True if current activity supports authentication. Otherwise, false. - public async Task IsValidActivityAsync(ITurnContext context, string? handlerName = null) + public async Task IsValidActivityAsync(ITurnContext context, string? settingName = null) { - if (handlerName == null) + if (settingName == null) { - handlerName = _default; + settingName = Default; } - IAuthentication auth = Get(handlerName); + IAuthentication auth = Get(settingName); return await auth.IsValidActivityAsync(context); } 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 48c1bc5230..553d2b73b1 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 @@ -30,7 +30,7 @@ internal abstract class BotAuthenticationBase /// /// Callback when user sign in fail /// - protected Func? _userSignInFailureHandler; + protected Func? _userSignInFailureHandler; /// /// Initializes the class @@ -163,7 +163,7 @@ public async Task HandleSignInActivity(ITurnContext context, TState state, Cance // Failed sign in if (_userSignInFailureHandler != null) { - await _userSignInFailureHandler(context, state, new TeamsAIAuthException("Authentication flow completed without a token.", TeamsAIAuthExceptionReason.CompletionWithoutToken)); + await _userSignInFailureHandler(context, state, new AuthException("Authentication flow completed without a token.", AuthExceptionReason.CompletionWithoutToken)); } } } @@ -174,7 +174,7 @@ public async Task HandleSignInActivity(ITurnContext context, TState state, Cance if (_userSignInFailureHandler != null) { - await _userSignInFailureHandler(context, state, new TeamsAIAuthException(message)); + await _userSignInFailureHandler(context, state, new AuthException(message)); } } } @@ -192,7 +192,7 @@ public void 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 - public void OnUserSignInFailure(Func handler) + public void OnUserSignInFailure(Func handler) { _userSignInFailureHandler = handler; } 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 797039c511..cb61a95e11 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 @@ -141,7 +141,7 @@ 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!"); + throw new AuthException("Invalid context, can not get storage key!"); } Activity activity = context.Activity; @@ -149,13 +149,13 @@ private string GetStorageKey(ITurnContext context) 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."); + throw new AuthException("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."); + throw new AuthException("Invalid signin/tokenExchange. Missing activity.value.id."); } return $"{channelId}/{conversationId}/{id}"; } 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 index 9ae67e5049..a30dc1c114 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/TeamsSsoPrompt.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/Bot/TeamsSsoPrompt.cs @@ -132,7 +132,7 @@ ref homeAccountId catch (Exception ex) { string message = $"Failed to get access token with error: {ex.Message}"; - throw new TeamsAIAuthException(message); + throw new AuthException(message); } } } 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 4242d3b5a4..6136f58bf8 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/IAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/IAuthentication.cs @@ -13,10 +13,16 @@ public enum SignInStatus /// Sign-in not complete and requires user interaction /// Pending, + /// /// Sign-in complete /// - Complete + Complete, + + /// + /// Error occurred during sign-in + /// + Error } /// @@ -34,6 +40,16 @@ public class SignInResponse /// public string? Token { get; set; } + /// + /// The exception object. Only available when sign-in status is Error. + /// + public Exception? Error { get; set; } + + /// + /// The cause of error. Only available when sign-in status is Error. + /// + public AuthExceptionReason? Cause { get; set; } + /// /// Initialize an instance of current class /// @@ -96,6 +112,14 @@ public interface IAuthentication /// /// The handler function to call when the user failed to signed in /// The class itself for chaining purpose - IAuthentication OnUserSignInFailure(Func handler); + IAuthentication OnUserSignInFailure(Func handler); + + /// + /// Check if the user is signed, if they are then return the token. + /// + /// The turn context. + /// The cancellation token + /// The token if the user is signed. Otherwise null. + Task IsUserSignedInAsync(ITurnContext turnContext, CancellationToken cancellationToken = default); } } 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 7562fae11b..7bbed9a5de 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 @@ -77,7 +77,7 @@ ref homeAccountId catch (Exception ex) { string message = $"Failed to exchange access token with error: {ex.Message}"; - throw new TeamsAIAuthException(message); + throw new AuthException(message); } } 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 85dd3e28cf..acc549499d 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/OAuthAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/OAuthAuthentication.cs @@ -7,7 +7,7 @@ namespace Microsoft.Teams.AI /// /// Handles authentication using OAuth Connection. /// - public class OAuthAuthentication : IAuthentication + internal class OAuthAuthentication : IAuthentication where TState : TurnState, new() { /// @@ -21,6 +21,17 @@ public void Initialize(Application app, string name, IStorage? storage = throw new NotImplementedException(); } + /// + /// Check if the user is signed, if they are then return the token. + /// + /// The turn context. + /// The cancellation token + /// The token if the user is signed. Otherwise null. + public Task IsUserSignedInAsync(ITurnContext turnContext, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + /// /// Whether the current activity is a valid activity that supports authentication /// @@ -36,7 +47,7 @@ public Task IsValidActivityAsync(ITurnContext context) /// /// The handler function to call when the user failed to signed in /// The class itself for chaining purpose - public IAuthentication OnUserSignInFailure(Func handler) + public IAuthentication OnUserSignInFailure(Func handler) { throw new NotImplementedException(); } 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 a40bf74b70..e4d5a3c1af 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TeamsSsoAuthentication.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TeamsSsoAuthentication.cs @@ -9,7 +9,7 @@ namespace Microsoft.Teams.AI /// /// Handles authentication based on Teams SSO. /// - public class TeamsSsoAuthentication : IAuthentication + internal class TeamsSsoAuthentication : IAuthentication where TState : TurnState, new() { private TeamsSsoBotAuthentication? _botAuth; @@ -58,7 +58,7 @@ public Task IsValidActivityAsync(ITurnContext context) /// The sign in response public async Task SignInUserAsync(ITurnContext context, TState state, CancellationToken cancellationToken = default) { - string token = await TryGetUserToken(context); + string token = await _TryGetUserToken(context); if (!string.IsNullOrEmpty(token)) { return new SignInResponse(SignInStatus.Complete) @@ -116,7 +116,7 @@ public IAuthentication OnUserSignInSuccess(Func /// The handler function to call when the user failed to signed in /// The class itself for chaining purpose - public IAuthentication OnUserSignInFailure(Func handler) + public IAuthentication OnUserSignInFailure(Func handler) { if (_botAuth != null) { @@ -125,7 +125,19 @@ public IAuthentication OnUserSignInFailure(Func TryGetUserToken(ITurnContext context) + /// + /// Check if the user is signed, if they are then return the token. + /// + /// The turn context. + /// The cancellation token + /// The token if the user is signed. Otherwise null. + public async Task IsUserSignedInAsync(ITurnContext context, CancellationToken cancellationToken = default) + { + string token = await _TryGetUserToken(context); + return token == "" ? null : token; + } + + private async Task _TryGetUserToken(ITurnContext context) { string homeAccountId = $"{context.Activity.From.AadObjectId}.{context.Activity.Conversation.TenantId}"; try diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TurnStateProperty.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TurnStateProperty.cs index 6fdd4ed500..4d7263ac5d 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TurnStateProperty.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Authentication/TurnStateProperty.cs @@ -1,9 +1,15 @@ using Microsoft.Bot.Builder; using Microsoft.Teams.AI.State; using Microsoft.Teams.AI.Exceptions; +using Microsoft.Bot.Builder.Dialogs; namespace Microsoft.Teams.AI { + /// + /// Maps the turn state property to a bot State property. + /// Note: Used to inject data into . + /// + /// internal class TurnStateProperty : IStatePropertyAccessor where TState : new() { diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Exceptions/TeamsAIAuthException.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Exceptions/AuthException.cs similarity index 58% rename from dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Exceptions/TeamsAIAuthException.cs rename to dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Exceptions/AuthException.cs index 01a1d33782..c42b640373 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Exceptions/TeamsAIAuthException.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Exceptions/AuthException.cs @@ -1,14 +1,20 @@ namespace Microsoft.Teams.AI.Exceptions { /// - /// Cause of an authentication exception. + /// Cause of user authentication exception. /// - public enum TeamsAIAuthExceptionReason + public enum AuthExceptionReason { /// /// The authentication flow completed without a token. /// CompletionWithoutToken, + + /// + /// The incomming activity is not valid for sign in flow. + /// + InvalidActivity, + /// /// Other error. /// @@ -16,21 +22,21 @@ public enum TeamsAIAuthExceptionReason } /// - /// An exception thrown when an authentication error occurs. + /// An exception thrown when user authentication error occurs. /// - public class TeamsAIAuthException : Exception + public class AuthException : Exception { /// /// The cause of the exception. /// - public TeamsAIAuthExceptionReason Cause { get; } + public AuthExceptionReason Cause { get; } /// /// Initializes the class /// /// The exception message /// The cause of the exception - public TeamsAIAuthException(string message, TeamsAIAuthExceptionReason reason = TeamsAIAuthExceptionReason.Other) : base(message) + public AuthException(string message, AuthExceptionReason reason = AuthExceptionReason.Other) : base(message) { Cause = reason; }