Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[C#] feat: Implement GetTokenOrStartSignInAsync method public interface #987

Merged
merged 4 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<string>(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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public override Task<DialogTurnResult> ContinueDialog(ITurnContext context, TSta
{
if (_throwExceptionWhenContinue)
{
throw new TeamsAIAuthException("mocked error");
throw new AuthException("mocked error");
}
return Task.FromResult(_continueDialogResult);
}
Expand Down Expand Up @@ -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());
Expand All @@ -179,8 +179,8 @@ public async void Test_HandleSignInActivity_CompleteWithoutToken()
var botAuth = new MockedBotAuthentication<TurnState>(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
Expand All @@ -199,8 +199,8 @@ public async void Test_HandleSignInActivity_ThrowException()
var botAuth = new MockedBotAuthentication<TurnState>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,7 +27,7 @@ public override Task<TokenResponse> HandleUserSignIn(ITurnContext context, strin
{
if (_signInResponse == null)
{
throw new TeamsAIAuthException("HandlerUserSignIn failed");
throw new AuthException("HandlerUserSignIn failed");
}
return Task.FromResult(_signInResponse);
}
Expand All @@ -36,18 +36,18 @@ public override Task<TokenResponse> 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; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ public void Initialize(Application<TState> app, string name, IStorage? storage =
return;
}

public Task<string?> IsUserSignedInAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
return Task.FromResult<string?>(null);
}

public Task<bool> IsValidActivityAsync(ITurnContext context)
{
return Task.FromResult(_validActivity);
}

public IAuthentication<TState> OnUserSignInFailure(Func<ITurnContext, TState, TeamsAIAuthException, Task> handler)
public IAuthentication<TState> OnUserSignInFailure(Func<ITurnContext, TState, AuthException, Task> handler)
{
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +31,8 @@ public class Application<TState> : IBot
private static readonly string CONFIG_SUBMIT_INVOKE_NAME = "config/submit";

private readonly AI<TState>? _ai;
private readonly AuthenticationManager<TState>? _authentication;

private readonly int _typingTimerDelay = 1000;
private TypingTimer? _typingTimer;

Expand Down Expand Up @@ -85,7 +88,7 @@ public Application(ApplicationOptions<TState> options)
pair.Value.Initialize(this, pair.Key, options.Storage);
}

Authentication = new AuthenticationManager<TState>(options.Authentication);
_authentication = new AuthenticationManager<TState>(options.Authentication);
if (options.Authentication.AutoSignIn != null)
{
_startSignIn = options.Authentication.AutoSignIn;
Expand Down Expand Up @@ -120,7 +123,19 @@ public Application(ApplicationOptions<TState> options)
/// <summary>
/// Accessing authentication specific features.
/// </summary>
public AuthenticationManager<TState>? Authentication { get; }
public AuthenticationManager<TState> Authentication
{

get
{
if (_authentication == null)
{
throw new ArgumentException("The Application.Authentication property is unavailable because no authentication options were configured.");
}

return _authentication;
}
}

/// <summary>
/// Fluent interface for accessing AI specific features.
Expand Down Expand Up @@ -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!);
}
}

Expand Down Expand Up @@ -929,6 +959,66 @@ private async Task _OnTurnAsync(ITurnContext turnContext, CancellationToken canc
}
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="turnContext"> The turn context.</param>
/// <param name="turnState">The turn state.</param>
/// <param name="settingName">The name of the authentication setting.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The access token for the user if they are signed, otherwise null.</returns>
/// <exception cref="TeamsAIException"></exception>
public async Task<string?> 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;
}

/// <summary>
/// Convert original handler to proactive conversation.
/// </summary>
Expand Down
Loading