diff --git a/core/src/Microsoft.Teams.Core/UserTokenClient.cs b/core/src/Microsoft.Teams.Core/UserTokenClient.cs
index 80270fb7..8bcbaef4 100644
--- a/core/src/Microsoft.Teams.Core/UserTokenClient.cs
+++ b/core/src/Microsoft.Teams.Core/UserTokenClient.cs
@@ -28,8 +28,6 @@ public class UserTokenClient(HttpClient httpClient, IConfiguration configuration
private readonly string _apiEndpoint = configuration["UserTokenApiEndpoint"] ?? "https://token.botframework.com";
private readonly JsonSerializerOptions _defaultOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
- internal AgenticIdentity? AgenticIdentity { get; set; }
-
///
/// Gets the token status for each connection for the given user.
///
@@ -39,6 +37,18 @@ public class UserTokenClient(HttpClient httpClient, IConfiguration configuration
/// The cancellation token.
/// A task that represents the asynchronous operation. The result contains an array of token status results for each connection.
public virtual async Task GetTokenStatusAsync(string userId, string channelId, string? include = null, CancellationToken cancellationToken = default)
+ => await GetTokenStatusAsync(userId, channelId, include, requestContext: null, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Gets the token status for each connection for the given user.
+ ///
+ /// The user ID.
+ /// The channel ID.
+ /// The optional include parameter.
+ /// Optional per-request properties used for authentication.
+ /// The cancellation token.
+ /// A task that represents the asynchronous operation. The result contains an array of token status results for each connection.
+ public virtual async Task GetTokenStatusAsync(string userId, string channelId, string? include, BotRequestContext? requestContext, CancellationToken cancellationToken = default)
{
Dictionary queryParams = new()
{
@@ -56,7 +66,7 @@ public virtual async Task GetTokenStatusAsync(string use
"api/usertoken/GetTokenStatus",
queryParams,
body: null,
- CreateRequestOptions("getting token status"),
+ CreateRequestOptions("getting token status", requestContext: requestContext),
cancellationToken).ConfigureAwait(false);
if (result == null || result.Count == 0)
@@ -77,6 +87,19 @@ public virtual async Task GetTokenStatusAsync(string use
/// The cancellation token.
/// A task that represents the asynchronous operation. The result contains the token, or null if no token is available.
public virtual async Task GetTokenAsync(string userId, string connectionName, string channelId, string? code = null, CancellationToken cancellationToken = default)
+ => await GetTokenAsync(userId, connectionName, channelId, code, requestContext: null, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Gets the user token for a particular connection.
+ ///
+ /// The user ID.
+ /// The connection name.
+ /// The channel ID.
+ /// The optional code.
+ /// Optional per-request properties used for authentication.
+ /// The cancellation token.
+ /// A task that represents the asynchronous operation. The result contains the token, or null if no token is available.
+ public virtual async Task GetTokenAsync(string userId, string connectionName, string channelId, string? code, BotRequestContext? requestContext, CancellationToken cancellationToken = default)
{
Dictionary queryParams = new()
{
@@ -96,7 +119,7 @@ public virtual async Task GetTokenStatusAsync(string use
"api/usertoken/GetToken",
queryParams,
body: null,
- CreateRequestOptions("getting token", returnNullOnNotFound: true),
+ CreateRequestOptions("getting token", returnNullOnNotFound: true, requestContext: requestContext),
cancellationToken).ConfigureAwait(false);
}
@@ -111,6 +134,20 @@ public virtual async Task GetTokenStatusAsync(string use
/// The cancellation token.
/// A task that represents the asynchronous operation. The result contains the sign-in resource with the sign-in link and token exchange information.
public virtual Task GetSignInResourceAsync(string userId, string connectionName, string channelId, string? finalRedirect = null, CancellationToken cancellationToken = default)
+ => GetSignInResourceAsync(userId, connectionName, channelId, finalRedirect, requestContext: null, cancellationToken);
+
+ ///
+ /// Get the token or raw signin link to be sent to the user for signin for a connection.
+ /// Builds the state parameter internally from the userId and connectionName.
+ ///
+ /// The user ID.
+ /// The connection name.
+ /// The channel ID.
+ /// The optional final redirect URL.
+ /// Optional per-request properties used for authentication.
+ /// The cancellation token.
+ /// A task that represents the asynchronous operation. The result contains the sign-in resource with the sign-in link and token exchange information.
+ public virtual Task GetSignInResourceAsync(string userId, string connectionName, string channelId, string? finalRedirect, BotRequestContext? requestContext, CancellationToken cancellationToken = default)
{
var tokenExchangeState = new
{
@@ -124,7 +161,7 @@ public virtual Task GetSignInResourceAsync(string userI
string state = Convert.ToBase64String(Encoding.UTF8.GetBytes(tokenExchangeStateJson));
Uri? finalRedirectUri = finalRedirect is not null ? new Uri(finalRedirect) : null;
- return GetSignInResourceAsync(state, finalRedirect: finalRedirectUri, cancellationToken: cancellationToken);
+ return GetSignInResourceAsync(state, codeChallenge: null, emulatorUrl: null, finalRedirect: finalRedirectUri, requestContext: requestContext, cancellationToken: cancellationToken);
}
///
@@ -137,6 +174,19 @@ public virtual Task GetSignInResourceAsync(string userI
/// The cancellation token.
/// The sign-in URL, or null if not available.
public virtual async Task GetSignInUrlAsync(string state, string? codeChallenge = null, Uri? emulatorUrl = null, Uri? finalRedirect = null, CancellationToken cancellationToken = default)
+ => await GetSignInUrlAsync(state, codeChallenge, emulatorUrl, finalRedirect, requestContext: null, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Gets the sign-in URL for the given state.
+ ///
+ /// The encoded state parameter.
+ /// The optional code challenge for PKCE.
+ /// The optional emulator URL.
+ /// The optional final redirect URL.
+ /// Optional per-request properties used for authentication.
+ /// The cancellation token.
+ /// The sign-in URL, or null if not available.
+ public virtual async Task GetSignInUrlAsync(string state, string? codeChallenge, Uri? emulatorUrl, Uri? finalRedirect, BotRequestContext? requestContext, CancellationToken cancellationToken = default)
{
Dictionary queryParams = new() { { "state", state } };
@@ -153,7 +203,7 @@ public virtual Task GetSignInResourceAsync(string userI
"api/botsignin/GetSignInUrl",
queryParams,
body: null,
- CreateRequestOptions("getting sign-in URL"),
+ CreateRequestOptions("getting sign-in URL", requestContext: requestContext),
cancellationToken).ConfigureAwait(false);
}
@@ -167,6 +217,19 @@ public virtual Task GetSignInResourceAsync(string userI
/// The cancellation token.
/// The sign-in resource result.
public virtual async Task GetSignInResourceAsync(string state, string? codeChallenge = null, Uri? emulatorUrl = null, Uri? finalRedirect = null, CancellationToken cancellationToken = default)
+ => await GetSignInResourceAsync(state, codeChallenge, emulatorUrl, finalRedirect, requestContext: null, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Gets the sign-in resource for the given state.
+ ///
+ /// The encoded state parameter.
+ /// The optional code challenge for PKCE.
+ /// The optional emulator URL.
+ /// The optional final redirect URL.
+ /// Optional per-request properties used for authentication.
+ /// The cancellation token.
+ /// The sign-in resource result.
+ public virtual async Task GetSignInResourceAsync(string state, string? codeChallenge, Uri? emulatorUrl, Uri? finalRedirect, BotRequestContext? requestContext, CancellationToken cancellationToken = default)
{
Dictionary queryParams = new() { { "state", state } };
@@ -183,7 +246,7 @@ public virtual async Task GetSignInResourceAsync(string
"api/botsignin/GetSignInResource",
queryParams,
body: null,
- CreateRequestOptions("getting sign-in resource"),
+ CreateRequestOptions("getting sign-in resource", requestContext: requestContext),
cancellationToken).ConfigureAwait(false))!;
}
@@ -196,6 +259,18 @@ public virtual async Task GetSignInResourceAsync(string
/// The token to exchange.
/// The cancellation token.
public virtual async Task ExchangeTokenAsync(string userId, string connectionName, string channelId, string? exchangeToken, CancellationToken cancellationToken = default)
+ => await ExchangeTokenAsync(userId, connectionName, channelId, exchangeToken, requestContext: null, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Exchanges a token for another token.
+ ///
+ /// The user ID.
+ /// The connection name.
+ /// The channel ID.
+ /// The token to exchange.
+ /// Optional per-request properties used for authentication.
+ /// The cancellation token.
+ public virtual async Task ExchangeTokenAsync(string userId, string connectionName, string channelId, string? exchangeToken, BotRequestContext? requestContext, CancellationToken cancellationToken = default)
{
Dictionary queryParams = new()
{
@@ -215,7 +290,7 @@ public virtual async Task ExchangeTokenAsync(string userId, stri
"api/usertoken/exchange",
queryParams,
JsonSerializer.Serialize(tokenExchangeRequest),
- CreateRequestOptions("exchanging token"),
+ CreateRequestOptions("exchanging token", requestContext: requestContext),
cancellationToken).ConfigureAwait(false))!;
}
@@ -228,6 +303,18 @@ public virtual async Task ExchangeTokenAsync(string userId, stri
/// A cancellation token that can be used to cancel the asynchronous operation.
/// A task that represents the asynchronous sign-out operation.
public virtual async Task SignOutUserAsync(string userId, string? connectionName = null, string? channelId = null, CancellationToken cancellationToken = default)
+ => await SignOutUserAsync(userId, connectionName, channelId, requestContext: null, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Signs the user out of a connection, revoking their OAuth token.
+ ///
+ /// The unique identifier of the user to sign out. Cannot be null or empty.
+ /// Optional name of the OAuth connection to sign out from. If null, signs out from all connections.
+ /// Optional channel identifier. If provided, limits sign-out to tokens for this channel.
+ /// Optional per-request properties used for authentication.
+ /// A cancellation token that can be used to cancel the asynchronous operation.
+ /// A task that represents the asynchronous sign-out operation.
+ public virtual async Task SignOutUserAsync(string userId, string? connectionName, string? channelId, BotRequestContext? requestContext, CancellationToken cancellationToken = default)
{
Dictionary queryParams = new()
{
@@ -250,7 +337,7 @@ await _botHttpClient.SendAsync(
"api/usertoken/SignOut",
queryParams,
body: null,
- CreateRequestOptions("signing out user"),
+ CreateRequestOptions("signing out user", requestContext: requestContext),
cancellationToken).ConfigureAwait(false);
}
@@ -264,6 +351,19 @@ await _botHttpClient.SendAsync(
/// The cancellation token.
/// A task that represents the asynchronous operation. The result contains a dictionary mapping resource URLs to their token results.
public virtual async Task> GetAadTokensAsync(string userId, string connectionName, string channelId, string[]? resourceUrls = null, CancellationToken cancellationToken = default)
+ => await GetAadTokensAsync(userId, connectionName, channelId, resourceUrls, requestContext: null, cancellationToken).ConfigureAwait(false);
+
+ ///
+ /// Gets AAD tokens for a user.
+ ///
+ /// The user ID.
+ /// The connection name.
+ /// The channel ID.
+ /// The resource URLs.
+ /// Optional per-request properties used for authentication.
+ /// The cancellation token.
+ /// A task that represents the asynchronous operation. The result contains a dictionary mapping resource URLs to their token results.
+ public virtual async Task> GetAadTokensAsync(string userId, string connectionName, string channelId, string[]? resourceUrls, BotRequestContext? requestContext, CancellationToken cancellationToken = default)
{
var body = new
{
@@ -279,14 +379,14 @@ public virtual async Task> GetAadTokensAsync
"api/usertoken/GetAadTokens",
queryParams: null,
JsonSerializer.Serialize(body),
- CreateRequestOptions("getting AAD tokens"),
+ CreateRequestOptions("getting AAD tokens", requestContext: requestContext),
cancellationToken).ConfigureAwait(false))!;
}
- private BotRequestOptions CreateRequestOptions(string operationDescription, bool returnNullOnNotFound = false) =>
+ private static BotRequestOptions CreateRequestOptions(string operationDescription, bool returnNullOnNotFound = false, BotRequestContext? requestContext = null) =>
new()
{
- RequestContext = BotRequestContext.FromAgenticIdentity(AgenticIdentity),
+ RequestContext = requestContext,
OperationDescription = operationDescription,
ReturnNullOnNotFound = returnNullOnNotFound
};
diff --git a/core/test/Microsoft.Teams.Core.UnitTests/UserTokenClientTests.cs b/core/test/Microsoft.Teams.Core.UnitTests/UserTokenClientTests.cs
new file mode 100644
index 00000000..92899b1d
--- /dev/null
+++ b/core/test/Microsoft.Teams.Core.UnitTests/UserTokenClientTests.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Net;
+using System.Text;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Teams.Core.Http;
+using Microsoft.Teams.Core.Schema;
+
+namespace Microsoft.Teams.Core.UnitTests;
+
+public class UserTokenClientTests
+{
+ [Fact]
+ public async Task GetTokenAsync_WithRequestContext_StampsRequestOptions()
+ {
+ CapturingHandler handler = new();
+ UserTokenClient client = CreateClient(handler);
+ AgenticIdentity identity = new()
+ {
+ AgenticAppId = "agentic-app",
+ AgenticUserId = "agentic-user",
+ AgenticAppBlueprintId = "agentic-blueprint",
+ };
+ BotRequestContext requestContext = new()
+ {
+ AgenticIdentity = identity,
+ BotAppId = "bot-app",
+ };
+
+ await client.GetTokenAsync("user", "connection", "msteams", code: null, requestContext);
+
+ Assert.NotNull(handler.Request);
+ Assert.True(handler.Request.Options.TryGetValue(new HttpRequestOptionsKey