Skip to content
Open
10 changes: 0 additions & 10 deletions Libraries/Microsoft.Teams.Api/Activities/Activity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,16 +233,6 @@ public virtual Activity WithRecipient(Account value)
#pragma warning restore ExperimentalTeamsTargeted
}

[Experimental("ExperimentalTeamsTargeted")]
public virtual Activity WithRecipient(Account value, bool isTargeted)
{
Recipient = value;
#pragma warning disable ExperimentalTeamsTargeted
Recipient.IsTargeted = null;
#pragma warning restore ExperimentalTeamsTargeted
return this;
}

[Experimental("ExperimentalTeamsTargeted")]
public virtual Activity WithRecipient(Account value, bool isTargeted)
{
Expand Down
5 changes: 3 additions & 2 deletions Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +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 ClientCredentials(string clientId, string clientSecret)
{
Expand All @@ -26,9 +27,9 @@ public ClientCredentials(string clientId, string clientSecret, string? tenantId)

public async Task<ITokenResponse> Resolve(IHttpClient client, string[] scopes, CancellationToken cancellationToken = default)
{
var tenantId = TenantId ?? "botframework.com";
var tenantId = TenantId ?? Cloud.LoginTenant;
var request = HttpRequest.Post(
$"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token"
$"{Cloud.LoginEndpoint}/{tenantId}/oauth2/v2.0/token"
);

request.Headers.Add("Content-Type", ["application/x-www-form-urlencoded"]);
Expand Down
202 changes: 202 additions & 0 deletions Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Teams.Api.Auth;

/// <summary>
/// Bundles all cloud-specific service endpoints for a given Azure environment.
/// Use predefined instances (<see cref="Public"/>, <see cref="USGov"/>, <see cref="USGovDoD"/>, <see cref="China"/>)
/// or construct a custom one.
/// </summary>
public class CloudEnvironment
{
/// <summary>
/// The Azure AD login endpoint (e.g. "https://login.microsoftonline.com").
/// </summary>
public string LoginEndpoint { get; }

/// <summary>
/// The default multi-tenant login tenant (e.g. "botframework.com").
/// </summary>
public string LoginTenant { get; }

/// <summary>
/// The Bot Framework OAuth scope (e.g. "https://api.botframework.com/.default").
/// </summary>
public string BotScope { get; }

/// <summary>
/// The Bot Framework token service base URL (e.g. "https://token.botframework.com").
/// </summary>
public string TokenServiceUrl { get; }

/// <summary>
/// The OpenID metadata URL for token validation (e.g. "https://login.botframework.com/v1/.well-known/openidconfiguration").
/// </summary>
public string OpenIdMetadataUrl { get; }

/// <summary>
/// The token issuer for Bot Framework tokens (e.g. "https://api.botframework.com").
/// </summary>
public string TokenIssuer { get; }

/// <summary>
/// The channel service URL. Empty for public cloud; set for sovereign clouds
/// (e.g. "https://botframework.azure.us").
/// </summary>
public string ChannelService { get; }

/// <summary>
/// The OAuth redirect URL (e.g. "https://token.botframework.com/.auth/web/redirect").
/// </summary>
public string OAuthRedirectUrl { get; }

/// <summary>
/// The Microsoft Graph token scope (e.g. "https://graph.microsoft.com/.default").
/// </summary>
public string GraphScope { get; }

public CloudEnvironment(
string loginEndpoint,
string loginTenant,
string botScope,
string tokenServiceUrl,
string openIdMetadataUrl,
string tokenIssuer,
string channelService,
string oauthRedirectUrl,
string graphScope = "https://graph.microsoft.com/.default")
{
LoginEndpoint = loginEndpoint;
LoginTenant = loginTenant;
BotScope = botScope;
TokenServiceUrl = tokenServiceUrl;
OpenIdMetadataUrl = openIdMetadataUrl;
TokenIssuer = tokenIssuer;
ChannelService = channelService;
OAuthRedirectUrl = oauthRedirectUrl;
GraphScope = graphScope;
}

/// <summary>
/// Microsoft public (commercial) cloud.
/// </summary>
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",
graphScope: "https://graph.microsoft.com/.default"
);

/// <summary>
/// US Government Community Cloud High (GCCH).
/// </summary>
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",
graphScope: "https://graph.microsoft.us/.default"
);

/// <summary>
/// US Government Department of Defense (DoD).
/// </summary>
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",
graphScope: "https://dod-graph.microsoft.us/.default"
);

/// <summary>
/// China cloud (21Vianet).
/// </summary>
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",
graphScope: "https://microsoftgraph.chinacloudapi.cn/.default"
);

