diff --git a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs index e198eb73..07d90593 100644 --- a/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs +++ b/Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs @@ -10,7 +10,12 @@ 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; + + /// + /// 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) { @@ -27,9 +32,10 @@ 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 instance = Instance.TrimEnd('/'); var request = HttpRequest.Post( - $"{Cloud.LoginEndpoint}/{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.Api/Auth/CloudEnvironment.cs b/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs deleted file mode 100644 index 7910727a..00000000 --- a/Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs +++ /dev/null @@ -1,175 +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. -/// Use predefined instances (, , , ) -/// or construct a custom one. -/// -public class CloudEnvironment -{ - /// - /// The Azure AD login endpoint (e.g. "https://login.microsoftonline.com"). - /// - public string LoginEndpoint { get; } - - /// - /// The default multi-tenant login tenant (e.g. "botframework.com"). - /// - public string LoginTenant { get; } - - /// - /// The Bot Framework OAuth scope (e.g. "https://api.botframework.com/.default"). - /// - public string BotScope { get; } - - /// - /// The Bot Framework token service base URL (e.g. "https://token.botframework.com"). - /// - public string TokenServiceUrl { get; } - - /// - /// The OpenID metadata URL for token validation (e.g. "https://login.botframework.com/v1/.well-known/openidconfiguration"). - /// - public string OpenIdMetadataUrl { get; } - - /// - /// The token issuer for Bot Framework tokens (e.g. "https://api.botframework.com"). - /// - public string TokenIssuer { get; } - - /// - /// The channel service URL. Empty for public cloud; set for sovereign clouds - /// (e.g. "https://botframework.azure.us"). - /// - public string ChannelService { get; } - - /// - /// The OAuth redirect URL (e.g. "https://token.botframework.com/.auth/web/redirect"). - /// - public string OAuthRedirectUrl { get; } - - public CloudEnvironment( - string loginEndpoint, - string loginTenant, - string botScope, - string tokenServiceUrl, - string openIdMetadataUrl, - string tokenIssuer, - string channelService, - string oauthRedirectUrl) - { - LoginEndpoint = loginEndpoint; - LoginTenant = loginTenant; - BotScope = botScope; - TokenServiceUrl = tokenServiceUrl; - OpenIdMetadataUrl = openIdMetadataUrl; - TokenIssuer = tokenIssuer; - ChannelService = channelService; - 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). - /// - 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 - ); - } - - /// - /// 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.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 ed2dd1f6..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 ?? CloudEnvironment.Public; - 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 46efca80..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 @@ -10,73 +10,31 @@ 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. - public string? LoginEndpoint { get; set; } - - /// Override the default login tenant. - public string? LoginTenant { get; set; } - - /// Override the Bot Framework OAuth scope. - public string? BotScope { get; set; } - - /// Override the Bot Framework token service URL. - public string? TokenServiceUrl { get; set; } - - /// Override the OpenID metadata URL for token validation. - public string? OpenIdMetadataUrl { get; set; } - - /// Override the token issuer for Bot Framework tokens. - public string? TokenIssuer { get; set; } - - /// Override the channel service URL. - public string? ChannelService { get; set; } - - /// Override the OAuth redirect URL. - public string? OAuthRedirectUrl { 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 == ""; } } - /// - /// Resolves the by starting from - /// (or the setting, or ), 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; - - 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) + var credentials = new ClientCredentials(ClientId, ClientSecret, TenantId); + + if (Instance is not null) { - Cloud = cloud - }; + credentials.Instance = Instance; + } + options.Credentials = credentials; } 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..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 @@ -31,19 +31,21 @@ 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) { - options.Credentials = new ClientCredentials( + var credentials = new ClientCredentials( settings.ClientId, settings.ClientSecret, settings.TenantId - ) - { Cloud = cloud }; + ); + + if (settings.Instance is not null) + { + credentials.Instance = settings.Instance; + } + + options.Credentials = credentials; } options.Logger ??= new ConsoleLogger(loggingSettings); @@ -61,9 +63,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(); - // client credentials if (settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) { @@ -71,8 +70,12 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder settings.ClientId, settings.ClientSecret, settings.TenantId - ) - { Cloud = cloud }; + ); + + if (settings.Instance is not null) + { + credentials.Instance = settings.Instance; + } appBuilder = appBuilder.AddCredentials(credentials); } 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..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 @@ -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,14 @@ 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.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 f7bf4b7f..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 @@ -1,32 +1,19 @@ -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(CloudEnvironment.Public) - { - } - - 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 string Instance { get; set; } = "https://login.microsoftonline.com"; public void AddDefaultAudiences(string ClientId) { @@ -43,13 +30,15 @@ public IEnumerable GetValidIssuersForTenant(string? tenantId) var validIssuers = new List(); if (!string.IsNullOrEmpty(tenantId)) { - validIssuers.Add($"{LoginEndpoint}/{tenantId}/"); + var instance = Instance.TrimEnd('/'); + validIssuers.Add($"{instance}/{tenantId}/"); } return validIssuers; } public string GetTenantSpecificOpenIdMetadataUrl(string? tenantId) { - return $"{LoginEndpoint}/{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.Api.Tests/Auth/CloudEnvironmentTests.cs b/Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs deleted file mode 100644 index 4ee37dce..00000000 --- a/Tests/Microsoft.Teams.Api.Tests/Auth/CloudEnvironmentTests.cs +++ /dev/null @@ -1,179 +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 Public_HasCorrectEndpoints() - { - var env = CloudEnvironment.Public; - - 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 USGov_HasCorrectEndpoints() - { - 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)); - } - - [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")); - } - - [Fact] - public void WithOverrides_AllNulls_ReturnsSameInstance() - { - var env = CloudEnvironment.Public; - - var result = env.WithOverrides(); - - Assert.Same(env, result); - } - - [Fact] - public void WithOverrides_SingleOverride_ReplacesOnlyThatProperty() - { - var env = CloudEnvironment.Public; - - 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 = CloudEnvironment.China; - - 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 = CloudEnvironment.Public; - - 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); - } -} 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..265d9840 --- /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_UsesCommon_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); + } +}