Add explicit unauthenticated request bypass#596
Conversation
Deprecate skipAuth in favor of DangerouslyAllowUnauthenticatedRequests and wire the setting through both AspNetCorePlugin and core TeamsBotApplication auth paths. Support configuration via Teams/AzureAd sections and keep backward compatibility for skipAuth callers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces an explicit “dangerous” authentication bypass switch (replacing the older skipAuth) and wires it through both the AspNetCore plugin path and the core TeamsBotApplication / AzureAd configuration path, so local-development scenarios can accept /api/messages requests without attempting bearer-token parsing.
Changes:
- Deprecates
skipAuthin favor ofDangerouslyAllowUnauthenticatedRequestsacross AspNetCore hosting extension overloads (with compatibility preserved). - Updates AspNetCore plugin request handling to optionally skip token extraction and use an
UnauthenticatedTokenwhen the bypass is enabled. - Extends configuration support for the bypass flag via
TeamsandAzureAdsections (including fallback/override behavior) with added unit tests.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Extensions/HostApplicationBuilderTests.cs | Updates/extends tests around the new bypass flag and deprecated skipAuth path. |
| Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/AspNetCorePluginTests.cs | Adds a test covering bypass behavior when no Authorization header is present. |
| Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/UnauthenticatedToken.cs | Introduces an IToken implementation for bypassed (unauthenticated) requests. |
| Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/Extensions/HostApplicationBuilder.cs | Refactors hosting extension overloads and adds the new bypass option/config plumbing. |
| Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePluginOptions.cs | Adds the DangerouslyAllowUnauthenticatedRequests option and an obsolete SkipAuth alias. |
| Libraries/Microsoft.Teams.Plugins/Microsoft.Teams.Plugins.AspNetCore/AspNetCorePlugin.cs | Skips token parsing under bypass and supplies an unauthenticated token to downstream handlers. |
| Libraries/Microsoft.Teams.Extensions/Microsoft.Teams.Extensions.Configuration/Microsoft.Teams.Apps.Extensions/TeamsSettings.cs | Adds TeamsSettings.DangerouslyAllowUnauthenticatedRequests configuration binding support. |
| core/test/Microsoft.Teams.Core.UnitTests/Hosting/JwtExtensionsTests.cs | Adds coverage for bypass authentication behavior in the core hosting path. |
| core/test/Microsoft.Teams.Core.UnitTests/Hosting/BotConfigTests.cs | Adds tests for config-section precedence and validation of the bypass flag. |
| core/src/Microsoft.Teams.Core/Hosting/JwtExtensions.cs | Enables bypass authentication when the new flag is set (even if ClientId exists). |
| core/src/Microsoft.Teams.Core/Hosting/BotConfig.cs | Adds the bypass flag to resolved bot config and supports Teams-section fallback parsing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var services = builder.Build().Services; | ||
| var authOptions = services.GetRequiredService<IAuthorizationPolicyProvider>(); | ||
| var pluginOptions = services.GetRequiredService<AspNetCorePluginOptions>(); | ||
|
|
||
| var policy = await authOptions.GetPolicyAsync(TeamsTokenAuthConstants.AuthorizationPolicy); | ||
|
|
||
| Assert.NotNull(policy); | ||
| Assert.True(policy.Requirements.OfType<AssertionRequirement>().Any()); | ||
| Assert.False(pluginOptions.DangerouslyAllowUnauthenticatedRequests); |
| var services = builder.Build().Services; | ||
| var authOptions = services.GetRequiredService<IAuthorizationPolicyProvider>(); | ||
| var pluginOptions = services.GetRequiredService<AspNetCorePluginOptions>(); | ||
|
|
||
| var policy = await authOptions.GetPolicyAsync(TeamsTokenAuthConstants.AuthorizationPolicy); | ||
|
|
||
| // Should allow all requests | ||
| Assert.NotNull(policy); | ||
| Assert.True(policy.Requirements.OfType<AssertionRequirement>().Any()); | ||
| Assert.True(pluginOptions.DangerouslyAllowUnauthenticatedRequests); |
| var services = builder.Build().Services; | ||
| var authOptions = services.GetRequiredService<IAuthorizationPolicyProvider>(); | ||
| var pluginOptions = services.GetRequiredService<AspNetCorePluginOptions>(); | ||
|
|
||
| var policy = await authOptions.GetPolicyAsync(TeamsTokenAuthConstants.AuthorizationPolicy); | ||
|
|
||
| Assert.NotNull(policy); | ||
| Assert.True(policy.Requirements.OfType<AssertionRequirement>().Any()); | ||
| Assert.True(pluginOptions.DangerouslyAllowUnauthenticatedRequests); |
| var services = builder.Build().Services; | ||
| var authOptions = services.GetRequiredService<IAuthorizationPolicyProvider>(); | ||
| var pluginOptions = services.GetRequiredService<AspNetCorePluginOptions>(); | ||
|
|
||
| var policy = await authOptions.GetPolicyAsync(TeamsTokenAuthConstants.AuthorizationPolicy); | ||
|
|
||
| // Should allow all requests | ||
| Assert.NotNull(policy); | ||
| Assert.True(policy.Requirements.OfType<AssertionRequirement>().Any()); | ||
| Assert.True(pluginOptions.DangerouslyAllowUnauthenticatedRequests); |
| var request = httpContext.Request; | ||
| var token = ExtractToken(request); | ||
| var options = httpContext.RequestServices?.GetService(typeof(AspNetCorePluginOptions)) as AspNetCorePluginOptions; | ||
| var dangerouslyAllowUnauthenticatedRequests = options?.DangerouslyAllowUnauthenticatedRequests == true; | ||
| var token = dangerouslyAllowUnauthenticatedRequests ? null : ExtractToken(request); | ||
| var activity = await ParseActivity(request).ConfigureAwait(false); |
Require the dangerous unauthenticated bypass flag before bypassing inbound bot authentication. When ClientId is absent without the flag, register an auth scheme that challenges with a 401 JSON error instead of accepting requests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Return the missing-auth-configuration challenge through ASP.NET Core Results.Problem instead of writing JSON manually, and assert the ProblemDetails response shape. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Log remediation guidance server-side while returning only a generic ProblemDetails title to clients. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| if (bool.TryParse(value, out bool result)) | ||
| { | ||
| return result; | ||
| } | ||
|
|
||
| throw new InvalidOperationException( | ||
| $"Configuration value '{section.Path}:{key}' is not a valid boolean: '{value}'."); |
There was a problem hiding this comment.
This behavior diverges from PY and TS, right? Do we want parity?
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.Teams.Api.Auth; | ||
|
|
||
| namespace Microsoft.Teams.Plugins.AspNetCore; | ||
|
|
||
| internal sealed class UnauthenticatedToken(string? serviceUrl) : IToken | ||
| { | ||
| public string? AppId => null; | ||
| public string? AppDisplayName => null; | ||
| public string? TenantId => null; | ||
| public string ServiceUrl { get; } = string.IsNullOrEmpty(serviceUrl) | ||
| ? "https://smba.trafficmanager.net/teams/" | ||
| : serviceUrl.EndsWith('/') ? serviceUrl : $"{serviceUrl}/"; | ||
| public CallerType From => CallerType.Azure; | ||
| public string FromId => "urn:botframework:azure"; | ||
| public DateTime? Expiration => null; | ||
| public bool IsExpired => false; | ||
| public IEnumerable<string> Scopes => []; | ||
| public override string ToString() => string.Empty; | ||
| } |
There was a problem hiding this comment.
This synthesizes trafficmanager when the activity has no service url, but TS and PY do not. Should all three have matching behavior here?
|
|
||
| internal const string BotFrameworkSectionName = "BotFramework"; | ||
|
|
||
| private const string TeamsSectionName = "Teams"; |
There was a problem hiding this comment.
Can we keep only one section ? and the default remains AzureAD
| return Task.FromResult(AuthenticateResult.Fail("Authentication not configured")); | ||
| } | ||
|
|
||
| protected override async Task HandleChallengeAsync(AuthenticationProperties properties) |
There was a problem hiding this comment.
This leaves us with no startup-time warning log and defers it to when a request is actually received .
|
|
||
| internal const string BotFrameworkSectionName = "BotFramework"; | ||
|
|
||
| private const string TeamsSectionName = "Teams"; |
There was a problem hiding this comment.
we don't need a new section imo, we can simply support the key at the root (or default section)
| if (string.IsNullOrWhiteSpace(clientId)) | ||
| { | ||
| builder.AddBypassAuthentication(schemeName, logger); | ||
| builder.AddAuthenticationNotConfigured(schemeName); |
There was a problem hiding this comment.
same logic should also be applied to this overload
Summary
skipAuthin favor ofDangerouslyAllowUnauthenticatedRequestswhile preserving compatibility./api/messagesrequests without parsing a bearer token when the dangerous bypass is enabled in the AspNetCore plugin path. This currently prevents 2.0 from being used for agents playground.DangerouslyAllowUnauthenticatedRequestsviaTeamsconfig/env and the coreTeamsBotApplicationauth path, includingAzureAdsection support.Behavior
DangerouslyAllowUnauthenticatedRequestsis enabled, unauthenticated requests are accepted without requiring or parsing theAuthorizationheader.ClientIdis missing and the dangerous bypass is not enabled, requests are rejected with401ProblemDetails containing only the genericAuthentication not configuredtitle/status.ClientIdor enableDangerouslyAllowUnauthenticatedRequestsis logged server-side and is not returned in the HTTP response body.Validation
dotnet test Tests/Microsoft.Teams.Plugins.AspNetCore.Tests/Microsoft.Teams.Plugins.AspNetCore.Tests.csproj --verbosity minimaldotnet test core/test/Microsoft.Teams.Core.UnitTests/Microsoft.Teams.Core.UnitTests.csproj --verbosity minimaldotnet build Microsoft.Teams.sln --verbosity minimal