Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jennyf/scopes roles #1742

Merged
merged 4 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
252 changes: 252 additions & 0 deletions src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public interface IAuthRequiredScopeMetadata
/// <summary>
/// Scopes accepted by this web API.
/// </summary>
IEnumerable<string>? AcceptedScope { get; }
string[]? AcceptedScope { get; }

/// <summary>
/// Fully qualified name of the configuration key containing the required scopes (separated
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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 or app permissions for a given endpoint
/// in a web API. It's the underlying data structure the requirement <see cref="ScopeAuthorizationRequirement"/> will look for
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// in order to validate scopes in the scope claims.
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public interface IAuthRequiredScopeOrAppPermissionMetadata
{
/// <summary>
/// App permissions accepted by this web API.
/// App permissions appear in the roles claim of the token.
/// </summary>
string[]? AcceptedAppPermission { get; }

/// <summary>
/// Fully qualified name of the configuration key containing the required scopes
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// or app permissions (separated by spaces).
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
string? RequiredAppPermissionsConfigurationKey { get; }

/// <summary>
/// Scopes accepted by this web API.
/// </summary>
string[]? AcceptedScope { get; }

/// <summary>
/// Fully qualified name of the configuration key containing the required scopes (separated
/// by spaces).
/// </summary>
string? RequiredScopesConfigurationKey { get; }
}
}
48 changes: 48 additions & 0 deletions src/Microsoft.Identity.Web/Policy/PolicyBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,53 @@ public static AuthorizationPolicyBuilder RequireScope(
authorizationPolicyBuilder.Requirements.Add(new ScopeAuthorizationRequirement(allowedValues));
return authorizationPolicyBuilder;
}

/// <summary>
/// Adds a <see cref="ScopeOrAppPermissionAuthorizationRequirement"/> 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="allowedScopeValues">Values the claim must process one or more of for evaluation to succeed.</param>
/// <param name="allowedAppPermissionValues">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 RequireScopeOrAppPermission(
this AuthorizationPolicyBuilder authorizationPolicyBuilder,
string[] allowedScopeValues,
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
string[] allowedAppPermissionValues)
{
if (authorizationPolicyBuilder == null)
{
throw new ArgumentNullException(nameof(authorizationPolicyBuilder));
}

return RequireScopeOrAppPermission(
authorizationPolicyBuilder,
(IEnumerable<string>)allowedScopeValues,
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
(IEnumerable<string>)allowedAppPermissionValues);
}

