Skip to content

Commit

Permalink
Allow Admin to disable Two-Factor Authentication (#16899)
Browse files Browse the repository at this point in the history
  • Loading branch information
MikeAlhayek authored Oct 17, 2024
1 parent 5417cbb commit 50e9bba
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OrchardCore.Admin;
using OrchardCore.Modules;
using OrchardCore.Mvc.Core.Utilities;

namespace OrchardCore.Users.Controllers;

[Admin]
[Feature(UserConstants.Features.TwoFactorAuthentication)]
public sealed class AdminTwoFactorAuthenticationController : Controller
{
private readonly UserManager<IUser> _userManager;
private readonly IAuthorizationService _authorizationService;

public AdminTwoFactorAuthenticationController(
UserManager<IUser> userManager,
IAuthorizationService authorizationService)
{
_userManager = userManager;
_authorizationService = authorizationService;
}

public async Task<IActionResult> Disable(string id)
{
if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.DisableTwoFactorAuthenticationForUsers))
{
return Forbid();
}

var user = await _userManager.FindByIdAsync(id);

if (user == null)
{
return NotFound();
}



if (await _userManager.GetTwoFactorEnabledAsync(user))
{
await _userManager.SetTwoFactorEnabledAsync(user, false);
}

return RedirectToAction(nameof(AdminController.Index), typeof(AdminController).ControllerName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ public override Task<IDisplayResult> DisplayAsync(User user, BuildDisplayContext
return CombineAsync(
Initialize<SummaryAdminUserViewModel>("UserFields", model => model.User = user).Location("SummaryAdmin", "Header:1"),
Initialize<SummaryAdminUserViewModel>("UserInfo", model => model.User = user).Location("DetailAdmin", "Content:5"),
Initialize<SummaryAdminUserViewModel>("UserButtons", model => model.User = user).Location("SummaryAdmin", "Actions:1")
Initialize<SummaryAdminUserViewModel>("UserButtons", model => model.User = user).Location("SummaryAdmin", "Actions:1"),
Initialize<SummaryAdminUserViewModel>("UserActionsMenu", model => model.User = user).Location("SummaryAdmin", "ActionsMenu:5")
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Users.Models;
using OrchardCore.Users.ViewModels;

namespace OrchardCore.Users.Drivers;

public sealed class UserRegistrationAdminDisplayDriver : DisplayDriver<User>
{
public override Task<IDisplayResult> DisplayAsync(User user, BuildDisplayContext context)
{
return CombineAsync(
Initialize<SummaryAdminUserViewModel>("UserSendConfirmationActionsMenu", model => model.User = user)
.Location("SummaryAdmin", "ActionsMenu:15")
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Users.Models;
using OrchardCore.Users.ViewModels;

namespace OrchardCore.Users.Drivers;

public sealed class UserTwoFactorDisplayDriver : DisplayDriver<User>
{
public override Task<IDisplayResult> DisplayAsync(User user, BuildDisplayContext context)
{
return CombineAsync(
Initialize<SummaryAdminUserViewModel>("UserTwoFactorActionsMenu", model => model.User = user)
.Location("SummaryAdmin", "ActionsMenu:10")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ internal static async Task<string> SendEmailConfirmationTokenAsync(this Controll
},
protocol: controller.HttpContext.Request.Scheme);

await SendEmailAsync(controller, user.Email, subject, new ConfirmEmailViewModel()
await SendEmailAsync(controller, user.Email, subject, new ConfirmEmailViewModel
{
User = user,
ConfirmEmailUrl = callbackUrl,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using OrchardCore.Security.Permissions;

namespace OrchardCore.Users.Services;

public sealed class TwoFactorPermissionProvider : IPermissionProvider
{
private readonly IEnumerable<Permission> _allPermissions =
[
CommonPermissions.DisableTwoFactorAuthenticationForUsers,
];
public Task<IEnumerable<Permission>> GetPermissionsAsync()
=> Task.FromResult(_allPermissions);

public IEnumerable<PermissionStereotype> GetDefaultStereotypes() =>
[
new PermissionStereotype
{
Name = OrchardCoreConstants.Roles.Administrator,
Permissions = _allPermissions,
},
];
}
1 change: 1 addition & 0 deletions src/OrchardCore.Modules/OrchardCore.Users/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ public override void ConfigureServices(IServiceCollection services)
o.MemberAccessStrategy.Register<ConfirmEmailViewModel>();
});

services.AddScoped<IDisplayDriver<User>, UserRegistrationAdminDisplayDriver>();
services.AddSiteDisplayDriver<RegistrationSettingsDisplayDriver>();
services.AddNavigationProvider<RegistrationAdminMenu>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.Modules;
using OrchardCore.Mvc.Core.Utilities;
using OrchardCore.Security.Permissions;
using OrchardCore.Users.Controllers;
using OrchardCore.Users.Drivers;
using OrchardCore.Users.Endpoints.EmailAuthenticator;
Expand All @@ -32,10 +33,12 @@ public override void ConfigureServices(IServiceCollection services)
options.Filters.Add<TwoFactorAuthenticationAuthorizationFilter>();
});

services.AddScoped<IDisplayDriver<User>, UserTwoFactorDisplayDriver>();
services.AddScoped<IUserClaimsProvider, TwoFactorAuthenticationClaimsProvider>();
services.AddScoped<IDisplayDriver<UserMenu>, TwoFactorUserMenuDisplayDriver>();
services.AddSiteDisplayDriver<TwoFactorLoginSettingsDisplayDriver>();
services.AddScoped<IDisplayDriver<TwoFactorMethod>, TwoFactorMethodDisplayDriver>();
services.AddPermissionProvider<TwoFactorPermissionProvider>();
}

public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@using Microsoft.AspNetCore.Identity
@using OrchardCore.Users.Models

@model SummaryAdminUserViewModel

@inject UserManager<IUser> UserManager
@inject IAuthorizationService AuthorizationService

@if (!Model.User.EmailConfirmed &&
Site.As<RegistrationSettings>().UsersMustValidateEmail &&
await AuthorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, Model.User))
{
<li>
<form method="post" class="d-inline-block" class="no-multisubmit">
<input name="id" type="hidden" value="@Model.User.UserId" />
<button asp-action="SendVerificationEmail" asp-controller="EmailConfirmation" class="dropdown-item">@T["Send verification email"]</button>
</form>
</li>
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@T["Dear {0},", Model.User.UserName]
</p>

<p>@T["Please <a href=\"{0}\">click here</a> to confirm your account.", Model.ConfirmEmailUrl]</p>
<p>@T["Please <a href=\"{0}\">click here</a> to confirm your account.", Html.Raw(Model.ConfirmEmailUrl)]</p>

<p>@T.Plural(totalMinutes, "Please be aware that this link will expire in {0} minute.", "Please be aware that this link will expire in {0} minutes.", totalMinutes)</p>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,22 @@
}
</div>

@if (Model.Actions != null)
{
<div>
<div>
@if (Model.Actions != null)
{
@await DisplayAsync(Model.Actions)
</div>
}
}

@if (Model.ActionsMenu != null && Model.ActionsMenu.HasItems)
{
<div class="btn-group" title="@T["Actions"]">
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle actions" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span>@T["Actions"]</span>
</button>
<ul class="actions-menu dropdown-menu dropdown-menu-end">
@await DisplayAsync(Model.ActionsMenu)
</ul>
</div>
}
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@using Microsoft.AspNetCore.Identity

@model SummaryAdminUserViewModel

@inject UserManager<IUser> UserManager
@inject IAuthorizationService AuthorizationService

@{
var isCurrentUser = Model.User.UserName == User.Identity.Name;
var canEdit = await AuthorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, Model.User);
var isLockedOut = await UserManager.IsLockedOutAsync(Model.User);
}

@if (canEdit)
{
<li>
<a asp-action="EditPassword" asp-route-id="@Model.User.UserId" class="dropdown-item">@T["Change password"]</a>
</li>
if (isLockedOut)
{
<li>
<a asp-action="Unlock" asp-route-id="@Model.User.UserId" class="dropdown-item" data-url-af="RemoveUrl UnsafeUrl" data-title="@T["Unlock user"]" data-message="@T["Are you sure you want to unlock this user?"]">@T["Unlock"]</a>
</li>
}
}

@if (!isCurrentUser && await AuthorizationService.AuthorizeAsync(User, CommonPermissions.DeleteUsers, Model.User))
{
<li>
<a asp-action="Delete" asp-route-id="@Model.User.UserId" class="dropdown-item text-danger" data-url-af="RemoveUrl UnsafeUrl">@T["Delete"]</a>
</li>
}
33 changes: 3 additions & 30 deletions src/OrchardCore.Modules/OrchardCore.Users/Views/UserButtons.cshtml
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
@model SummaryAdminUserViewModel
@using OrchardCore.Entities
@using OrchardCore.Users.Models
@using Microsoft.AspNetCore.Identity
@using OrchardCore.Users
@inject UserManager<IUser> UserManager

@inject IAuthorizationService AuthorizationService

@{
var isCurrentUser = Model.User.UserName == User.Identity.Name;
var canEdit = await AuthorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, Model.User);
var isLockedOut = await UserManager.IsLockedOutAsync(Model.User);
}

@if (canEdit)
@if (await AuthorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, Model.User))
{
<a asp-action="Edit" asp-route-id="@Model.User.UserId" class="btn btn-primary btn-sm">@T["Edit"]</a>
}
Expand All @@ -20,25 +15,3 @@
{
<a asp-action="Display" asp-route-id="@Model.User.UserId" class="btn btn-success btn-sm">@T["View"]</a>
}

@if (canEdit)
{
<a asp-action="EditPassword" asp-route-id="@Model.User.UserId" class="btn btn-secondary btn-sm">@T["Edit Password"]</a>
if (isLockedOut)
{
<a asp-action="Unlock" asp-route-id="@Model.User.UserId" class="btn btn-danger btn-sm" data-url-af="RemoveUrl UnsafeUrl" data-title="@T["Unlock user"]" data-message="@T["Are you sure you want to unlock this user?"]">@T["Unlock"]</a>
}
}

@if (!isCurrentUser && await AuthorizationService.AuthorizeAsync(User, CommonPermissions.DeleteUsers, Model.User))
{
<a asp-action="Delete" asp-route-id="@Model.User.UserId" class="btn btn-danger btn-sm" data-url-af="RemoveUrl UnsafeUrl">@T["Delete"]</a>
}

@if (canEdit && !Model.User.EmailConfirmed && Site.As<RegistrationSettings>().UsersMustValidateEmail)
{
<form method="post" class="d-inline-block" class="no-multisubmit">
<input name="id" type="hidden" value="@Model.User.UserId" />
<button asp-action="SendVerificationEmail" asp-controller="Registration" class="btn btn-info btn-sm">@T["Send verification email"]</button>
</form>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@using Microsoft.AspNetCore.Identity
@using OrchardCore.Users.Models

@model SummaryAdminUserViewModel

@inject UserManager<IUser> UserManager
@inject IAuthorizationService AuthorizationService

@if (await AuthorizationService.AuthorizeAsync(User, CommonPermissions.DisableTwoFactorAuthenticationForUsers, Model.User) &&
await UserManager.GetTwoFactorEnabledAsync(Model.User))
{
<li>
<form method="post" class="d-inline-block" class="no-multisubmit">
<input name="id" type="hidden" value="@Model.User.UserId" />
<button asp-action="Disable" asp-controller="AdminTwoFactorAuthentication" class="dropdown-item">@T["Disable two-factor authentication"]</button>
</form>
</li>
}
2 changes: 2 additions & 0 deletions src/OrchardCore/OrchardCore.Users.Core/CommonPermissions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public static class CommonPermissions

public static readonly Permission AssignRoleToUsers = new("AssignRoleToUsers", "Assign any role to users", [EditUsers], true);

public static readonly Permission DisableTwoFactorAuthenticationForUsers = new("DisableTwoFactorAuthenticationForUsers", "Disable two-factor authentication for any user", [ManageUsers], true);

public static readonly Permission EditOwnUser = new("ManageOwnUserInformation", "Edit own user information", [EditUsers]);

public static Permission CreateEditUsersInRolePermission(string roleName) =>
Expand Down
4 changes: 4 additions & 0 deletions src/docs/releases/2.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ Also, note the following updates in `ExternalLoginSettings`:
!!! warning
When updating recipes to configure `LoginSettings` or `RegistrationSettings`, ensure that the settings reflect the new class structure.

### Users With Permission Can Disable Two-Factor Authentication

Users granted the new `DisableTwoFactorAuthenticationForUsers` permission can now disable two-factor authentication for other users directly from the Users Admin UI.

### User Registration Feature

#### User Registration Feature No Longer Required for External Authentication
Expand Down

0 comments on commit 50e9bba

Please sign in to comment.