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

feat: add requirement for denying guest users #2179

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ public static class ClaimsPrincipalExtensions
/// </summary>
private const string NameIdentifierId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";

/// <summary>
/// Issuer claim: "iss".
/// </summary>
private const string Iss = "iss";

/// <summary>
/// Identity Provider claim: "idp".
/// </summary>
private const string Idp = "idp";

/// <summary>
/// Old Identity Provider claim: "http://schemas.microsoft.com/identity/claims/identityprovider".
/// </summary>
private const string IdentityProvider = "http://schemas.microsoft.com/identity/claims/identityprovider";

private const string MsaTenantId = "9188040d-6c67-4c5b-b112-36a304b66dad";
private const string Consumers = "consumers";
private const string Organizations = "organizations";
Expand Down Expand Up @@ -116,6 +131,17 @@ public static class ClaimsPrincipalExtensions
return GetClaimValue(claimsPrincipal, Tid, TenantId);
}

/// <summary>
/// Gets the Identity Provider associated with the <see cref="ClaimsPrincipal"/>.
/// </summary>
/// <param name="claimsPrincipal">The <see cref="ClaimsPrincipal"/> from which to retrieve the tenant ID.</param>
/// <returns>Tenant Identity Provider used to log in the user, or <c>null</c> if it cannot be found.</returns>
/// <remarks>This method returns the Identity Provider both in case the developer has enabled or not claims mapping, and if none is present, it returns the Issuer.</remarks>
public static string? GetIdentityProvider(this ClaimsPrincipal claimsPrincipal)
{
return GetClaimValue(claimsPrincipal, Idp, IdentityProvider, Iss);
}

/// <summary>
/// Gets the login-hint associated with a <see cref="ClaimsPrincipal"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;

namespace Microsoft.Identity.Web
{
internal class DenyGuestsAuthorizationsHandler : AuthorizationHandler<DenyGuestsAuthorizationRequirement>
{
private const string Acct = "acct";
private const string TenantMember = "0";

private const string Iss = "iss";

protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
DenyGuestsAuthorizationRequirement requirement)
{
_ = Throws.IfNull(context);

_ = Throws.IfNull(requirement);

// acct is an optional claim
// if it is present, it dictates wether the user is a guest or not
var acct = context.User.FindFirstValue(Acct);

if (!string.IsNullOrEmpty(acct))
{
if (acct == TenantMember)
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}

// if acct is not present
// we can use the iss and idp claim to determine if the user is a guest
var iss = context.User.FindFirstValue(Iss);
var idp = context.User.GetIdentityProvider();

if (!string.IsNullOrEmpty(iss) && iss == idp)
{
context.Succeed(requirement);
return Task.CompletedTask;
}

return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Authorization;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Implements an <see cref="IAuthorizationRequirement"/>
/// which requires the current user to be a member of the tenant.
/// </summary>
public class DenyGuestsAuthorizationRequirement : IAuthorizationRequirement
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not locked on the naming of this. I see many requirements are prefixed with Require, but RequireTenantMembers or RequireNonGuests sounds weird and isn't as explicit in what it tries to achieve.

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

using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Extensions for building the deny guest requirement during application startup.
/// </summary>
public static class DenyGuestsExtensions
{
/// <summary>
/// This method adds support for the deny guests requirement.
/// </summary>
/// <param name="services">The services being configured.</param>
/// <returns>Services.</returns>
public static IServiceCollection AddDenyGuestsAuthorization(this IServiceCollection services)
{
services.AddAuthorization();

services.TryAddEnumerable(ServiceDescriptor.Singleton<IAuthorizationHandler, DenyGuestsAuthorizationsHandler>());

return services;
}
}
}
17 changes: 15 additions & 2 deletions src/Microsoft.Identity.Web/Policy/PolicyBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

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

namespace Microsoft.Identity.Web
Expand All @@ -21,6 +19,21 @@ namespace Microsoft.Identity.Web
/// </example>
public static class PolicyBuilderExtensions
{
/// <summary>
/// Adds a <see cref="DenyGuestsAuthorizationRequirement"/> to the current instance which requires
/// that the current user is a member of the tenant.
/// </summary>
/// <param name="authorizationPolicyBuilder">Used for building policies during application startup.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static AuthorizationPolicyBuilder DenyGuests(this AuthorizationPolicyBuilder authorizationPolicyBuilder)
{
_ = Throws.IfNull(authorizationPolicyBuilder);

authorizationPolicyBuilder.Requirements.Add(new DenyGuestsAuthorizationRequirement());

return authorizationPolicyBuilder;
}

/// <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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ private static void AddMicrosoftIdentityWebApiImplementation(
builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClient();
builder.Services.TryAddSingleton<MicrosoftIdentityIssuerValidatorFactory>();
builder.Services.AddDenyGuestsAuthorization();
builder.Services.AddRequiredScopeAuthorization();
builder.Services.AddRequiredScopeOrAppPermissionAuthorization();
builder.Services.AddOptions<AadIssuerValidatorOptions>();
Expand Down