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

Enhance the Two-Factor Authentication settings #16707

Merged
merged 6 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -42,7 +42,10 @@ public override IDisplayResult Edit(ISite site, RoleLoginSettings settings, Buil
{
model.RequireTwoFactorAuthenticationForSpecificRoles = settings.RequireTwoFactorAuthenticationForSpecificRoles;
var roles = await _roleService.GetRolesAsync();
model.Roles = roles.Select(role => new RoleEntry()

model.Roles = roles
.Where(role => !RoleHelper.SystemRoleNames.Contains(role.RoleName))
.Select(role => new RoleEntry()
{
Role = role.RoleName,
IsSelected = settings.Roles != null && settings.Roles.Contains(role.RoleName, StringComparer.OrdinalIgnoreCase),
Expand All @@ -66,7 +69,8 @@ public override async Task<IDisplayResult> UpdateAsync(ISite site, RoleLoginSett

if (model.RequireTwoFactorAuthenticationForSpecificRoles)
{
var roles = await _roleService.GetRolesAsync();
var roles = (await _roleService.GetRolesAsync())
.Where(role => !RoleHelper.SystemRoleNames.Contains(role.RoleName));

var selectedRoles = model.Roles.Where(x => x.IsSelected)
.Join(roles, e => e.Role, r => r.RoleName, (e, r) => r.RoleName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<input type="checkbox" class="form-check-input" asp-for="UseEmailAsAuthenticatorDisplayName" />
<span asp-validation-for="UseEmailAsAuthenticatorDisplayName"></span>
<label class="form-check-label" asp-for="UseEmailAsAuthenticatorDisplayName">@T["Show email address in the app"]</label>
<span class="hint dashed">@T["When selected, the users will see their email address in the authenticatior app. Otherwise, the username will be displayed."]</span>
<span class="hint dashed">@T["When selected, the users will see their email address in the authenticator app. Otherwise, the username will be displayed."]</span>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
<span asp-validation-for="Subject"></span>
<label class="form-label" asp-for="Subject">@T["Email Subject"]</label>
<input type="text" class="form-control" asp-for="Subject" />
<span class="hint dashed">@T["The subject to the email for sending the verification code. Liquid syntax is supported."]</span>
<span class="hint">@T["The subject to the email for sending the verification code. Liquid syntax is supported."]</span>
</div>

<div class="mb-3" asp-validation-class-for="Body">
<span asp-validation-for="Body"></span>
<label class="form-label" asp-for="Body">@T["Email Body"]</label>
<textarea class="form-control" asp-for="Body"></textarea>
<span class="hint dashed">@T["The body to the email for sending the verification code. Liquid syntax is supported."]</span>
<span class="hint">@T["The body to the email for sending the verification code. Liquid syntax is supported."]</span>
</div>
</fieldset>
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</div>

<div class="mb-3 ps-3 collapse@(Model.RequireTwoFactorAuthenticationForSpecificRoles ? " show": string.Empty)" asp-validation-class-for="Roles" id="rolesContainer">
<span class="hint dashed">@T["Select the roles to require two-factor authentication for."]</span>
<span class="hint">@T["Select the roles to require two-factor authentication for."]</span>

@for (var i = 0; i < Model.Roles.Length; i++)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,43 @@
</div>
</div>

<h5 class="mb-3">@T["External Authentication"]<span class="hint dashed">@T["Settings when registering with external authentication providers"]</span></h5>
<h5 class="mb-3">@T["External Authentication"] <span class="hint dashed">@T["Settings when registering with external authentication providers"]</span></h5>

<div class="mb-3" asp-validation-class-for="NoUsernameForExternalUsers">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="NoUsernameForExternalUsers" data-external-registration />
<span asp-validation-for="NoUsernameForExternalUsers"></span>
<label class="form-check-label" asp-for="NoUsernameForExternalUsers">@T["Do not ask username"]</label>
<span class="hint dashed">@T["When a new user logs in with an external provider, they are not required to provide a local username. You can customize how it works by providing an IExternalLoginEventHandler or writing a script."]</span>
</div>
</div>
<div class="mb-3" asp-validation-class-for="NoEmailForExternalUsers">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="NoEmailForExternalUsers" data-external-registration />
<span asp-validation-for="NoEmailForExternalUsers"></span>
<label class="form-check-label" asp-for="NoEmailForExternalUsers">@T["Do not ask email address"]</label>
<span class="hint dashed">@T["When a new user logs in with an external provider and the email claim is defined, they are not required to provide a local email address."]</span>
</div>
</div>
<div class="mb-3" asp-validation-class-for="NoPasswordForExternalUsers">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="NoPasswordForExternalUsers" data-external-registration />
<span asp-validation-for="NoPasswordForExternalUsers"></span>
<label class="form-check-label" asp-for="NoPasswordForExternalUsers">@T["Do not create local password for external users"]</label>
<span class="hint dashed">@T["When a new user logs in with an external provider, they are not required to provide a local password."]</span>
</div>
</div>
<div class="mb-3" asp-validation-class-for="UseScriptToGenerateUsername">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseScriptToGenerateUsername" />
<span asp-validation-for="UseScriptToGenerateUsername"></span>
<label class="form-check-label" asp-for="UseScriptToGenerateUsername">@T["Use a script to generate userName based on external provider claims"]</label>
<span class="hint dashed">@T["If selected, any IExternalLoginEventHandlers defined in modules are not triggered"]</span>
</div>
<pre>
</div>

<div class="@(Model.UseScriptToGenerateUsername ? string.Empty : "d-none")" id="ScriptToGenerateUsername_Wrapper">
<pre>
********************************************************************************************
* context : {userName,loginProvider,externalClaims[]} *
* ======================================================================================== *
Expand All @@ -88,83 +115,86 @@
* *
********************************************************************************************
</pre>
</div>
<div class="mb-3" asp-validation-class-for="GenerateUsernameScript">
<button type="button" class="btn btn-secondary mb-2" onclick="resetScript()">@T["Reset Script"]</button>
<textarea asp-for="GenerateUsernameScript" rows="1" class="form-control content-preview-text"></textarea>
</div>

<div class="mb-3" asp-validation-class-for="NoUsernameForExternalUsers">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="NoUsernameForExternalUsers" data-external-registration />
<span asp-validation-for="NoUsernameForExternalUsers"></span>
<label class="form-check-label" asp-for="NoUsernameForExternalUsers">@T["Do not ask username"]</label>
<span class="hint dashed">@T["When a new user logs in with an external provider, they are not required to provide a local username. You can customize how it works by providing an IExternalLoginEventHandler or writing a script."]</span>
</div>
</div>
<div class="mb-3" asp-validation-class-for="NoEmailForExternalUsers">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="NoEmailForExternalUsers" data-external-registration />
<span asp-validation-for="NoEmailForExternalUsers"></span>
<label class="form-check-label" asp-for="NoEmailForExternalUsers">@T["Do not ask email address"]</label>
<span class="hint dashed">@T["When a new user logs in with an external provider and the email claim is defined, they are not required to provide a local email address."]</span>
<div class="mb-3" asp-validation-class-for="GenerateUsernameScript">
<textarea asp-for="GenerateUsernameScript" rows="5" class="form-control content-preview-text"></textarea>
</div>
</div>
<div class="mb-3" asp-validation-class-for="NoPasswordForExternalUsers">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="NoPasswordForExternalUsers" data-external-registration />
<span asp-validation-for="NoPasswordForExternalUsers"></span>
<label class="form-check-label" asp-for="NoPasswordForExternalUsers">@T["Do not create local password for external users"]</label>
<span class="hint dashed">@T["When a new user logs in with an external provider, they are not required to provide a local password."]</span>

<div class="mb-3">
<button type="button" class="btn btn-secondary mb-2" id="ResetScriptButton">@T["Reset Script"]</button>
</div>

</div>

<script at="Foot">
//<![CDATA[
function refresh(e) {
var usersCanRegisterElement = document.getElementById("@Html.IdFor(m => m.UsersCanRegister)");
var externalRegistrationInputs = $("input[data-external-registration]");
var externalRegistrationInputs = document.querySelector("input[data-external-registration]");

if (usersCanRegisterElement.value == "NoRegistration") {
externalRegistrationInputs.prop('disabled', true);
externalRegistrationInputs.disabled = true;
} else {
externalRegistrationInputs.prop('disabled', false);
externalRegistrationInputs.disabled = false;
}
}

function resetScript(element) {

if (element.editor.doc.getValue() != '') {
return;
}

element.editor.doc.setValue(
'/* Uncomment to map AzureAd\n' +
'// Uncomment to output the context variable in the logs\n' +
'// log("warning", JSON.stringify(context));\n' +
'switch (context.loginProvider) {\n' +
' case "AzureAd":\n' +
' context.userName = "azad" + uuid();\n' +
' break;\n' +
' default:\n' +
' log("Warning", "Provider " + context.loginProvider + " was not handled");\n' +
' // Uncomment to generate a username as a uuid\n' +
' // context.userName = "u" + uuid();\n' +
' break;\n' +
'}\n' +
'*/\n'
);
}

function resetScript(keepText) {
var editor = $('#@Html.IdFor(x => x.GenerateUsernameScript)').data('editor');
if (!keepText) {
editor.doc.setValue(
'/* Uncomment to map AzureAd\n' +
'// Uncomment to output the context variable in the logs\n' +
'// log("warning", JSON.stringify(context));\n' +
'switch (context.loginProvider) {\n' +
' case "AzureAd":\n' +
' context.userName = "azad" + uuid();\n' +
' break;\n' +
' default:\n' +
' log("Warning", "Provider " + context.loginProvider + " was not handled");\n' +
' // Uncomment to generate a username as a uuid\n' +
' // context.userName = "u" + uuid();\n' +
' break;\n' +
'}\n' +
'*/\n'
);
function toggleEditorState(element) {
if (element.checked) {
document.getElementById("ScriptToGenerateUsername_Wrapper").classList.remove('d-none');
document.getElementById('@Html.IdFor(x => x.GenerateUsernameScript)').classList.remove('d-none');
} else {
document.getElementById("ScriptToGenerateUsername_Wrapper").classList.add('d-none');
document.getElementById('@Html.IdFor(x => x.GenerateUsernameScript)').classList.add('d-none');
}
}
document.getElementById('ResetScriptButton').addEventListener('click', (e) => {

var textArea = document.getElementById('@Html.IdFor(x => x.GenerateUsernameScript)');

resetScript(textArea);
});

document.addEventListener('DOMContentLoaded', () => {
var useGenerateUserName = document.getElementById('@Html.IdFor(x => x.UseScriptToGenerateUsername)');

useGenerateUserName.addEventListener("change", (e) => toggleEditorState(e.target));
toggleEditorState(useGenerateUserName);

window.onload = function () {
refresh();

$("#@Html.IdFor(m => m.UsersCanRegister)").change(function () {
document.getElementById("@Html.IdFor(m => m.UsersCanRegister)").addEventListener('change', function () {
refresh();
});

var textArea = document.getElementById('@Html.IdFor(x => x.GenerateUsernameScript)');
if (textArea == null) {
return;
}

var editor = CodeMirror.fromTextArea(textArea, {
autoRefresh: true,
lineNumbers: true,
Expand All @@ -173,8 +203,9 @@
autoCloseTags: true,
mode: "javascript"
});
$('#@Html.IdFor(x => x.GenerateUsernameScript)').data('editor', editor);
resetScript(editor.doc.getValue() != '');
};
//]]>

textArea.editor = editor;

resetScript(textArea);
});
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
<span asp-validation-for="Body"></span>
<label class="form-check-label" asp-for="Body">@T["Message Body"]</label>
<textarea class="form-control" asp-for="Body"></textarea>
<span class="hint dashed">@T["The body to the message for sending the verification code. Liquid syntax is supported."]</span>
<span class="hint">@T["The body to the message for sending the verification code. Liquid syntax is supported."]</span>
</div>
</fieldset>
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.Extensions.Options

@inject IOptions<CookieAuthenticationOptions> CookieAuthenticationOptions

@model OrchardCore.Users.Models.TwoFactorLoginSettings

<fieldset>
Expand All @@ -8,7 +13,10 @@
<input type="checkbox" class="form-check-input" asp-for="AllowRememberClientTwoFactorAuthentication" />
<span asp-validation-for="AllowRememberClientTwoFactorAuthentication"></span>
<label class="form-check-label" asp-for="AllowRememberClientTwoFactorAuthentication">@T["Allow users to remember client"]</label>
<span class="hint dashed">@T["When selected, users may use Remember Client during login to avoid having to provide a token every time."]</span>
<span class="hint dashed">
@T["When selected, users may use Remember Client during login to avoid having to provide a token every time."]
@T["The client will be remembered for:"] @await DisplayAsync(await New.Duration(timeSpan: CookieAuthenticationOptions.Value.ExpireTimeSpan))
</span>
</div>
</div>

Expand All @@ -25,15 +33,15 @@
<span asp-validation-for="NumberOfRecoveryCodesToGenerate"></span>
<label class="form-check-label" asp-for="NumberOfRecoveryCodesToGenerate">@T["Number of recovery codes to generate"]</label>
<input type="number" class="form-control" min="1" asp-for="NumberOfRecoveryCodesToGenerate" />
<span class="hint dashed">@T["The number of recovery codes to generate. Default is 5."]</span>
<span class="hint">@T["The number of recovery codes to generate. Default is 5."]</span>
</div>

<div class="mb-3" asp-validation-class-for="UseSiteTheme">
<div class="form-check">
<input type="checkbox" class="form-check-input" asp-for="UseSiteTheme" />
<span asp-validation-for="UseSiteTheme"></span>
<label class="form-check-label" asp-for="UseSiteTheme">@T["Use site theme for two-factor authentication pages"]</label>
<span class="hint dashed">@T["Requires an active site theme."]</span>
<span class="hint">@T["Requires an active site theme."]</span>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
using OrchardCore.DisplayManagement.Descriptors;
using OrchardCore.DisplayManagement.Html;
using OrchardCore.Modules;

namespace OrchardCore.DisplayManagement.Shapes;
Expand Down Expand Up @@ -113,6 +114,58 @@ public IHtmlContent TimeSpan(DateTime? Utc, DateTime? Origin)
: H["in a moment"];
}

[Shape]
public IHtmlContent Duration(TimeSpan? timeSpan)
{
if (timeSpan == null)
{
return HtmlString.Empty;
}

var days = (int)timeSpan.Value.TotalDays;
var hours = timeSpan.Value.Hours;
var minutes = timeSpan.Value.Minutes;
var seconds = timeSpan.Value.Seconds;

var builder = new HtmlContentBuilder();

if (days > 0)
{
builder.AppendHtml(H.Plural(days, "{1} day", "{1} days", days));
}

if (hours > 0)
{
if (builder.Count > 0)
{
builder.AppendWhitespace();
}
builder.AppendHtml(H.Plural(hours, "{1} hour", "{1} hours", hours));
Copy link
Member

Choose a reason for hiding this comment

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

You can't concatenate translated strings. It has to be a single one, which is this case doesn't seem possible because of pluralization (days could be one and hours multiple).

The only solution I could think of is to only keep the big part like "{0} days", or "{o} hours" and not concatenate.

Copy link
Member Author

Choose a reason for hiding this comment

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

why wouldn't this work? each phrase can be translated separately and then build into html content.

Here is an example

image

Copy link
Member

Choose a reason for hiding this comment

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

why wouldn't this work

For one some people write from right to left ;) And pretty sure we can find languages where durations don't work this way too.

}

if (minutes > 0)
{
if (builder.Count > 0)
{
builder.AppendWhitespace();
}

builder.AppendHtml(H.Plural(hours, "{1} minute", "{1} minutes", minutes));
}

if (seconds > 0)
{
if (builder.Count > 0)
{
builder.AppendWhitespace();
}

builder.AppendHtml(H.Plural(seconds, "{1} second", "{1} seconds", seconds));
}

return builder;
}

[Shape]
public async Task<IHtmlContent> DateTime(IHtmlHelper Html, DateTime? Utc, string Format)
{
Expand Down