Skip to content

Commit

Permalink
VPN-6318: Add reauth to subscription management flow (#10141)
Browse files Browse the repository at this point in the history
* VPN-6318 add flow for reauth when going to subscription details

* polish

* fix apostraphes

* linter

* fix up tests

* no need for two different tests w/ web based auth

* reauth doesn't work for wasm

* fix wasm tests

* skip on wasm

* skip test for WASM

* remove unneeded state
  • Loading branch information
mcleinman authored Jan 4, 2025
1 parent 579ae2e commit 8298419
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 88 deletions.
25 changes: 25 additions & 0 deletions src/profileflow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "leakdetector.h"
#include "logger.h"
#include "mozillavpn.h"
#include "tasks/authenticate/taskauthenticate.h"
#include "tasks/getsubscriptiondetails/taskgetsubscriptiondetails.h"
#include "taskscheduler.h"

Expand Down Expand Up @@ -78,10 +79,34 @@ void ProfileFlow::start() {
}
});

connect(task, &TaskGetSubscriptionDetails::mustTransitionAuthToWeb, this,
[this]() { setState(StateAuthenticationNeeded); });

TaskScheduler::scheduleTask(task);
m_currentTask = task;
}

void ProfileFlow::reauthenticateViaWeb() {
// We need the user to login on the browser (rather than the client),
// then we'll try getting the profile again.
TaskAuthenticate* taskAuthenticate =
new TaskAuthenticate(AuthenticationListener::AuthenticationInBrowser);
connect(taskAuthenticate, &TaskAuthenticate::authenticationAborted, this,
[this]() {
logger.debug() << "Authentication failed";
setState(StateError);
ProfileFlow::reset();
});
connect(
taskAuthenticate, &TaskAuthenticate::authenticationCompleted, this,
[this]() {
logger.debug() << "Authentication succeeded, restarting profile flow";
ProfileFlow::start();
});

TaskScheduler::scheduleTask(taskAuthenticate);
}

