-
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?
Conversation
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
| using Microsoft.Teams.Api.Activities; | ||
| using Microsoft.Teams.Common.Http; | ||
|
|
||
| using IHttpClientFactory = Microsoft.Teams.Common.Http.IHttpClientFactory; |
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.
could not find a better way to avoid namespace conflicts
| internal App() : this(null!, null) | ||
| { } | ||
|
|
||
| public App(IHttpCredentials credentials, AppOptions? options = null) |
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.
we need to register our credentials in the DI, so we can get receive the IAuthorizationHeaderProvider
| private readonly IServiceProvider _serviceProvider; | ||
| protected AppOptions _options; | ||
|
|
||
| public AppBuilder(IServiceProvider serviceProvider) |
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.
this is an idea to consolidate our AppBuilder with the service provider.. not perfect, but it works :)
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.
Pull Request Overview
This pull request modernizes the authentication mechanism by migrating from custom OAuth2 token acquisition to Microsoft Authentication Library (MSAL) via IAuthorizationHeaderProvider. The changes introduce dependency injection patterns throughout the codebase, update NuGet packages to support MSAL, and refactor the App and AppBuilder classes to require credentials through constructor injection rather than options.
- Refactored
ClientCredentialsto use MSAL'sIAuthorizationHeaderProviderinstead of manually calling token endpoints - Updated dependency injection setup across plugins and extensions to support the new authentication flow
- Modified
Appconstructor signature to requireIHttpCredentialsas a constructor parameter
Reviewed Changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| Libraries/Microsoft.Teams.Api/Auth/ClientCredentials.cs | Replaced manual OAuth2 token acquisition with MSAL IAuthorizationHeaderProvider |
| Libraries/Microsoft.Teams.Api/Microsoft.Teams.Api.csproj | Added Microsoft.Identity.Web.AgentIdentities and updated System.IdentityModel.Tokens.Jwt |
| Libraries/Microsoft.Teams.Apps/App.cs | Changed constructor to require IHttpCredentials as first parameter |
| Libraries/Microsoft.Teams.Apps/AppBuilder.cs | Added service provider-based constructor and updated Build() to inject credentials |
| Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs | Added MSAL token acquisition services and configured DI for credentials |
| Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/ApplicationBuilder.cs | Updated App instantiation to pass credentials via constructor |
| Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/ServiceCollection.cs | Commented out old AddTeams overload that created App without DI |
| Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Hosting/Microsoft.Teams.Apps.Extensions/HostApplicationBuilder.cs | Commented out manual ClientCredentials setup, switched to DI-based App registration |
| Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs | Commented out credential setup logic in Apply method |
| Tests/Microsoft.Teams.Apps.Tests/AppTests.cs | Updated all test cases to pass credentials as constructor parameter |
| Samples/Samples.Echo/appsettings.json | Updated configuration structure for MSAL with Instance, TenantId, and ClientCredentials array |
| Samples/Samples.Echo/appsettings.Development.json | Updated configuration structure for MSAL |
| Samples/Samples.Echo/Program.cs | Simplified sample to use new Teams extension methods |
| Libraries/Microsoft.Teams.Api/Clients/*.cs | Added IHttpClientFactory alias for consistency (12 client files) |
| Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore.DevTools/Microsoft.Teams.Plugins.AspNetCore.DevTools.csproj | Updated System.IdentityModel.Tokens.Jwt version |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #pragma warning disable ASP0000 // Use 'new(...)' | ||
| AppBuilder appBuilder = new AppBuilder(builder.Services.BuildServiceProvider()); | ||
| #pragma warning restore ASP0000 | ||
|
|
||
| builder.AddTeamsCore(appBuilder); |
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 BuildServiceProvider() call creates a service provider during configuration, which is an anti-pattern in ASP.NET Core. This can lead to:
- Memory leaks from captured scoped services
- Multiple service provider instances with inconsistent state
- Services resolved before the final container is built
Consider refactoring to avoid building the service provider here. Instead, register the AppBuilder as a factory or use a different approach to defer the App creation until after the container is fully configured.
| #pragma warning disable ASP0000 // Use 'new(...)' | |
| AppBuilder appBuilder = new AppBuilder(builder.Services.BuildServiceProvider()); | |
| #pragma warning restore ASP0000 | |
| builder.AddTeamsCore(appBuilder); | |
| // Register AppBuilder as a singleton using the final service provider | |
| builder.Services.AddSingleton<AppBuilder>(sp => new AppBuilder(sp)); | |
| builder.AddTeamsCore(); |
| var tokenResult = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(scopes[0], options, cancellationToken); | ||
| return new TokenResponse | ||
| { | ||
| AccessToken = tokenResult.Substring("Bearer ".Length), |
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, |
|
|
||
| var res = await client.SendAsync<TokenResponse>(request, cancellationToken); | ||
| return res.Body; | ||
| var tokenResult = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(scopes[0], 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.
Only the first scope (scopes[0]) is being used, but the method accepts an array. This could lead to unexpected behavior if callers pass multiple scopes. Consider either:
- Joining all scopes with a space delimiter (standard OAuth2 practice):
string.Join(" ", scopes) - Changing the parameter type to
string scopeif only one scope is supported - Documenting that only the first scope is used
| var tokenResult = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(scopes[0], options, cancellationToken); | |
| var tokenResult = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(string.Join(" ", scopes), options, cancellationToken); |
| public App Build() | ||
| { | ||
| return new App(_options); | ||
| return new App(_serviceProvider.GetService<IHttpCredentials>()!, _options); |
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 Build() method will throw a NullReferenceException when called on an AppBuilder instance created with the parameterless constructor (line 21-25), since _serviceProvider is set to null!. Consider adding a null check and appropriate error handling:
public App Build()
{
var credentials = _serviceProvider?.GetService<IHttpCredentials>();
return new App(credentials!, _options);
}Or throw a more descriptive exception if _serviceProvider is null.
| 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); |
| Credentials = credentials.Object, | ||
| }; | ||
| var app = new App(options); | ||
| var app = new App(credentials.Object,options); |
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.
Missing space after comma in constructor call. Should be:
var app = new App(credentials.Object, options);| var app = new App(credentials.Object,options); | |
| var app = new App(credentials.Object, options); |
| Credentials = null, | ||
| }; | ||
| var app = new App(options); | ||
| var app = new App(null!, options); |
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.
Using null! (null-forgiving operator) to pass credentials suggests the IHttpCredentials parameter in the App constructor should be nullable. Consider making the parameter nullable (IHttpCredentials? credentials) to better reflect that null is an acceptable value, as demonstrated by the test case "Test_App_Start_DoesNot_GetBotToken_WhenNoCredentials".
| var app = new App(null!, options); | |
| var app = new App(null, options); |
| { | ||
| 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>()); |
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.
Missing space after comma in constructor call. Should be:
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>()); |
| AuthorizationHeaderProviderOptions options = new(); | ||
| options.AcquireTokenOptions = new AcquireTokenOptions() | ||
| { | ||
| { "grant_type", "client_credentials" }, | ||
| { "client_id", ClientId }, | ||
| { "client_secret", ClientSecret }, | ||
| { "scope", string.Join(",", scopes) } | ||
| }; |
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 AcquireTokenOptions is instantiated but left empty. Consider either:
- Removing these lines if no options are needed:
AuthorizationHeaderProviderOptions options = new(); - Adding a comment explaining why an empty options object is required
- Passing
nullif the API supports it
This will improve code clarity and reduce unnecessary object allocation.
| Credentials = null, | ||
| }; | ||
| var app = new App(options); | ||
| var app = new App(null!, options); |
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.
This assignment to app is useless, since its value is never read.
| var app = new App(null!, options); |
| settings.TenantId | ||
| ); | ||
| } | ||
| //if (options.Credentials is null && settings.ClientId is not null && settings.ClientSecret is not null && !settings.Empty) |
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.
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
| var app = new App(options); | ||
|
|
||
| //var app = new App(options); | ||
| builder.Services.AddSingleton<App>(); |
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.
this is the key part.. to properly register our app in DI.
| collection.AddSingleton<IContext.Accessor>(); | ||
| return collection; | ||
| } | ||
| //public static IServiceCollection AddTeams(this IServiceCollection collection, AppOptions options) |
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.
this method was not called anywhere
| builder.Services.AddSingleton<AppOptions>(); | ||
| builder.Services.Configure<MicrosoftIdentityApplicationOptions>(builder.Configuration.GetSection(authSectionName)); | ||
| #pragma warning disable ASP0000 // Use 'new(...)' | ||
| AppBuilder appBuilder = new AppBuilder(builder.Services.BuildServiceProvider()); |
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.
this is the worst part of this approach.. Without revisiting DI, App, AppBuilder and AppOptions I could not find a better way
| "Level": "debug" | ||
| } | ||
| }, | ||
| "Teams": { |
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.
This is the config provided by M.I.W. I just renamed to Teams
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.
I wanted to refactor the Echo to use minimal APIs
This pull request introduces significant changes to the authentication mechanism and dependency injection setup in the Teams API and Apps libraries. The main update is the refactoring of the
ClientCredentialsclass to useIAuthorizationHeaderProviderfor token acquisition, moving away from direct management of client ID/secret. Additionally, the application initialization and dependency injection patterns are updated to support these changes, and related code paths for credentials setup are commented out to reflect the new approach. Package references are also updated to support these changes.Authentication Refactor:
ClientCredentialsto depend onIAuthorizationHeaderProvider, removing direct handling of client ID, client secret, and tenant ID, and updating theResolvemethod to use the new provider for token acquisition.Microsoft.Identity.Web.AgentIdentitiesand updatedSystem.IdentityModel.Tokens.Jwtto a newer version to support the new authentication flow. [1] [2]Dependency Injection and App Initialization:
AppandAppBuilderto requireIHttpCredentialsvia dependency injection, and adjusted the construction and registration ofAppinstances to use DI instead of manual instantiation. [1] [2] [3]Codebase Consistency:
IHttpClientFactoryalias imports across all API client classes for consistency and clarity. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12]Project File and Copyright
Miscellaneous
usingstatements and credential setup code in extension/configuration classes. [1] [2]These changes collectively modernize the authentication flow, improve testability and maintainability via dependency injection, and update the codebase for compatibility with newer identity libraries.