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:
Hao Kung 2018-10-30 16:23:55 -07:00 committed by GitHub
parent 9405d058fa
commit da9318f431
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 170 additions and 87 deletions

View File

@ -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" />

View File

@ -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 {

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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");
}
}
}

View File

@ -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();
}
}
}