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; + } + + } +}