Skip to content

Commit da9318f

Browse files
authored
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
1 parent 9405d05 commit da9318f

File tree

8 files changed

+166
-83
lines changed

8 files changed

+166
-83
lines changed

src/UI/Areas/Identity/Pages/V3/Account/Manage/TwoFactorAuthentication.cshtml

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@page
2+
@using Microsoft.AspNetCore.Http.Features
23
@model TwoFactorAuthenticationModel
34
@{
45
ViewData["Title"] = "Two-factor authentication (2FA)";
@@ -7,50 +8,64 @@
78

89
<partial name="_StatusMessage" model="Model.StatusMessage" />
910
<h4>@ViewData["Title"]</h4>
10-
@if (Model.Is2faEnabled)
11-
{
12-
if (Model.RecoveryCodesLeft == 0)
11+
@{
12+
var consentFeature = HttpContext.Features.Get<ITrackingConsentFeature>();
13+
@if (consentFeature?.CanTrack ?? true)
1314
{
14-
<div class="alert alert-danger">
15-
<strong>You have no recovery codes left.</strong>
16-
<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>
17-
</div>
15+
@if (Model.Is2faEnabled)
16+
{
17+
if (Model.RecoveryCodesLeft == 0)
18+
{
19+
<div class="alert alert-danger">
20+
<strong>You have no recovery codes left.</strong>
21+
<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>
22+
</div>
23+
}
24+
else if (Model.RecoveryCodesLeft == 1)
25+
{
26+
<div class="alert alert-danger">
27+
<strong>You have 1 recovery code left.</strong>
28+
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
29+
</div>
30+
}
31+
else if (Model.RecoveryCodesLeft <= 3)
32+
{
33+
<div class="alert alert-warning">
34+
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
35+
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
36+
</div>
37+
}
38+
39+
if (Model.IsMachineRemembered)
40+
{
41+
<form method="post" style="display: inline-block">
42+
<button type="submit" class="btn btn-default">Forget this browser</button>
43+
</form>
44+
}
45+
<a asp-page="./Disable2fa" class="btn btn-default">Disable 2FA</a>
46+
<a asp-page="./GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a>
47+
}
48+
49+
<h5>Authenticator app</h5>
50+
@if (!Model.HasAuthenticator)
51+
{
52+
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Add authenticator app</a>
53+
}
54+
else
55+
{
56+
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Set up authenticator app</a>
57+
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-default">Reset authenticator app</a>
58+
}
1859
}
19-
else if (Model.RecoveryCodesLeft == 1)
60+
else
2061
{
2162
<div class="alert alert-danger">
22-
<strong>You have 1 recovery code left.</strong>
23-
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
24-
</div>
25-
}
26-
else if (Model.RecoveryCodesLeft <= 3)
27-
{
28-
<div class="alert alert-warning">
29-
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
30-
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
63+
<strong>Privacy and cookie policy have not been accepted.</strong>
64+
<p>You must accept the policy before you can enable two factor authentication.</p>
3165
</div>
3266
}
33-
34-
if (Model.IsMachineRemembered)
35-
{
36-
<form method="post" style="display: inline-block">
37-
<button type="submit" class="btn btn-default">Forget this browser</button>
38-
</form>
39-
}
40-
<a asp-page="./Disable2fa" class="btn btn-default">Disable 2FA</a>
41-
<a asp-page="./GenerateRecoveryCodes" class="btn btn-default">Reset recovery codes</a>
4267
}
4368

44-
<h5>Authenticator app</h5>
45-
@if (!Model.HasAuthenticator)
46-
{
47-
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Add authenticator app</a>
48-
}
49-
else
50-
{
51-
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-default">Setup authenticator app</a>
52-
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-default">Reset authenticator app</a>
53-
}
5469

5570
@section Scripts {
5671
<partial name="_ValidationScriptsPartial" />

src/UI/Areas/Identity/Pages/V4/Account/Manage/TwoFactorAuthentication.cshtml

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@page
2+
@using Microsoft.AspNetCore.Http.Features
23
@model TwoFactorAuthenticationModel
34
@{
45
ViewData["Title"] = "Two-factor authentication (2FA)";
@@ -7,49 +8,62 @@
78

89
<partial name="_StatusMessage" for="StatusMessage" />
910
<h4>@ViewData["Title"]</h4>
10-
@if (Model.Is2faEnabled)
11-
{
12-
if (Model.RecoveryCodesLeft == 0)
11+
@{
12+
var consentFeature = HttpContext.Features.Get<ITrackingConsentFeature>();
13+
@if (consentFeature?.CanTrack ?? true)
1314
{
14-
<div class="alert alert-danger">
15-
<strong>You have no recovery codes left.</strong>
16-
<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>
17-
</div>
15+
@if (Model.Is2faEnabled)
16+
{
17+
if (Model.RecoveryCodesLeft == 0)
18+
{
19+
<div class="alert alert-danger">
20+
<strong>You have no recovery codes left.</strong>
21+
<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>
22+
</div>
23+
}
24+
else if (Model.RecoveryCodesLeft == 1)
25+
{
26+
<div class="alert alert-danger">
27+
<strong>You have 1 recovery code left.</strong>
28+
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
29+
</div>
30+
}
31+
else if (Model.RecoveryCodesLeft <= 3)
32+
{
33+
<div class="alert alert-warning">
34+
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
35+
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
36+
</div>
37+
}
38+
39+
if (Model.IsMachineRemembered)
40+
{
41+
<form method="post" style="display: inline-block">
42+
<button type="submit" class="btn btn-primary">Forget this browser</button>
43+
</form>
44+
}
45+
<a asp-page="./Disable2fa" class="btn btn-primary">Disable 2FA</a>
46+
<a asp-page="./GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
47+
}
48+
49+
<h5>Authenticator app</h5>
50+
@if (!Model.HasAuthenticator)
51+
{
52+
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
53+
}
54+
else
55+
{
56+
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Set up authenticator app</a>
57+
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
58+
}
1859
}
19-
else if (Model.RecoveryCodesLeft == 1)
60+
else
2061
{
2162
<div class="alert alert-danger">
22-
<strong>You have 1 recovery code left.</strong>
23-
<p>You can <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
24-
</div>
25-
}
26-
else if (Model.RecoveryCodesLeft <= 3)
27-
{
28-
<div class="alert alert-warning">
29-
<strong>You have @Model.RecoveryCodesLeft recovery codes left.</strong>
30-
<p>You should <a asp-page="./GenerateRecoveryCodes">generate a new set of recovery codes</a>.</p>
63+
<strong>Privacy and cookie policy have not been accepted.</strong>
64+
<p>You must accept the policy before you can enable two factor authentication.</p>
3165
</div>
3266
}
33-
34-
if (Model.IsMachineRemembered)
35-
{
36-
<form method="post" style="display: inline-block">
37-
<button type="submit" class="btn btn-primary">Forget this browser</button>
38-
</form>
39-
}
40-
<a asp-page="./Disable2fa" class="btn btn-primary">Disable 2FA</a>
41-
<a asp-page="./GenerateRecoveryCodes" class="btn btn-primary">Reset recovery codes</a>
42-
}
43-
44-
<h5>Authenticator app</h5>
45-
@if (!Model.HasAuthenticator)
46-
{
47-
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Add authenticator app</a>
48-
}
49-
else
50-
{
51-
<a id="enable-authenticator" asp-page="./EnableAuthenticator" class="btn btn-primary">Setup authenticator app</a>
52-
<a id="reset-authenticator" asp-page="./ResetAuthenticator" class="btn btn-primary">Reset authenticator app</a>
5367
}
5468

5569
@section Scripts {

test/Identity.FunctionalTests/Infrastructure/DefaultUIContext.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ internal DefaultUIContext WithSocialLoginProvider() =>
3232
internal DefaultUIContext WithPasswordLogin() =>
3333
new DefaultUIContext(this) { PasswordLoginEnabled = true };
3434

35+
internal DefaultUIContext WithCookieConsent() =>
36+
new DefaultUIContext(this) { CookiePolicyAccepted = true };
37+
3538
public string AuthenticatorKey
3639
{
3740
get => GetValue<string>(nameof(AuthenticatorKey));
@@ -84,5 +87,11 @@ public bool PasswordLoginEnabled
8487
get => GetValue<bool>(nameof(PasswordLoginEnabled));
8588
set => SetValue(nameof(PasswordLoginEnabled), value);
8689
}
90+
91+
public bool CookiePolicyAccepted
92+
{
93+
get => GetValue<bool>(nameof(CookiePolicyAccepted));
94+
set => SetValue(nameof(CookiePolicyAccepted), value);
95+
}
8796
}
8897
}

test/Identity.FunctionalTests/ManagementTests.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,23 @@ public async Task CanEnableTwoFactorAuthentication()
4343
var index = await UserStories.RegisterNewUserAsync(client, userName, password);
4444

4545
// Act & Assert
46-
await UserStories.EnableTwoFactorAuthentication(index);
46+
Assert.NotNull(await UserStories.EnableTwoFactorAuthentication(index));
47+
}
48+
49+
[Fact]
50+
public async Task CannotEnableTwoFactorAuthenticationWithoutCookieConsent()
51+
{
52+
// Arrange
53+
var client = ServerFactory
54+
.CreateClient();
55+
56+
var userName = $"{Guid.NewGuid()}@example.com";
57+
var password = $"!Test.Password1$";
58+
59+
var index = await UserStories.RegisterNewUserAsync(client, userName, password);
60+
61+
// Act & Assert
62+
Assert.Null(await UserStories.EnableTwoFactorAuthentication(index, consent: false));
4763
}
4864

4965
[Fact]
@@ -241,6 +257,7 @@ void ConfigureTestServices(IServiceCollection services) =>
241257
var twoFactorKey = showRecoveryCodes.Context.AuthenticatorKey;
242258

243259
// Use a new client to simulate a new browser session.
260+
await UserStories.AcceptCookiePolicy(newClient);
244261
var index = await UserStories.LoginExistingUser2FaAsync(newClient, userName, password, twoFactorKey);
245262
await UserStories.ResetAuthenticator(index);
246263

test/Identity.FunctionalTests/Pages/Account/Manage/Index.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,27 @@ public Index(HttpClient client, IHtmlDocument manage, DefaultUIContext context)
4747
}
4848
}
4949

50-
public async Task<TwoFactorAuthentication> ClickTwoFactorLinkAsync()
50+
public async Task<TwoFactorAuthentication> ClickTwoFactorLinkAsync(bool consent = true)
5151
{
52+
// Accept cookie consent if requested
53+
if (consent)
54+
{
55+
await UserStories.AcceptCookiePolicy(Client);
56+
}
57+
5258
var goToTwoFactor = await Client.GetAsync(_twoFactorLink.Href);
5359
var twoFactor = await ResponseAssert.IsHtmlDocumentAsync(goToTwoFactor);
5460

55-
return new TwoFactorAuthentication(Client, twoFactor, Context);
61+
var context = consent ? Context.WithCookieConsent() : Context;
62+
return new TwoFactorAuthentication(Client, twoFactor, context);
5663
}
5764

5865
public async Task<TwoFactorAuthentication> ClickTwoFactorEnabledLinkAsync()
5966
{
6067
var goToTwoFactor = await Client.GetAsync(_twoFactorLink.Href);
6168
var twoFactor = await ResponseAssert.IsHtmlDocumentAsync(goToTwoFactor);
6269
Context.TwoFactorEnabled = true;
70+
Context.CookiePolicyAccepted = true;
6371
return new TwoFactorAuthentication(Client, twoFactor, Context);
6472
}
6573

test/Identity.FunctionalTests/Pages/Account/Manage/TwoFactorAuthentication.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@ public class TwoFactorAuthentication : DefaultUIPage
1616
public TwoFactorAuthentication(HttpClient client, IHtmlDocument twoFactor, DefaultUIContext context)
1717
: base(client, twoFactor, context)
1818
{
19-
if (!Context.TwoFactorEnabled)
19+
if (Context.CookiePolicyAccepted)
2020
{
21-
_enableAuthenticatorLink = HtmlAssert.HasLink("#enable-authenticator", twoFactor);
21+
if (!Context.TwoFactorEnabled)
22+
{
23+
_enableAuthenticatorLink = HtmlAssert.HasLink("#enable-authenticator", twoFactor);
24+
}
25+
else
26+
{
27+
_resetAuthenticatorLink = HtmlAssert.HasLink("#reset-authenticator", twoFactor);
28+
}
2229
}
2330
else
2431
{
25-
_resetAuthenticatorLink = HtmlAssert.HasLink("#reset-authenticator", twoFactor);
32+
Assert.Contains("You must accept the policy before you can enable two factor authentication.", twoFactor.DocumentElement.TextContent);
2633
}
2734
}
2835

test/Identity.FunctionalTests/UserStories.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ internal static async Task<Index> RegisterNewUserAsync(HttpClient client, string
2727
return await register.SubmitRegisterFormForValidUserAsync(userName, password);
2828
}
2929

30+
3031
internal static async Task<Index> LoginExistingUserAsync(HttpClient client, string userName, string password)
3132
{
3233
var index = await Index.CreateAsync(client);
@@ -105,12 +106,16 @@ internal static async Task<Index> LoginExistingUser2FaAsync(HttpClient client, s
105106
return await login2Fa.Send2FACodeAsync(twoFactorKey);
106107
}
107108

108-
internal static async Task<ShowRecoveryCodes> EnableTwoFactorAuthentication(Index index)
109+
internal static async Task<ShowRecoveryCodes> EnableTwoFactorAuthentication(Index index, bool consent = true)
109110
{
110111
var manage = await index.ClickManageLinkAsync();
111-
var twoFactor = await manage.ClickTwoFactorLinkAsync();
112-
var enableAuthenticator = await twoFactor.ClickEnableAuthenticatorLinkAsync();
113-
return await enableAuthenticator.SendValidCodeAsync();
112+
var twoFactor = await manage.ClickTwoFactorLinkAsync(consent);
113+
if (consent)
114+
{
115+
var enableAuthenticator = await twoFactor.ClickEnableAuthenticatorLinkAsync();
116+
return await enableAuthenticator.SendValidCodeAsync();
117+
}
118+
return null;
114119
}
115120

116121
internal static async Task<ResetAuthenticator> ResetAuthenticator(Index index)
@@ -219,5 +224,11 @@ internal static async Task<JObject> DownloadPersonalData(Index index, string use
219224
ResponseAssert.IsOK(download);
220225
return JsonConvert.DeserializeObject<JObject>(await download.Content.ReadAsStringAsync());
221226
}
227+
228+
internal static async Task AcceptCookiePolicy(HttpClient client)
229+
{
230+
var goToPrivacy = await client.GetAsync("/Privacy");
231+
}
232+
222233
}
223234
}

test/WebSites/Identity.DefaultUI.WebSite/Pages/Privacy.cshtml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using Microsoft.AspNetCore.Http.Features;
45
using Microsoft.AspNetCore.Mvc.RazorPages;
56

67
namespace Identity.DefaultUI.WebSite.Pages
@@ -9,6 +10,7 @@ public class PrivacyModel : PageModel
910
{
1011
public void OnGet()
1112
{
13+
HttpContext.Features.Get<ITrackingConsentFeature>().GrantConsent();
1214
}
1315
}
1416
}

0 commit comments

Comments
 (0)