From 048d8d2ec706c6fcb3f8e0891842b85ee0968032 Mon Sep 17 00:00:00 2001
From: David Baker <dave@matrix.org>
Date: Thu, 13 Jun 2019 16:24:09 +0100
Subject: [PATCH] Simplify email registration

You now don't get automatically logged in after finishing
registration. This makes a whole class of failures involving race
conditions and multiple devices impossible.

https://github.com/vector-im/riot-web/issues/9586
---
 .../structures/auth/Registration.js           | 108 ++++++++++++++----
 src/i18n/strings/en_EN.json                   |   3 +
 2 files changed, 91 insertions(+), 20 deletions(-)

diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js
index bf4a86e410..9a7c927603 100644
--- a/src/components/structures/auth/Registration.js
+++ b/src/components/structures/auth/Registration.js
@@ -28,6 +28,7 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
 import * as ServerType from '../../views/auth/ServerTypeSelector';
 import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
 import classNames from "classnames";
+import * as Lifecycle from '../../../Lifecycle';
 
 // Phases
 // Show controls to configure server details
@@ -80,6 +81,9 @@ module.exports = React.createClass({
             // Phase of the overall registration dialog.
             phase: PHASE_REGISTRATION,
             flows: null,
+            // If set, we've registered but are not going to log
+            // the user in to their new account automatically.
+            completedNoSignin: false,
 
             // We perform liveliness checks later, but for now suppress the errors.
             // We also track the server dead errors independently of the regular errors so
@@ -209,6 +213,7 @@ module.exports = React.createClass({
                     errorText: _t("Registration has been disabled on this homeserver."),
                 });
             } else {
+                console.log("Unable to query for supported registration methods.", e);
                 this.setState({
                     errorText: _t("Unable to query for supported registration methods."),
                 });
@@ -282,21 +287,27 @@ module.exports = React.createClass({
             return;
         }
 
-        this.setState({
-            // we're still busy until we get unmounted: don't show the registration form again
-            busy: true,
+        const newState = {
             doingUIAuth: false,
-        });
+        };
+        if (response.access_token) {
+            const cli = await this.props.onLoggedIn({
+                userId: response.user_id,
+                deviceId: response.device_id,
+                homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
+                identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
+                accessToken: response.access_token,
+            });
 
-        const cli = await this.props.onLoggedIn({
-            userId: response.user_id,
-            deviceId: response.device_id,
-            homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
-            identityServerUrl: this.state.matrixClient.getIdentityServerUrl(),
-            accessToken: response.access_token,
-        });
+            this._setupPushers(cli);
+            // we're still busy until we get unmounted: don't show the registration form again
+            newState.busy = true;
+        } else {
+            newState.busy = false;
+            newState.completedNoSignin = true;
+        }
 
-        this._setupPushers(cli);
+        this.setState(newState);
     },
 
     _setupPushers: function(matrixClient) {
@@ -352,7 +363,16 @@ module.exports = React.createClass({
         });
     },
 
-    _makeRegisterRequest: function(auth) {
+    _makeRegisterRequest: function(auth, inhibitLogin) {
+        // default is to inhibit login if we're trying to register with an email address
+        // We do this so that the client that gets spawned when clicking on the email
+        // verification link doesn't get logged in (it can't choose different params
+        // because it doesn't have the password and it can only supply a complete
+        // set of parameters). If the original client is still around when the
+        // registration completes, it can resubmit with inhibitLogin=false to
+        // log itself in!
+        if (inhibitLogin === undefined) inhibitLogin = Boolean(this.state.formVals.email);
+
         // Only send the bind params if we're sending username / pw params
         // (Since we need to send no params at all to use the ones saved in the
         // session).
@@ -360,6 +380,8 @@ module.exports = React.createClass({
             email: true,
             msisdn: true,
         } : {};
+        // Likewise inhibitLogin
+        if (!this.state.formVals.password) inhibitLogin = null;
 
         return this.state.matrixClient.register(
             this.state.formVals.username,
@@ -368,6 +390,7 @@ module.exports = React.createClass({
             auth,
             bindThreepids,
             null,
+            inhibitLogin,
         );
     },
 
@@ -379,6 +402,19 @@ module.exports = React.createClass({
         };
     },
 
+    // Links to the login page shown after registration is completed are routed through this
+    // which checks the user hasn't already logged in somewhere else (perhaps we should do
+    // this more generally?)
+    _onLoginClickWithCheck: async function(ev) {
+        ev.preventDefault();
+
+        const sessionLoaded = await Lifecycle.loadSession({});
+        if (!sessionLoaded) {
+            // ok fine, there's still no session: really go to the login page
+            this.props.onLoginClick();
+        }
+    },
+
     renderServerComponent() {
         const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
         const ServerConfig = sdk.getComponent("auth.ServerConfig");
@@ -528,17 +564,49 @@ module.exports = React.createClass({
             </a>;
         }
 
+        let body;
+        if (this.state.completedNoSignin) {
+            let regDoneText;
+            if (this.state.formVals.password) {
+                // We're the client that started the registration
+                regDoneText = _t(
+                    "<a>Log in</a> to your new account.", {},
+                    {
+                        a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
+                    },
+                );
+            } else {
+                // We're not the original client: the user probably got to us by clicking the
+                // email validation link. We can't offer a 'go straight to your account' link
+                // as we don't have the original creds.
+                regDoneText = _t(
+                    "You can now close this window or <a>log in</a> to your new account.", {},
+                    {
+                        a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
+                    },
+                );
+            }
+            body = <div>
+                <h2>{_t("Registration Successful")}</h2>
+                <h3>{ regDoneText }</h3>
+            </div>;
+        } else {
+            body = <div>
+                <h2>{ _t('Create your account') }</h2>
+                { errorText }
+                { serverDeadSection }
+                { this.renderServerComponent() }
+                { this.renderRegisterComponent() }
+                { goBack }
+                { signIn }
+            </div>;
+        }
+
         return (
             <AuthPage>
                 <AuthHeader />
                 <AuthBody>
-                    <h2>{ _t('Create your account') }</h2>
-                    { errorText }
-                    { serverDeadSection }
-                    { this.renderServerComponent() }
-                    { this.renderRegisterComponent() }
-                    { goBack }
-                    { signIn }
+                    { body }
                 </AuthBody>
             </AuthPage>
         );
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 69ac59f984..53fd82f6f2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1557,6 +1557,9 @@
     "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
     "Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
     "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
+    "<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.",
+    "You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
+    "Registration Successful": "Registration Successful",
     "Create your account": "Create your account",
     "Commands": "Commands",
     "Results from DuckDuckGo": "Results from DuckDuckGo",