Skip to content

Commit f2bb617

Browse files
authored
Jennyf/scopes roles (#1742)
* initial commit for app permissions * add test coverage * remove IEnumerable and use string[] * PR comments
1 parent bb2d213 commit f2bb617

14 files changed

+926
-4
lines changed

src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml

Lines changed: 252 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.Identity.Web/Policy/IAuthRequiredScopeMetadata.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public interface IAuthRequiredScopeMetadata
1515
/// <summary>
1616
/// Scopes accepted by this web API.
1717
/// </summary>
18-
IEnumerable<string>? AcceptedScope { get; }
18+
string[]? AcceptedScope { get; }
1919

2020
/// <summary>
2121
/// Fully qualified name of the configuration key containing the required scopes (separated
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.Identity.Web
7+
{
8+
/// <summary>
9+
/// This is the metadata that describes required auth scopes or app permissions for a given endpoint
10+
/// in a web API. It's the underlying data structure the requirement <see cref="ScopeOrAppPermissionAuthorizationRequirement"/> will look for
11+
/// in order to validate scopes in the scope claims or app permissions in the roles claim.
12+
/// </summary>
13+
public interface IAuthRequiredScopeOrAppPermissionMetadata
14+
{
15+
/// <summary>
16+
/// App permissions accepted by this web API.
17+
/// App permissions appear in the roles claim of the token.
18+
/// </summary>
19+
string[]? AcceptedAppPermission { get; }
20+
21+
/// <summary>
22+
/// Fully qualified name of the configuration key containing the required
23+
/// app permissions (separated by spaces).
24+
/// </summary>
25+
string? RequiredAppPermissionsConfigurationKey { get; }
26+
27+
/// <summary>
28+
/// Scopes accepted by this web API.
29+
/// </summary>
30+
string[]? AcceptedScope { get; }
31+
32+
/// <summary>
33+
/// Fully qualified name of the configuration key containing the required scopes (separated
34+
/// by spaces).
35+
/// </summary>
36+
string? RequiredScopesConfigurationKey { get; }
37+
}
38+
}

src/Microsoft.Identity.Web/Policy/PolicyBuilderExtensions.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,29 @@ public static AuthorizationPolicyBuilder RequireScope(
5858
authorizationPolicyBuilder.Requirements.Add(new ScopeAuthorizationRequirement(allowedValues));
5959
return authorizationPolicyBuilder;
6060
}
61+
62+
/// <summary>
63+
/// Adds a <see cref="ScopeOrAppPermissionAuthorizationRequirement"/> to the current instance which requires
64+
/// that the current user has the specified claim and that the claim value must be one of the allowed values.
65+
/// </summary>
66+
/// <param name="authorizationPolicyBuilder">Used for building policies during application startup.</param>
67+
/// <param name="allowedScopeValues">scopes (the value of scope or scp) accepted by this app.</param>
68+
/// <param name="allowedAppPermissionValues">App permission (in role claim) that this app accepts.</param>
69+
/// <returns>A reference to this instance after the operation has completed.</returns>
70+
public static AuthorizationPolicyBuilder RequireScopeOrAppPermission(
71+
this AuthorizationPolicyBuilder authorizationPolicyBuilder,
72+
IEnumerable<string> allowedScopeValues,
73+
IEnumerable<string> allowedAppPermissionValues)
74+
{
75+
if (authorizationPolicyBuilder == null)
76+
{
77+
throw new ArgumentNullException(nameof(authorizationPolicyBuilder));
78+
}
79+
80+
authorizationPolicyBuilder.Requirements.Add(new ScopeOrAppPermissionAuthorizationRequirement(
81+
allowedScopeValues,
82+
allowedAppPermissionValues));
83+
return authorizationPolicyBuilder;
84+
}
6185
}
6286
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Authorization;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace Microsoft.Identity.Web
9+
{
10+
/// <summary>
11+
/// RequireScopeOrAppPermissionOptions.
12+
/// </summary>
13+
internal class RequireScopeOrAppPermissionOptions : IPostConfigureOptions<AuthorizationOptions>
14+
{
15+
private readonly AuthorizationPolicy _defaultPolicy;
16+
17+
/// <summary>
18+
/// Sets the default policy.
19+
/// </summary>
20+
public RequireScopeOrAppPermissionOptions()
21+
{
22+
_defaultPolicy = new AuthorizationPolicyBuilder()
23+
.AddRequirements(new ScopeOrAppPermissionAuthorizationRequirement())
24+
.Build();
25+
}
26+
27+
/// <inheritdoc/>
28+
public void PostConfigure(
29+
string name,
30+
AuthorizationOptions options)
31+
{
32+
if (options == null)
33+
{
34+
throw new ArgumentNullException(nameof(options));
35+
}
36+
37+
options.DefaultPolicy = options.DefaultPolicy is null
38+
? _defaultPolicy
39+
: AuthorizationPolicy.Combine(options.DefaultPolicy, _defaultPolicy);
40+
}
41+
}
42+
}

