diff --git a/changelog.md b/changelog.md
index 5558cb905..49a3e6ef5 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,7 @@
### Bug fixes
- Move Credential Guard key attestation into the optional `Microsoft.Identity.Web.KeyAttestation` package. Managed identity mTLS proof-of-possession uses the unattested flow unless the package is installed and `AddMicrosoftIdentityWebKeyAttestation()` is registered, preventing native key-attestation binaries and symbols from being published with unrelated applications.
+- OWIN web APIs now reject bearer tokens that contain neither scope nor role claims by default, matching ASP.NET Core behavior and preventing unprovisioned app-only tokens from authenticating. Applications using ACL-based authorization must set `AllowWebApiToBeAuthorizedByACL` to `true`.
## 4.14.2
diff --git a/src/Microsoft.Identity.Web.OWIN/AppBuilderExtension.cs b/src/Microsoft.Identity.Web.OWIN/AppBuilderExtension.cs
index 458625bfa..5b161fe8b 100644
--- a/src/Microsoft.Identity.Web.OWIN/AppBuilderExtension.cs
+++ b/src/Microsoft.Identity.Web.OWIN/AppBuilderExtension.cs
@@ -3,6 +3,7 @@
using System;
using System.Globalization;
+using System.Linq;
using System.Security.Authentication;
using System.Security.Claims;
using System.Threading.Tasks;
@@ -38,6 +39,11 @@ public static class AppBuilderExtension
/// Update the OWIN options if you want to finesse the token validation.
/// Configuration section in which to read the options.
/// The app builder to chain.
+ ///
+ /// By default, bearer tokens must contain a scope or role claim. Set
+ /// to
+ /// true when the application uses ACL-based authorization instead.
+ ///
public static IAppBuilder AddMicrosoftIdentityWebApi(
this IAppBuilder app,
OwinTokenAcquirerFactory tokenAcquirerFactory,
@@ -49,12 +55,12 @@ public static IAppBuilder AddMicrosoftIdentityWebApi(
var configuration = tokenAcquirerFactory.Configuration;
// Configure the Microsoft authentication options
- services.Configure(
- string.Empty,
+ Action configureOptions =
configureMicrosoftIdentityApplicationOptions ?? (option =>
{
configuration?.GetSection(configurationSection).Bind(option);
- }));
+ });
+ services.Configure(string.Empty, configureOptions);
services.Configure(
string.Empty,
(option =>
@@ -85,9 +91,45 @@ public static IAppBuilder AddMicrosoftIdentityWebApi(
updateOptions(options);
}
+ MicrosoftIdentityApplicationOptions applicationOptions = new();
+ configureOptions(applicationOptions);
+ ConfigureClaimsValidation(options, applicationOptions.AllowWebApiToBeAuthorizedByACL);
+
return app.UseOAuthBearerAuthentication(options);
}
+ internal static void ConfigureClaimsValidation(
+ OAuthBearerAuthenticationOptions options,
+ bool allowWebApiToBeAuthorizedByACL)
+ {
+ if (allowWebApiToBeAuthorizedByACL)
+ {
+ return;
+ }
+
+ IOAuthBearerAuthenticationProvider provider =
+ options.Provider ?? new OAuthBearerAuthenticationProvider();
+
+ options.Provider = new OAuthBearerAuthenticationProvider
+ {
+ OnRequestToken = provider.RequestToken,
+ OnApplyChallenge = provider.ApplyChallenge,
+ OnValidateIdentity = async context =>
+ {
+ if (!context.Ticket.Identity.Claims.Any(x => x.Type == ClaimConstants.Scope
+ || x.Type == ClaimConstants.Scp
+ || x.Type == ClaimConstants.Roles
+ || x.Type == ClaimConstants.Role))
+ {
+ context.Rejected();
+ return;
+ }
+
+ await provider.ValidateIdentity(context).ConfigureAwait(false);
+ },
+ };
+ }
+
///
/// Adds a protected web app.
///
diff --git a/src/Microsoft.Identity.Web.OWIN/Microsoft.Identity.Web.OWIN.xml b/src/Microsoft.Identity.Web.OWIN/Microsoft.Identity.Web.OWIN.xml
index 7b7d1cf25..988d16fc5 100644
--- a/src/Microsoft.Identity.Web.OWIN/Microsoft.Identity.Web.OWIN.xml
+++ b/src/Microsoft.Identity.Web.OWIN/Microsoft.Identity.Web.OWIN.xml
@@ -43,6 +43,11 @@
Update the OWIN options if you want to finesse the token validation.
Configuration section in which to read the options.
The app builder to chain.
+
+ By default, bearer tokens must contain a scope or role claim. Set
+ to
+ true when the application uses ACL-based authorization instead.
+
diff --git a/src/Microsoft.Identity.Web.OWIN/Properties/InternalsVisibleTo.cs b/src/Microsoft.Identity.Web.OWIN/Properties/InternalsVisibleTo.cs
index 4d17c83a2..bdaa961fd 100644
--- a/src/Microsoft.Identity.Web.OWIN/Properties/InternalsVisibleTo.cs
+++ b/src/Microsoft.Identity.Web.OWIN/Properties/InternalsVisibleTo.cs
@@ -4,3 +4,4 @@
using System.Runtime.CompilerServices;
// Allow this assembly to be serviced when run on desktop CLR
+[assembly: InternalsVisibleTo("Microsoft.Identity.Web.Test, PublicKey=00240000048000009400000006020000002400005253413100040000010001002D96616729B54F6D013D71559A017F50AA4861487226C523959D1579B93F3FDF71C08B980FD3130062B03D3DE115C4B84E7AC46AEF5E192A40E7457D5F3A08F66CEAB71143807F2C3CB0DA5E23B38F0559769978406F6E5D30CEADD7985FC73A5A609A8B74A1DF0A29399074A003A226C943D480FEC96DBEC7106A87896539AD")]
diff --git a/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj b/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj
index 66d1922e4..aede018a3 100644
--- a/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj
+++ b/tests/Microsoft.Identity.Web.Test/Microsoft.Identity.Web.Test.csproj
@@ -10,6 +10,7 @@
+
@@ -53,6 +54,11 @@
+
+
+
+
+
diff --git a/tests/Microsoft.Identity.Web.Test/OwinClaimsValidationTests.cs b/tests/Microsoft.Identity.Web.Test/OwinClaimsValidationTests.cs
new file mode 100644
index 000000000..8a6f56ccb
--- /dev/null
+++ b/tests/Microsoft.Identity.Web.Test/OwinClaimsValidationTests.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.IdentityModel.Tokens;
+using Microsoft.Owin;
+using Microsoft.Owin.Security;
+using Microsoft.Owin.Security.OAuth;
+using Xunit;
+
+namespace Microsoft.Identity.Web.Test
+{
+ public class OwinClaimsValidationTests
+ {
+ [Fact]
+ public async Task MissingScopesAndRoles_AuthenticationFailsAsync()
+ {
+ OAuthBearerAuthenticationOptions options = CreateOptions();
+ AppBuilderExtension.ConfigureClaimsValidation(options, allowWebApiToBeAuthorizedByACL: false);
+ OAuthValidateIdentityContext context = CreateContext(options);
+
+ await options.Provider.ValidateIdentity(context);
+
+ Assert.False(context.IsValidated);
+ }
+
+ [Theory]
+ [InlineData(ClaimConstants.Scope)]
+ [InlineData(ClaimConstants.Scp)]
+ [InlineData(ClaimConstants.Roles)]
+ [InlineData(ClaimTypes.Role)]
+ public async Task ScopeOrRoleClaim_AuthenticationSucceedsAsync(string claimType)
+ {
+ OAuthBearerAuthenticationOptions options = CreateOptions();
+ AppBuilderExtension.ConfigureClaimsValidation(options, allowWebApiToBeAuthorizedByACL: false);
+ OAuthValidateIdentityContext context = CreateContext(options, claimType);
+
+ await options.Provider.ValidateIdentity(context);
+
+ Assert.True(context.IsValidated);
+ }
+
+ [Fact]
+ public async Task MissingScopesAndRoles_WithAclAuthorization_AuthenticationSucceedsAsync()
+ {
+ OAuthBearerAuthenticationOptions options = CreateOptions();
+ AppBuilderExtension.ConfigureClaimsValidation(options, allowWebApiToBeAuthorizedByACL: true);
+ OAuthValidateIdentityContext context = CreateContext(options);
+
+ await options.Provider.ValidateIdentity(context);
+
+ Assert.True(context.IsValidated);
+ }
+
+ private static OAuthBearerAuthenticationOptions CreateOptions()
+ {
+ return new OAuthBearerAuthenticationOptions
+ {
+ Provider = new OAuthBearerAuthenticationProvider(),
+ };
+ }
+
+ private static OAuthValidateIdentityContext CreateContext(
+ OAuthBearerAuthenticationOptions options,
+ string? claimType = null)
+ {
+ ClaimsIdentity identity = claimType is null
+ ? new CaseSensitiveClaimsIdentity(authenticationType: "Bearer")
+ : new CaseSensitiveClaimsIdentity(new[] { new Claim(claimType, "value") }, "Bearer");
+ AuthenticationTicket ticket = new(identity, new AuthenticationProperties());
+ OAuthValidateIdentityContext context = new(new OwinContext(), options, ticket);
+ context.Validated();
+ return context;
+ }
+
+ }
+}