Skip to content

Commit

Permalink
update to use policies (#1456)
Browse files Browse the repository at this point in the history
* initial commit to use policy instead of filter

* add policies per david's feedback

* PR feedback

* update test
  • Loading branch information
jennyf19 authored Sep 29, 2021
1 parent 4feafe3 commit 2179386
Show file tree
Hide file tree
Showing 18 changed files with 3,222 additions and 343 deletions.
1 change: 1 addition & 0 deletions src/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
<Compile Remove="DownstreamWebApiSupport\**" />
<Compile Remove="InstanceDiscovery\**" />
<Compile Remove="Resource\**" />
<Compile Remove="Policy\**" />
<Compile Remove="WebApiExtensions\**" />
<Compile Remove="WebAppExtensions\**" />
<Compile Remove="TokenCacheProviders\Session\**" />
Expand Down
2,644 changes: 2,642 additions & 2 deletions src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions src/Microsoft.Identity.Web/Policy/IAuthRequiredScopeMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;

namespace Microsoft.Identity.Web
{
/// <summary>
/// This is the metadata that describes required auth scopes for a given endpoint
/// in a web API. It's the underlying data structure the requirement <see cref="ScopeAuthorizationRequirement"/> will look for
/// in order to validate scopes in the scope claims.
/// </summary>
public interface IAuthRequiredScopeMetadata
{
/// <summary>
/// Scopes accepted by this web API.
/// </summary>
IEnumerable<string>? AcceptedScope { get; }

/// <summary>
/// Fully qualified name of the configuration key containing the required scopes (separated
/// by spaces).
/// </summary>
string? RequiredScopeConfigurationKey { get; }
}
}
62 changes: 62 additions & 0 deletions src/Microsoft.Identity.Web/Policy/PolicyBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Extensions for building the RequiredScope policy during application startup.
/// </summary>
/// <example>
/// <code>
/// services.AddAuthorization(o =>
/// { o.AddPolicy("Custom",
/// policyBuilder =>policyBuilder.RequireScope("access_as_user"));
/// });
/// </code>
/// </example>
public static class PolicyBuilderExtensions
{
/// <summary>
/// Adds a <see cref="ScopeAuthorizationRequirement"/> to the current instance which requires
/// that the current user has the specified claim and that the claim value must be one of the allowed values.
/// </summary>
/// <param name="authorizationPolicyBuilder">Used for building policies during application startup.</param>
/// <param name="allowedValues">Values the claim must process one or more of for evaluation to succeed.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static AuthorizationPolicyBuilder RequireScope(
this AuthorizationPolicyBuilder authorizationPolicyBuilder,
params string[] allowedValues)
{
if (authorizationPolicyBuilder == null)
{
throw new ArgumentNullException(nameof(authorizationPolicyBuilder));
}

return RequireScope(authorizationPolicyBuilder, (IEnumerable<string>)allowedValues);
}

/// <summary>
/// Adds a <see cref="ScopeAuthorizationRequirement"/> to the current instance which requires
/// that the current user has the specified claim and that the claim value must be one of the allowed values.
/// </summary>
/// <param name="authorizationPolicyBuilder">Used for building policies during application startup.</param>
/// <param name="allowedValues">Values the claim must process one or more of for evaluation to succeed.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static AuthorizationPolicyBuilder RequireScope(
this AuthorizationPolicyBuilder authorizationPolicyBuilder,
IEnumerable<string> allowedValues)
{
if (authorizationPolicyBuilder == null)
{
throw new ArgumentNullException(nameof(authorizationPolicyBuilder));
}

authorizationPolicyBuilder.Requirements.Add(new ScopeAuthorizationRequirement(allowedValues));
return authorizationPolicyBuilder;
}
}
}
42 changes: 42 additions & 0 deletions src/Microsoft.Identity.Web/Policy/RequireScopeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;

