Skip to content

Commit

Permalink
[C#] feat: Implement GetTokenOrStartSignInAsync method public inter…
Browse files Browse the repository at this point in the history
…face (#987)

## Linked issues

closes: #894  (issue number)

## Details

- Rename `TeamsAIAuthException` to `AuthException`
- Rename `TeamsAIAuthReason` to `AuthExceptionReason`
- Implement helper methods in `AuthUtilities`
- Implement `GetTokenOrStartSignInAsync`.
- Add `IsUserSignedIn` to `IAuthentication` interface - this is needed
so it can be called in the `Application` class.
- Make `AuthenticationManager._default` private property public
`Default`.

## Attestation Checklist

- [x] My code follows the style guidelines of this project

- I have checked for/fixed spelling, linting, and other errors
- I have commented my code for clarity
- I have made corresponding changes to the documentation (we use
[TypeDoc](https://typedoc.org/) to document our code)
- My changes generate no new warnings
- I have added tests that validates my changes, and provides sufficient
test coverage. I have tested with:
  - Local testing
  - E2E testing in Teams
- New and existing unit tests pass locally with my changes
  • Loading branch information
singhk97 authored Dec 1, 2023
1 parent 7a2761e commit cec9db2
Show file tree
Hide file tree
Showing 16 changed files with 358 additions and 69 deletions.
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

0 comments on commit cec9db2

Please sign in to comment.