-
Notifications
You must be signed in to change notification settings - Fork 15
Use MSAL to mint tokens #203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,46 +1,24 @@ | ||||||||||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||||
| // Licensed under the MIT License. | ||||||||||
|
|
||||||||||
| using Microsoft.Identity.Abstractions; | ||||||||||
| using Microsoft.Teams.Common.Http; | ||||||||||
|
|
||||||||||
| namespace Microsoft.Teams.Api.Auth; | ||||||||||
|
|
||||||||||
| public class ClientCredentials : IHttpCredentials | ||||||||||
| public class ClientCredentials(IAuthorizationHeaderProvider authorizationHeaderProvider) : IHttpCredentials | ||||||||||
| { | ||||||||||
| public string ClientId { get; set; } | ||||||||||
| public string ClientSecret { get; set; } | ||||||||||
| public string? TenantId { get; set; } | ||||||||||
|
|
||||||||||
| public ClientCredentials(string clientId, string clientSecret) | ||||||||||
| { | ||||||||||
| ClientId = clientId; | ||||||||||
| ClientSecret = clientSecret; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| public ClientCredentials(string clientId, string clientSecret, string? tenantId) | ||||||||||
| { | ||||||||||
| ClientId = clientId; | ||||||||||
| ClientSecret = clientSecret; | ||||||||||
| TenantId = tenantId; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| public async Task<ITokenResponse> Resolve(IHttpClient client, string[] scopes, CancellationToken cancellationToken = default) | ||||||||||
| { | ||||||||||
| var tenantId = TenantId ?? "botframework.com"; | ||||||||||
| var request = HttpRequest.Post( | ||||||||||
| $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token" | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| request.Headers.Add("Content-Type", ["application/x-www-form-urlencoded"]); | ||||||||||
| request.Body = new Dictionary<string, string>() | ||||||||||
| AuthorizationHeaderProviderOptions options = new(); | ||||||||||
| options.AcquireTokenOptions = new AcquireTokenOptions() | ||||||||||
| { | ||||||||||
| { "grant_type", "client_credentials" }, | ||||||||||
| { "client_id", ClientId }, | ||||||||||
| { "client_secret", ClientSecret }, | ||||||||||
| { "scope", string.Join(",", scopes) } | ||||||||||
| }; | ||||||||||
|
Comment on lines
+13
to
16
|
||||||||||
|
|
||||||||||
| var res = await client.SendAsync<TokenResponse>(request, cancellationToken); | ||||||||||
| return res.Body; | ||||||||||
| var tokenResult = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(scopes[0], options, cancellationToken); | ||||||||||
|
||||||||||
| var tokenResult = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(scopes[0], options, cancellationToken); | |
| var tokenResult = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(string.Join(" ", scopes), options, cancellationToken); |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Substring call will throw an ArgumentOutOfRangeException if tokenResult is shorter than "Bearer " (7 characters) or doesn't start with "Bearer ". Consider using a safer approach like:
AccessToken = tokenResult.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
? tokenResult.Substring("Bearer ".Length)
: tokenResult,| AccessToken = tokenResult.Substring("Bearer ".Length), | |
| AccessToken = tokenResult.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) | |
| ? tokenResult.Substring("Bearer ".Length) | |
| : tokenResult, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,8 @@ | |
| using Microsoft.Teams.Api.Activities; | ||
| using Microsoft.Teams.Common.Http; | ||
|
|
||
| using IHttpClientFactory = Microsoft.Teams.Common.Http.IHttpClientFactory; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could not find a better way to avoid namespace conflicts |
||
|
|
||
| namespace Microsoft.Teams.Api.Clients; | ||
|
|
||
| public class ActivityClient : Client | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,11 +52,14 @@ internal string UserAgent | |
| } | ||
| } | ||
|
|
||
| public App(AppOptions? options = null) | ||
| internal App() : this(null!, null) | ||
| { } | ||
|
|
||
| public App(IHttpCredentials credentials, AppOptions? options = null) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to register our credentials in the DI, so we can get receive the IAuthorizationHeaderProvider |
||
| { | ||
| Logger = options?.Logger ?? new ConsoleLogger(); | ||
| Storage = options?.Storage ?? new LocalStorage<object>(); | ||
| Credentials = options?.Credentials; | ||
| Credentials = credentials; | ||
| Plugins = options?.Plugins ?? []; | ||
| OAuth = options?.OAuth ?? new OAuthSettings(); | ||
| Provider = options?.Provider; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,16 +1,26 @@ | ||||||||||||||||||||||||||||||||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||||||||||||||||||||||||||
| // Licensed under the MIT License. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| using Microsoft.Extensions.DependencyInjection; | ||||||||||||||||||||||||||||||||
| using Microsoft.Teams.Apps.Plugins; | ||||||||||||||||||||||||||||||||
| using Microsoft.Teams.Common.Http; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| namespace Microsoft.Teams.Apps; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public partial class AppBuilder | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| private readonly IServiceProvider _serviceProvider; | ||||||||||||||||||||||||||||||||
| protected AppOptions _options; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public AppBuilder(IServiceProvider serviceProvider) | ||||||||||||||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is an idea to consolidate our AppBuilder with the service provider.. not perfect, but it works :) |
||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| _serviceProvider = serviceProvider; | ||||||||||||||||||||||||||||||||
| _options = serviceProvider.GetService(typeof(AppOptions)) as AppOptions ?? throw new InvalidOperationException("AppOptions not found in DI container"); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public AppBuilder(AppOptions? options = null) | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| _serviceProvider = null!; | ||||||||||||||||||||||||||||||||
| _options = options ?? new AppOptions(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -100,6 +110,6 @@ public AppBuilder AddOAuth(string defaultConnectionName) | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| public App Build() | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| return new App(_options); | ||||||||||||||||||||||||||||||||
| return new App(_serviceProvider.GetService<IHttpCredentials>()!, _options); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
| return new App(_serviceProvider.GetService<IHttpCredentials>()!, _options); | |
| IHttpCredentials? credentials = null; | |
| if (_serviceProvider != null) | |
| { | |
| credentials = _serviceProvider.GetService<IHttpCredentials>(); | |
| } | |
| if (credentials == null) | |
| { | |
| credentials = _options.Credentials; | |
| } | |
| if (credentials == null) | |
| { | |
| throw new InvalidOperationException("No IHttpCredentials available. Provide credentials via AddCredentials or ensure they are registered in the service provider."); | |
| } | |
| return new App(credentials, _options); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
|
|
||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Hosting; | ||
| using Microsoft.Teams.Api.Auth; | ||
| // using Microsoft.Teams.Api.Auth; | ||
| using Microsoft.Teams.Apps.Plugins; | ||
| using Microsoft.Teams.Common.Logging; | ||
| using Microsoft.Teams.Extensions.Logging; | ||
|
|
@@ -32,22 +32,22 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder | |
| var loggingSettings = builder.Configuration.GetTeamsLogging(); | ||
|
|
||
| // client credentials | ||
| if (options.Credentials is null && settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) | ||
| { | ||
| options.Credentials = new ClientCredentials( | ||
| settings.ClientId, | ||
| settings.ClientSecret, | ||
| settings.TenantId | ||
| ); | ||
| } | ||
| //if (options.Credentials is null && settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Credentials are now managed by M.I.W, I leave this as commented just to see the diff, if we al agree I can delete the commented code |
||
| //{ | ||
| // options.Credentials = new ClientCredentials( | ||
| // settings.ClientId, | ||
| // settings.ClientSecret, | ||
| // settings.TenantId | ||
| // ); | ||
| //} | ||
|
|
||
| options.Logger ??= new ConsoleLogger(loggingSettings); | ||
| var app = new App(options); | ||
|
|
||
| //var app = new App(options); | ||
| builder.Services.AddSingleton<App>(); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is the key part.. to properly register our app in DI. |
||
| builder.Services.AddSingleton(settings); | ||
| builder.Services.AddSingleton(loggingSettings); | ||
| builder.Logging.AddTeams(app.Logger); | ||
| builder.Services.AddTeams(app); | ||
| //builder.Logging.AddTeams(app.Logger); | ||
| //builder.Services.AddTeams(app); | ||
| return builder; | ||
| } | ||
|
|
||
|
|
@@ -57,14 +57,14 @@ public static IHostApplicationBuilder AddTeamsCore(this IHostApplicationBuilder | |
| var loggingSettings = builder.Configuration.GetTeamsLogging(); | ||
|
|
||
| // client credentials | ||
| if (settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) | ||
| { | ||
| appBuilder = appBuilder.AddCredentials(new ClientCredentials( | ||
| settings.ClientId, | ||
| settings.ClientSecret, | ||
| settings.TenantId | ||
| )); | ||
| } | ||
| //if (settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) | ||
| //{ | ||
| // appBuilder = appBuilder.AddCredentials(new ClientCredentials( | ||
| // settings.ClientId, | ||
| // settings.ClientSecret, | ||
| // settings.TenantId | ||
| // )); | ||
| //} | ||
|
|
||
| var app = appBuilder.Build(); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,20 +37,20 @@ public static IServiceCollection AddTeams(this IServiceCollection collection) | |
| return collection; | ||
| } | ||
|
|
||
| public static IServiceCollection AddTeams(this IServiceCollection collection, AppOptions options) | ||
| { | ||
| var app = new App(options); | ||
| var log = new TeamsLogger(app.Logger); | ||
|
|
||
| collection.AddSingleton(app.Logger); | ||
| collection.AddSingleton(app.Storage); | ||
| collection.AddSingleton<ILoggerFactory>(_ => new LoggerFactory([new TeamsLoggerProvider(log)])); | ||
| collection.AddSingleton<ILogger>(log); | ||
| collection.AddSingleton(app); | ||
| collection.AddHostedService<TeamsService>(); | ||
| collection.AddSingleton<IContext.Accessor>(); | ||
| return collection; | ||
| } | ||
| //public static IServiceCollection AddTeams(this IServiceCollection collection, AppOptions options) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this method was not called anywhere |
||
| //{ | ||
| // var app = new App(options); | ||
| // var log = new TeamsLogger(app.Logger); | ||
|
|
||
| // collection.AddSingleton(app.Logger); | ||
| // collection.AddSingleton(app.Storage); | ||
| // collection.AddSingleton<ILoggerFactory>(_ => new LoggerFactory([new TeamsLoggerProvider(log)])); | ||
| // collection.AddSingleton<ILogger>(log); | ||
| // collection.AddSingleton(app); | ||
| // collection.AddHostedService<TeamsService>(); | ||
| // collection.AddSingleton<IContext.Accessor>(); | ||
| // return collection; | ||
| //} | ||
|
|
||
| public static IServiceCollection AddTeams(this IServiceCollection collection, AppBuilder builder) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||
| using Microsoft.Teams.Apps; | ||||||
| using Microsoft.Teams.Apps.Annotations; | ||||||
| using Microsoft.Teams.Apps.Plugins; | ||||||
| using Microsoft.Teams.Common.Http; | ||||||
|
|
||||||
| namespace Microsoft.Teams.Plugins.AspNetCore.Extensions; | ||||||
|
|
||||||
|
|
@@ -22,7 +23,7 @@ public static partial class ApplicationBuilderExtensions | |||||
| public static App UseTeams(this IApplicationBuilder builder, bool routing = true) | ||||||
| { | ||||||
| var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly(); | ||||||
| var app = builder.ApplicationServices.GetService<App>() ?? new App(builder.ApplicationServices.GetService<AppOptions>()); | ||||||
| var app = builder.ApplicationServices.GetService<App>() ?? new App(builder.ApplicationServices.GetService<IHttpCredentials>()!,builder.ApplicationServices.GetService<AppOptions>()); | ||||||
|
||||||
| var app = builder.ApplicationServices.GetService<App>() ?? new App(builder.ApplicationServices.GetService<IHttpCredentials>()!,builder.ApplicationServices.GetService<AppOptions>()); | |
| var app = builder.ApplicationServices.GetService<App>() ?? new App(builder.ApplicationServices.GetService<IHttpCredentials>()!, builder.ApplicationServices.GetService<AppOptions>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
replaced to use M.I.W that already handles different credential types