Skip to content

Add two factor authentication flow #25480

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
85e303e
Add two factor step to api state flow
peppy Nov 15, 2023
80c879e
Adjust member ordering in `LoginForm`
peppy Nov 16, 2023
e9d4cf2
Setup basic form flow
peppy Nov 15, 2023
c4e461b
Add two factor auth form
peppy Nov 16, 2023
0e4244a
Add `APIAccess` flow for 2fa
peppy Nov 16, 2023
285f740
Update various components to handle new state
peppy Nov 16, 2023
f7fa9c9
Add test coverage of 2FA flow
peppy Nov 16, 2023
167f5b4
Tidy up localisations and connect missing links
peppy Nov 16, 2023
7472dc9
Update `APIState` checks
peppy Nov 16, 2023
2cfaa1c
Merge branch 'master' into 2fa
bdach Jan 23, 2024
d0d09a8
Fix login overlay test not clearing second auth factor
bdach Jan 23, 2024
d7e9570
Fix account creation overlay test not clearing second auth factor
bdach Jan 23, 2024
5d26afc
Fix `OsuGameTestScene` not clearing second auth factor
bdach Jan 23, 2024
0d7834a
Ensure all remaining test usages of `IAPIAccess.Login()` also authent…
bdach Jan 23, 2024
7c14040
Add request structures for verification endpoints
bdach Jan 24, 2024
7b47215
Split `/me` request from `/users` requests
bdach Jan 24, 2024
ddc2bbe
Add `session_verified` attribute to `/me` response
bdach Jan 24, 2024
445a745
Implement verification from within client
bdach Jan 24, 2024
602c3bc
Hook up reissue request
bdach Jan 24, 2024
62a0c23
Split out raw websocket logic from conjoined notifications client con…
bdach Jan 24, 2024
e3eb7a8
Support verification via clicking link from e-mail
bdach Jan 24, 2024
2f87477
Fix nullability inspection
bdach Jan 24, 2024
3d3506b
Merge branch 'decouple-notification-websocket-from-chat' into 2fa
bdach Jan 25, 2024
04cae87
Handle forced logouts due to password change too
bdach Jan 26, 2024
a2e69d3
Add basic testing of failure flow
bdach Jan 26, 2024
b39b112
Merge branch 'decouple-notification-websocket-from-chat' into 2fa
bdach Jan 26, 2024
4a2602a
Merge branch 'master' into 2fa
peppy Jan 29, 2024
363fd1d
Remove no longer relevant changes
bdach Jan 29, 2024
96811a8
Fix `APIAccess` spamming requests while waiting for second factor
bdach Jan 29, 2024
6a469f2
Use `switch` instead of `if-else`
peppy Jan 29, 2024
540ff0d
Add loading layer when requesting a code reissue
peppy Jan 29, 2024
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
Expand Up @@ -184,7 +184,11 @@ public void TestGuestScoreIsStoredAsGuest()
CreateTest();

AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("log back in", () => API.Login("username", "password"));
AddStep("log back in", () =>
{
API.Login("username", "password");
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
});

AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));

Expand Down
71 changes: 71 additions & 0 deletions osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
// See the LICENCE file in the repository root for full licence text.

using System.Linq;
using System.Net;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
using osu.Game.Overlays.Login;
using osu.Game.Users.Drawables;
using osuTK.Input;

Expand All @@ -18,6 +21,8 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public partial class TestSceneLoginOverlay : OsuManualInputManagerTestScene
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;

private LoginOverlay loginOverlay = null!;

[BackgroundDependencyLoader]
Expand All @@ -40,9 +45,69 @@ public void SetUpSteps()
public void TestLoginSuccess()
{
AddStep("logout", () => API.Logout());
assertAPIState(APIState.Offline);

AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());

assertAPIState(APIState.RequiresSecondFactorAuth);
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);

AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case VerifySessionRequest verifySessionRequest:
if (verifySessionRequest.VerificationKey == "88800088")
verifySessionRequest.TriggerSuccess();
else
verifySessionRequest.TriggerFailure(new WebException());
return true;
}

return false;
});
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
assertAPIState(APIState.Online);
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
}

private void assertAPIState(APIState expected) =>
AddUntilStep($"login state is {expected}", () => API.State.Value, () => Is.EqualTo(expected));

[Test]
public void TestVerificationFailure()
{
bool verificationHandled = false;
AddStep("reset flag", () => verificationHandled = false);
AddStep("logout", () => API.Logout());
assertAPIState(APIState.Offline);

AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());

assertAPIState(APIState.RequiresSecondFactorAuth);
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);

AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case VerifySessionRequest verifySessionRequest:
if (verifySessionRequest.VerificationKey == "88800088")
verifySessionRequest.TriggerSuccess();
else
verifySessionRequest.TriggerFailure(new WebException());
verificationHandled = true;
return true;
}

return false;
});
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "abcdefgh");
AddUntilStep("wait for verification handled", () => verificationHandled);
assertAPIState(APIState.RequiresSecondFactorAuth);
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
}

[Test]
Expand Down Expand Up @@ -78,6 +143,12 @@ public void TestClickingOnFlagClosesOverlay()
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());

assertAPIState(APIState.RequiresSecondFactorAuth);
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);

AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
assertAPIState(APIState.Online);

