Skip to content
Merged
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;
Comment thread
corinagum marked this conversation as resolved.
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
171 changes: 171 additions & 0 deletions Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// 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 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 graphScope)
{
LoginEndpoint = loginEndpoint.TrimEnd('/');
LoginTenant = loginTenant;
BotScope = botScope;
TokenServiceUrl = tokenServiceUrl.TrimEnd('/');
OpenIdMetadataUrl = openIdMetadataUrl;
TokenIssuer = tokenIssuer;
GraphScope = graphScope;
}
Comment thread
corinagum marked this conversation as resolved.

/// <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",
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",
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",
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",
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? graphScope = null)
{
if (loginEndpoint is null && loginTenant is null && botScope is null &&
tokenServiceUrl is null && openIdMetadataUrl is null && tokenIssuer is null &&
graphScope is null)
{
return this;
}

return new CloudEnvironment(
loginEndpoint ?? LoginEndpoint,
loginTenant ?? LoginTenant,
botScope ?? BotScope,
tokenServiceUrl ?? TokenServiceUrl,
openIdMetadataUrl ?? OpenIdMetadataUrl,
tokenIssuer ?? TokenIssuer,
graphScope ?? GraphScope
);
}

/// <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)
{
ArgumentNullException.ThrowIfNull(name);

if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Cloud environment name cannot be empty or whitespace.", nameof(name));
}

return 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))
};
}
}
4 changes: 4 additions & 0 deletions Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,12 @@ public ApiClient(ApiClient client, CancellationToken cancellationToken) : base(c
{
ServiceUrl = client.ServiceUrl;
Bots = new BotClient(_http, cancellationToken);
Bots.Token.ActiveBotScope = client.Bots.Token.ActiveBotScope;
Bots.Token.ActiveGraphScope = client.Bots.Token.ActiveGraphScope;
Bots.SignIn.TokenServiceUrl = client.Bots.SignIn.TokenServiceUrl;
Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken);
Users = new UserClient(_http, cancellationToken);
Users.Token.TokenServiceUrl = client.Users.Token.TokenServiceUrl;
Teams = new TeamClient(ServiceUrl, _http, cancellationToken);
Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken);
}
Expand Down
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";

Comment thread
corinagum marked this conversation as resolved.
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}"
);
Comment thread
corinagum marked this conversation as resolved.

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
6 changes: 4 additions & 2 deletions Libraries/Microsoft.Teams.Api/Clients/BotTokenClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ 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 string ActiveGraphScope { get; set; } = GraphScope;

public BotTokenClient() : this(default)
{
Expand Down Expand Up @@ -38,12 +40,12 @@ 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)
{
var token = cancellationToken != default ? cancellationToken : _cancellationToken;
return await credentials.Resolve(http ?? _http, [GraphScope], token);
return await credentials.Resolve(http ?? _http, [ActiveGraphScope], token);
}
}
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";

Comment thread
corinagum marked this conversation as resolved.
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);
Comment thread
corinagum marked this conversation as resolved.
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
8 changes: 7 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;

Comment thread
corinagum marked this conversation as resolved.
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,10 @@ public App(AppOptions? options = null)
};

Api = new ApiClient("https://smba.trafficmanager.net/teams/", Client);
Api.Bots.Token.ActiveBotScope = cloud.BotScope;
Api.Bots.Token.ActiveGraphScope = cloud.GraphScope;
Api.Bots.SignIn.TokenServiceUrl = cloud.TokenServiceUrl;
Api.Users.Token.TokenServiceUrl = cloud.TokenServiceUrl;
Comment thread
corinagum marked this conversation as resolved.
Comment thread
corinagum marked this conversation as resolved.
Container = new Container();
Container.Register(Logger);
Container.Register(Storage);
Expand Down
7 changes: 7 additions & 0 deletions Libraries/Microsoft.Teams.Apps/AppBuilder.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 Down Expand Up @@ -98,6 +99,12 @@ public AppBuilder AddOAuth(string defaultConnectionName)
return this;
}

public AppBuilder AddCloud(CloudEnvironment cloud)
{
_options.Cloud = cloud;
return this;
}

public App Build()
{
return new App(_options);
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