Block enabled 2fa in the UI without cookie consent (#2035)
* Block enabled 2fa in the UI without cookie consent * Guard against feature not being there * Set up tweak * Fix
This commit is contained in:
parent
9405d058fa
commit
da9318f431
|
|
@ -1,4 +1,5 @@
|
|||
@page
|
||||
@using Microsoft.AspNetCore.Http.Features
|
||||
@model TwoFactorAuthenticationModel
|
||||
@{
|
||||
ViewData["Title"] = "Two-factor authentication (2FA)";
|
||||
|
|
@ -7,50 +8,64 @@
|
|||
|
||||
<partial name="_StatusMessage" model="Model.StatusMessage" />
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@if (Model.Is2faEnabled)
|
||||
{
|
||||
if (Model.RecoveryCodesLeft == 0)
|
||||
@{
|
||||
var consentFeature = HttpContext.Features.Get<ITrackingConsentFeature>();
|
||||
@if (consentFeature?.CanTrack ?? true)
|
||||
{
|
||||
@if (Model.Is2faEnabled)
|
||||
{
|
||||
if (Model.RecoveryCodesLeft == 0)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have no recovery codes left.</strong>
|
||||
<p>You must <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft == 1)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have 1 recovery code left.</strong>
|
||||
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft <= 3)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
|
||||
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (Model.IsMachineRemembered)
|
||||
{
|
||||
<form method="post" style="display: inline-block">
|
||||
<button type="submit" class="btn btn-default">Forget this browser</button>
|
||||
</form>
|
||||
}
|
||||
<a asp-page="./Disable2fa" class="btn btn-default">Disable 2FA</a>
|
||||
<a asp-page="./GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a>
|
||||
}
|
||||
|
||||
<h5>Authenticator app</h5>
|
||||
@if (!Model.HasAuthenticator)
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Add authenticator app</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Set up authenticator app</a>
|
||||
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-default">Reset authenticator app</a>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have no recovery codes left.</strong>
|
||||
<p>You must <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
|
||||
<strong>Privacy and cookie policy have not been accepted.</strong>
|
||||
<p>You must accept the policy before you can enable two factor authentication.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft == 1)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have 1 recovery code left.</strong>
|
||||
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft <= 3)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
|
||||
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (Model.IsMachineRemembered)
|
||||
{
|
||||
<form method="post" style="display: inline-block">
|
||||
<button type="submit" class="btn btn-default">Forget this browser</button>
|
||||
</form>
|
||||
}
|
||||
<a asp-page="./Disable2fa" class="btn btn-default">Disable 2FA</a>
|
||||
<a asp-page="./GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a>
|
||||
}
|
||||
|
||||
<h5>Authenticator app</h5>
|
||||
@if (!Model.HasAuthenticator)
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Add authenticator app</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Setup authenticator app</a>
|
||||
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-default">Reset authenticator app</a>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
@page
|
||||
@using Microsoft.AspNetCore.Http.Features
|
||||
@model TwoFactorAuthenticationModel
|
||||
@{
|
||||
ViewData["Title"] = "Two-factor authentication (2FA)";
|
||||
|
|
@ -7,49 +8,62 @@
|
|||
|
||||
<partial name="_StatusMessage" for="StatusMessage" />
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@if (Model.Is2faEnabled)
|
||||
{
|
||||
if (Model.RecoveryCodesLeft == 0)
|
||||
@{
|
||||
var consentFeature = HttpContext.Features.Get<ITrackingConsentFeature>();
|
||||
@if (consentFeature?.CanTrack ?? true)
|
||||
{
|
||||
@if (Model.Is2faEnabled)
|
||||
{
|
||||
if (Model.RecoveryCodesLeft == 0)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have no recovery codes left.</strong>
|
||||
<p>You must <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft == 1)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have 1 recovery code left.</strong>
|
||||
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft <= 3)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
|
||||
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (Model.IsMachineRemembered)
|
||||
{
|
||||
<form method="post" style="display: inline-block">
|
||||
<button type="submit" class="btn btn-primary">Forget this browser</button>
|
||||
</form>
|
||||
}
|
||||
<a asp-page="./Disable2fa" class="btn btn-primary">Disable 2FA</a>
|
||||
<a asp-page="./GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
|
||||
}
|
||||
|
||||
<h5>Authenticator app</h5>
|
||||
@if (!Model.HasAuthenticator)
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Set up authenticator app</a>
|
||||
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have no recovery codes left.</strong>
|
||||
<p>You must <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
|
||||
<strong>Privacy and cookie policy have not been accepted.</strong>
|
||||
<p>You must accept the policy before you can enable two factor authentication.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft == 1)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<strong>You have 1 recovery code left.</strong>
|
||||
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
else if (Model.RecoveryCodesLeft <= 3)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
|
||||
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (Model.IsMachineRemembered)
|
||||
{
|
||||
<form method="post" style="display: inline-block">
|
||||
<button type="submit" class="btn btn-primary">Forget this browser</button>
|
||||
</form>
|
||||
}
|
||||
<a asp-page="./Disable2fa" class="btn btn-primary">Disable 2FA</a>
|
||||
<a asp-page="./GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
|
||||
}
|
||||
|
||||
<h5>Authenticator app</h5>
|
||||
@if (!Model.HasAuthenticator)
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Setup authenticator app</a>
|
||||
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
internal DefaultUIContext WithPasswordLogin() =>
|
||||
new DefaultUIContext(this) { PasswordLoginEnabled = true };
|
||||
|
||||
internal DefaultUIContext WithCookieConsent() =>
|
||||
new DefaultUIContext(this) { CookiePolicyAccepted = true };
|
||||
|
||||
public string AuthenticatorKey
|
||||
{
|
||||
get => GetValue<string>(nameof(AuthenticatorKey));
|
||||
|
|
@ -84,5 +87,11 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
get => GetValue<bool>(nameof(PasswordLoginEnabled));
|
||||
set => SetValue(nameof(PasswordLoginEnabled), value);
|
||||
}
|
||||
|
||||
public bool CookiePolicyAccepted
|
||||
{
|
||||
get => GetValue<bool>(nameof(CookiePolicyAccepted));
|
||||
set => SetValue(nameof(CookiePolicyAccepted), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,23 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
var index = await UserStories.RegisterNewUserAsync(client, userName, password);
|
||||
|
||||
// Act & Assert
|
||||
await UserStories.EnableTwoFactorAuthentication(index);
|
||||
Assert.NotNull(await UserStories.EnableTwoFactorAuthentication(index));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CannotEnableTwoFactorAuthenticationWithoutCookieConsent()
|
||||
{
|
||||
// Arrange
|
||||
var client = ServerFactory
|
||||
.CreateClient();
|
||||
|
||||
var userName = $"{Guid.NewGuid()}@example.com";
|
||||
var password = $"!Test.Password1$";
|
||||
|
||||
var index = await UserStories.RegisterNewUserAsync(client, userName, password);
|
||||
|
||||
// Act & Assert
|
||||
Assert.Null(await UserStories.EnableTwoFactorAuthentication(index, consent: false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -241,6 +257,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
var twoFactorKey = showRecoveryCodes.Context.AuthenticatorKey;
|
||||
|
||||
// Use a new client to simulate a new browser session.
|
||||
await UserStories.AcceptCookiePolicy(newClient);
|
||||
var index = await UserStories.LoginExistingUser2FaAsync(newClient, userName, password, twoFactorKey);
|
||||
await UserStories.ResetAuthenticator(index);
|
||||
|
||||
|
|
|
|||
|
|
@ -47,12 +47,19 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<TwoFactorAuthentication> ClickTwoFactorLinkAsync()
|
||||
public async Task<TwoFactorAuthentication> ClickTwoFactorLinkAsync(bool consent = true)
|
||||
{
|
||||
// Accept cookie consent if requested
|
||||
if (consent)
|
||||
{
|
||||
await UserStories.AcceptCookiePolicy(Client);
|
||||
}
|
||||
|
||||
var goToTwoFactor = await Client.GetAsync(_twoFactorLink.Href);
|
||||
var twoFactor = await ResponseAssert.IsHtmlDocumentAsync(goToTwoFactor);
|
||||
|
||||
return new TwoFactorAuthentication(Client, twoFactor, Context);
|
||||
var context = consent ? Context.WithCookieConsent() : Context;
|
||||
return new TwoFactorAuthentication(Client, twoFactor, context);
|
||||
}
|
||||
|
||||
public async Task<TwoFactorAuthentication> ClickTwoFactorEnabledLinkAsync()
|
||||
|
|
@ -60,6 +67,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
|
|||
var goToTwoFactor = await Client.GetAsync(_twoFactorLink.Href);
|
||||
var twoFactor = await ResponseAssert.IsHtmlDocumentAsync(goToTwoFactor);
|
||||
Context.TwoFactorEnabled = true;
|
||||
Context.CookiePolicyAccepted = true;
|
||||
return new TwoFactorAuthentication(Client, twoFactor, Context);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,13 +16,20 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests.Account.Manage
|
|||
public TwoFactorAuthentication(HttpClient client, IHtmlDocument twoFactor, DefaultUIContext context)
|
||||
: base(client, twoFactor, context)
|
||||
{
|
||||
if (!Context.TwoFactorEnabled)
|
||||
if (Context.CookiePolicyAccepted)
|
||||
{
|
||||
_enableAuthenticatorLink = HtmlAssert.HasLink("#enable-authenticator", twoFactor);
|
||||
if (!Context.TwoFactorEnabled)
|
||||
{
|
||||
_enableAuthenticatorLink = HtmlAssert.HasLink("#enable-authenticator", twoFactor);
|
||||
}
|
||||
else
|
||||
{
|
||||
_resetAuthenticatorLink = HtmlAssert.HasLink("#reset-authenticator", twoFactor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_resetAuthenticatorLink = HtmlAssert.HasLink("#reset-authenticator", twoFactor);
|
||||
Assert.Contains("You must accept the policy before you can enable two factor authentication.", twoFactor.DocumentElement.TextContent);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
return await register.SubmitRegisterFormForValidUserAsync(userName, password);
|
||||
}
|
||||
|
||||
|
||||
internal static async Task<Index> LoginExistingUserAsync(HttpClient client, string userName, string password)
|
||||
{
|
||||
var index = await Index.CreateAsync(client);
|
||||
|
|
@ -105,12 +106,16 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
return await login2Fa.Send2FACodeAsync(twoFactorKey);
|
||||
}
|
||||
|
||||
internal static async Task<ShowRecoveryCodes> EnableTwoFactorAuthentication(Index index)
|
||||
internal static async Task<ShowRecoveryCodes> EnableTwoFactorAuthentication(Index index, bool consent = true)
|
||||
{
|
||||
var manage = await index.ClickManageLinkAsync();
|
||||
var twoFactor = await manage.ClickTwoFactorLinkAsync();
|
||||
var enableAuthenticator = await twoFactor.ClickEnableAuthenticatorLinkAsync();
|
||||
return await enableAuthenticator.SendValidCodeAsync();
|
||||
var twoFactor = await manage.ClickTwoFactorLinkAsync(consent);
|
||||
if (consent)
|
||||
{
|
||||
var enableAuthenticator = await twoFactor.ClickEnableAuthenticatorLinkAsync();
|
||||
return await enableAuthenticator.SendValidCodeAsync();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static async Task<ResetAuthenticator> ResetAuthenticator(Index index)
|
||||
|
|
@ -219,5 +224,11 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests
|
|||
ResponseAssert.IsOK(download);
|
||||
return JsonConvert.DeserializeObject<JObject>(await download.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
internal static async Task AcceptCookiePolicy(HttpClient client)
|
||||
{
|
||||
var goToPrivacy = await client.GetAsync("/Privacy");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Identity.DefaultUI.WebSite.Pages
|
||||
|
|
@ -9,6 +10,7 @@ namespace Identity.DefaultUI.WebSite.Pages
|
|||
{
|
||||
public void OnGet()
|
||||
{
|
||||
HttpContext.Features.Get<ITrackingConsentFeature>().GrantConsent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue