Skip to content
This repository has been archived by the owner on Dec 20, 2018. It is now read-only.

Block enabled 2fa in the UI without cookie consent #2035

Merged
merged 4 commits into from
Oct 30, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -1,4 +1,5 @@
@page
@using Microsoft.AspNetCore.Http.Features
@model TwoFactorAuthenticationModel
@{
ViewData["Title"] = "Two-factor authentication (2FA)";
Expand All @@ -7,51 +8,62 @@

<partial name="_StatusMessage" model="Model.StatusMessage" />
<h4>@ViewData["Title"]</h4>
@if (Model.Is2faEnabled)
@if (HttpContext.Features.Get<ITrackingConsentFeature>().CanTrack)
HaoK marked this conversation as resolved.
Show resolved Hide resolved
{
if (Model.RecoveryCodesLeft == 0)
@if (Model.Is2faEnabled)
{
<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>
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>
}
else if (Model.RecoveryCodesLeft <= 3)

<h5>Authenticator app</h5>
@if (!Model.HasAuthenticator)
{
<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>
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Add authenticator app</a>
}

if (Model.IsMachineRemembered)
else
{
<form method="post" style="display: inline-block">
<button type="submit" class="btn btn-default">Forget this browser</button>
</form>
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Setup authenticator app</a>
HaoK marked this conversation as resolved.
Show resolved Hide resolved
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-default">Reset authenticator app</a>
}
<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>
<div class="alert alert-danger">
<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>
}


@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@page
@using Microsoft.AspNetCore.Http.Features
@model TwoFactorAuthenticationModel
@{
ViewData["Title"] = "Two-factor authentication (2FA)";
Expand All @@ -7,49 +8,59 @@

<partial name="_StatusMessage" for="StatusMessage" />
<h4>@ViewData["Title"]</h4>
@if (Model.Is2faEnabled)
@if (HttpContext.Features.Get<ITrackingConsentFeature>().CanTrack)
{
if (Model.RecoveryCodesLeft == 0)
@if (Model.Is2faEnabled)
{
<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>
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>
}
else if (Model.RecoveryCodesLeft <= 3)

<h5>Authenticator app</h5>
@if (!Model.HasAuthenticator)
{
<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>
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
}

if (Model.IsMachineRemembered)
else
{
<form method="post" style="display: inline-block">
<button type="submit" class="btn btn-primary">Forget this browser</button>
</form>
<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>
}
<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>
<div class="alert alert-danger">
<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>
}

@section Scripts {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ internal DefaultUIContext WithSocialLoginProvider() =>
internal DefaultUIContext WithPasswordLogin() =>
new DefaultUIContext(this) { PasswordLoginEnabled = true };

internal DefaultUIContext WithCookieConsent() =>
new DefaultUIContext(this) { CookiePolicyAccepted = true };

public string AuthenticatorKey
{
get => GetValue<string>(nameof(AuthenticatorKey));
Expand Down Expand Up @@ -84,5 +87,11 @@ public bool PasswordLoginEnabled
get => GetValue<bool>(nameof(PasswordLoginEnabled));
set => SetValue(nameof(PasswordLoginEnabled), value);
}

public bool CookiePolicyAccepted
{
get => GetValue<bool>(nameof(CookiePolicyAccepted));
set => SetValue(nameof(CookiePolicyAccepted), value);
}
}
}
19 changes: 18 additions & 1 deletion test/Identity.FunctionalTests/ManagementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,23 @@ public async Task CanEnableTwoFactorAuthentication()
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]
Expand Down Expand Up @@ -241,6 +257,7 @@ void ConfigureTestServices(IServiceCollection services) =>
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);

Expand Down
12 changes: 10 additions & 2 deletions test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,27 @@ public Index(HttpClient client, IHtmlDocument manage, DefaultUIContext context)
}
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@ public class TwoFactorAuthentication : DefaultUIPage
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);
}
}

Expand Down
19 changes: 15 additions & 4 deletions test/Identity.FunctionalTests/UserStories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal static async Task<Index> RegisterNewUserAsync(HttpClient client, string
return await register.SubmitRegisterFormForValidUserAsync(userName, password);
}


internal static async Task<Index> LoginExistingUserAsync(HttpClient client, string userName, string password)
{
var index = await Index.CreateAsync(client);
Expand Down Expand Up @@ -105,12 +106,16 @@ internal static async Task<Index> LoginExistingUser2FaAsync(HttpClient client, s
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)
Expand Down Expand Up @@ -219,5 +224,11 @@ internal static async Task<JObject> DownloadPersonalData(Index index, string use
ResponseAssert.IsOK(download);
return JsonConvert.DeserializeObject<JObject>(await download.Content.ReadAsStringAsync());
}

internal static async Task AcceptCookiePolicy(HttpClient client)
{
var goToPrivacy = await client.GetAsync("/Privacy");
}

}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,6 +10,7 @@ public class PrivacyModel : PageModel
{
public void OnGet()
{
HttpContext.Features.Get<ITrackingConsentFeature>().GrantConsent();
}
}
}