Skip to content
Draft
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
Expand Up @@ -778,6 +778,11 @@ public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancel
AssertionHelpers.ThrowIfNull(turnContext, nameof(turnContext));
AssertionHelpers.ThrowIfNull(turnContext.Activity, nameof(turnContext.Activity));

if (_userAuth != null)
{
turnContext.Services.Set<UserAuthorization>(_userAuth);
}

try
{
// Start typing timer if configured
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Agents.Builder.App.UserAuth;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Agents.Builder.App
{
/// <summary>
/// AgentApplication ITurnContext extensions
/// </summary>
public static class TurnContextExtensions
{
public static IDictionary<string, string> GetTurnTokens(this ITurnContext turnContext)
{
var userAuth = turnContext.Services.Get<UserAuthorization>();
if (userAuth == null)
{
return new Dictionary<string, string>();
}
return userAuth.GetTurnTokens();
}

public static Task<string> GetTurnTokenAsync(this ITurnContext turnContext, string handlerName = null, CancellationToken cancellationToken = default)
{
var userAuth = turnContext.Services.Get<UserAuthorization>();
if (userAuth == null)
{
return null;
}

return userAuth.GetTurnTokenAsync(turnContext, handlerName, cancellationToken);
}

public static Task<string> ExchangeTurnTokenAsync(this ITurnContext turnContext, string handlerName = default, string exchangeConnection = default, IList<string> exchangeScopes = default, CancellationToken cancellationToken = default)
{
var userAuth = turnContext.Services.Get<UserAuthorization>();
if (userAuth == null)
{
return null;
}

return userAuth.ExchangeTurnTokenAsync(turnContext, handlerName, exchangeConnection, exchangeScopes, cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.Agents.Builder.Errors;
using System.Collections.Generic;
using Microsoft.Agents.Core.Errors;
using System.Linq;

namespace Microsoft.Agents.Builder.App.UserAuth
{
Expand All @@ -35,7 +36,7 @@ public class UserAuthorization
private readonly IUserAuthorizationDispatcher _dispatcher;
private readonly UserAuthorizationOptions _options;
private readonly AgentApplication _app;
private readonly Dictionary<string, TokenResponse> _authTokens = [];
private readonly List<HandlerToken> _authTokens = [];

/// <summary>
/// Callback when user sign in fail
Expand Down Expand Up @@ -76,7 +77,7 @@ public UserAuthorization(AgentApplication app, UserAuthorizationOptions options)
[Obsolete("Use Task<string> GetTurnTokenAsync(ITurnContext, string) instead")]
public string GetTurnToken(string handlerName)
{
return _authTokens.TryGetValue(handlerName, out var token) ? token.Token : default;
return _authTokens.Find(ht => ht.Handler.Equals(handlerName))?.TokenResponse.Token;
}

/// <summary>
Expand All @@ -100,30 +101,41 @@ public async Task<string> GetTurnTokenAsync(ITurnContext turnContext, string han

public async Task<string> ExchangeTurnTokenAsync(ITurnContext turnContext, string handlerName = default, string exchangeConnection = default, IList<string> exchangeScopes = default, CancellationToken cancellationToken = default)
{
handlerName ??= DefaultHandlerName;
if (_authTokens == null || _authTokens.Count == 0)
{
return null;
}

TokenResponse token;
if (string.IsNullOrEmpty(handlerName))
{
// Cached turn tokens are stored in the order of addition (the order on the route).
// If no handler name is provided, return the first.
token = _authTokens[0].TokenResponse;
}
else
{
token = _authTokens.Find(ht => ht.Handler.Equals(handlerName))?.TokenResponse;
}

if (_authTokens.TryGetValue(handlerName, out var token))
if (token != null)
{
// An exchangeable token needs to be exchanged.
if (!turnContext.IsAgenticRequest())
// Return a non-expired non-exchangeable token.
if (!turnContext.IsAgenticRequest() && !token.IsExchangeable)
{
if (!token.IsExchangeable)
var diff = token.Expiration - DateTimeOffset.UtcNow;
if (diff.HasValue && diff?.TotalMinutes >= 5)
{
var diff = token.Expiration - DateTimeOffset.UtcNow;
if (diff.HasValue && diff?.TotalMinutes >= 5)
{
return token.Token;
}
return token.Token;
}
}


// Get a new token if near expiration, or it's an exchangeable token.
// Refresh an exchangeable or expired token
var handler = _dispatcher.Get(handlerName);
var response = await handler.GetRefreshedUserTokenAsync(turnContext, exchangeConnection, exchangeScopes, cancellationToken).ConfigureAwait(false);
if (response?.Token != null)
{
_authTokens[handlerName] = response;
CacheToken(handlerName, response);
return response.Token;
}

Expand All @@ -135,6 +147,11 @@ public async Task<string> ExchangeTurnTokenAsync(ITurnContext turnContext, strin
return null;
}

public IDictionary<string,string> GetTurnTokens()
{
return (IDictionary<string, string>)_authTokens.Select(ht => new KeyValuePair<string, string>(ht.Handler, ht.TokenResponse.Token));
}

public async Task SignOutUserAsync(ITurnContext turnContext, ITurnState turnState, string? flowName = null, CancellationToken cancellationToken = default)
{
var flow = flowName ?? DefaultHandlerName;
Expand Down Expand Up @@ -266,23 +283,25 @@ await _app.Options.Adapter.ProcessProactiveAsync(
return true;
}

/// <summary>
/// Set token in state
/// </summary>
/// <param name="name">The name of token</param>
/// <param name="response">The value of token</param>
private void CacheToken(string name, SignInResponse response)
private void CacheToken(string name, SignInResponse signInResponse)
{
_authTokens[name] = response.TokenResponse;
CacheToken(name, signInResponse.TokenResponse);
}

private void CacheToken(string name, TokenResponse tokenResponse)
{
var existing = _authTokens.Find(ht => ht.Handler.Equals(name));
if (existing != null)
{
existing.TokenResponse = tokenResponse;
return;
}
_authTokens.Add(new HandlerToken() { Handler = name, TokenResponse = tokenResponse });
}

/// <summary>
/// Delete token from turn state
/// </summary>
/// <param name="name">The name of token</param>
private void DeleteCachedToken(string name)
{
_authTokens.Remove(name);
_authTokens.RemoveAll(ht => ht.Handler.Equals(name));
}

private static SignInState GetSignInState(ITurnState turnState)
Expand All @@ -304,4 +323,10 @@ class SignInState
public string RuntimeOBOConnectionName { get; set; }
public IList<string> RuntimeOBOScopes { get; set; }
}

class HandlerToken
{
public string Handler { get; set; }
public TokenResponse TokenResponse { get; set; }
}
}
31 changes: 8 additions & 23 deletions src/samples/Authorization/AutoSignIn/AuthAgent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ public AuthAgent(AgentApplicationOptions options) : base(options)
// For this example we will register a welcome message for the user when they join the conversation, then configure sign-in and sign-out commands.
// Additionally, we will add events to handle notifications of sign-in success and failure, these notifications will report the local log instead of back to the calling agent.

// This constructor should only register events and setup state as it will be called for each request.

// When a conversation update event is triggered.
OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeMessageAsync);

Expand All @@ -60,7 +58,7 @@ public AuthAgent(AgentApplicationOptions options) : base(options)
// The UserAuthorization Class provides methods and properties to manage and access user authorization tokens
// You can use this class to interact with the UserAuthorization process, including signing in and signing out users, accessing tokens, and handling authorization events.

// Register Events for SignIn Failure on the UserAuthorization class to notify the Agent in the event of an OAuth failure.
// Register handler on the UserAuthorization class to notify the Agent in the event of an OAuth failure.
// For a production Agent, this would typically be used to provide instructions to the end-user. For example, call/email or
// handoff to a live person (depending on Agent capabilities).
UserAuthorization.OnUserSignInFailure(OnUserSignInFailure);
Expand Down Expand Up @@ -108,7 +106,7 @@ private async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turn
/// <param name="cancellationToken"><see cref="CancellationToken"/></param>
private async Task OnMe(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
{
// If successful, the user will be the token will be available from the UserAuthorization.GetTurnTokenAsync(turnContext, DefaultHandlerName) call.
// If successful, the user will be the token will be available via UserAuthorization.GetTurnTokenAsync(turnContext, "me").
// If not successful, this handler won't be reached. Instead, OnUserSignInFailure handler would have been called.

// For this sample, two OAuth Connections are setup to demonstrate multiple OAuth Connections and Auto SignIn handling and routing.
Expand All @@ -118,20 +116,6 @@ private async Task OnMe(ITurnContext turnContext, ITurnState turnState, Cancella
var displayName = await GetDisplayName(turnContext);
var graphInfo = await GetGraphInfo(turnContext, "me");

// Just to verify "auto" handler setup. This wouldn't be needed in a production Agent and here just to verify sample setup.
if (displayName.Equals(_defaultDisplayName) || graphInfo == null)
{
await turnContext.SendActivityAsync($"Failed to get information from handlers '{UserAuthorization.DefaultHandlerName}' and/or 'me'. \nDid you update the scope correctly in Azure bot Service?. If so type in -signout to force signout the current user", cancellationToken: cancellationToken);
return;
}

// Just to verify we in fact have two different tokens. This wouldn't be needed in a production Agent and here just to verify sample setup.
if (await UserAuthorization.GetTurnTokenAsync(turnContext, UserAuthorization.DefaultHandlerName, cancellationToken: cancellationToken) == await UserAuthorization.GetTurnTokenAsync(turnContext, "me", cancellationToken))
{
await turnContext.SendActivityAsync($"It would seem '{UserAuthorization.DefaultHandlerName}' and 'me' are using the same OAuth Connection", cancellationToken: cancellationToken);
return;
}

var meInfo = $"Name: {displayName}{Environment.NewLine}Job Title: {graphInfo["jobTitle"]?.GetValue<string>()}{Environment.NewLine}Email: {graphInfo["mail"]?.GetValue<string>()}";
await turnContext.SendActivityAsync(meInfo, cancellationToken: cancellationToken);
}
Expand All @@ -148,9 +132,9 @@ private async Task OnMessageAsync(ITurnContext turnContext, ITurnState turnState
// When Auto Sign in is properly configured, the user will be automatically signed in when they first connect to the agent using the default
// handler chosen in the UserAuthorization configuration.
// IMPORTANT: The ReadMe associated with this sample, instructs you on configuring the Azure Bot Service Registration with the scopes to allow
// you to read your own information from Graph. you must have completed that for this sample to work correctly.
// you to read your own information from Graph. You must have completed that for this sample to work correctly.

// If successful, the user will be the token will be available from the UserAuthorization.GetTurnTokenAsync(turnContext, DefaultHandlerName) call.
// If successful, the user will be the token will be available from the ITurnContext.GetTurnTokenAsync(turnContext) call.
// If not successful, this handler won't be reached. Instead, OnUserSignInFailure handler would have been called.

// We have the access token, now try to get your user name from graph.
Expand Down Expand Up @@ -188,17 +172,18 @@ private async Task OnUserSignInFailure(ITurnContext turnContext, ITurnState turn
private async Task<string> GetDisplayName(ITurnContext turnContext)
{
string displayName = _defaultDisplayName;
var graphInfo = await GetGraphInfo(turnContext, UserAuthorization.DefaultHandlerName);
var graphInfo = await GetGraphInfo(turnContext);
if (graphInfo != null)
{
displayName = graphInfo!["displayName"]!.GetValue<string>();
}
return displayName;
}

private async Task<JsonNode> GetGraphInfo(ITurnContext turnContext, string handleName)
private async Task<JsonNode> GetGraphInfo(ITurnContext turnContext, string? handlerName = null)
{
string accessToken = await UserAuthorization.GetTurnTokenAsync(turnContext, handleName);
// In this sample, a null handlerName will always return the "auto" token.
string accessToken = await turnContext.GetTurnTokenAsync(handlerName!);
string graphApiUrl = $"https://graph.microsoft.com/v1.0/me";
try
{
Expand Down
14 changes: 6 additions & 8 deletions src/samples/Authorization/OBOAuthorization/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

var app = new AgentApplication(sp.GetRequiredService<AgentApplicationOptions>());

CopilotClient GetClient(AgentApplication app, ITurnContext turnContext)
CopilotClient GetClient(ITurnContext turnContext)
{
var settings = new ConnectionSettings(builder.Configuration.GetSection("CopilotStudioAgent"));
string[] scopes = [CopilotClient.ScopeFromSettings(settings)];
Expand All @@ -50,7 +50,7 @@ CopilotClient GetClient(AgentApplication app, ITurnContext turnContext)
// In this sample, the Azure Bot OAuth Connection is configured to return an
// exchangeable token, that can be exchange for different scopes. This can be
// done multiple times using different scopes.
return await app.UserAuthorization.ExchangeTurnTokenAsync(turnContext, "mcs", exchangeScopes: scopes);
return await turnContext.ExchangeTurnTokenAsync("mcs", exchangeScopes: scopes);
},
NullLogger.Instance,
"mcs");
Expand All @@ -60,20 +60,18 @@ CopilotClient GetClient(AgentApplication app, ITurnContext turnContext)
{
// Force a user signout to reset the user state
// This is needed to reset the token in Azure Bot Services if needed.
// Typically this wouldn't be need in a production Agent. Made available to assist it starting from scratch.
// Typically this wouldn't be need in a production Agent. Made available to assist it starting from scratch for this sample.
await app.UserAuthorization.SignOutUserAsync(turnContext, turnState, cancellationToken: cancellationToken);
await turnContext.SendActivityAsync("You have signed out", cancellationToken: cancellationToken);
}, rank: RouteRank.First);

// Since Auto SignIn is enabled, by the time this is called the token is already available via UserAuthorization.GetTurnTokenAsync or
// UserAuthorization.ExchangeTurnTokenAsync.
// NOTE: This is a slightly unusual way to handle incoming Activities (but perfectly) valid. For this sample,
// we just want to proxy messages to/from a Copilot Studio Agent.
// By the time this is called the token is already available via ITurnContext.GetTurnTokenAsync or
// ITurnContext.ExchangeTurnTokenAsync.
app.OnActivity((turnContext, cancellationToken) => Task.FromResult(true), async (turnContext, turnState, cancellationToken) =>
{

var mcsConversationId = turnState.Conversation.GetValue<string>(MCSConversationPropertyName);
var cpsClient = GetClient(app, turnContext);
var cpsClient = GetClient(turnContext);

if (string.IsNullOrEmpty(mcsConversationId))
{
Expand Down
Loading
Loading