Fix UnauthorizedAccessException with skipAuth#297
Conversation
When using `builder.AddTeams(skipAuth: true)`, the app was throwing UnauthorizedAccessException from ExtractToken() even though the authorization policy was correctly bypassed. - Make ActivityEvent.Token nullable instead of required - Update ExtractToken() to return null when no Authorization header - Add null-conditional access to Token properties in App.cs - Add tests for skipAuth scenario Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes issue #274 where UnauthorizedAccessException was thrown when using skipAuth: true mode. The fix makes the authentication token optional throughout the Teams SDK to support anonymous/unauthenticated scenarios.
Changes:
- Modified
ExtractToken()to returnnullinstead of throwing an exception when no Authorization header is present - Changed
ActivityEvent.Tokenfrom required to nullable to allow activities without authentication - Added null-conditional operators when accessing Token properties in App.cs
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs | Changed ExtractToken() to return null when Authorization header is missing instead of throwing |
| Libraries/Microsoft.Teams.Apps/Events/ActivityEvent.cs | Made Token property nullable to support unauthenticated scenarios |
| Libraries/Microsoft.Teams.Apps/App.cs | Added null-conditional operators when accessing Token.ServiceUrl, Token.AppId, and Token.TenantId |
| Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs | Added tests for ExtractToken returning null and Do() working without auth header |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Create AnonymousToken class for skipAuth scenarios (matches Python/TypeScript behavior) - Add NormalizeServiceUrl helper to ensure trailing slash on serviceUrl - Fix 404 errors when AgentsPlayground sends serviceUrl without trailing slash - Update tests to verify AnonymousToken is used when no auth header Manually tested with AgentsPlayground - bot responses work correctly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (4)
Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs:24
- The nested client instantiations (Conversations, Teams, Meetings) receive the unnormalized
serviceUrlparameter instead of the normalizedServiceUrlproperty. This means that even though the ApiClient's ServiceUrl property is normalized, the nested clients will receive URLs without trailing slashes, potentially causing 404 errors.
The pattern should be:
Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken);
Teams = new TeamClient(ServiceUrl, _http, cancellationToken);
Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken);
This applies to lines 21, 23-24 in the first constructor.
Conversations = new ConversationClient(serviceUrl, _http, cancellationToken);
Users = new UserClient(_http, cancellationToken);
Teams = new TeamClient(serviceUrl, _http, cancellationToken);
Meetings = new MeetingClient(serviceUrl, _http, cancellationToken);
Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs:34
- The nested client instantiations (Conversations, Teams, Meetings) receive the unnormalized
serviceUrlparameter instead of the normalizedServiceUrlproperty. This means that even though the ApiClient's ServiceUrl property is normalized, the nested clients will receive URLs without trailing slashes, potentially causing 404 errors.
The pattern should be:
Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken);
Teams = new TeamClient(ServiceUrl, _http, cancellationToken);
Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken);
Conversations = new ConversationClient(serviceUrl, _http, cancellationToken);
Users = new UserClient(_http, cancellationToken);
Teams = new TeamClient(serviceUrl, _http, cancellationToken);
Meetings = new MeetingClient(serviceUrl, _http, cancellationToken);
Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs:44
- The nested client instantiations (Conversations, Teams, Meetings) receive the unnormalized
serviceUrlparameter instead of the normalizedServiceUrlproperty. This means that even though the ApiClient's ServiceUrl property is normalized, the nested clients will receive URLs without trailing slashes, potentially causing 404 errors.
The pattern should be:
Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken);
Teams = new TeamClient(ServiceUrl, _http, cancellationToken);
Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken);
Conversations = new ConversationClient(serviceUrl, _http, cancellationToken);
Users = new UserClient(_http, cancellationToken);
Teams = new TeamClient(serviceUrl, _http, cancellationToken);
Meetings = new MeetingClient(serviceUrl, _http, cancellationToken);
Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs:54
- The nested client instantiations (Conversations, Teams, Meetings) receive the unnormalized
serviceUrlparameter instead of the normalizedServiceUrlproperty. This means that even though the ApiClient's ServiceUrl property is normalized, the nested clients will receive URLs without trailing slashes, potentially causing 404 errors.
The pattern should be:
Conversations = new ConversationClient(ServiceUrl, _http, cancellationToken);
Teams = new TeamClient(ServiceUrl, _http, cancellationToken);
Meetings = new MeetingClient(ServiceUrl, _http, cancellationToken);
Conversations = new ConversationClient(serviceUrl, _http, cancellationToken);
Users = new UserClient(_http, cancellationToken);
Teams = new TeamClient(serviceUrl, _http, cancellationToken);
Meetings = new MeetingClient(serviceUrl, _http, cancellationToken);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs
Outdated
Show resolved
Hide resolved
…eUrl - ApiClient: Pass normalized ServiceUrl property (not serviceUrl param) to nested clients - ConversationClient: Pass normalized ServiceUrl property to ActivityClient and MemberClient - App.cs: Add fallback to empty string when both Activity.ServiceUrl and Token.ServiceUrl are null - AspNetCorePlugin: Use default serviceUrl when activity.ServiceUrl is null for AnonymousToken - Update ApiClientTests to expect trailing slash in normalized ServiceUrl Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // If no token was extracted, create an anonymous token with serviceUrl from the activity (or default) | ||
| // This matches Python/TypeScript SDK behavior for skipAuth scenarios | ||
| IToken resolvedToken = (IToken?)token ?? new AnonymousToken(activity.ServiceUrl ?? "https://smba.trafficmanager.net/teams"); |
There was a problem hiding this comment.
When activity.ServiceUrl is an empty string (not null), it will be passed to AnonymousToken, which will normalize it to just "/" due to the trailing slash logic. This could cause issues when making API calls. Consider using string.IsNullOrEmpty(activity.ServiceUrl) instead of just the null coalescing operator to ensure empty strings also fall back to the default service URL.
| // Ensure serviceUrl has trailing slash for consistency | ||
| ServiceUrl = serviceUrl.EndsWith('/') ? serviceUrl : serviceUrl + '/'; |
There was a problem hiding this comment.
The constructor accepts a serviceUrl parameter that could be an empty string. When an empty string is passed, the normalization logic will convert it to "/", which is not a valid service URL. Consider adding validation to handle empty strings, either by throwing an exception or falling back to a default service URL like "https://smba.trafficmanager.net/teams".
| // Ensure serviceUrl has trailing slash for consistency | |
| ServiceUrl = serviceUrl.EndsWith('/') ? serviceUrl : serviceUrl + '/'; | |
| // Use a default service URL if the provided value is null, empty, or whitespace | |
| var normalizedServiceUrl = string.IsNullOrWhiteSpace(serviceUrl) | |
| ? "https://smba.trafficmanager.net/teams" | |
| : serviceUrl; | |
| // Ensure serviceUrl has trailing slash for consistency | |
| ServiceUrl = normalizedServiceUrl.EndsWith('/') | |
| ? normalizedServiceUrl | |
| : normalizedServiceUrl + '/'; |
Summary
Fixes #274:
UnauthorizedAccessExceptionthrown when usingbuilder.AddTeams(skipAuth: true)Changes
1. Token handling for skipAuth scenarios
ExtractToken()now returnsnullwhen no Authorization header (instead of throwing)AnonymousTokenclass that implementsITokenwith default values (empty appId, serviceUrl from activity, etc.)AnonymousToken- matches Python/TypeScript SDK behaviorActivityEvent.Tokennullable with null-conditional access in consumers2. ServiceUrl normalization fix
NormalizeServiceUrl()helper inClientbase classhttp://localhost:56150/_connector→http://localhost:56150/_connector/)Files Changed
Libraries/Microsoft.Teams.Api/Auth/AnonymousToken.cs(new)Libraries/Microsoft.Teams.Api/Clients/Client.csLibraries/Microsoft.Teams.Api/Clients/ActivityClient.csLibraries/Microsoft.Teams.Api/Clients/ApiClient.csLibraries/Microsoft.Teams.Api/Clients/ConversationClient.csLibraries/Microsoft.Teams.Api/Clients/MeetingClient.csLibraries/Microsoft.Teams.Api/Clients/MemberClient.csLibraries/Microsoft.Teams.Api/Clients/TeamClient.csLibraries/Microsoft.Teams.Apps/App.csLibraries/Microsoft.Teams.Apps/Events/ActivityEvent.csLibraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.csTests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.csTest plan
Test_ExtractToken_ReturnsNull_WhenNoAuthHeadertestTest_Do_Http_WorksWithoutAuthHeaderto verify AnonymousToken is created🤖 Generated with Claude Code