/// <summary>
/// Adds a <see cref="ScopeOrAppPermissionAuthorizationRequirement"/> 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="allowedScopeValues">Values the claim must process one or more of for evaluation to succeed.</param>
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// <param name="allowedAppPermissionValues">Values the claim must process one or more of for evaluation to succeed.</param>
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// <returns>A reference to this instance after the operation has completed.</returns>
public static AuthorizationPolicyBuilder RequireScopeOrAppPermission(
this AuthorizationPolicyBuilder authorizationPolicyBuilder,
IEnumerable<string> allowedScopeValues,
IEnumerable<string> allowedAppPermissionValues)
{
if (authorizationPolicyBuilder == null)
{
throw new ArgumentNullException(nameof(authorizationPolicyBuilder));
}

authorizationPolicyBuilder.Requirements.Add(new ScopeOrAppPermissionAuthorizationRequirement(
allowedScopeValues,
allowedAppPermissionValues));
return authorizationPolicyBuilder;
}
}
}
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>
/// RequireScopeOrAppPermissionOptions.
/// </summary>
internal class RequireScopeOrAppPermissionOptions : IPostConfigureOptions<AuthorizationOptions>
{
private readonly AuthorizationPolicy _defaultPolicy;

/// <summary>
/// Sets the default policy.
/// </summary>
public RequireScopeOrAppPermissionOptions()
{
_defaultPolicy = new AuthorizationPolicyBuilder()
.AddRequirements(new ScopeOrAppPermissionAuthorizationRequirement())
.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
Expand Up @@ -19,7 +19,7 @@ public class RequiredScopeAttribute : Attribute, IAuthRequiredScopeMetadata
/// <summary>
/// Scopes accepted by this web API.
/// </summary>
public IEnumerable<string>? AcceptedScope { get; set; }
public string[]? AcceptedScope { get; set; }

/// <summary>
/// Fully qualified name of the configuration key containing the required scopes (separated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public RequiredScopeMetadata(string[] scope)
AcceptedScope = scope;
}

public IEnumerable<string>? AcceptedScope { get; }
public string[]? AcceptedScope { get; }

public string? RequiredScopesConfigurationKey { get; }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;

namespace Microsoft.Identity.Web.Resource
{
/// <summary>
/// This attribute is used on a controller, pages, or controller actions
/// to declare (and validate) the scopes or app permissions required by a web API.
/// These scopes or app permissions can be declared in two ways:
/// hardcoding them, or declaring them in the configuration. Depending on your
/// choice, use either one or the other of the constructors.
/// For details, see https://aka.ms/ms-id-web/required-scope-or-app-permissions-attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequiredScopeOrAppPermissionAttribute : Attribute, IAuthRequiredScopeOrAppPermissionMetadata
{
/// <summary>
/// Scopes accepted by this web API.
/// </summary>
public string[]? AcceptedScope { get; set; }

/// <summary>
/// Fully qualified name of the configuration key containing the required scopes (separated
/// by spaces).
/// </summary>
/// <example>
/// If the appsettings.json file contains a section named "AzureAd", in which
/// a property named "Scopes" contains the required scopes, the attribute on the
/// controller/page/action to protect should be set to the following:
/// <code>
/// [RequiredScope(RequiredScopesConfigurationKey="AzureAd:Scopes")]
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// </code>
/// </example>
public string? RequiredScopesConfigurationKey { get; set; }

/// <summary>
/// Unused: Compatibility of interface with the Authorization Filter.
/// </summary>
public bool IsReusable { get; set; }
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// App permissions accepted by this web API.
/// App permissions appear in the roles claim of the token.
/// </summary>
public string[]? AcceptedAppPermission { get; set; }

/// <summary>
/// Fully qualified name of the configuration key containing the required app permissions (separated
/// by spaces).
/// </summary>
/// <example>
/// If the appsettings.json file contains a section named "AzureAd", in which
/// a property named "AppPermissions" contains the required app permissions, the attribute on the
/// controller/page/action to protect should be set to the following:
/// <code>
/// [RequiredScopeOrAppPermission(RequiredAppPermissionsConfigurationKey="AzureAd:AppPermissions")]
/// </code>
/// </example>
public string? RequiredAppPermissionsConfigurationKey { get; set; }

/// <summary>
/// Verifies that the web API is called with the right app permissions.
/// If the token obtained for this API is on behalf of the authenticated user does not have
/// any of these <paramref name="acceptedScopes"/> in its scope claim,
/// nor <paramref name="acceptedAppPermissions"/> in its roles claim, the
/// method updates the HTTP response providing a status code 403 (Forbidden)
/// and writes to the response body a message telling which scopes are expected in the token.
/// </summary>
/// <param name="acceptedScopes">Scopes accepted by this web API.</param>
/// <param name="acceptedAppPermissions">App permissions accepted by this web API.</param>
/// <remarks>When neither the scopes nor app permissions match, the response is a 403 (Forbidden),
/// because the user is authenticated (hence not 401), but not authorized.</remarks>
/// <example>
/// Add the following attribute on the controller/page/action to protect:
///
/// <code>
/// [RequiredScopeOrAppPermissionAttribute(new string[] { "access_as_user" }, new string[] { "access_as_app" })]
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
/// </code>
/// </example>
/// <seealso cref="M:RequiredScopeOrAppPermissionAttribute()"/> and <see cref="RequiredAppPermissionsConfigurationKey"/>
/// if you want to express the required scopes or app permissions from the configuration.
public RequiredScopeOrAppPermissionAttribute(string[] acceptedScopes, string[] acceptedAppPermissions)
{
AcceptedScope = acceptedScopes ?? throw new ArgumentNullException(nameof(acceptedScopes));
AcceptedAppPermission = acceptedAppPermissions ?? throw new ArgumentNullException(nameof(acceptedAppPermissions));
}

/// <summary>
/// Default constructor.
/// </summary>
/// <example>
/// <code>
/// [RequiredScopeOrAppPermission(RequiredScopesConfigurationKey="AzureAD:Scope", RequiredAppPermissionsConfigurationKey="AzureAD:AppPermission")]
/// class Controller : BaseController
/// {
/// }
/// </code>
/// </example>
public RequiredScopeOrAppPermissionAttribute()
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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 or app permission attribute during application startup.
/// </summary>
public static class RequiredScopeOrAppPermissionExtensions
{
/// <summary>
/// This method adds support for the required scope or app permission attribute. It adds a default policy that
/// adds a scope requirement or app permission requirement.
/// This requirement looks for IAuthRequiredScopeOrAppPermissionMetadata on the current endpoint.
/// </summary>
/// <param name="services">The services being configured.</param>
/// <returns>Services.</returns>
public static IServiceCollection AddRequiredScopeOrAppPermissionAuthorization(this IServiceCollection services)
{
services.AddAuthorization();

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

/// <summary>
/// This method adds metadata to route endpoint to describe required scopes or app permissions. It's the imperative version of
/// the [RequiredScopeOrAppPermission] attribute.
/// </summary>
/// <typeparam name="TBuilder">Class implementing <see cref="IEndpointConventionBuilder"/>.</typeparam>
/// <param name="endpointConventionBuilder">To customize the endpoints.</param>
/// <param name="scope">Scope.</param>
/// <param name="appPermission">App permission.</param>
/// <returns>Builder.</returns>
public static TBuilder RequireScope<TBuilder>(this TBuilder endpointConventionBuilder, string[] scope, string[] appPermission)
jennyf19 marked this conversation as resolved.
Show resolved Hide resolved
where TBuilder : IEndpointConventionBuilder
{
return endpointConventionBuilder.WithMetadata(new RequiredScopeOrAppPermissionMetadata(scope, appPermission));
}

private sealed class RequiredScopeOrAppPermissionMetadata : IAuthRequiredScopeMetadata
{
public RequiredScopeOrAppPermissionMetadata(string[] scope, string[] appPermission)
{
AcceptedScope = scope;
AcceptedAppPermission = appPermission;
}

public string[]? AcceptedScope { get; }
public string[]? AcceptedAppPermission { get; }

public string? RequiredScopesConfigurationKey { get; }
public string? RequiredAppPermissionsConfigurationKey { get; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
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;
Expand Down
Loading