/// <summary>
/// Creates a new <see cref="CloudEnvironment"/> by applying non-null overrides on top of this instance.
/// Returns the same instance if all overrides are null (no allocation).
/// </summary>
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,
string? graphScope = null)
{
// Normalize empty/whitespace to null so blank config values don't override valid defaults
loginEndpoint = NullIfWhiteSpace(loginEndpoint);
loginTenant = NullIfWhiteSpace(loginTenant);
botScope = NullIfWhiteSpace(botScope);
tokenServiceUrl = NullIfWhiteSpace(tokenServiceUrl);
openIdMetadataUrl = NullIfWhiteSpace(openIdMetadataUrl);
tokenIssuer = NullIfWhiteSpace(tokenIssuer);
channelService = NullIfWhiteSpace(channelService);
oauthRedirectUrl = NullIfWhiteSpace(oauthRedirectUrl);
graphScope = NullIfWhiteSpace(graphScope);

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 && graphScope is null)
{
return this;
}

return new CloudEnvironment(
loginEndpoint ?? LoginEndpoint,
loginTenant ?? LoginTenant,
botScope ?? BotScope,
tokenServiceUrl ?? TokenServiceUrl,
openIdMetadataUrl ?? OpenIdMetadataUrl,
tokenIssuer ?? TokenIssuer,
channelService ?? ChannelService,
oauthRedirectUrl ?? OAuthRedirectUrl,
graphScope ?? GraphScope
);
}
Comment thread
corinagum marked this conversation as resolved.

private static string? NullIfWhiteSpace(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value;

/// <summary>
/// Resolves a cloud environment name (case-insensitive) to its corresponding instance.
/// Valid names: "Public", "USGov", "USGovDoD", "China".
/// </summary>
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))
};
}
6 changes: 4 additions & 2 deletions Libraries/Microsoft.Teams.Api/Clients/BotSignInClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace Microsoft.Teams.Api.Clients;

