From d86d2a7976f1d0ddef8de87065c77bd83190c040 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:10:03 +0000 Subject: [PATCH 1/5] Initial plan From 80d2e8633458321e93c5dc43e704e60843759982 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:17:19 +0000 Subject: [PATCH 2/5] Remove hardcoded sovereign cloud presets, use configuration-only approach Remove static preset instances (USGov, USGovDoD, China, Public) and FromName() from CloudEnvironment. Make constructor use default parameter values matching existing public cloud defaults. Remove Cloud string property from TeamsSettings. All sovereign cloud configuration now comes from appsettings.json endpoint properties or programmatic CloudEnvironment construction. Addresses review feedback from rido-min: don't hardcode Entra URLs we don't own in the codebase. Co-authored-by: rido-min <14916339+rido-min@users.noreply.github.com> --- .../Auth/ClientCredentials.cs | 2 +- .../Auth/CloudEnvironment.cs | 106 +++--------------- Libraries/Microsoft.Teams.Apps/App.cs | 2 +- .../TeamsSettings.cs | 24 ++-- .../Extensions/TeamsValidationSettings.cs | 2 +- .../Auth/CloudEnvironmentTests.cs | 105 +++++------------ 6 files changed, 56 insertions(+), 185 deletions(-) diff --git a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs index e198eb73..bb04a40b 100644 --- a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs +++ b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs @@ -10,7 +10,7 @@ public class ClientCredentials : IHttpCredentials public string ClientId { get; set; } public string ClientSecret { get; set; } public string? TenantId { get; set; } - public CloudEnvironment Cloud { get; set; } = CloudEnvironment.Public; + public CloudEnvironment Cloud { get; set; } = new(); public ClientCredentials(string clientId, string clientSecret) { diff --git a/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs b/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs index 7910727a..6cb83289 100644 --- a/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs +++ b/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs @@ -5,61 +5,60 @@ namespace Microsoft.Teams.Api.Auth; /// /// Bundles all cloud-specific service endpoints for a given Azure environment. -/// Use predefined instances (, , , ) -/// or construct a custom one. +/// All properties default to Microsoft public (commercial) cloud values. +/// Configure endpoints via appsettings.json or programmatically for sovereign clouds. /// public class CloudEnvironment { /// - /// The Azure AD login endpoint (e.g. "https://login.microsoftonline.com"). + /// The Azure AD login endpoint. /// public string LoginEndpoint { get; } /// - /// The default multi-tenant login tenant (e.g. "botframework.com"). + /// The default login tenant. /// public string LoginTenant { get; } /// - /// The Bot Framework OAuth scope (e.g. "https://api.botframework.com/.default"). + /// The Bot Framework OAuth scope. /// public string BotScope { get; } /// - /// The Bot Framework token service base URL (e.g. "https://token.botframework.com"). + /// The Bot Framework token service base URL. /// public string TokenServiceUrl { get; } /// - /// The OpenID metadata URL for token validation (e.g. "https://login.botframework.com/v1/.well-known/openidconfiguration"). + /// The OpenID metadata URL for token validation. /// public string OpenIdMetadataUrl { get; } /// - /// The token issuer for Bot Framework tokens (e.g. "https://api.botframework.com"). + /// The token issuer for Bot Framework tokens. /// public string TokenIssuer { get; } /// - /// The channel service URL. Empty for public cloud; set for sovereign clouds - /// (e.g. "https://botframework.azure.us"). + /// The channel service URL. Empty for public cloud. /// public string ChannelService { get; } /// - /// The OAuth redirect URL (e.g. "https://token.botframework.com/.auth/web/redirect"). + /// The OAuth redirect URL. /// public string OAuthRedirectUrl { get; } public CloudEnvironment( - string loginEndpoint, - string loginTenant, - string botScope, - string tokenServiceUrl, - string openIdMetadataUrl, - string tokenIssuer, - string channelService, - string oauthRedirectUrl) + string loginEndpoint = "https://login.microsoftonline.com", + string loginTenant = "botframework.com", + string botScope = "https://api.botframework.com/.default", + string tokenServiceUrl = "https://token.botframework.com", + string openIdMetadataUrl = "https://login.botframework.com/v1/.well-known/openidconfiguration", + string tokenIssuer = "https://api.botframework.com", + string channelService = "", + string oauthRedirectUrl = "https://token.botframework.com/.auth/web/redirect") { LoginEndpoint = loginEndpoint; LoginTenant = loginTenant; @@ -71,62 +70,6 @@ public CloudEnvironment( OAuthRedirectUrl = oauthRedirectUrl; } - /// - /// Microsoft public (commercial) cloud. - /// - public static readonly CloudEnvironment Public = new( - loginEndpoint: "https://login.microsoftonline.com", - loginTenant: "botframework.com", - botScope: "https://api.botframework.com/.default", - tokenServiceUrl: "https://token.botframework.com", - openIdMetadataUrl: "https://login.botframework.com/v1/.well-known/openidconfiguration", - tokenIssuer: "https://api.botframework.com", - channelService: "", - oauthRedirectUrl: "https://token.botframework.com/.auth/web/redirect" - ); - - /// - /// US Government Community Cloud High (GCCH). - /// - public static readonly CloudEnvironment USGov = new( - loginEndpoint: "https://login.microsoftonline.us", - loginTenant: "MicrosoftServices.onmicrosoft.us", - botScope: "https://api.botframework.us/.default", - tokenServiceUrl: "https://tokengcch.botframework.azure.us", - openIdMetadataUrl: "https://login.botframework.azure.us/v1/.well-known/openidconfiguration", - tokenIssuer: "https://api.botframework.us", - channelService: "https://botframework.azure.us", - oauthRedirectUrl: "https://tokengcch.botframework.azure.us/.auth/web/redirect" - ); - - /// - /// US Government Department of Defense (DoD). - /// - public static readonly CloudEnvironment USGovDoD = new( - loginEndpoint: "https://login.microsoftonline.us", - loginTenant: "MicrosoftServices.onmicrosoft.us", - botScope: "https://api.botframework.us/.default", - tokenServiceUrl: "https://apiDoD.botframework.azure.us", - openIdMetadataUrl: "https://login.botframework.azure.us/v1/.well-known/openidconfiguration", - tokenIssuer: "https://api.botframework.us", - channelService: "https://botframework.azure.us", - oauthRedirectUrl: "https://apiDoD.botframework.azure.us/.auth/web/redirect" - ); - - /// - /// China cloud (21Vianet). - /// - public static readonly CloudEnvironment China = new( - loginEndpoint: "https://login.partner.microsoftonline.cn", - loginTenant: "microsoftservices.partner.onmschina.cn", - botScope: "https://api.botframework.azure.cn/.default", - tokenServiceUrl: "https://token.botframework.azure.cn", - openIdMetadataUrl: "https://login.botframework.azure.cn/v1/.well-known/openidconfiguration", - tokenIssuer: "https://api.botframework.azure.cn", - channelService: "https://botframework.azure.cn", - oauthRedirectUrl: "https://token.botframework.azure.cn/.auth/web/redirect" - ); - /// /// Creates a new by applying non-null overrides on top of this instance. /// Returns the same instance if all overrides are null (no allocation). @@ -159,17 +102,4 @@ tokenServiceUrl is null && openIdMetadataUrl is null && tokenIssuer is null && oauthRedirectUrl ?? OAuthRedirectUrl ); } - - /// - /// Resolves a cloud environment name (case-insensitive) to its corresponding instance. - /// Valid names: "Public", "USGov", "USGovDoD", "China". - /// - public static CloudEnvironment FromName(string name) => name.ToLowerInvariant() switch - { - "public" => Public, - "usgov" => USGov, - "usgovdod" => USGovDoD, - "china" => China, - _ => throw new ArgumentException($"Unknown cloud environment: '{name}'. Valid values are: Public, USGov, USGovDoD, China.", nameof(name)) - }; } diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index ed2dd1f6..d61efb85 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -51,7 +51,7 @@ internal string UserAgent public App(AppOptions? options = null) { - var cloud = options?.Cloud ?? CloudEnvironment.Public; + var cloud = options?.Cloud ?? new CloudEnvironment(); Logger = options?.Logger ?? new ConsoleLogger(); Storage = options?.Storage ?? new LocalStorage(); diff --git a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs index 46efca80..f56a531e 100644 --- a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs +++ b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs @@ -10,30 +10,29 @@ public class TeamsSettings public string? ClientId { get; set; } public string? ClientSecret { get; set; } public string? TenantId { get; set; } - public string? Cloud { get; set; } - /// Override the Azure AD login endpoint. + /// The Azure AD login endpoint. public string? LoginEndpoint { get; set; } - /// Override the default login tenant. + /// The default login tenant. public string? LoginTenant { get; set; } - /// Override the Bot Framework OAuth scope. + /// The Bot Framework OAuth scope. public string? BotScope { get; set; } - /// Override the Bot Framework token service URL. + /// The Bot Framework token service URL. public string? TokenServiceUrl { get; set; } - /// Override the OpenID metadata URL for token validation. + /// The OpenID metadata URL for token validation. public string? OpenIdMetadataUrl { get; set; } - /// Override the token issuer for Bot Framework tokens. + /// The token issuer for Bot Framework tokens. public string? TokenIssuer { get; set; } - /// Override the channel service URL. + /// The channel service URL. public string? ChannelService { get; set; } - /// Override the OAuth redirect URL. + /// The OAuth redirect URL. public string? OAuthRedirectUrl { get; set; } public bool Empty @@ -43,14 +42,11 @@ public bool Empty /// /// Resolves the by starting from - /// (or the setting, or ), then applying - /// any per-endpoint overrides from settings. + /// (or a new default instance), then applying any per-endpoint overrides from settings. /// public CloudEnvironment ResolveCloud(CloudEnvironment? programmaticCloud = null) { - var baseCloud = programmaticCloud - ?? (Cloud is not null ? CloudEnvironment.FromName(Cloud) : null) - ?? CloudEnvironment.Public; + var baseCloud = programmaticCloud ?? new CloudEnvironment(); return baseCloud.WithOverrides( loginEndpoint: LoginEndpoint, diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs index f7bf4b7f..1c15d46a 100644 --- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs +++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs @@ -9,7 +9,7 @@ public class TeamsValidationSettings public List Issuers; public string LoginEndpoint; - public TeamsValidationSettings() : this(CloudEnvironment.Public) + public TeamsValidationSettings() : this(new CloudEnvironment()) { } diff --git a/Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs b/Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs index 4ee37dce..7e592127 100644 --- a/Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs +++ b/Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs @@ -8,9 +8,9 @@ namespace Microsoft.Teams.Api.Tests.Auth; public class CloudEnvironmentTests { [Fact] - public void Public_HasCorrectEndpoints() + public void DefaultConstructor_HasPublicCloudEndpoints() { - var env = CloudEnvironment.Public; + var env = new CloudEnvironment(); Assert.Equal("https://login.microsoftonline.com", env.LoginEndpoint); Assert.Equal("botframework.com", env.LoginTenant); @@ -23,88 +23,33 @@ public void Public_HasCorrectEndpoints() } [Fact] - public void USGov_HasCorrectEndpoints() + public void Constructor_AcceptsCustomEndpoints() { - var env = CloudEnvironment.USGov; - - Assert.Equal("https://login.microsoftonline.us", env.LoginEndpoint); - Assert.Equal("MicrosoftServices.onmicrosoft.us", env.LoginTenant); - Assert.Equal("https://api.botframework.us/.default", env.BotScope); - Assert.Equal("https://tokengcch.botframework.azure.us", env.TokenServiceUrl); - Assert.Equal("https://login.botframework.azure.us/v1/.well-known/openidconfiguration", env.OpenIdMetadataUrl); - Assert.Equal("https://api.botframework.us", env.TokenIssuer); - Assert.Equal("https://botframework.azure.us", env.ChannelService); - Assert.Equal("https://tokengcch.botframework.azure.us/.auth/web/redirect", env.OAuthRedirectUrl); - } - - [Fact] - public void USGovDoD_HasCorrectEndpoints() - { - var env = CloudEnvironment.USGovDoD; - - Assert.Equal("https://login.microsoftonline.us", env.LoginEndpoint); - Assert.Equal("MicrosoftServices.onmicrosoft.us", env.LoginTenant); - Assert.Equal("https://api.botframework.us/.default", env.BotScope); - Assert.Equal("https://apiDoD.botframework.azure.us", env.TokenServiceUrl); - Assert.Equal("https://login.botframework.azure.us/v1/.well-known/openidconfiguration", env.OpenIdMetadataUrl); - Assert.Equal("https://api.botframework.us", env.TokenIssuer); - Assert.Equal("https://botframework.azure.us", env.ChannelService); - Assert.Equal("https://apiDoD.botframework.azure.us/.auth/web/redirect", env.OAuthRedirectUrl); - } - - [Fact] - public void China_HasCorrectEndpoints() - { - var env = CloudEnvironment.China; - - Assert.Equal("https://login.partner.microsoftonline.cn", env.LoginEndpoint); - Assert.Equal("microsoftservices.partner.onmschina.cn", env.LoginTenant); - Assert.Equal("https://api.botframework.azure.cn/.default", env.BotScope); - Assert.Equal("https://token.botframework.azure.cn", env.TokenServiceUrl); - Assert.Equal("https://login.botframework.azure.cn/v1/.well-known/openidconfiguration", env.OpenIdMetadataUrl); - Assert.Equal("https://api.botframework.azure.cn", env.TokenIssuer); - Assert.Equal("https://botframework.azure.cn", env.ChannelService); - Assert.Equal("https://token.botframework.azure.cn/.auth/web/redirect", env.OAuthRedirectUrl); - } - - [Theory] - [InlineData("Public", "https://login.microsoftonline.com")] - [InlineData("public", "https://login.microsoftonline.com")] - [InlineData("PUBLIC", "https://login.microsoftonline.com")] - [InlineData("USGov", "https://login.microsoftonline.us")] - [InlineData("usgov", "https://login.microsoftonline.us")] - [InlineData("USGovDoD", "https://login.microsoftonline.us")] - [InlineData("usgovdod", "https://login.microsoftonline.us")] - [InlineData("China", "https://login.partner.microsoftonline.cn")] - [InlineData("china", "https://login.partner.microsoftonline.cn")] - public void FromName_ResolvesCorrectly(string name, string expectedLoginEndpoint) - { - var env = CloudEnvironment.FromName(name); - Assert.Equal(expectedLoginEndpoint, env.LoginEndpoint); - } - - [Theory] - [InlineData("invalid")] - [InlineData("")] - [InlineData("Azure")] - public void FromName_ThrowsForUnknownName(string name) - { - Assert.Throws(() => CloudEnvironment.FromName(name)); - } + var env = new CloudEnvironment( + loginEndpoint: "https://custom.login.example", + loginTenant: "custom-tenant", + botScope: "https://custom.scope/.default", + tokenServiceUrl: "https://custom.token.example", + openIdMetadataUrl: "https://custom.openid.example", + tokenIssuer: "https://custom.issuer.example", + channelService: "https://custom.channel.example", + oauthRedirectUrl: "https://custom.redirect.example" + ); - [Fact] - public void FromName_ReturnsStaticInstances() - { - Assert.Same(CloudEnvironment.Public, CloudEnvironment.FromName("Public")); - Assert.Same(CloudEnvironment.USGov, CloudEnvironment.FromName("USGov")); - Assert.Same(CloudEnvironment.USGovDoD, CloudEnvironment.FromName("USGovDoD")); - Assert.Same(CloudEnvironment.China, CloudEnvironment.FromName("China")); + Assert.Equal("https://custom.login.example", env.LoginEndpoint); + Assert.Equal("custom-tenant", env.LoginTenant); + Assert.Equal("https://custom.scope/.default", env.BotScope); + Assert.Equal("https://custom.token.example", env.TokenServiceUrl); + Assert.Equal("https://custom.openid.example", env.OpenIdMetadataUrl); + Assert.Equal("https://custom.issuer.example", env.TokenIssuer); + Assert.Equal("https://custom.channel.example", env.ChannelService); + Assert.Equal("https://custom.redirect.example", env.OAuthRedirectUrl); } [Fact] public void WithOverrides_AllNulls_ReturnsSameInstance() { - var env = CloudEnvironment.Public; + var env = new CloudEnvironment(); var result = env.WithOverrides(); @@ -114,7 +59,7 @@ public void WithOverrides_AllNulls_ReturnsSameInstance() [Fact] public void WithOverrides_SingleOverride_ReplacesOnlyThatProperty() { - var env = CloudEnvironment.Public; + var env = new CloudEnvironment(); var result = env.WithOverrides(loginTenant: "my-tenant-id"); @@ -132,7 +77,7 @@ public void WithOverrides_SingleOverride_ReplacesOnlyThatProperty() [Fact] public void WithOverrides_MultipleOverrides_ReplacesCorrectProperties() { - var env = CloudEnvironment.China; + var env = new CloudEnvironment(); var result = env.WithOverrides( loginEndpoint: "https://custom.login.cn", @@ -154,7 +99,7 @@ public void WithOverrides_MultipleOverrides_ReplacesCorrectProperties() [Fact] public void WithOverrides_AllOverrides_ReplacesAllProperties() { - var env = CloudEnvironment.Public; + var env = new CloudEnvironment(); var result = env.WithOverrides( loginEndpoint: "a", From c791fd714845542678bd4cb115939ee51084c495 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:40:34 +0000 Subject: [PATCH 3/5] Remove CloudEnvironment class entirely, revert to pre-PR state Remove the CloudEnvironment class and all its usages across the codebase. Revert ClientCredentials, App, AppOptions, TeamsSettings, TeamsValidationSettings, BotTokenClient, UserTokenClient, BotSignInClient, and both HostApplicationBuilder files to their original state with hardcoded public cloud defaults. The SDK should not duplicate the configuration schema offered by MSAL/Microsoft Identity Web. Cloud endpoint configuration should follow the AzureAd configuration pattern described in the Microsoft Identity Web wiki. Co-authored-by: rido-min <14916339+rido-min@users.noreply.github.com> --- .../Auth/ClientCredentials.cs | 5 +- .../Auth/CloudEnvironment.cs | 105 --------------- .../Clients/BotSignInClient.cs | 6 +- .../Clients/BotTokenClient.cs | 3 +- .../Clients/UserTokenClient.cs | 12 +- Libraries/Microsoft.Teams.Apps/App.cs | 7 +- Libraries/Microsoft.Teams.Apps/AppOptions.cs | 2 - .../TeamsSettings.cs | 53 +------- .../HostApplicationBuilder.cs | 17 +-- .../Extensions/HostApplicationBuilder.cs | 4 +- .../Extensions/TeamsValidationSettings.cs | 38 ++---- .../Auth/CloudEnvironmentTests.cs | 124 ------------------ 12 files changed, 28 insertions(+), 348 deletions(-) delete mode 100644 Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs delete mode 100644 Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs diff --git a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs index bb04a40b..7ae6d974 100644 --- a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs +++ b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs @@ -10,7 +10,6 @@ public class ClientCredentials : IHttpCredentials public string ClientId { get; set; } public string ClientSecret { get; set; } public string? TenantId { get; set; } - public CloudEnvironment Cloud { get; set; } = new(); public ClientCredentials(string clientId, string clientSecret) { @@ -27,9 +26,9 @@ public ClientCredentials(string clientId, string clientSecret, string? tenantId) public async Task Resolve(IHttpClient client, string[] scopes, CancellationToken cancellationToken = default) { - var tenantId = TenantId ?? Cloud.LoginTenant; + var tenantId = TenantId ?? "botframework.com"; var request = HttpRequest.Post( - $"{Cloud.LoginEndpoint}/{tenantId}/oauth2/v2.0/token" + $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token" ); request.Headers.Add("Content-Type", ["application/x-www-form-urlencoded"]); diff --git a/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs b/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs deleted file mode 100644 index 6cb83289..00000000 --- a/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.Teams.Api.Auth; - -/// -/// Bundles all cloud-specific service endpoints for a given Azure environment. -/// All properties default to Microsoft public (commercial) cloud values. -/// Configure endpoints via appsettings.json or programmatically for sovereign clouds. -/// -public class CloudEnvironment -{ - /// - /// The Azure AD login endpoint. - /// - public string LoginEndpoint { get; } - - /// - /// The default login tenant. - /// - public string LoginTenant { get; } - - /// - /// The Bot Framework OAuth scope. - /// - public string BotScope { get; } - - /// - /// The Bot Framework token service base URL. - /// - public string TokenServiceUrl { get; } - - /// - /// The OpenID metadata URL for token validation. - /// - public string OpenIdMetadataUrl { get; } - - /// - /// The token issuer for Bot Framework tokens. - /// - public string TokenIssuer { get; } - - /// - /// The channel service URL. Empty for public cloud. - /// - public string ChannelService { get; } - - /// - /// The OAuth redirect URL. - /// - public string OAuthRedirectUrl { get; } - - public CloudEnvironment( - string loginEndpoint = "https://login.microsoftonline.com", - string loginTenant = "botframework.com", - string botScope = "https://api.botframework.com/.default", - string tokenServiceUrl = "https://token.botframework.com", - string openIdMetadataUrl = "https://login.botframework.com/v1/.well-known/openidconfiguration", - string tokenIssuer = "https://api.botframework.com", - string channelService = "", - string oauthRedirectUrl = "https://token.botframework.com/.auth/web/redirect") - { - LoginEndpoint = loginEndpoint; - LoginTenant = loginTenant; - BotScope = botScope; - TokenServiceUrl = tokenServiceUrl; - OpenIdMetadataUrl = openIdMetadataUrl; - TokenIssuer = tokenIssuer; - ChannelService = channelService; - OAuthRedirectUrl = oauthRedirectUrl; - } - - /// - /// Creates a new by applying non-null overrides on top of this instance. - /// Returns the same instance if all overrides are null (no allocation). - /// - public CloudEnvironment WithOverrides( - string? loginEndpoint = null, - string? loginTenant = null, - string? botScope = null, - string? tokenServiceUrl = null, - string? openIdMetadataUrl = null, - string? tokenIssuer = null, - string? channelService = null, - string? oauthRedirectUrl = null) - { - if (loginEndpoint is null && loginTenant is null && botScope is null && - tokenServiceUrl is null && openIdMetadataUrl is null && tokenIssuer is null && - channelService is null && oauthRedirectUrl is null) - { - return this; - } - - return new CloudEnvironment( - loginEndpoint ?? LoginEndpoint, - loginTenant ?? LoginTenant, - botScope ?? BotScope, - tokenServiceUrl ?? TokenServiceUrl, - openIdMetadataUrl ?? OpenIdMetadataUrl, - tokenIssuer ?? TokenIssuer, - channelService ?? ChannelService, - oauthRedirectUrl ?? OAuthRedirectUrl - ); - } -} diff --git a/Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs b/Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs index cf1e1f8f..52217361 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs @@ -7,8 +7,6 @@ namespace Microsoft.Teams.Api.Clients; public class BotSignInClient : Client { - public string TokenServiceUrl { get; set; } = "https://token.botframework.com"; - public BotSignInClient() : base() { @@ -33,7 +31,7 @@ public async Task GetUrlAsync(GetUrlRequest request) { var query = QueryString.Serialize(request); var req = HttpRequest.Get( - $"{TokenServiceUrl}/api/botsignin/GetSignInUrl?{query}" + $"https://token.botframework.com/api/botsignin/GetSignInUrl?{query}" ); var res = await _http.SendAsync(req, _cancellationToken); @@ -44,7 +42,7 @@ public async Task GetUrlAsync(GetUrlRequest request) { var query = QueryString.Serialize(request); var req = HttpRequest.Get( - $"{TokenServiceUrl}/api/botsignin/GetSignInResource?{query}" + $"https://token.botframework.com/api/botsignin/GetSignInResource?{query}" ); var res = await _http.SendAsync(req, _cancellationToken); diff --git a/Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs b/Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs index 505144ba..8255d89c 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs @@ -7,9 +7,8 @@ namespace Microsoft.Teams.Api.Clients; public class BotTokenClient : Client { - public static readonly string DefaultBotScope = "https://api.botframework.com/.default"; + public static readonly string BotScope = "https://api.botframework.com/.default"; public static readonly string GraphScope = "https://graph.microsoft.com/.default"; - public string BotScope { get; set; } = DefaultBotScope; public BotTokenClient() : this(default) { diff --git a/Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs b/Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs index e2642629..cf264d6a 100644 --- a/Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs +++ b/Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs @@ -10,8 +10,6 @@ namespace Microsoft.Teams.Api.Clients; public class UserTokenClient : Client { - public string TokenServiceUrl { get; set; } = "https://token.botframework.com"; - private readonly JsonSerializerOptions _jsonSerializerOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull @@ -40,7 +38,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio public async Task GetAsync(GetTokenRequest request) { var query = QueryString.Serialize(request); - var req = HttpRequest.Get($"{TokenServiceUrl}/api/usertoken/GetToken?{query}"); + var req = HttpRequest.Get($"https://token.botframework.com/api/usertoken/GetToken?{query}"); var res = await _http.SendAsync(req, _cancellationToken); return res.Body; } @@ -48,7 +46,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio public async Task> GetAadAsync(GetAadTokenRequest request) { var query = QueryString.Serialize(request); - var req = HttpRequest.Post($"{TokenServiceUrl}/api/usertoken/GetAadTokens?{query}", body: request); + var req = HttpRequest.Post($"https://token.botframework.com/api/usertoken/GetAadTokens?{query}", body: request); var res = await _http.SendAsync>(req, _cancellationToken); return res.Body; } @@ -56,7 +54,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio public async Task> GetStatusAsync(GetTokenStatusRequest request) { var query = QueryString.Serialize(request); - var req = HttpRequest.Get($"{TokenServiceUrl}/api/usertoken/GetTokenStatus?{query}"); + var req = HttpRequest.Get($"https://token.botframework.com/api/usertoken/GetTokenStatus?{query}"); var res = await _http.SendAsync>(req, _cancellationToken); return res.Body; } @@ -64,7 +62,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio public async Task SignOutAsync(SignOutRequest request) { var query = QueryString.Serialize(request); - var req = HttpRequest.Delete($"{TokenServiceUrl}/api/usertoken/SignOut?{query}"); + var req = HttpRequest.Delete($"https://token.botframework.com/api/usertoken/SignOut?{query}"); await _http.SendAsync(req, _cancellationToken); } @@ -81,7 +79,7 @@ public async Task SignOutAsync(SignOutRequest request) // This is required for the Bot Framework Token Service to process the request correctly. var body = JsonSerializer.Serialize(request.GetBody(), _jsonSerializerOptions); - var req = HttpRequest.Post($"{TokenServiceUrl}/api/usertoken/exchange?{query}", body); + var req = HttpRequest.Post($"https://token.botframework.com/api/usertoken/exchange?{query}", body); req.Headers.Add("Content-Type", new List() { "application/json" }); var res = await _http.SendAsync(req, _cancellationToken); diff --git a/Libraries/Microsoft.Teams.Apps/App.cs b/Libraries/Microsoft.Teams.Apps/App.cs index d61efb85..5f7768bc 100644 --- a/Libraries/Microsoft.Teams.Apps/App.cs +++ b/Libraries/Microsoft.Teams.Apps/App.cs @@ -51,8 +51,6 @@ internal string UserAgent public App(AppOptions? options = null) { - var cloud = options?.Cloud ?? new CloudEnvironment(); - Logger = options?.Logger ?? new ConsoleLogger(); Storage = options?.Storage ?? new LocalStorage(); Credentials = options?.Credentials; @@ -79,7 +77,7 @@ public App(AppOptions? options = null) if (Token.IsExpired) { - var res = Credentials.Resolve(TokenClient, [.. Token.Scopes.DefaultIfEmpty(cloud.BotScope)]) + var res = Credentials.Resolve(TokenClient, [.. Token.Scopes.DefaultIfEmpty(BotTokenClient.BotScope)]) .ConfigureAwait(false) .GetAwaiter() .GetResult(); @@ -92,9 +90,6 @@ public App(AppOptions? options = null) }; Api = new ApiClient("https://smba.trafficmanager.net/teams/", Client); - Api.Bots.Token.BotScope = cloud.BotScope; - Api.Bots.SignIn.TokenServiceUrl = cloud.TokenServiceUrl; - Api.Users.Token.TokenServiceUrl = cloud.TokenServiceUrl; Container = new Container(); Container.Register(Logger); Container.Register(Storage); diff --git a/Libraries/Microsoft.Teams.Apps/AppOptions.cs b/Libraries/Microsoft.Teams.Apps/AppOptions.cs index 766016bc..b923afa2 100644 --- a/Libraries/Microsoft.Teams.Apps/AppOptions.cs +++ b/Libraries/Microsoft.Teams.Apps/AppOptions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Teams.Api.Auth; using Microsoft.Teams.Apps.Plugins; namespace Microsoft.Teams.Apps; @@ -16,7 +15,6 @@ public class AppOptions public Common.Http.IHttpCredentials? Credentials { get; set; } public IList Plugins { get; set; } = []; public OAuthSettings OAuth { get; set; } = new OAuthSettings(); - public CloudEnvironment? Cloud { get; set; } public AppOptions() { diff --git a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs index f56a531e..11a4e532 100644 --- a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs +++ b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs @@ -11,69 +11,18 @@ public class TeamsSettings public string? ClientSecret { get; set; } public string? TenantId { get; set; } - /// The Azure AD login endpoint. - public string? LoginEndpoint { get; set; } - - /// The default login tenant. - public string? LoginTenant { get; set; } - - /// The Bot Framework OAuth scope. - public string? BotScope { get; set; } - - /// The Bot Framework token service URL. - public string? TokenServiceUrl { get; set; } - - /// The OpenID metadata URL for token validation. - public string? OpenIdMetadataUrl { get; set; } - - /// The token issuer for Bot Framework tokens. - public string? TokenIssuer { get; set; } - - /// The channel service URL. - public string? ChannelService { get; set; } - - /// The OAuth redirect URL. - public string? OAuthRedirectUrl { get; set; } - public bool Empty { get { return ClientId == "" || ClientSecret == ""; } } - /// - /// Resolves the by starting from - /// (or a new default instance), then applying any per-endpoint overrides from settings. - /// - public CloudEnvironment ResolveCloud(CloudEnvironment? programmaticCloud = null) - { - var baseCloud = programmaticCloud ?? new CloudEnvironment(); - - return baseCloud.WithOverrides( - loginEndpoint: LoginEndpoint, - loginTenant: LoginTenant, - botScope: BotScope, - tokenServiceUrl: TokenServiceUrl, - openIdMetadataUrl: OpenIdMetadataUrl, - tokenIssuer: TokenIssuer, - channelService: ChannelService, - oauthRedirectUrl: OAuthRedirectUrl - ); - } - public AppOptions Apply(AppOptions? options = null) { options ??= new AppOptions(); - var cloud = ResolveCloud(options.Cloud); - options.Cloud = cloud; - if (ClientId is not null && ClientSecret is not null && !Empty) { - var credentials = new ClientCredentials(ClientId, ClientSecret, TenantId) - { - Cloud = cloud - }; - options.Credentials = credentials; + options.Credentials = new ClientCredentials(ClientId, ClientSecret, TenantId); } return options; diff --git a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs index 2c0342d6..01f28920 100644 --- a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs +++ b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs @@ -31,10 +31,6 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder var settings = builder.Configuration.GetTeams(); var loggingSettings = builder.Configuration.GetTeamsLogging(); - // cloud environment (base preset + per-endpoint overrides) - var cloud = settings.ResolveCloud(options.Cloud); - options.Cloud = cloud; - // client credentials if (options.Credentials is null && settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) { @@ -42,8 +38,7 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder settings.ClientId, settings.ClientSecret, settings.TenantId - ) - { Cloud = cloud }; + ); } options.Logger ??= new ConsoleLogger(loggingSettings); @@ -61,20 +56,14 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder var settings = builder.Configuration.GetTeams(); var loggingSettings = builder.Configuration.GetTeamsLogging(); - // cloud environment (base preset + per-endpoint overrides) - var cloud = settings.ResolveCloud(); - // client credentials if (settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) { - var credentials = new ClientCredentials( + appBuilder = appBuilder.AddCredentials(new ClientCredentials( settings.ClientId, settings.ClientSecret, settings.TenantId - ) - { Cloud = cloud }; - - appBuilder = appBuilder.AddCredentials(credentials); + )); } var app = appBuilder.Build(); diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs index f0ab906f..760d94da 100644 --- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs +++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Teams.Api.Auth; using Microsoft.Teams.Apps; using Microsoft.Teams.Apps.Extensions; @@ -120,9 +119,8 @@ public static class EntraTokenAuthConstants public static IHostApplicationBuilder AddTeamsTokenAuthentication(this IHostApplicationBuilder builder, bool skipAuth = false) { var settings = builder.Configuration.GetTeams(); - var cloud = settings.ResolveCloud(); - var teamsValidationSettings = new TeamsValidationSettings(cloud); + var teamsValidationSettings = new TeamsValidationSettings(); if (!string.IsNullOrEmpty(settings.ClientId)) { teamsValidationSettings.AddDefaultAudiences(settings.ClientId); diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs index 1c15d46a..cce7d638 100644 --- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs +++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs @@ -1,32 +1,18 @@ -using Microsoft.Teams.Api.Auth; - namespace Microsoft.Teams.Plugins.AspNetCore.Extensions; public class TeamsValidationSettings { - public string OpenIdMetadataUrl; + public string OpenIdMetadataUrl = "https://login.botframework.com/v1/.well-known/openidconfiguration"; public List Audiences = []; - public List Issuers; - public string LoginEndpoint; - - public TeamsValidationSettings() : this(new CloudEnvironment()) - { - } - - public TeamsValidationSettings(CloudEnvironment cloud) - { - LoginEndpoint = cloud.LoginEndpoint; - OpenIdMetadataUrl = cloud.OpenIdMetadataUrl; - Issuers = [ - cloud.TokenIssuer, - "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", // Emulator Auth v3.1, 1.0 token - "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", // Emulator Auth v3.1, 2.0 token - "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Emulator Auth v3.2, 1.0 token - "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Emulator Auth v3.2, 2.0 token - "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", // Copilot Auth v1.0 token - "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", // Copilot Auth v2.0 token - ]; - } + public List Issuers = [ + "https://api.botframework.com", + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", // Emulator Auth v3.1, 1.0 token + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", // Emulator Auth v3.1, 2.0 token + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", // Emulator Auth v3.2, 1.0 token + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", // Emulator Auth v3.2, 2.0 token + "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", // Copilot Auth v1.0 token + "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", // Copilot Auth v2.0 token + ]; public void AddDefaultAudiences(string ClientId) { @@ -43,13 +29,13 @@ public IEnumerable GetValidIssuersForTenant(string? tenantId) var validIssuers = new List(); if (!string.IsNullOrEmpty(tenantId)) { - validIssuers.Add($"{LoginEndpoint}/{tenantId}/"); + validIssuers.Add($"https://login.microsoftonline.com/{tenantId}/"); } return validIssuers; } public string GetTenantSpecificOpenIdMetadataUrl(string? tenantId) { - return $"{LoginEndpoint}/{tenantId ?? "common"}/v2.0/.well-known/openid-configuration"; + return $"https://login.microsoftonline.com/{tenantId ?? "common"}/v2.0/.well-known/openid-configuration"; } } \ No newline at end of file diff --git a/Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs b/Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs deleted file mode 100644 index 7e592127..00000000 --- a/Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.Teams.Api.Auth; - -namespace Microsoft.Teams.Api.Tests.Auth; - -public class CloudEnvironmentTests -{ - [Fact] - public void DefaultConstructor_HasPublicCloudEndpoints() - { - var env = new CloudEnvironment(); - - Assert.Equal("https://login.microsoftonline.com", env.LoginEndpoint); - Assert.Equal("botframework.com", env.LoginTenant); - Assert.Equal("https://api.botframework.com/.default", env.BotScope); - Assert.Equal("https://token.botframework.com", env.TokenServiceUrl); - Assert.Equal("https://login.botframework.com/v1/.well-known/openidconfiguration", env.OpenIdMetadataUrl); - Assert.Equal("https://api.botframework.com", env.TokenIssuer); - Assert.Equal("", env.ChannelService); - Assert.Equal("https://token.botframework.com/.auth/web/redirect", env.OAuthRedirectUrl); - } - - [Fact] - public void Constructor_AcceptsCustomEndpoints() - { - var env = new CloudEnvironment( - loginEndpoint: "https://custom.login.example", - loginTenant: "custom-tenant", - botScope: "https://custom.scope/.default", - tokenServiceUrl: "https://custom.token.example", - openIdMetadataUrl: "https://custom.openid.example", - tokenIssuer: "https://custom.issuer.example", - channelService: "https://custom.channel.example", - oauthRedirectUrl: "https://custom.redirect.example" - ); - - Assert.Equal("https://custom.login.example", env.LoginEndpoint); - Assert.Equal("custom-tenant", env.LoginTenant); - Assert.Equal("https://custom.scope/.default", env.BotScope); - Assert.Equal("https://custom.token.example", env.TokenServiceUrl); - Assert.Equal("https://custom.openid.example", env.OpenIdMetadataUrl); - Assert.Equal("https://custom.issuer.example", env.TokenIssuer); - Assert.Equal("https://custom.channel.example", env.ChannelService); - Assert.Equal("https://custom.redirect.example", env.OAuthRedirectUrl); - } - - [Fact] - public void WithOverrides_AllNulls_ReturnsSameInstance() - { - var env = new CloudEnvironment(); - - var result = env.WithOverrides(); - - Assert.Same(env, result); - } - - [Fact] - public void WithOverrides_SingleOverride_ReplacesOnlyThatProperty() - { - var env = new CloudEnvironment(); - - var result = env.WithOverrides(loginTenant: "my-tenant-id"); - - Assert.NotSame(env, result); - Assert.Equal("my-tenant-id", result.LoginTenant); - Assert.Equal(env.LoginEndpoint, result.LoginEndpoint); - Assert.Equal(env.BotScope, result.BotScope); - Assert.Equal(env.TokenServiceUrl, result.TokenServiceUrl); - Assert.Equal(env.OpenIdMetadataUrl, result.OpenIdMetadataUrl); - Assert.Equal(env.TokenIssuer, result.TokenIssuer); - Assert.Equal(env.ChannelService, result.ChannelService); - Assert.Equal(env.OAuthRedirectUrl, result.OAuthRedirectUrl); - } - - [Fact] - public void WithOverrides_MultipleOverrides_ReplacesCorrectProperties() - { - var env = new CloudEnvironment(); - - var result = env.WithOverrides( - loginEndpoint: "https://custom.login.cn", - loginTenant: "custom-tenant", - tokenServiceUrl: "https://custom.token.cn" - ); - - Assert.Equal("https://custom.login.cn", result.LoginEndpoint); - Assert.Equal("custom-tenant", result.LoginTenant); - Assert.Equal("https://custom.token.cn", result.TokenServiceUrl); - // unchanged - Assert.Equal(env.BotScope, result.BotScope); - Assert.Equal(env.OpenIdMetadataUrl, result.OpenIdMetadataUrl); - Assert.Equal(env.TokenIssuer, result.TokenIssuer); - Assert.Equal(env.ChannelService, result.ChannelService); - Assert.Equal(env.OAuthRedirectUrl, result.OAuthRedirectUrl); - } - - [Fact] - public void WithOverrides_AllOverrides_ReplacesAllProperties() - { - var env = new CloudEnvironment(); - - var result = env.WithOverrides( - loginEndpoint: "a", - loginTenant: "b", - botScope: "c", - tokenServiceUrl: "d", - openIdMetadataUrl: "e", - tokenIssuer: "f", - channelService: "g", - oauthRedirectUrl: "h" - ); - - Assert.Equal("a", result.LoginEndpoint); - Assert.Equal("b", result.LoginTenant); - Assert.Equal("c", result.BotScope); - Assert.Equal("d", result.TokenServiceUrl); - Assert.Equal("e", result.OpenIdMetadataUrl); - Assert.Equal("f", result.TokenIssuer); - Assert.Equal("g", result.ChannelService); - Assert.Equal("h", result.OAuthRedirectUrl); - } -} From faf26301d1bd175877ba444aeba36fa1f778490c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:58:34 +0000 Subject: [PATCH 4/5] Add Instance property for sovereign cloud Entra ID URL configuration Follow the Microsoft Identity Web configuration schema by adding an Instance property to TeamsSettings and ClientCredentials. This allows overriding the Entra ID login endpoint for sovereign clouds (e.g., https://login.microsoftonline.us for US Gov) through appsettings.json: "Teams": { "Instance": "https://login.microsoftonline.us", "TenantId": "...", "ClientId": "...", "ClientSecret": "..." } The Instance property flows through to: - ClientCredentials.Resolve() for token acquisition - TeamsValidationSettings for issuer and OpenID metadata URLs Co-authored-by: rido-min <14916339+rido-min@users.noreply.github.com> --- .../Auth/ClientCredentials.cs | 9 +- .../TeamsSettings.cs | 15 +++- .../HostApplicationBuilder.cs | 20 ++++- .../Extensions/HostApplicationBuilder.cs | 6 ++ .../Extensions/TeamsValidationSettings.cs | 7 +- .../Auth/ClientCredentialsTests.cs | 35 ++++++++ .../TeamsValidationSettingsTests.cs | 89 +++++++++++++++++++ 7 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 Tests/Microsoft.Teams.Api.Tests/Auth/ClientCredentialsTests.cs create mode 100644 Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs diff --git a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs index 7ae6d974..07d90593 100644 --- a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs +++ b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs @@ -11,6 +11,12 @@ public class ClientCredentials : IHttpCredentials public string ClientSecret { get; set; } public string? TenantId { get; set; } + /// + /// The Entra ID login endpoint, following the Microsoft Identity Web configuration schema. + /// Override this for sovereign clouds (e.g. "https://login.microsoftonline.us" for US Gov). + /// + public string Instance { get; set; } = "https://login.microsoftonline.com"; + public ClientCredentials(string clientId, string clientSecret) { ClientId = clientId; @@ -27,8 +33,9 @@ public ClientCredentials(string clientId, string clientSecret, string? tenantId) public async Task Resolve(IHttpClient client, string[] scopes, CancellationToken cancellationToken = default) { var tenantId = TenantId ?? "botframework.com"; + var instance = Instance.TrimEnd('/'); var request = HttpRequest.Post( - $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token" + $"{instance}/{tenantId}/oauth2/v2.0/token" ); request.Headers.Add("Content-Type", ["application/x-www-form-urlencoded"]); diff --git a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs index 11a4e532..f0b8bffc 100644 --- a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs +++ b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs @@ -11,6 +11,12 @@ public class TeamsSettings public string? ClientSecret { get; set; } public string? TenantId { get; set; } + /// + /// The Entra ID login endpoint, following the Microsoft Identity Web configuration schema. + /// Override this for sovereign clouds (e.g. "https://login.microsoftonline.us" for US Gov). + /// + public string? Instance { get; set; } + public bool Empty { get { return ClientId == "" || ClientSecret == ""; } @@ -22,7 +28,14 @@ public AppOptions Apply(AppOptions? options = null) if (ClientId is not null && ClientSecret is not null && !Empty) { - options.Credentials = new ClientCredentials(ClientId, ClientSecret, TenantId); + var credentials = new ClientCredentials(ClientId, ClientSecret, TenantId); + + if (Instance is not null) + { + credentials.Instance = Instance; + } + + options.Credentials = credentials; } return options; diff --git a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs index 01f28920..666eec4a 100644 --- a/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs +++ b/Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs @@ -34,11 +34,18 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder // client credentials if (options.Credentials is null && settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) { - options.Credentials = new ClientCredentials( + var credentials = new ClientCredentials( settings.ClientId, settings.ClientSecret, settings.TenantId ); + + if (settings.Instance is not null) + { + credentials.Instance = settings.Instance; + } + + options.Credentials = credentials; } options.Logger ??= new ConsoleLogger(loggingSettings); @@ -59,11 +66,18 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder // client credentials if (settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) { - appBuilder = appBuilder.AddCredentials(new ClientCredentials( + var credentials = new ClientCredentials( settings.ClientId, settings.ClientSecret, settings.TenantId - )); + ); + + if (settings.Instance is not null) + { + credentials.Instance = settings.Instance; + } + + appBuilder = appBuilder.AddCredentials(credentials); } var app = appBuilder.Build(); diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs index 760d94da..81862751 100644 --- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs +++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs @@ -121,6 +121,12 @@ public static IHostApplicationBuilder AddTeamsTokenAuthentication(this IHostAppl var settings = builder.Configuration.GetTeams(); var teamsValidationSettings = new TeamsValidationSettings(); + + if (!string.IsNullOrEmpty(settings.Instance)) + { + teamsValidationSettings.Instance = settings.Instance; + } + if (!string.IsNullOrEmpty(settings.ClientId)) { teamsValidationSettings.AddDefaultAudiences(settings.ClientId); diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs index cce7d638..863469b5 100644 --- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs +++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs @@ -13,6 +13,7 @@ public class TeamsValidationSettings "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", // Copilot Auth v1.0 token "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", // Copilot Auth v2.0 token ]; + public string Instance = "https://login.microsoftonline.com"; public void AddDefaultAudiences(string ClientId) { @@ -29,13 +30,15 @@ public IEnumerable GetValidIssuersForTenant(string? tenantId) var validIssuers = new List(); if (!string.IsNullOrEmpty(tenantId)) { - validIssuers.Add($"https://login.microsoftonline.com/{tenantId}/"); + var instance = Instance.TrimEnd('/'); + validIssuers.Add($"{instance}/{tenantId}/"); } return validIssuers; } public string GetTenantSpecificOpenIdMetadataUrl(string? tenantId) { - return $"https://login.microsoftonline.com/{tenantId ?? "common"}/v2.0/.well-known/openid-configuration"; + var instance = Instance.TrimEnd('/'); + return $"{instance}/{tenantId ?? "common"}/v2.0/.well-known/openid-configuration"; } } \ No newline at end of file diff --git a/Tests/Microsoft.Teams.Api.Tests/Auth/ClientCredentialsTests.cs b/Tests/Microsoft.Teams.Api.Tests/Auth/ClientCredentialsTests.cs new file mode 100644 index 00000000..aaa4d618 --- /dev/null +++ b/Tests/Microsoft.Teams.Api.Tests/Auth/ClientCredentialsTests.cs @@ -0,0 +1,35 @@ +using Microsoft.Teams.Api.Auth; + +namespace Microsoft.Teams.Api.Tests.Auth; + +public class ClientCredentialsTests +{ + [Fact] + public void Instance_DefaultsToPublicCloud() + { + var credentials = new ClientCredentials("client-id", "client-secret"); + + Assert.Equal("https://login.microsoftonline.com", credentials.Instance); + } + + [Fact] + public void Instance_CanBeOverridden() + { + var credentials = new ClientCredentials("client-id", "client-secret") + { + Instance = "https://login.microsoftonline.us" + }; + + Assert.Equal("https://login.microsoftonline.us", credentials.Instance); + } + + [Fact] + public void Constructor_WithTenantId_SetsProperties() + { + var credentials = new ClientCredentials("client-id", "client-secret", "tenant-id"); + + Assert.Equal("client-id", credentials.ClientId); + Assert.Equal("client-secret", credentials.ClientSecret); + Assert.Equal("tenant-id", credentials.TenantId); + } +} diff --git a/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs new file mode 100644 index 00000000..879343bc --- /dev/null +++ b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs @@ -0,0 +1,89 @@ +using Microsoft.Teams.Plugins.AspNetCore.Extensions; + +namespace Microsoft.Teams.Plugins.AspNetCore.Tests.Extensions; + +public class TeamsValidationSettingsTests +{ + [Fact] + public void Instance_DefaultsToPublicCloud() + { + var settings = new TeamsValidationSettings(); + + Assert.Equal("https://login.microsoftonline.com", settings.Instance); + } + + [Fact] + public void GetValidIssuersForTenant_UsesDefaultInstance() + { + var settings = new TeamsValidationSettings(); + var issuers = settings.GetValidIssuersForTenant("test-tenant").ToList(); + + Assert.Single(issuers); + Assert.Equal("https://login.microsoftonline.com/test-tenant/", issuers[0]); + } + + [Fact] + public void GetValidIssuersForTenant_UsesCustomInstance() + { + var settings = new TeamsValidationSettings + { + Instance = "https://login.microsoftonline.us" + }; + var issuers = settings.GetValidIssuersForTenant("test-tenant").ToList(); + + Assert.Single(issuers); + Assert.Equal("https://login.microsoftonline.us/test-tenant/", issuers[0]); + } + + [Fact] + public void GetValidIssuersForTenant_HandlesTrailingSlashInInstance() + { + var settings = new TeamsValidationSettings + { + Instance = "https://login.microsoftonline.us/" + }; + var issuers = settings.GetValidIssuersForTenant("test-tenant").ToList(); + + Assert.Single(issuers); + Assert.Equal("https://login.microsoftonline.us/test-tenant/", issuers[0]); + } + + [Fact] + public void GetTenantSpecificOpenIdMetadataUrl_UsesDefaultInstance() + { + var settings = new TeamsValidationSettings(); + var url = settings.GetTenantSpecificOpenIdMetadataUrl("test-tenant"); + + Assert.Equal("https://login.microsoftonline.com/test-tenant/v2.0/.well-known/openid-configuration", url); + } + + [Fact] + public void GetTenantSpecificOpenIdMetadataUrl_UsesCustomInstance() + { + var settings = new TeamsValidationSettings + { + Instance = "https://login.microsoftonline.us" + }; + var url = settings.GetTenantSpecificOpenIdMetadataUrl("test-tenant"); + + Assert.Equal("https://login.microsoftonline.us/test-tenant/v2.0/.well-known/openid-configuration", url); + } + + [Fact] + public void GetTenantSpecificOpenIdMetadataUrl_FallsBackToCommon_WhenTenantIdIsNull() + { + var settings = new TeamsValidationSettings(); + var url = settings.GetTenantSpecificOpenIdMetadataUrl(null); + + Assert.Equal("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", url); + } + + [Fact] + public void GetValidIssuersForTenant_ReturnsEmpty_WhenTenantIdIsNull() + { + var settings = new TeamsValidationSettings(); + var issuers = settings.GetValidIssuersForTenant(null).ToList(); + + Assert.Empty(issuers); + } +} From 995d56d96597a020d8b50d2f7b9ec8f656daa31e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:02:40 +0000 Subject: [PATCH 5/5] Address code review: use property for Instance, improve test naming Co-authored-by: rido-min <14916339+rido-min@users.noreply.github.com> --- .../Extensions/TeamsValidationSettings.cs | 2 +- .../Extensions/TeamsValidationSettingsTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs index 863469b5..551ef9ac 100644 --- a/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs +++ b/Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/TeamsValidationSettings.cs @@ -13,7 +13,7 @@ public class TeamsValidationSettings "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", // Copilot Auth v1.0 token "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", // Copilot Auth v2.0 token ]; - public string Instance = "https://login.microsoftonline.com"; + public string Instance { get; set; } = "https://login.microsoftonline.com"; public void AddDefaultAudiences(string ClientId) { diff --git a/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs index 879343bc..265d9840 100644 --- a/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs +++ b/Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/TeamsValidationSettingsTests.cs @@ -70,7 +70,7 @@ public void GetTenantSpecificOpenIdMetadataUrl_UsesCustomInstance() } [Fact] - public void GetTenantSpecificOpenIdMetadataUrl_FallsBackToCommon_WhenTenantIdIsNull() + public void GetTenantSpecificOpenIdMetadataUrl_UsesCommon_WhenTenantIdIsNull() { var settings = new TeamsValidationSettings(); var url = settings.GetTenantSpecificOpenIdMetadataUrl(null);