void ProfileFlow::reset() {
logger.debug() << "Reset profile flow";

Expand Down
1 change: 1 addition & 0 deletions src/profileflow.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class ProfileFlow final : public QObject {
~ProfileFlow();

Q_INVOKABLE void start();
Q_INVOKABLE void reauthenticateViaWeb();
Q_INVOKABLE void reset();

enum State {
Expand Down
11 changes: 11 additions & 0 deletions src/tasks/getsubscriptiondetails/taskgetsubscriptiondetails.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "authenticationinapp/authenticationinappsession.h"
#include "authenticationlistener.h"
#include "constants.h"
#include "feature/feature.h"
#include "leakdetector.h"
#include "logger.h"
#include "models/subscriptiondata.h"
Expand Down Expand Up @@ -126,6 +127,16 @@ void TaskGetSubscriptionDetails::initAuthentication() {
logger.debug() << "Init authentication";
Q_ASSERT(!m_authenticationInAppSession);

// If transitioning from in-app auth to web-based auth, bounce to web-based
#ifndef MZ_WASM
if (!Feature::get(Feature::Feature_inAppAuthentication)->isSupported()) {
logger.info() << "Starting web-based re-authentication.";
emit mustTransitionAuthToWeb();
emit completed();
return;
}
#endif

emit needsAuthentication();

QByteArray pkceCodeVerifier;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class TaskGetSubscriptionDetails final : public Task {
signals:
void operationCompleted(bool status);
void needsAuthentication();
void mustTransitionAuthToWeb();

private:
void initAuthentication();
Expand Down
2 changes: 2 additions & 0 deletions src/translations/strings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ settings:
appExclusionClearAllApps:
value: Clear all
comment: Button label to clear all apps from the exclusions list
reauthTitle: Sign In Again
reauthDescription: For your security, please sign in again to access your account information. You’ll complete this process on Mozilla’s website.

localNetworkAccess:
labelTitle: Exclude local network traffic
Expand Down
52 changes: 48 additions & 4 deletions src/ui/screens/settings/ViewSettingsMenu.qml
Original file line number Diff line number Diff line change
Expand Up @@ -142,25 +142,29 @@ MZViewBase {
VPNProfileFlow.state === VPNProfileFlow.StateReady
&& stackview.currentItem.objectName !== "subscriptionManagmentView"
) {
reauthPopup.close();
return stackview.push("qrc:/qt/qml/Mozilla/VPN/screens/settings/ViewSubscriptionManagement/ViewSubscriptionManagement.qml");
}
// Only push the profile view if it’s not already in the stack
if (
VPNProfileFlow.state === VPNProfileFlow.StateAuthenticationNeeded
&& stackview.currentItem.objectName !== "reauthenticationFlow"
) {
if (VPNProfileFlow.state === VPNProfileFlow.StateAuthenticationNeeded) {
if (!MZFeatureList.get("inAppAuthentication").isSupported) {
reauthPopup.open();
} else if (stackview.currentItem.objectName !== "reauthenticationFlow") {
reauthPopup.close();
return stackview.push("qrc:/qt/qml/Mozilla/VPN/screens/settings/ViewSubscriptionManagement/ViewReauthenticationFlow.qml", {
_onClose: () => {
VPNProfileFlow.reset();
stackview.pop(null, StackView.Immediate);
}
});
}
}

// An error occurred during the profile flow. Let’s reset and return
// to the main settings view.
const hasError = VPNProfileFlow.state === VPNProfileFlow.StateError;
if (hasError) {
reauthPopup.close();
if (stackview.currentItem.objectName === "reauthenticationFlow") {
stackview.pop(null, StackView.Immediate);
}
Expand All @@ -171,4 +175,44 @@ MZViewBase {
Component.onCompleted: {
Glean.impression.settingsScreen.record({screen:telemetryScreenId});
}

MZSimplePopup {
id: reauthPopup

anchors.centerIn: Overlay.overlay
imageSrc: "qrc:/nebula/resources/updateStatusUpdateAvailable.svg"
imageSize: Qt.size(80, 80)
title: MZI18n.SettingsReauthTitle
description: MZI18n.SettingsReauthDescription
buttons: [
MZButton {
id: reauthButton
objectName: "reauthButton"
text: MZI18n.InAppAuthContinueToSignIn
Layout.fillWidth: true
onClicked: {
VPNProfileFlow.reauthenticateViaWeb();
loader.state = "active";
reauthButton.enabled = false;
}

Rectangle {
width: MZTheme.theme.rowHeight
height: MZTheme.theme.rowHeight
anchors.right: parent.right
color: MZTheme.colors.transparent

MZButtonLoader {
id: loader
color: MZTheme.colors.transparent
iconUrl: "qrc:/nebula/resources/spinner.svg"
}
}
}
]

onClosed: {
VPNProfileFlow.reset();
}
}
}
1 change: 1 addition & 0 deletions tests/functional/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ const screenSettings = {
STACKVIEW: new QmlQueryComposer('//settings-stackView'),
APP_PREFERENCES: new QmlQueryComposer('//settingsPreferences'),
USER_PROFILE: new QmlQueryComposer('//settingsUserProfile'),
REAUTH_BUTTON: new QmlQueryComposer('//reauthButton'),

privacyView: {
VIEW: new QmlQueryComposer('//privacySettingsView'),
Expand Down
91 changes: 7 additions & 84 deletions tests/functional/testSubscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,11 @@ describe('Subscription view', function() {
this.ctx.resetCallbacks();
});

it('Authentication needed - sample', async () => {
it('Authentication needed', async () => {
// With moving to in browser auth, this test no longer makes sense for WASM
if (this.ctx.wasm) {
return;
}
this.ctx.fxaLoginCallback = (req) => {
this.ctx.fxaOverrideEndpoints.POSTs['/v1/account/login'].body = {
sessionToken: 'session',
Expand All @@ -394,96 +398,15 @@ describe('Subscription view', function() {

await vpn.waitForQuery(queries.screenSettings.STACKVIEW.ready());

await vpn.waitForQuery(
queries.screenAuthenticationInApp.AUTH_SIGNIN_PASSWORD_INPUT.visible());
await vpn.setQueryProperty(
queries.screenAuthenticationInApp.AUTH_SIGNIN_PASSWORD_INPUT.visible(),
'text', 'P4ssw0rd!!');
await vpn.waitForQuery(
queries.screenAuthenticationInApp.AUTH_SIGNIN_BUTTON.visible()
.enabled());

this.ctx.guardianSubscriptionDetailsCallback = req => {
this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails']
.status = 200;
this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails']
.body = SUBSCRIPTION_DETAILS;
};

await vpn.waitForQueryAndClick(
queries.screenAuthenticationInApp.AUTH_SIGNIN_BUTTON.visible()
.enabled());

await vpn.waitForQuery(
queries.screenSettings.subscriptionView.SCREEN.visible());
});

it('Authentication needed - totp', async () => {
this.ctx.guardianSubscriptionDetailsCallback = req => {
this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails']
.status = 401;
this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails']
.body = {}
};

await vpn.waitForQueryAndClick(queries.navBar.SETTINGS.visible());
await vpn.waitForQuery(queries.global.SCREEN_LOADER.ready());

await vpn.waitForQuery(queries.screenSettings.USER_PROFILE.visible());
await vpn.waitForQueryAndClick(
queries.screenSettings.USER_PROFILE.visible());

await vpn.waitForQuery(queries.screenSettings.STACKVIEW.ready());

await vpn.waitForQuery(
queries.screenAuthenticationInApp.AUTH_SIGNIN_PASSWORD_INPUT.visible());
await vpn.setQueryProperty(
queries.screenAuthenticationInApp.AUTH_SIGNIN_PASSWORD_INPUT.visible(),
'text', 'P4ssw0rd!!');

await vpn.waitForQuery(
queries.screenAuthenticationInApp.AUTH_SIGNIN_BUTTON.visible()
.enabled());

this.ctx.fxaLoginCallback = (req) => {
this.ctx.fxaOverrideEndpoints.POSTs['/v1/account/login'].body = {
sessionToken: 'session',
verified: false,
verificationMethod: 'totp-2fa'
};
this.ctx.fxaOverrideEndpoints.POSTs['/v1/account/login'].status = 200;
};

await vpn.waitForQueryAndClick(
queries.screenAuthenticationInApp.AUTH_SIGNIN_BUTTON.visible()
.enabled());

await vpn.waitForQuery(
queries.screenAuthenticationInApp.AUTH_TOTP_TEXT_INPUT.visible());
await vpn.waitForQuery(
queries.screenAuthenticationInApp.AUTH_TOTP_BUTTON.visible()
.disabled());
await vpn.setQueryProperty(
queries.screenAuthenticationInApp.AUTH_TOTP_TEXT_INPUT.visible(),
'text', '123456');
await vpn.waitForQuery(
queries.screenAuthenticationInApp.AUTH_TOTP_BUTTON.visible().enabled());

this.ctx.fxaTotpCallback = (req) => {
this.ctx.fxaOverrideEndpoints.POSTs['/v1/session/verify/totp'].body = {
success: true
}
};

this.ctx.guardianSubscriptionDetailsCallback = req => {
this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails']
.status = 200;
this.ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails']
.body = SUBSCRIPTION_DETAILS;
};

await vpn.waitForQueryAndClick(
queries.screenAuthenticationInApp.AUTH_TOTP_BUTTON.visible().enabled());
queries.screenSettings.REAUTH_BUTTON.visible());
await vpn.mockInBrowserAuthentication();

await vpn.waitForQuery(
queries.screenSettings.subscriptionView.SCREEN.visible());
Expand Down

0 comments on commit 8298419

Please sign in to comment.