Skip to content

Fix UnauthorizedAccessException with skipAuth#297

Open
rajan-chari wants to merge 5 commits intomainfrom
fix/issue-274-skipauth-unauthorized
Open

Fix UnauthorizedAccessException with skipAuth#297
rajan-chari wants to merge 5 commits intomainfrom
fix/issue-274-skipauth-unauthorized

Conversation

@rajan-chari
Copy link
Contributor

@rajan-chari rajan-chari commented Jan 29, 2026

Summary

Fixes #274: UnauthorizedAccessException thrown when using builder.AddTeams(skipAuth: true)

Changes

1. Token handling for skipAuth scenarios

  • ExtractToken() now returns null when no Authorization header (instead of throwing)
  • Created AnonymousToken class that implements IToken with default values (empty appId, serviceUrl from activity, etc.)
  • When no auth header is present, creates AnonymousToken - matches Python/TypeScript SDK behavior
  • Made ActivityEvent.Token nullable with null-conditional access in consumers

2. ServiceUrl normalization fix

  • Added NormalizeServiceUrl() helper in Client base class
  • All API clients now ensure serviceUrl has trailing slash
  • Fixes 404 errors when AgentsPlayground sends serviceUrl without trailing slash (e.g., http://localhost:56150/_connectorhttp://localhost:56150/_connector/)

Files Changed

  • Libraries/Microsoft.Teams.Api/Auth/AnonymousToken.cs (new)
  • Libraries/Microsoft.Teams.Api/Clients/Client.cs
  • Libraries/Microsoft.Teams.Api/Clients/ActivityClient.cs
  • Libraries/Microsoft.Teams.Api/Clients/ApiClient.cs
  • Libraries/Microsoft.Teams.Api/Clients/ConversationClient.cs
  • Libraries/Microsoft.Teams.Api/Clients/MeetingClient.cs
  • Libraries/Microsoft.Teams.Api/Clients/MemberClient.cs
  • Libraries/Microsoft.Teams.Api/Clients/TeamClient.cs
  • Libraries/Microsoft.Teams.Apps/App.cs
  • Libraries/Microsoft.Teams.Apps/Events/ActivityEvent.cs
  • Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs
  • Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs

Test plan

  • Added Test_ExtractToken_ReturnsNull_WhenNoAuthHeader test
  • Updated Test_Do_Http_WorksWithoutAuthHeader to verify AnonymousToken is created
  • All 1,148 existing tests pass
  • Manually tested with AgentsPlayground and Samples.Echo - bot responses work correctly

🤖 Generated with Claude Code

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>
Copy link
Contributor

Copilot AI left a 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 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 return null instead of throwing an exception when no Authorization header is present
  • Changed ActivityEvent.Token from 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.

rajan-chari and others added 2 commits January 30, 2026 15:21
- 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>
Copilot AI review requested due to automatic review settings January 30, 2026 20:24
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 serviceUrl parameter instead of the normalized ServiceUrl property. 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 serviceUrl parameter instead of the normalized ServiceUrl property. 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 serviceUrl parameter instead of the normalized ServiceUrl property. 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 serviceUrl parameter instead of the normalized ServiceUrl property. 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.

rajan-chari and others added 2 commits January 30, 2026 15:41
…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>
Copilot AI review requested due to automatic review settings February 24, 2026 22:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +33
// Ensure serviceUrl has trailing slash for consistency
ServiceUrl = serviceUrl.EndsWith('/') ? serviceUrl : serviceUrl + '/';
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Suggested change
// 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 + '/';

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UnauthorizedAccessException when running without Auth settings

3 participants