Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 45 additions & 3 deletions src/Microsoft.Identity.Web.OWIN/AppBuilderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Globalization;
using System.Linq;
using System.Security.Authentication;
using System.Security.Claims;
using System.Threading.Tasks;
Expand Down Expand Up @@ -38,6 +39,11 @@ public static class AppBuilderExtension
/// <param name="updateOptions">Update the OWIN options if you want to finesse the token validation.</param>
/// <param name="configurationSection">Configuration section in which to read the options.</param>
/// <returns>The app builder to chain.</returns>
/// <remarks>
/// By default, bearer tokens must contain a scope or role claim. Set
/// <see cref="Microsoft.Identity.Abstractions.IdentityApplicationOptions.AllowWebApiToBeAuthorizedByACL"/> to
/// <c>true</c> when the application uses ACL-based authorization instead.
/// </remarks>
public static IAppBuilder AddMicrosoftIdentityWebApi(
this IAppBuilder app,
OwinTokenAcquirerFactory tokenAcquirerFactory,
Expand All @@ -49,12 +55,12 @@ public static IAppBuilder AddMicrosoftIdentityWebApi(
var configuration = tokenAcquirerFactory.Configuration;

// Configure the Microsoft authentication options
services.Configure(
string.Empty,
Action<MicrosoftIdentityApplicationOptions> configureOptions =
configureMicrosoftIdentityApplicationOptions ?? (option =>
{
configuration?.GetSection(configurationSection).Bind(option);
}));
});
services.Configure(string.Empty, configureOptions);
services.Configure<ConfidentialClientApplicationOptions>(
string.Empty,
(option =>
Expand Down Expand Up @@ -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);
},
};
}

/// <summary>
/// Adds a protected web app.
/// </summary>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Identity.Web.AgentIdentities\Microsoft.Identity.Web.AgentIdentities.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Identity.Web.DownstreamApi\Microsoft.Identity.Web.DownstreamApi.csproj" />
<ProjectReference Condition="'$(TargetFramework)' == 'net472'" Include="..\..\src\Microsoft.Identity.Web.OWIN\Microsoft.Identity.Web.OWIN.csproj" />
<ProjectReference Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" Include="..\..\src\Microsoft.Identity.Web.MicrosoftGraph\Microsoft.Identity.Web.MicrosoftGraph.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Identity.Web\Microsoft.Identity.Web.csproj" />
<ProjectReference Include="..\..\src\Microsoft.Identity.Web.Azure\Microsoft.Identity.Web.Azure.csproj" />
Expand Down Expand Up @@ -53,6 +54,11 @@
<Compile Include="CacheExtensionsTests.cs" />
<Compile Include="CacheEncryptionTests.cs" />
<Compile Include="TestDistributedCache.cs" />
<Compile Include="OwinClaimsValidationTests.cs" />
</ItemGroup>

<ItemGroup>
<Compile Remove="OwinClaimsValidationTests.cs" Condition="'$(TargetFramework)' != 'net472'" />
</ItemGroup>

<ItemGroup>
Expand Down
78 changes: 78 additions & 0 deletions tests/Microsoft.Identity.Web.Test/OwinClaimsValidationTests.cs
Original file line number Diff line number Diff line change
@@ -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;
}

}
}
Loading