src/Microsoft.Identity.Web/Policy/RequiredScopeAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class RequiredScopeAttribute : Attribute, IAuthRequiredScopeMetadata
1919
/// <summary>
2020
/// Scopes accepted by this web API.
2121
/// </summary>
22-
public IEnumerable<string>? AcceptedScope { get; set; }
22+
public string[]? AcceptedScope { get; set; }
2323

2424
/// <summary>
2525
/// Fully qualified name of the configuration key containing the required scopes (separated

src/Microsoft.Identity.Web/Policy/RequiredScopeExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public RequiredScopeMetadata(string[] scope)
5252
AcceptedScope = scope;
5353
}
5454

55-
public IEnumerable<string>? AcceptedScope { get; }
55+
public string[]? AcceptedScope { get; }
5656

5757
public string? RequiredScopesConfigurationKey { get; }
5858
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.Identity.Web.Resource
8+
{
9+
/// <summary>
10+
/// This attribute is used on a controller, pages, or controller actions
11+
/// to declare (and validate) the scopes or app permissions required by a web API.
12+
/// These scopes or app permissions can be declared in two ways:
13+
/// hardcoding them, or declaring them in the configuration. Depending on your
14+
/// choice, use either one or the other of the constructors.
15+
/// For details, see https://aka.ms/ms-id-web/required-scope-or-app-permissions-attribute.
16+
/// </summary>
17+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
18+
public class RequiredScopeOrAppPermissionAttribute : Attribute, IAuthRequiredScopeOrAppPermissionMetadata
19+
{
20+
/// <summary>
21+
/// Scopes accepted by this web API.
22+
/// </summary>
23+
public string[]? AcceptedScope { get; set; }
24+
25+
/// <summary>
26+
/// Fully qualified name of the configuration key containing the required scopes (separated
27+
/// by spaces).
28+
/// </summary>
29+
/// <example>
30+
/// If the appsettings.json file contains a section named "AzureAd", in which
31+
/// a property named "Scopes" contains the required scopes, the attribute on the
32+
/// controller/page/action to protect should be set to the following:
33+
/// <code>
34+
/// [RequiredScopeOrAppPermission(RequiredScopesConfigurationKey="AzureAd:Scopes")]
35+
/// </code>
36+
/// </example>
37+
public string? RequiredScopesConfigurationKey { get; set; }
38+
39+
/// <summary>
40+
/// App permissions accepted by this web API.
41+
/// App permissions appear in the roles claim of the token.
42+
/// </summary>
43+
public string[]? AcceptedAppPermission { get; set; }
44+
45+
/// <summary>
46+
/// Fully qualified name of the configuration key containing the required app permissions (separated
47+
/// by spaces).
48+
/// </summary>
49+
/// <example>
50+
/// If the appsettings.json file contains a section named "AzureAd", in which
51+
/// a property named "AppPermissions" contains the required app permissions, the attribute on the
52+
/// controller/page/action to protect should be set to the following:
53+
/// <code>
54+
/// [RequiredScopeOrAppPermission(RequiredAppPermissionsConfigurationKey="AzureAd:AppPermissions")]
55+
/// </code>
56+
/// </example>
57+
public string? RequiredAppPermissionsConfigurationKey { get; set; }
58+
59+
/// <summary>
60+
/// Verifies that the web API is called with the right app permissions.
61+
/// If the token obtained for this API is on behalf of the authenticated user does not have
62+
/// any of these <paramref name="acceptedScopes"/> in its scope claim,
63+
/// nor <paramref name="acceptedAppPermissions"/> in its roles claim, the
64+
/// method updates the HTTP response providing a status code 403 (Forbidden)
65+
/// and writes to the response body a message telling which scopes are expected in the token.
66+
/// </summary>
67+
/// <param name="acceptedScopes">Scopes accepted by this web API.</param>
68+
/// <param name="acceptedAppPermissions">App permissions accepted by this web API.</param>
69+
/// <remarks>When neither the scopes nor app permissions match, the response is a 403 (Forbidden),
70+
/// because the user is authenticated (hence not 401), but not authorized.</remarks>
71+
/// <example>
72+
/// Add the following attribute on the controller/page/action to protect:
73+
///
74+
/// <code>
75+
/// [RequiredScopeOrAppPermission(new [] { "access_as_user" }, new [] { "access_as_app" })]
76+
/// </code>
77+
/// </example>
78+
/// <seealso cref="M:RequiredScopeOrAppPermissionAttribute()"/> and <see cref="RequiredAppPermissionsConfigurationKey"/>
79+
/// if you want to express the required scopes or app permissions from the configuration.
80+
public RequiredScopeOrAppPermissionAttribute(string[] acceptedScopes, string[] acceptedAppPermissions)
81+
{
82+
AcceptedScope = acceptedScopes ?? throw new ArgumentNullException(nameof(acceptedScopes));
83+
AcceptedAppPermission = acceptedAppPermissions ?? throw new ArgumentNullException(nameof(acceptedAppPermissions));
84+
}
85+
86+
/// <summary>
87+
/// Default constructor.
88+
/// </summary>
89+
/// <example>
90+
/// <code>
91+
/// [RequiredScopeOrAppPermission(RequiredScopesConfigurationKey="AzureAD:Scope", RequiredAppPermissionsConfigurationKey="AzureAD:AppPermission")]
92+
/// class Controller : BaseController
93+
/// {
94+
/// }
95+
/// </code>
96+
/// </example>
97+
public RequiredScopeOrAppPermissionAttribute()
98+
{
99+
}
100+
}
101+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.AspNetCore.Authorization;
7+
using Microsoft.AspNetCore.Builder;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.DependencyInjection.Extensions;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Microsoft.Identity.Web
13+
{
14+
/// <summary>
15+
/// Extensions for building the required scope or app permission attribute during application startup.
16+
/// </summary>
17+
public static class RequiredScopeOrAppPermissionExtensions
18+
{
19+
/// <summary>
20+
/// This method adds support for the required scope or app permission attribute. It adds a default policy that
21+
/// adds a scope requirement or app permission requirement.
22+
/// This requirement looks for IAuthRequiredScopeOrAppPermissionMetadata on the current endpoint.
23+
/// </summary>
24+
/// <param name="services">The services being configured.</param>
25+
/// <returns>Services.</returns>
26+
public static IServiceCollection AddRequiredScopeOrAppPermissionAuthorization(this IServiceCollection services)
27+
{
28+
services.AddAuthorization();
29+
30+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<AuthorizationOptions>, RequireScopeOrAppPermissionOptions>());
31+
services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthorizationHandler, ScopeOrAppPermissionAuthorizationHandler>());
32+
return services;
33+
}
34+
35+
/// <summary>
36+
/// This method adds metadata to route endpoint to describe required scopes or app permissions. It's the imperative version of
37+
/// the [RequiredScopeOrAppPermission] attribute.
38+
/// </summary>
39+
/// <typeparam name="TBuilder">Class implementing <see cref="IEndpointConventionBuilder"/>.</typeparam>
40+
/// <param name="endpointConventionBuilder">To customize the endpoints.</param>
41+
/// <param name="scope">Scope.</param>
42+
/// <param name="appPermission">App permission.</param>
43+
/// <returns>Builder.</returns>
44+
public static TBuilder RequireScopeOrAppPermission<TBuilder>(this TBuilder endpointConventionBuilder, string[] scope, string[] appPermission)
45+
where TBuilder : IEndpointConventionBuilder
46+
{
47+
return endpointConventionBuilder.WithMetadata(new RequiredScopeOrAppPermissionMetadata(scope, appPermission));
48+
}
49+
50+
private sealed class RequiredScopeOrAppPermissionMetadata : IAuthRequiredScopeMetadata
51+
{
52+
public RequiredScopeOrAppPermissionMetadata(string[] scope, string[] appPermission)
53+
{
54+
AcceptedScope = scope;
55+
AcceptedAppPermission = appPermission;
56+
}
57+
58+
public string[]? AcceptedScope { get; }
59+
public string[]? AcceptedAppPermission { get; }
60+
61+
public string? RequiredScopesConfigurationKey { get; }
62+
public string? RequiredAppPermissionsConfigurationKey { get; }
63+
}
64+
}
65+
}

src/Microsoft.Identity.Web/Policy/ScopeAuthorizationHandler.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7-
using System.Security.Claims;
87
using System.Threading.Tasks;
98
using Microsoft.AspNetCore.Authorization;
109
using Microsoft.AspNetCore.Http;

0 commit comments

Comments
 (0)