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

Commit

Permalink
Block enabled 2fa in the UI without cookie consent (#2035)
Browse files Browse the repository at this point in the history
* Block enabled 2fa in the UI without cookie consent

* Guard against feature not being there

* Set up tweak

* Fix
  • Loading branch information
HaoK authored Oct 30, 2018
1 parent 9405d05 commit da9318f
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 83 deletions.
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,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)
{
<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>
@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 if (Model.RecoveryCodesLeft == 1)
else
{
<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>
<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>
}

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" />
Expand Down
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,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)
{
<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>
@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 if (Model.RecoveryCodesLeft == 1)
else
{
<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>
<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>
}

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

0 comments on commit da9318f

Please sign in to comment.