Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

# local app settings files
appsettings.Local.json
appsettings.Development.json
launchsettings.json

# User-specific files
*.rsuser
Expand Down Expand Up @@ -486,3 +488,6 @@ $RECYCLE.BIN/

# Vim temporary swap files
*.swp

# Claude local settings
.claude/settings.local.json
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
175 changes: 175 additions & 0 deletions Libraries/Microsoft.Teams.Api/Auth/CloudEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// 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; }

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;
}

/// <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"
);

/// <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"
);

/// <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"
);

/// <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"
);

/// <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)
{
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
);
}

/// <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 @@ -31,7 +33,7 @@ public async Task<string> GetUrlAsync(GetUrlRequest request)
{
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, _cancellationToken);
Expand All @@ -42,7 +44,7 @@ public async Task<string> GetUrlAsync(GetUrlRequest request)
{
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, _cancellationToken);
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 @@ -37,7 +38,7 @@ public BotTokenClient(IHttpClientFactory factory, CancellationToken cancellation

public virtual async Task<ITokenResponse> GetAsync(IHttpCredentials credentials, IHttpClient? http = null)
{
return await credentials.Resolve(http ?? _http, [BotScope], _cancellationToken);
return await credentials.Resolve(http ?? _http, [ActiveBotScope], _cancellationToken);
}

public async Task<ITokenResponse> GetGraphAsync(IHttpCredentials credentials, IHttpClient? http = null)
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 @@ -38,31 +40,31 @@ public UserTokenClient(IHttpClientFactory factory, CancellationToken cancellatio
public async Task<Token.Response> GetAsync(GetTokenRequest request)
{
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, _cancellationToken);
return res.Body;
}

public async Task<IDictionary<string, Token.Response>> GetAadAsync(GetAadTokenRequest request)
{
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, _cancellationToken);
return res.Body;
}

public async Task<IList<Token.Status>> GetStatusAsync(GetTokenStatusRequest request)
{
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, _cancellationToken);
return res.Body;
}

public async Task SignOutAsync(SignOutRequest request)
{
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, _cancellationToken);
}

Expand All @@ -79,7 +81,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($"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, _cancellationToken);
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