AddStep("click on flag", () =>
{
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First());
Expand Down
12 changes: 8 additions & 4 deletions osu.Game.Tests/Visual/Menus/TestSceneToolbarUserButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public partial class TestSceneToolbarUserButton : OsuManualInputManagerTestScene
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;

public TestSceneToolbarUserButton()
{
Container mainContainer;
Expand Down Expand Up @@ -69,18 +71,20 @@ public TestSceneToolbarUserButton()
[Test]
public void TestLoginLogout()
{
AddStep("Log out", () => ((DummyAPIAccess)API).Logout());
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
AddStep("Log out", () => dummyAPI.Logout());
AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
AddStep("Authenticate via second factor", () => dummyAPI.AuthenticateSecondFactor("abcdefgh"));
}

[Test]
public void TestStates()
{
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
AddStep("Authenticate via second factor", () => dummyAPI.AuthenticateSecondFactor("abcdefgh"));

foreach (var state in Enum.GetValues<APIState>())
{
AddStep($"Change state to {state}", () => ((DummyAPIAccess)API).SetState(state));
AddStep($"Change state to {state}", () => dummyAPI.SetState(state));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.AccountCreation;
Expand Down Expand Up @@ -59,7 +60,11 @@ public void TestOverlayVisibility()
AddStep("click button", () => accountCreation.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddUntilStep("warning screen is present", () => accountCreation.ChildrenOfType<ScreenWarning>().SingleOrDefault()?.IsPresent == true);

AddStep("log back in", () => API.Login("dummy", "password"));
AddStep("log back in", () =>
{
API.Login("dummy", "password");
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
});
AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden);
}
}
Expand Down
1 change: 1 addition & 0 deletions osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public void SetUp()
Schedule(() =>
{
API.Login("test", "test");
dummyAPI.AuthenticateSecondFactor("abcdefgh");
Child = commentsContainer = new CommentsContainer();
});
}
Expand Down
13 changes: 11 additions & 2 deletions osu.Game.Tests/Visual/Online/TestSceneFavouriteButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osuTK;
Expand Down Expand Up @@ -34,14 +35,22 @@ public void TestLoggedOutIn()
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
AddStep("log out", () => API.Logout());
checkEnabled(false);
AddStep("log in", () => API.Login("test", "test"));
AddStep("log in", () =>
{
API.Login("test", "test");
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
});
checkEnabled(true);
}

[Test]
public void TestBeatmapChange()
{
AddStep("log in", () => API.Login("test", "test"));
AddStep("log in", () =>
{
API.Login("test", "test");
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
});
AddStep("set valid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet { OnlineID = 88 });
checkEnabled(true);
AddStep("set invalid beatmap", () => favourite.BeatmapSet.Value = new APIBeatmapSet());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public void SetUpSteps()
else
{
int userId = int.Parse(getUserRequest.Lookup);
string rulesetName = getUserRequest.Ruleset.ShortName;
string rulesetName = getUserRequest.Ruleset!.ShortName;
var response = new APIUser
{
Id = userId,
Expand Down Expand Up @@ -177,7 +177,11 @@ public void TestStatisticsUpdateNotFiredIfUserLoggedOut()
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);

AddStep("log in user", () => dummyAPI.Login("user", "password"));
AddStep("log in user", () =>
{
dummyAPI.Login("user", "password");
dummyAPI.AuthenticateSecondFactor("abcdefgh");
});
}

[Test]
Expand Down
12 changes: 10 additions & 2 deletions osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ public void TestActualUser()
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
AddToggleStep("toggle visibility", visible => profile.State.Value = visible ? Visibility.Visible : Visibility.Hidden);
AddStep("log out", () => dummyAPI.Logout());
AddStep("log back in", () => dummyAPI.Login("username", "password"));
AddStep("log back in", () =>
{
dummyAPI.Login("username", "password");
dummyAPI.AuthenticateSecondFactor("abcdefgh");
});
}

[Test]
Expand Down Expand Up @@ -98,7 +102,11 @@ public void TestLogin()
});
AddStep("logout", () => dummyAPI.Logout());
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
AddStep("login", () => dummyAPI.Login("username", "password"));
AddStep("login", () =>
{
dummyAPI.Login("username", "password");
dummyAPI.AuthenticateSecondFactor("abcdefgh");
});
AddWaitStep("wait some", 3);
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
}
Expand Down
7 changes: 6 additions & 1 deletion osu.Game.Tests/Visual/Online/TestSceneVotePill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using osu.Game.Overlays;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;

namespace osu.Game.Tests.Visual.Online
{
Expand Down Expand Up @@ -72,7 +73,11 @@ public void TestOfflineRandomCommentPill()
AddAssert("Login overlay is visible", () => login.State.Value == Visibility.Visible);
}

private void logIn() => API.Login("localUser", "password");
private void logIn()
{
API.Login("localUser", "password");
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
}

private Comment getUserComment() => new Comment
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,11 @@ void assertLoggedOutState()
assertLoggedOutState();

// moving from logged out -> logged in
AddStep("log back in", () => dummyAPI.Login("username", "password"));
AddStep("log back in", () =>
{
dummyAPI.Login("username", "password");
dummyAPI.AuthenticateSecondFactor("abcdefgh");
});
assertLoggedInState();
}

Expand Down
Loading