Skip to content

Commit

Permalink
[C#] feat: add teams sso implementation (#929)
Browse files Browse the repository at this point in the history
## Linked issues

closes: #861

## Details

1. Implement token exchange and sign in logic for Teams SSO auth
implementation
2. Updated the `IAuthentication` interface to initialize after
`Application` instance is created - the authentication classes has
dependency to `Application` when initialize
3. Change visibility of some classes to `internal`
4. Update the namespace of authentication classes to
`Microsoft.Teams.AI` to align with other classes under the `Application`
folder


## 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

### Additional information

Because some classes from botbuilder cannot be mocked, the UT for the
new code is some kind of blocked. I have finished manual testing against
the code. Will follow up with the UT problem.
  • Loading branch information
blackchoey authored Nov 29, 2023
1 parent 7e028a1 commit c21be62
Show file tree
Hide file tree
Showing 24 changed files with 725 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public override Task<string> GetSignInLink(ITurnContext context)
return Task.FromResult("mocked link");
}

public override Task<TokenResponse> HandlerUserSignIn(ITurnContext context, string magicCode)
public override Task<TokenResponse> HandleUserSignIn(ITurnContext context, string magicCode)
{
if (_signInResponse == null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TState> : IAuthentication<TState>
where TState : TurnState
where TState : TurnState, new()
{
private string _mockedToken;
private SignInStatus _mockedStatus;
Expand All @@ -17,11 +18,26 @@ public MockedAuthentication(SignInStatus mockedStatus = SignInStatus.Complete, s
_validActivity = validActivity;
}

public void Initialize(Application<TState> app, string name, IStorage? storage = null)
{
return;
}

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

public IAuthentication<TState> OnUserSignInFailure(Func<ITurnContext, TState, TeamsAIAuthException, Task> handler)
{
return this;
}

public IAuthentication<TState> OnUserSignInSuccess(Func<ITurnContext, TState, Task> handler)
{
return this;
}

public Task<SignInResponse> SignInUser(ITurnContext context, TState state)
{
var result = new SignInResponse(_mockedStatus);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,25 @@ public Application(ApplicationOptions<TState> options)
MessageExtensions = new MessageExtensions<TState>(this);
TaskModules = new TaskModules<TState>(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<Route<TState>>();
_invokeRoutes = new ConcurrentQueue<Route<TState>>();
_beforeTurn = new ConcurrentQueue<TurnEventHandlerAsync<TState>>();
_afterTurn = new ConcurrentQueue<TurnEventHandlerAsync<TState>>();

if (options.Authentication != null)
{
// Initialize the authentication classes
foreach (KeyValuePair<string, IAuthentication<TState>> pair in options.Authentication.Authentications)
{
pair.Value.Initialize(this, pair.Key, options.Storage);
}

Authentication = new AuthenticationManager<TState>(options.Authentication);
if (options.Authentication.AutoSignIn != null)
{
Expand All @@ -78,17 +95,6 @@ public Application(ApplicationOptions<TState> 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<Route<TState>>();
_invokeRoutes = new ConcurrentQueue<Route<TState>>();
_beforeTurn = new ConcurrentQueue<TurnEventHandlerAsync<TState>>();
_afterTurn = new ConcurrentQueue<TurnEventHandlerAsync<TState>>();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.Teams.AI
/// </summary>
/// <typeparam name="TState">Type of the turn state.</typeparam>
public class ApplicationOptions<TState>
where TState : TurnState
where TState : TurnState, new()
{
/// <summary>
/// Optional. Bot adapter being used.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema;

namespace Microsoft.Teams.AI.Application.Authentication.AdaptiveCards
namespace Microsoft.Teams.AI
{
/// <summary>
/// Base class for adaptive card authentication that handles common logic
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Microsoft.Teams.AI.Application.Authentication.AdaptiveCards
namespace Microsoft.Teams.AI
{
/// <summary>
/// Handles authentication for Adaptive Cards in Teams using OAuth Connection.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Microsoft.Teams.AI.Application.Authentication.AdaptiveCards
namespace Microsoft.Teams.AI
{
/// <summary>
/// Handles authentication for Adaptive Cards in Teams based on Teams SSO.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Microsoft.Teams.AI
/// <summary>
/// Authentication utilities
/// </summary>
public class AuthUtilities
internal class AuthUtilities
{
/// <summary>
/// Set token in state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@ public async Task<bool> IsValidActivity(ITurnContext context, string? handlerNam
return await auth.IsValidActivity(context);
}

private IAuthentication<TState> Get(string name)
/// <summary>
/// Get an authentication class via name
/// </summary>
/// <param name="name">The name of authentication class</param>
/// <returns>The authentication class</returns>
/// <exception cref="TeamsAIException">When cannot find the class with given name</exception>
public IAuthentication<TState> Get(string name)
{
if (_authentications.ContainsKey(name))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Microsoft.Teams.AI
/// Options for authentication.
/// </summary>
public class AuthenticationOptions<TState>
where TState : TurnState
where TState : TurnState, new()
{
/// <summary>
/// The authentication classes to sign-in and sign-out users.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// Base class for bot authentication that handles common logic
Expand Down Expand Up @@ -107,7 +107,7 @@ public async Task<SignInResponse> AuthenticateAsync(ITurnContext context, TState
/// </summary>
/// <param name="context">The turn context</param>
/// <returns>True if valid. Otherwise, false.</returns>
public bool IsValidActivity(ITurnContext context)
public virtual bool IsValidActivity(ITurnContext context)
{
return context.Activity.Type == ActivityTypes.Message
&& !string.IsNullOrEmpty(context.Activity.Text);
Expand Down Expand Up @@ -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
/// </summary>
/// <param name="handler">The handler function to call when the user has successfully signed in</param>
/// <returns>The class itself for chaining purpose</returns>
public BotAuthenticationBase<TState> OnUserSignInSuccess(Func<ITurnContext, TState, Task> handler)
public void OnUserSignInSuccess(Func<ITurnContext, TState, Task> handler)
{
_userSignInSuccessHandler = handler;
return this;
}

/// <summary>
/// The handler function is called when the user sign in flow fails
/// </summary>
/// <param name="handler">The handler function to call when the user failed to signed in</param>
/// <returns>The class itself for chaining purpose</returns>
public BotAuthenticationBase<TState> OnUserSignInFailure(Func<ITurnContext, TState, TeamsAIAuthException, Task> handler)
public void OnUserSignInFailure(Func<ITurnContext, TState, TeamsAIAuthException, Task> handler)
{
_userSignInFailureHandler = handler;
return this;
}

/// <summary>
Expand All @@ -206,7 +202,7 @@ public BotAuthenticationBase<TState> OnUserSignInFailure(Func<ITurnContext, TSta
/// <param name="context">The turn context</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>True if the activity should be handled by current authentication hanlder. Otherwise, false.</returns>
protected Task<bool> VerifyStateRouteSelector(ITurnContext context, CancellationToken cancellationToken)
protected virtual Task<bool> VerifyStateRouteSelector(ITurnContext context, CancellationToken cancellationToken)
{
return Task.FromResult(context.Activity.Type == ActivityTypes.Invoke
&& context.Activity.Name == SignInConstants.VerifyStateOperationName);
Expand All @@ -218,7 +214,7 @@ protected Task<bool> VerifyStateRouteSelector(ITurnContext context, Cancellation
/// <param name="context">The turn context</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>True if the activity should be handled by current authentication hanlder. Otherwise, false.</returns>
protected Task<bool> TokenExchangeRouteSelector(ITurnContext context, CancellationToken cancellationToken)
protected virtual Task<bool> TokenExchangeRouteSelector(ITurnContext context, CancellationToken cancellationToken)
{
return Task.FromResult(context.Activity.Type == ActivityTypes.Invoke
&& context.Activity.Name == SignInConstants.TokenExchangeOperationName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// Handles authentication for bot in Teams using OAuth Connection.
/// </summary>
public class OAuthBotAuthentication<TState> : BotAuthenticationBase<TState>
internal class OAuthBotAuthentication<TState> : BotAuthenticationBase<TState>
where TState : TurnState, new()
{
/// <summary>
Expand Down
Loading

0 comments on commit c21be62

Please sign in to comment.