public class BotSignInClient : Client
{
public string TokenServiceUrl { get; set; } = "https://token.botframework.com";

public BotSignInClient() : base()
{

Expand All @@ -32,7 +34,7 @@ public async Task<string> GetUrlAsync(GetUrlRequest request, CancellationToken c
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
var req = HttpRequest.Get(
$"https://token.botframework.com/api/botsignin/GetSignInUrl?{query}"
$"{TokenServiceUrl}/api/botsignin/GetSignInUrl?{query}"
);

var res = await _http.SendAsync(req, token);
Expand All @@ -44,7 +46,7 @@ public async Task<string> GetUrlAsync(GetUrlRequest request, CancellationToken c
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
var req = HttpRequest.Get(
$"https://token.botframework.com/api/botsignin/GetSignInResource?{query}"
$"{TokenServiceUrl}/api/botsignin/GetSignInResource?{query}"
);

var res = await _http.SendAsync<SignIn.UrlResponse>(req, token);
Expand Down
3 changes: 2 additions & 1 deletion Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class BotTokenClient : Client
{
public static readonly string BotScope = "https://api.botframework.com/.default";
public static readonly string GraphScope = "https://graph.microsoft.com/.default";
public string ActiveBotScope { get; set; } = BotScope;

public BotTokenClient() : this(default)
{
Expand Down Expand Up @@ -38,7 +39,7 @@ public BotTokenClient(IHttpClientFactory factory, CancellationToken cancellation
public virtual async Task<ITokenResponse> GetAsync(IHttpCredentials credentials, IHttpClient? http = null, CancellationToken cancellationToken = default)
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
return await credentials.Resolve(http ?? _http, [BotScope], token);
return await credentials.Resolve(http ?? _http, [ActiveBotScope], token);
}

public async Task<ITokenResponse> GetGraphAsync(IHttpCredentials credentials, IHttpClient? http = null, CancellationToken cancellationToken = default)
Expand Down
12 changes: 7 additions & 5 deletions Libraries/Microsoft.Teams.Api/Clients/UserTokenClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ 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
Expand Down Expand Up @@ -39,7 +41,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
var req = HttpRequest.Get($"https://token.botframework.com/api/usertoken/GetToken?{query}");
var req = HttpRequest.Get($"{TokenServiceUrl}/api/usertoken/GetToken?{query}");
var res = await _http.SendAsync<Token.Response>(req, token);
return res.Body;
}
Expand All @@ -48,7 +50,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
var req = HttpRequest.Post($"https://token.botframework.com/api/usertoken/GetAadTokens?{query}", body: request);
var req = HttpRequest.Post($"{TokenServiceUrl}/api/usertoken/GetAadTokens?{query}", body: request);
var res = await _http.SendAsync<IDictionary<string, Token.Response>>(req, token);
return res.Body;
}
Expand All @@ -57,7 +59,7 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
var req = HttpRequest.Get($"https://token.botframework.com/api/usertoken/GetTokenStatus?{query}");
var req = HttpRequest.Get($"{TokenServiceUrl}/api/usertoken/GetTokenStatus?{query}");
var res = await _http.SendAsync<IList<Token.Status>>(req, token);
return res.Body;
}
Expand All @@ -66,7 +68,7 @@ public async Task SignOutAsync(SignOutRequest request, CancellationToken cancell
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
var query = QueryString.Serialize(request);
var req = HttpRequest.Delete($"https://token.botframework.com/api/usertoken/SignOut?{query}");
var req = HttpRequest.Delete($"{TokenServiceUrl}/api/usertoken/SignOut?{query}");
await _http.SendAsync(req, token);
}

Expand All @@ -84,7 +86,7 @@ public async Task SignOutAsync(SignOutRequest request, CancellationToken cancell
// 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($"https://token.botframework.com/api/usertoken/exchange?{query}", body);
var req = HttpRequest.Post($"{TokenServiceUrl}/api/usertoken/exchange?{query}", body);
req.Headers.Add("Content-Type", new List<string>() { "application/json" });

var res = await _http.SendAsync<Token.Response>(req, token);
Expand Down
7 changes: 6 additions & 1 deletion Libraries/Microsoft.Teams.Apps/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ internal string UserAgent

public App(AppOptions? options = null)
{
var cloud = options?.Cloud ?? CloudEnvironment.Public;

Logger = options?.Logger ?? new ConsoleLogger();
Storage = options?.Storage ?? new LocalStorage<object>();
Credentials = options?.Credentials;
Expand All @@ -77,7 +79,7 @@ public App(AppOptions? options = null)

if (Token.IsExpired)
{
var res = Credentials.Resolve(TokenClient, [.. Token.Scopes.DefaultIfEmpty(BotTokenClient.BotScope)])
var res = Credentials.Resolve(TokenClient, [.. Token.Scopes.DefaultIfEmpty(Api!.Bots.Token.ActiveBotScope)])
.ConfigureAwait(false)
.GetAwaiter()
.GetResult();
Expand All @@ -90,6 +92,9 @@ public App(AppOptions? options = null)
};

Api = new ApiClient("https://smba.trafficmanager.net/teams/", Client);
Api.Bots.Token.ActiveBotScope = cloud.BotScope;
Api.Bots.SignIn.TokenServiceUrl = cloud.TokenServiceUrl;
Api.Users.Token.TokenServiceUrl = cloud.TokenServiceUrl;
Container = new Container();
Container.Register(Logger);
Container.Register(Storage);
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Microsoft.Teams.Apps/AppOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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;
Expand All @@ -15,6 +16,7 @@ public class AppOptions
public Common.Http.IHttpCredentials? Credentials { get; set; }
public IList<IPlugin> Plugins { get; set; } = [];
public OAuthSettings OAuth { get; set; } = new OAuthSettings();
public CloudEnvironment? Cloud { get; set; }

public AppOptions()
{
Expand Down
Loading
Loading