From 36361db306f77f53cb0df40021ae62b028a78a2f Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 6 Jul 2026 12:17:58 -0700 Subject: [PATCH 1/2] Add BotRequestContext overloads to UserTokenClient Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Teams.Core/UserTokenClient.cs | 226 +++++++++++++++++- .../UserTokenClientTests.cs | 60 +++++ 2 files changed, 274 insertions(+), 12 deletions(-) create mode 100644 core/test/Microsoft.Teams.Core.UnitTests/UserTokenClientTests.cs diff --git a/core/src/Microsoft.Teams.Core/UserTokenClient.cs b/core/src/Microsoft.Teams.Core/UserTokenClient.cs index 80270fb7..215253ae 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,30 @@ 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, agenticIdentity: 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. + /// The optional agentic identity for user-delegated token acquisition. + /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) + => await GetTokenStatusAsync(userId, channelId, include, BotRequestContext.FromAgenticIdentity(agenticIdentity), 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 +78,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 +99,32 @@ 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, agenticIdentity: null, cancellationToken).ConfigureAwait(false); + + /// + /// Gets the user token for a particular connection. + /// + /// The user ID. + /// The connection name. + /// The channel ID. + /// The optional code. + /// The optional agentic identity for user-delegated token acquisition. + /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) + => await GetTokenAsync(userId, connectionName, channelId, code, BotRequestContext.FromAgenticIdentity(agenticIdentity), 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 +144,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 +159,34 @@ 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, agenticIdentity: 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. + /// The optional agentic identity for user-delegated token acquisition. + /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) + => GetSignInResourceAsync(userId, connectionName, channelId, finalRedirect, BotRequestContext.FromAgenticIdentity(agenticIdentity), 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 +200,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 +213,32 @@ 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, agenticIdentity: 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. + /// The optional agentic identity for user-delegated token acquisition. + /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) + => await GetSignInUrlAsync(state, codeChallenge, emulatorUrl, finalRedirect, BotRequestContext.FromAgenticIdentity(agenticIdentity), 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 +255,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 +269,32 @@ 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, agenticIdentity: 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. + /// The optional agentic identity for user-delegated token acquisition. + /// The cancellation token. + /// The sign-in resource result. + public virtual async Task GetSignInResourceAsync(string state, string? codeChallenge, Uri? emulatorUrl, Uri? finalRedirect, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) + => await GetSignInResourceAsync(state, codeChallenge, emulatorUrl, finalRedirect, BotRequestContext.FromAgenticIdentity(agenticIdentity), 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 +311,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 +324,30 @@ 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, agenticIdentity: null, cancellationToken).ConfigureAwait(false); + + /// + /// Exchanges a token for another token. + /// + /// The user ID. + /// The connection name. + /// The channel ID. + /// The token to exchange. + /// The optional agentic identity for user-delegated token acquisition. + /// The cancellation token. + public virtual async Task ExchangeTokenAsync(string userId, string connectionName, string channelId, string? exchangeToken, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) + => await ExchangeTokenAsync(userId, connectionName, channelId, exchangeToken, BotRequestContext.FromAgenticIdentity(agenticIdentity), 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 +367,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 +380,30 @@ 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, agenticIdentity: 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. + /// The optional agentic identity for user-delegated token acquisition. + /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) + => await SignOutUserAsync(userId, connectionName, channelId, BotRequestContext.FromAgenticIdentity(agenticIdentity), 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 +426,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 +440,32 @@ 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, agenticIdentity: null, cancellationToken).ConfigureAwait(false); + + /// + /// Gets AAD tokens for a user. + /// + /// The user ID. + /// The connection name. + /// The channel ID. + /// The resource URLs. + /// The optional agentic identity for user-delegated token acquisition. + /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) + => await GetAadTokensAsync(userId, connectionName, channelId, resourceUrls, BotRequestContext.FromAgenticIdentity(agenticIdentity), 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 +481,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..5bb2e0f4 --- /dev/null +++ b/core/test/Microsoft.Teams.Core.UnitTests/UserTokenClientTests.cs @@ -0,0 +1,60 @@ +// 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_WithAgenticIdentity_StampsRequestOption() + { + CapturingHandler handler = new(); + UserTokenClient client = CreateClient(handler); + AgenticIdentity identity = new() + { + AgenticAppId = "agentic-app", + AgenticUserId = "agentic-user", + AgenticAppBlueprintId = "agentic-blueprint", + }; + + await client.GetTokenAsync("user", "connection", "msteams", code: null, identity); + + Assert.NotNull(handler.Request); + Assert.True(handler.Request.Options.TryGetValue(new HttpRequestOptionsKey(BotRequestContext.AgenticIdentityKey), out object? value)); + Assert.Same(identity, value); + } + + private static UserTokenClient CreateClient(HttpMessageHandler handler) + { + IConfiguration configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["UserTokenApiEndpoint"] = "https://token.test" + }) + .Build(); + + return new UserTokenClient(new HttpClient(handler), configuration, NullLogger.Instance); + } + + private sealed class CapturingHandler : HttpMessageHandler + { + public HttpRequestMessage? Request { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Request = request; + HttpResponseMessage response = new(HttpStatusCode.OK) + { + Content = new StringContent("""{"connectionName":"connection","token":"token"}""", Encoding.UTF8, "application/json") + }; + return Task.FromResult(response); + } + } +} From 3fb6d3b045bfba8cd754915d37529061ccbdf31d Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Mon, 6 Jul 2026 12:26:32 -0700 Subject: [PATCH 2/2] Drop AgenticIdentity overloads; keep only BotRequestContext Removes the AgenticIdentity? convenience overloads from UserTokenClient so each operation exposes just the original overload plus a BotRequestContext? overload. Callers holding an AgenticIdentity convert via the public BotRequestContext.FromAgenticIdentity(...). Test updated to exercise the BotRequestContext overload directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Teams.Core/UserTokenClient.cs | 118 ++---------------- .../UserTokenClientTests.cs | 15 ++- 2 files changed, 19 insertions(+), 114 deletions(-) diff --git a/core/src/Microsoft.Teams.Core/UserTokenClient.cs b/core/src/Microsoft.Teams.Core/UserTokenClient.cs index 215253ae..8bcbaef4 100644 --- a/core/src/Microsoft.Teams.Core/UserTokenClient.cs +++ b/core/src/Microsoft.Teams.Core/UserTokenClient.cs @@ -37,19 +37,7 @@ 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, agenticIdentity: 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. - /// The optional agentic identity for user-delegated token acquisition. - /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) - => await GetTokenStatusAsync(userId, channelId, include, BotRequestContext.FromAgenticIdentity(agenticIdentity), cancellationToken).ConfigureAwait(false); + => await GetTokenStatusAsync(userId, channelId, include, requestContext: null, cancellationToken).ConfigureAwait(false); /// /// Gets the token status for each connection for the given user. @@ -99,20 +87,7 @@ 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, agenticIdentity: null, cancellationToken).ConfigureAwait(false); - - /// - /// Gets the user token for a particular connection. - /// - /// The user ID. - /// The connection name. - /// The channel ID. - /// The optional code. - /// The optional agentic identity for user-delegated token acquisition. - /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) - => await GetTokenAsync(userId, connectionName, channelId, code, BotRequestContext.FromAgenticIdentity(agenticIdentity), cancellationToken).ConfigureAwait(false); + => await GetTokenAsync(userId, connectionName, channelId, code, requestContext: null, cancellationToken).ConfigureAwait(false); /// /// Gets the user token for a particular connection. @@ -159,21 +134,7 @@ 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, agenticIdentity: 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. - /// The optional agentic identity for user-delegated token acquisition. - /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) - => GetSignInResourceAsync(userId, connectionName, channelId, finalRedirect, BotRequestContext.FromAgenticIdentity(agenticIdentity), cancellationToken); + => 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. @@ -213,20 +174,7 @@ 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, agenticIdentity: 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. - /// The optional agentic identity for user-delegated token acquisition. - /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) - => await GetSignInUrlAsync(state, codeChallenge, emulatorUrl, finalRedirect, BotRequestContext.FromAgenticIdentity(agenticIdentity), cancellationToken).ConfigureAwait(false); + => await GetSignInUrlAsync(state, codeChallenge, emulatorUrl, finalRedirect, requestContext: null, cancellationToken).ConfigureAwait(false); /// /// Gets the sign-in URL for the given state. @@ -269,20 +217,7 @@ 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, agenticIdentity: 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. - /// The optional agentic identity for user-delegated token acquisition. - /// The cancellation token. - /// The sign-in resource result. - public virtual async Task GetSignInResourceAsync(string state, string? codeChallenge, Uri? emulatorUrl, Uri? finalRedirect, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) - => await GetSignInResourceAsync(state, codeChallenge, emulatorUrl, finalRedirect, BotRequestContext.FromAgenticIdentity(agenticIdentity), cancellationToken).ConfigureAwait(false); + => await GetSignInResourceAsync(state, codeChallenge, emulatorUrl, finalRedirect, requestContext: null, cancellationToken).ConfigureAwait(false); /// /// Gets the sign-in resource for the given state. @@ -324,19 +259,7 @@ 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, agenticIdentity: null, cancellationToken).ConfigureAwait(false); - - /// - /// Exchanges a token for another token. - /// - /// The user ID. - /// The connection name. - /// The channel ID. - /// The token to exchange. - /// The optional agentic identity for user-delegated token acquisition. - /// The cancellation token. - public virtual async Task ExchangeTokenAsync(string userId, string connectionName, string channelId, string? exchangeToken, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) - => await ExchangeTokenAsync(userId, connectionName, channelId, exchangeToken, BotRequestContext.FromAgenticIdentity(agenticIdentity), cancellationToken).ConfigureAwait(false); + => await ExchangeTokenAsync(userId, connectionName, channelId, exchangeToken, requestContext: null, cancellationToken).ConfigureAwait(false); /// /// Exchanges a token for another token. @@ -380,19 +303,7 @@ 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, agenticIdentity: 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. - /// The optional agentic identity for user-delegated token acquisition. - /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) - => await SignOutUserAsync(userId, connectionName, channelId, BotRequestContext.FromAgenticIdentity(agenticIdentity), cancellationToken).ConfigureAwait(false); + => await SignOutUserAsync(userId, connectionName, channelId, requestContext: null, cancellationToken).ConfigureAwait(false); /// /// Signs the user out of a connection, revoking their OAuth token. @@ -440,20 +351,7 @@ 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, agenticIdentity: null, cancellationToken).ConfigureAwait(false); - - /// - /// Gets AAD tokens for a user. - /// - /// The user ID. - /// The connection name. - /// The channel ID. - /// The resource URLs. - /// The optional agentic identity for user-delegated token acquisition. - /// 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, AgenticIdentity? agenticIdentity, CancellationToken cancellationToken = default) - => await GetAadTokensAsync(userId, connectionName, channelId, resourceUrls, BotRequestContext.FromAgenticIdentity(agenticIdentity), cancellationToken).ConfigureAwait(false); + => await GetAadTokensAsync(userId, connectionName, channelId, resourceUrls, requestContext: null, cancellationToken).ConfigureAwait(false); /// /// Gets AAD tokens for a user. diff --git a/core/test/Microsoft.Teams.Core.UnitTests/UserTokenClientTests.cs b/core/test/Microsoft.Teams.Core.UnitTests/UserTokenClientTests.cs index 5bb2e0f4..92899b1d 100644 --- a/core/test/Microsoft.Teams.Core.UnitTests/UserTokenClientTests.cs +++ b/core/test/Microsoft.Teams.Core.UnitTests/UserTokenClientTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.Teams.Core.UnitTests; public class UserTokenClientTests { [Fact] - public async Task GetTokenAsync_WithAgenticIdentity_StampsRequestOption() + public async Task GetTokenAsync_WithRequestContext_StampsRequestOptions() { CapturingHandler handler = new(); UserTokenClient client = CreateClient(handler); @@ -23,12 +23,19 @@ public async Task GetTokenAsync_WithAgenticIdentity_StampsRequestOption() AgenticUserId = "agentic-user", AgenticAppBlueprintId = "agentic-blueprint", }; + BotRequestContext requestContext = new() + { + AgenticIdentity = identity, + BotAppId = "bot-app", + }; - await client.GetTokenAsync("user", "connection", "msteams", code: null, identity); + await client.GetTokenAsync("user", "connection", "msteams", code: null, requestContext); Assert.NotNull(handler.Request); - Assert.True(handler.Request.Options.TryGetValue(new HttpRequestOptionsKey(BotRequestContext.AgenticIdentityKey), out object? value)); - Assert.Same(identity, value); + Assert.True(handler.Request.Options.TryGetValue(new HttpRequestOptionsKey(BotRequestContext.AgenticIdentityKey), out object? identityValue)); + Assert.Same(identity, identityValue); + Assert.True(handler.Request.Options.TryGetValue(new HttpRequestOptionsKey(BotRequestContext.BotAppIdKey), out object? botAppIdValue)); + Assert.Equal("bot-app", botAppIdValue); } private static UserTokenClient CreateClient(HttpMessageHandler handler)