namespace Microsoft.Identity.Web
{
/// <summary>
/// RequireScopeOptions.
/// </summary>
internal class RequireScopeOptions : IPostConfigureOptions<AuthorizationOptions>
{
private readonly AuthorizationPolicy _defaultPolicy;

/// <summary>
/// Sets the default policy.
/// </summary>
public RequireScopeOptions()
{
_defaultPolicy = new AuthorizationPolicyBuilder()
.AddRequirements(new ScopeAuthorizationRequirement())
.Build();
}

/// <inheritdoc/>
public void PostConfigure(
string name,
AuthorizationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

options.DefaultPolicy = options.DefaultPolicy is null
? _defaultPolicy
: AuthorizationPolicy.Combine(options.DefaultPolicy, _defaultPolicy);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;

namespace Microsoft.Identity.Web.Resource
{
Expand All @@ -12,8 +13,14 @@ namespace Microsoft.Identity.Web.Resource
/// choice, use either one or the other of the constructors.
/// For details, see https://aka.ms/ms-id-web/required-scope-attribute.
/// </summary>
public class RequiredScopeAttribute : TypeFilterAttribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequiredScopeAttribute : Attribute, IAuthRequiredScopeMetadata
{
/// <summary>
/// Scopes accepted by this web API.
/// </summary>
public IEnumerable<string>? AcceptedScope { get; set; }

/// <summary>
/// Fully qualified name of the configuration key containing the required scopes (separated
/// by spaces).
Expand All @@ -26,11 +33,7 @@ public class RequiredScopeAttribute : TypeFilterAttribute
/// [RequiredScope(RequiredScopesConfigurationKey="AzureAd:Scopes")]
/// </code>
/// </example>
public string RequiredScopesConfigurationKey
{
get { return string.Empty; }
set { Arguments = new object[] { new string[] { Constants.RequiredScopesSetting, value } }; }
}
public string? RequiredScopeConfigurationKey { get; set; }

/// <summary>
/// Verifies that the web API is called with the right scopes.
Expand All @@ -49,24 +52,26 @@ public string RequiredScopesConfigurationKey
/// [RequiredScope("access_as_user")]
/// </code>
/// </example>
/// <seealso cref="M:RequiredScopeAttribute()"/> and <see cref="RequiredScopesConfigurationKey"/>
/// <seealso cref="M:RequiredScopeAttribute()"/> and <see cref="RequiredScopeConfigurationKey"/>
/// if you want to express the required scopes from the configuration.
public RequiredScopeAttribute(params string[] acceptedScopes)
: base(typeof(RequiredScopeFilter))
{
Arguments = new object[] { acceptedScopes };
IsReusable = true;
AcceptedScope = acceptedScopes ?? throw new ArgumentNullException(nameof(acceptedScopes));
}

/// <summary>
/// Default constructor, to be used along with the <see cref="RequiredScopesConfigurationKey"/>
/// property when you want to get the scopes to validate from the configuration, instead
/// of hardcoding them in the code.
/// Default constructor.
/// </summary>
/// <example>
/// <code>
/// [RequiredScope(RequiredScopesConfigurationKey="AzureAD:Scope")]
/// class Controller : BaseController
/// {
/// }
/// </code>
/// </example>
public RequiredScopeAttribute()
: base(typeof(RequiredScopeFilter))
{
IsReusable = true;
}
}
}
60 changes: 60 additions & 0 deletions src/Microsoft.Identity.Web/Policy/RequiredScopeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Extensions for building the required scope attribute during application startup.
/// </summary>
public static class RequiredScopeExtensions
{
/// <summary>
/// This method adds support for the required scope attribute. It adds a default policy that
/// adds a scope requirement. This requirement looks for IAuthRequiredScopeMetadata on the current endpoint.
/// </summary>
/// <param name="services">The services being configured.</param>
/// <returns>Services.</returns>
public static IServiceCollection AddRequiredScopeAuthorization(this IServiceCollection services)
{
services.AddAuthorization();

services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<AuthorizationOptions>, RequireScopeOptions>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthorizationHandler, ScopeAuthorizationHandler>());
return services;
}

/// <summary>
/// This method adds metadata to route endpoint to describe required scopes. It's the imperative version of
/// the [RequiredScope] attribute.
/// </summary>
/// <typeparam name="TBuilder">Class implementing <see cref="IEndpointConventionBuilder"/>.</typeparam>
/// <param name="endpointConventionBuilder">To customize the endpoints.</param>
/// <param name="scope">Scope.</param>
/// <returns>Builder.</returns>
public static TBuilder RequireScope<TBuilder>(this TBuilder endpointConventionBuilder, params string[] scope)
where TBuilder : IEndpointConventionBuilder
{
return endpointConventionBuilder.WithMetadata(new RequiredScopeMetadata(scope));
}

private sealed class RequiredScopeMetadata : IAuthRequiredScopeMetadata
{
public RequiredScopeMetadata(string[] scope)
{
AcceptedScope = scope;
}

public IEnumerable<string>? AcceptedScope { get; }

public string? RequiredScopeConfigurationKey { get; }
}
}
}
92 changes: 92 additions & 0 deletions src/Microsoft.Identity.Web/Policy/ScopeAuthorizationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Scope authorization handler that needs to be called for a specific requirement type.
/// In this case, <see cref="ScopeAuthorizationRequirement"/>.
/// </summary>
internal class ScopeAuthorizationHandler : AuthorizationHandler<ScopeAuthorizationRequirement>
{
private readonly IConfiguration _configuration;

/// <summary>
/// Constructor for the scope authorization handler, which takes a configuration.
/// </summary>
/// <param name="configuration">Configuration.</param>
public ScopeAuthorizationHandler(IConfiguration configuration)
{
_configuration = configuration;
}

/// <summary>
/// Makes a decision if authorization is allowed based on a specific requirement.
/// </summary>
/// <param name="context">AuthorizationHandlerContext.</param>
/// <param name="requirement">Scope requirement.</param>
/// <returns>Task.</returns>
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ScopeAuthorizationRequirement requirement)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

// The resource is either the HttpContext or the Endpoint directly when used with the
// authorization middleware
var endpoint = context.Resource switch
{
HttpContext httpContext => httpContext.GetEndpoint(),
Endpoint ep => ep,
_ => null,
};

var data = endpoint?.Metadata.GetMetadata<IAuthRequiredScopeMetadata>();

IEnumerable<string>? scopes = null;
if (requirement?.RequiredScopesConfigurationKey != null)
{
scopes = _configuration.GetValue<string>(requirement?.RequiredScopesConfigurationKey)?.Split(' ');
}

if (scopes is null)
{
scopes = requirement?.AllowedValues ?? data?.AcceptedScope;
}

// Can't determine what to do without scope metadata, so proceed
if (scopes is null)
{
context.Succeed(requirement);
return Task.CompletedTask;
}

Claim? scopeClaim = context.User.FindFirst(ClaimConstants.Scp) ?? context.User.FindFirst(ClaimConstants.Scope);

if (scopeClaim is null)
{
return Task.CompletedTask;
}

if (scopeClaim != null && scopeClaim.Value.Split(' ').Intersect(scopes).Any())
{
context.Succeed(requirement);
return Task.CompletedTask;
}

return Task.CompletedTask;
}
}
}
Loading

0 comments on commit 2179386

Please sign in to comment.