From 0f34f8b494fc98da4fdfd765408b91e4d18b2ad8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 17:25:14 +0000 Subject: [PATCH 01/13] Extend from a Signup class to keep hs/is URL logic together --- src/Signup.js | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index ba91ff60b0..8caf868fb4 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -3,14 +3,43 @@ var MatrixClientPeg = require("./MatrixClientPeg"); var dis = require("./dispatcher"); var q = require("q"); -class Register { - -} - -class Login { +class Signup { constructor(hsUrl, isUrl) { this._hsUrl = hsUrl; this._isUrl = isUrl; + } + + getHomeserverUrl() { + return this._hsUrl; + } + + getIdentityServerUrl() { + return this._isUrl; + } + + setHomeserverUrl(hsUrl) { + this._hsUrl = hsUrl; + } + + setIdentityServerUrl(isUrl) { + this._isUrl = isUrl; + } +} + +class Register extends Signup { + constructor(hsUrl, isUrl) { + super(hsUrl, isUrl); + this._state = "start"; + } + + getState() { + return this._state; + } +} + +class Login extends Signup { + constructor(hsUrl, isUrl) { + super(hsUrl, isUrl); this._currentFlowIndex = 0; this._flows = []; } From 1fca3f66066ef65fe8cf3a8602deb195121eff11 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 17:38:37 +0000 Subject: [PATCH 02/13] Better const name --- src/Signup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Signup.js b/src/Signup.js index 8caf868fb4..2446130405 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -29,7 +29,7 @@ class Signup { class Register extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); - this._state = "start"; + this._state = "Register.START"; } getState() { From 991a96cfc5b4e3875b0bcf21006e582eea26e9ad Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Nov 2015 17:13:43 +0000 Subject: [PATCH 03/13] Get dummy registrations working This means you can now register on localhost without needing an email. Email and Recaptcha are still broken. --- src/Signup.js | 192 +++++++++++++++++++++++++++++++++++++++++++- src/SignupStages.js | 125 ++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 src/SignupStages.js diff --git a/src/Signup.js b/src/Signup.js index 2446130405..b679df4e8a 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -1,8 +1,11 @@ "use strict"; var MatrixClientPeg = require("./MatrixClientPeg"); +var SignupStages = require("./SignupStages"); var dis = require("./dispatcher"); var q = require("q"); +const EMAIL_STAGE_TYPE = "m.login.email.identity"; + class Signup { constructor(hsUrl, isUrl) { this._hsUrl = hsUrl; @@ -26,17 +29,200 @@ class Signup { } } + class Register extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); - this._state = "Register.START"; + this.setStep("START"); + this.data = null; // from the server + this.username = null; // desired + this.email = null; // desired + this.password = null; // desired + this.params = {}; // random other stuff (e.g. query params) + this.credentials = null; } - getState() { - return this._state; + setClientSecret(secret) { + this.params.clientSecret = secret; + } + + setSessionId(sessionId) { + this.params.sessionId = sessionId; + } + + setRegistrationUrl(regUrl) { + this.params.registrationUrl = regUrl; + } + + setIdSid(idSid) { + this.params.idSid = idSid; + } + + getStep() { + return this._step; + } + + getCredentials() { + return this.credentials; + } + + setStep(step) { + this._step = 'Register.' + step; + // TODO: + // It's a shame this is going to the global dispatcher, we only really + // want things which have an instance of this class to be able to add + // listeners... + console.log("Dispatching 'registration_step_update' for step %s", this._step); + dis.dispatch({ + action: "registration_step_update" + }); + } + + register(formVals) { + var {username, password, email} = formVals; + this.email = email; + this.username = username; + this.password = password; + + // feels a bit wrong to be clobbering the global client for something we + // don't even know if it'll work, but we'll leave this here for now to + // not complicate matters further. It would be nicer to isolate this + // logic entirely from the rest of the app though. + MatrixClientPeg.replaceUsingUrls( + this._hsUrl, + this._isUrl + ); + return this._tryRegister(); + } + + _tryRegister(authDict) { + console.log("_tryRegister %s", JSON.stringify(authDict)); + var self = this; + return MatrixClientPeg.get().register( + this.username, this.password, this._sessionId, authDict + ).then(function(result) { + console.log("Got a final response"); + self.credentials = result; + self.setStep("COMPLETE"); + return result; // contains the credentials + }, function(error) { + console.error(error); + if (error.httpStatus === 401 && error.data && error.data.flows) { + self.data = error.data || {}; + var flow = self.chooseFlow(error.data.flows); + + if (flow) { + var flowStage = self.firstUncompletedStageIndex(flow); + return self.startStage(flow.stages[flowStage]); + } + else { + throw new Error("Unable to register - missing email address?"); + } + } else { + if (error.errcode === 'M_USER_IN_USE') { + throw new Error("Username in use"); + } else if (error.httpStatus == 401) { + throw new Error("Authorisation failed!"); + } else if (error.httpStatus >= 400 && error.httpStatus < 500) { + throw new Error(`Registration failed! (${error.httpStatus})`); + } else if (error.httpStatus >= 500 && error.httpStatus < 600) { + throw new Error( + `Server error during registration! (${error.httpStatus})` + ); + } else if (error.name == "M_MISSING_PARAM") { + // The HS hasn't remembered the login params from + // the first try when the login email was sent. + throw new Error( + "This home server does not support resuming registration." + ); + } + } + }); + } + + firstUncompletedStageIndex(flow) { + if (!this.completedStages) { + return 0; + } + for (var i = 0; i < flow.stages.length; ++i) { + if (this.completedStages.indexOf(flow.stages[i]) == -1) { + return i; + } + } + } + + numCompletedStages(flow) { + if (!this.completedStages) { + return 0; + } + var nCompleted = 0; + for (var i = 0; i < flow.stages.length; ++i) { + if (this.completedStages.indexOf(flow.stages[i]) > -1) { + ++nCompleted; + } + } + return nCompleted; + } + + startStage(stageName) { + var self = this; + this.setStep(`STEP_${stageName}`); + var StageClass = SignupStages[stageName]; + if (!StageClass) { + // no idea how to handle this! + throw new Error("Unknown stage: " + stageName); + } + + var stage = new StageClass(MatrixClientPeg.get(), this); + return stage.complete().then(function(request) { + if (request.auth) { + return self._tryRegister(request.auth); + } + }); + } + + hasCompletedStage(stageType) { + var completed = (this.data || {}).completed || []; + return completed.indexOf(stageType) !== -1; + } + + chooseFlow(flows) { + // If the user gave us an email then we want to pick an email + // flow we can do, else any other flow. + var emailFlow = null; + var otherFlow = null; + flows.forEach(function(flow) { + var flowHasEmail = false; + for (var stageI = 0; stageI < flow.stages.length; ++stageI) { + var stage = flow.stages[stageI]; + + if (!SignupStages[stage]) { + // we can't do this flow, don't have a Stage impl. + return; + } + + if (stage === EMAIL_STAGE_TYPE) { + flowHasEmail = true; + } + } + + if (flowHasEmail) { + emailFlow = flow; + } else { + otherFlow = flow; + } + }); + + if (this.email || this.hasCompletedStage(EMAIL_STAGE_TYPE)) { + // we've been given an email or we've already done an email part + return emailFlow; + } else { + return otherFlow; + } } } + class Login extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); diff --git a/src/SignupStages.js b/src/SignupStages.js new file mode 100644 index 0000000000..a4b51f0abb --- /dev/null +++ b/src/SignupStages.js @@ -0,0 +1,125 @@ +"use strict"; +var q = require("q"); + +class Stage { + constructor(type, matrixClient, signupInstance) { + this.type = type; + this.client = matrixClient; + this.signupInstance = signupInstance; + } + + complete() { + // Return a promise which is: + // RESOLVED => With an Object which has an 'auth' key which is the auth dict + // to submit. + // REJECTED => With an Error if there was a problem with this stage. + // Has a "message" string and an "isFatal" flag. + return q.reject("NOT IMPLEMENTED"); + } +} +Stage.TYPE = "NOT IMPLEMENTED"; + + +class DummyStage extends Stage { + constructor(matrixClient, signupInstance) { + super(DummyStage.TYPE, matrixClient, signupInstance); + } + + complete() { + return q({ + auth: { + type: DummyStage.TYPE + } + }); + } +} +DummyStage.TYPE = "m.login.dummy"; + + +class RecaptchaStage extends Stage { + constructor(matrixClient, signupInstance) { + super(RecaptchaStage.TYPE, matrixClient, signupInstance); + } + + complete() { + var publicKey; + if (this.signupInstance.params['m.login.recaptcha']) { + publicKey = this.signupInstance.params['m.login.recaptcha'].public_key; + } + if (!publicKey) { + return q.reject({ + message: "This server has not supplied enough information for Recaptcha " + + "authentication", + isFatal: true + }); + } + + var defer = q.defer(); + global.grecaptcha.render('mx_recaptcha', { + sitekey: publicKey, + callback: function(response) { + return defer.resolve({ + auth: { + type: 'm.login.recaptcha', + response: response + } + }); + } + }); + + return defer.promise; + } +} +RecaptchaStage.TYPE = "m.login.recaptcha"; + + +class EmailIdentityStage extends Stage { + constructor(matrixClient, signupInstance) { + super(EmailIdentityStage.TYPE, matrixClient, signupInstance); + } + + complete() { + var config = { + clientSecret: this.client.generateClientSecret(), + sendAttempt: 1 + }; + this.signupInstance.params[EmailIdentityStage.TYPE] = config; + + var nextLink = this.signupInstance.params.registrationUrl + + '?client_secret=' + + encodeURIComponent(config.clientSecret) + + "&hs_url=" + + encodeURIComponent(this.signupInstance.getHomeserverUrl()) + + "&is_url=" + + encodeURIComponent(this.signupInstance.getIdentityServerUrl()) + + "&session_id=" + + encodeURIComponent(this.signupInstance.getSessionId()); + + return this.client.requestEmailToken( + this.signupInstance.email, + config.clientSecret, + config.sendAttempt, + nextLink + ).then(function(response) { + return {}; // don't want to make a request + }, function(error) { + console.error(error); + var e = { + isFatal: true + }; + if (error.errcode == 'THREEPID_IN_USE') { + e.message = "Email in use"; + } else { + e.message = 'Unable to contact the given identity server'; + } + return e; + }); + } +} +EmailIdentityStage.TYPE = "m.login.email.identity"; + +module.exports = { + [DummyStage.TYPE]: DummyStage, + [RecaptchaStage.TYPE]: RecaptchaStage, + [EmailIdentityStage.TYPE]: EmailIdentityStage +}; \ No newline at end of file From 3e903be73dd3652435833fe1a43539775ce052ec Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Nov 2015 17:43:38 +0000 Subject: [PATCH 04/13] Get Recaptcha working again. Add a backchannel for stage prodding. Recaptcha is a special snowflake because it dynamically loads the script and THEN renders with info from the registration request. This means we need a back-channel for the UI component to 'tell' the stage that everything is loaded. This Just Works which is nice. --- src/Signup.js | 16 +++++++++++++++- src/SignupStages.js | 40 +++++++++++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index b679df4e8a..79686b7abf 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -38,8 +38,9 @@ class Register extends Signup { this.username = null; // desired this.email = null; // desired this.password = null; // desired - this.params = {}; // random other stuff (e.g. query params) + this.params = {}; // random other stuff (e.g. query params, NOT params from the server) this.credentials = null; + this.activeStage = null; } setClientSecret(secret) { @@ -66,6 +67,10 @@ class Register extends Signup { return this.credentials; } + getServerData() { + return this.data || {}; + } + setStep(step) { this._step = 'Register.' + step; // TODO: @@ -112,6 +117,7 @@ class Register extends Signup { var flow = self.chooseFlow(error.data.flows); if (flow) { + console.log("Active flow => %s", JSON.stringify(flow)); var flowStage = self.firstUncompletedStageIndex(flow); return self.startStage(flow.stages[flowStage]); } @@ -174,6 +180,7 @@ class Register extends Signup { } var stage = new StageClass(MatrixClientPeg.get(), this); + this.activeStage = stage; return stage.complete().then(function(request) { if (request.auth) { return self._tryRegister(request.auth); @@ -220,6 +227,13 @@ class Register extends Signup { return otherFlow; } } + + tellStage(stageName, data) { + if (this.activeStage && this.activeStage.type === stageName) { + console.log("Telling stage %s about something..", stageName); + this.activeStage.onReceiveData(data); + } + } } diff --git a/src/SignupStages.js b/src/SignupStages.js index a4b51f0abb..a4d7ac9d17 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -16,6 +16,10 @@ class Stage { // Has a "message" string and an "isFatal" flag. return q.reject("NOT IMPLEMENTED"); } + + onReceiveData() { + // NOP + } } Stage.TYPE = "NOT IMPLEMENTED"; @@ -39,12 +43,22 @@ DummyStage.TYPE = "m.login.dummy"; class RecaptchaStage extends Stage { constructor(matrixClient, signupInstance) { super(RecaptchaStage.TYPE, matrixClient, signupInstance); + this.defer = q.defer(); + this.publicKey = null; + } + + onReceiveData(data) { + if (data !== "loaded") { + return; + } + this._attemptRender(); } complete() { var publicKey; - if (this.signupInstance.params['m.login.recaptcha']) { - publicKey = this.signupInstance.params['m.login.recaptcha'].public_key; + var serverParams = this.signupInstance.getServerData().params; + if (serverParams && serverParams["m.login.recaptcha"]) { + publicKey = serverParams["m.login.recaptcha"].public_key; } if (!publicKey) { return q.reject({ @@ -53,12 +67,26 @@ class RecaptchaStage extends Stage { isFatal: true }); } + this.publicKey = publicKey; + this._attemptRender(); - var defer = q.defer(); + return this.defer.promise; + } + + _attemptRender() { + if (!global.grecaptcha) { + console.error("grecaptcha not loaded!"); + return; + } + if (!this.publicKey) { + console.error("No public key for recaptcha!"); + return; + } + var self = this; global.grecaptcha.render('mx_recaptcha', { - sitekey: publicKey, + sitekey: this.publicKey, callback: function(response) { - return defer.resolve({ + return self.defer.resolve({ auth: { type: 'm.login.recaptcha', response: response @@ -66,8 +94,6 @@ class RecaptchaStage extends Stage { }); } }); - - return defer.promise; } } RecaptchaStage.TYPE = "m.login.recaptcha"; From f2f5496b78f54c098c79e83a9e8d33d6ebeb35b8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 11:41:32 +0000 Subject: [PATCH 05/13] Get email auth sending working (not the link back though) --- src/Signup.js | 8 +++++++- src/SignupStages.js | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 79686b7abf..db69441d6f 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -184,7 +184,13 @@ class Register extends Signup { return stage.complete().then(function(request) { if (request.auth) { return self._tryRegister(request.auth); - } + } + else { + // never resolve the promise chain. This is for things like email auth + // which display a "check your email" message and relies on the + // link in the email to actually register you. + return q.defer().promise; + } }); } diff --git a/src/SignupStages.js b/src/SignupStages.js index a4d7ac9d17..3521b4ba39 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -119,7 +119,7 @@ class EmailIdentityStage extends Stage { "&is_url=" + encodeURIComponent(this.signupInstance.getIdentityServerUrl()) + "&session_id=" + - encodeURIComponent(this.signupInstance.getSessionId()); + encodeURIComponent(this.signupInstance.params.sessionId); return this.client.requestEmailToken( this.signupInstance.email, From 8d7d338f44f6296ddb39f2c4c6d26befd768f9f4 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 13:58:34 +0000 Subject: [PATCH 06/13] Pass the right session ID --- src/SignupStages.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SignupStages.js b/src/SignupStages.js index 3521b4ba39..d49a488ec8 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -83,6 +83,7 @@ class RecaptchaStage extends Stage { return; } var self = this; + // FIXME: Tight coupling here and in CaptchaForm.js global.grecaptcha.render('mx_recaptcha', { sitekey: this.publicKey, callback: function(response) { @@ -119,7 +120,7 @@ class EmailIdentityStage extends Stage { "&is_url=" + encodeURIComponent(this.signupInstance.getIdentityServerUrl()) + "&session_id=" + - encodeURIComponent(this.signupInstance.params.sessionId); + encodeURIComponent(this.signupInstance.getServerData().session); return this.client.requestEmailToken( this.signupInstance.email, From 7568a3b2d346236447206271795484095de28e9f Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 14:16:49 +0000 Subject: [PATCH 07/13] Hookup 2nd stage email registration; not finished as we aren't storing u/p --- src/Signup.js | 23 +++++++++++++++++++++++ src/SignupStages.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Signup.js b/src/Signup.js index db69441d6f..7e3b3335ff 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -234,6 +234,29 @@ class Register extends Signup { } } + recheckState() { + // feels a bit wrong to be clobbering the global client for something we + // don't even know if it'll work, but we'll leave this here for now to + // not complicate matters further. It would be nicer to isolate this + // logic entirely from the rest of the app though. + MatrixClientPeg.replaceUsingUrls( + this._hsUrl, + this._isUrl + ); + // We've been given a bunch of data from a previous register step, + // this only happens for email auth currently. It's kinda ming we need + // to know this though. A better solution would be to ask the stages if + // they are ready to do something rather than accepting that we know about + // email auth and its internals. + this.params.hasEmailInfo = ( + this.params.clientSecret && this.params.sessionId && this.params.idSid + ); + + if (this.params.hasEmailInfo) { + this.startStage(EMAIL_STAGE_TYPE); + } + } + tellStage(stageName, data) { if (this.activeStage && this.activeStage.type === stageName) { console.log("Telling stage %s about something..", stageName); diff --git a/src/SignupStages.js b/src/SignupStages.js index d49a488ec8..53e75e39ea 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -105,7 +105,36 @@ class EmailIdentityStage extends Stage { super(EmailIdentityStage.TYPE, matrixClient, signupInstance); } + _completeVerify() { + console.log("_completeVerify"); + var isLocation = document.createElement('a'); + isLocation.href = this.signupInstance.getIdentityServerUrl(); + + return q({ + auth: { + type: 'm.login.email.identity', + threepid_creds: { + sid: this.signupInstance.params.idSid, + client_secret: this.signupInstance.params.clientSecret, + id_server: isLocation.host + } + } + }); + } + + /** + * Complete the email stage. + * + * This is called twice under different circumstances: + * 1) When requesting an email token from the IS + * 2) When validating query parameters received from the link in the email + */ complete() { + console.log("Email complete()"); + if (this.signupInstance.params.hasEmailInfo) { + return this._completeVerify(); + } + var config = { clientSecret: this.client.generateClientSecret(), sendAttempt: 1 From cc746767187cdcc1721b236b9d133160761d9ca0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 15:19:30 +0000 Subject: [PATCH 08/13] Mostly fix 2nd step email registration - Don't send u/p: null - Remove unused functions - Moar logging Still doesn't work yet though. --- src/Signup.js | 49 +++++++++++++++++++++------------------------ src/SignupStages.js | 4 ++-- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 7e3b3335ff..0107075f9c 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -35,12 +35,15 @@ class Register extends Signup { super(hsUrl, isUrl); this.setStep("START"); this.data = null; // from the server - this.username = null; // desired - this.email = null; // desired - this.password = null; // desired this.params = {}; // random other stuff (e.g. query params, NOT params from the server) this.credentials = null; this.activeStage = null; + this.registrationPromise = null; + // These values MUST be undefined else we'll send "username: null" which + // will error on Synapse rather than having the key absent. + this.username = undefined; // desired + this.email = undefined; // desired + this.password = undefined; // desired } setClientSecret(secret) { @@ -71,6 +74,10 @@ class Register extends Signup { return this.data || {}; } + getPromise() { + return this.registrationPromise; + } + setStep(step) { this._step = 'Register.' + step; // TODO: @@ -97,6 +104,7 @@ class Register extends Signup { this._hsUrl, this._isUrl ); + console.log("register(formVals)"); return this._tryRegister(); } @@ -104,7 +112,7 @@ class Register extends Signup { console.log("_tryRegister %s", JSON.stringify(authDict)); var self = this; return MatrixClientPeg.get().register( - this.username, this.password, this._sessionId, authDict + this.username, this.password, this.params.sessionId, authDict ).then(function(result) { console.log("Got a final response"); self.credentials = result; @@ -114,12 +122,13 @@ class Register extends Signup { console.error(error); if (error.httpStatus === 401 && error.data && error.data.flows) { self.data = error.data || {}; + console.log("RAW: %s", JSON.stringify(error.data)); var flow = self.chooseFlow(error.data.flows); if (flow) { console.log("Active flow => %s", JSON.stringify(flow)); - var flowStage = self.firstUncompletedStageIndex(flow); - return self.startStage(flow.stages[flowStage]); + var flowStage = self.firstUncompletedStage(flow); + return self.startStage(flowStage); } else { throw new Error("Unable to register - missing email address?"); @@ -146,30 +155,14 @@ class Register extends Signup { }); } - firstUncompletedStageIndex(flow) { - if (!this.completedStages) { - return 0; - } + firstUncompletedStage(flow) { for (var i = 0; i < flow.stages.length; ++i) { - if (this.completedStages.indexOf(flow.stages[i]) == -1) { - return i; + if (!this.hasCompletedStage(flow.stages[i])) { + return flow.stages[i]; } } } - numCompletedStages(flow) { - if (!this.completedStages) { - return 0; - } - var nCompleted = 0; - for (var i = 0; i < flow.stages.length; ++i) { - if (this.completedStages.indexOf(flow.stages[i]) > -1) { - ++nCompleted; - } - } - return nCompleted; - } - startStage(stageName) { var self = this; this.setStep(`STEP_${stageName}`); @@ -182,6 +175,7 @@ class Register extends Signup { var stage = new StageClass(MatrixClientPeg.get(), this); this.activeStage = stage; return stage.complete().then(function(request) { + console.log("Stage %s completed with %s", stageName, JSON.stringify(request)); if (request.auth) { return self._tryRegister(request.auth); } @@ -189,6 +183,7 @@ class Register extends Signup { // never resolve the promise chain. This is for things like email auth // which display a "check your email" message and relies on the // link in the email to actually register you. + console.log("Waiting for external action."); return q.defer().promise; } }); @@ -253,8 +248,10 @@ class Register extends Signup { ); if (this.params.hasEmailInfo) { - this.startStage(EMAIL_STAGE_TYPE); + console.log("recheckState has email info.. starting email info.."); + this.registrationPromise = this.startStage(EMAIL_STAGE_TYPE); } + return this.registrationPromise; } tellStage(stageName, data) { diff --git a/src/SignupStages.js b/src/SignupStages.js index 53e75e39ea..2a63ae6058 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -69,7 +69,6 @@ class RecaptchaStage extends Stage { } this.publicKey = publicKey; this._attemptRender(); - return this.defer.promise; } @@ -87,7 +86,8 @@ class RecaptchaStage extends Stage { global.grecaptcha.render('mx_recaptcha', { sitekey: this.publicKey, callback: function(response) { - return self.defer.resolve({ + console.log("Received captcha response"); + self.defer.resolve({ auth: { type: 'm.login.recaptcha', response: response From b12f0f1df710820db16ffbf69d42213bd47a1e4e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 16:07:58 +0000 Subject: [PATCH 09/13] Minor refactoring; remove debug logging; add comments --- src/Signup.js | 28 ++++++++++++++++----------- src/SignupStages.js | 47 ++++++++++++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index 0107075f9c..02a59ebe6e 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -6,6 +6,10 @@ var q = require("q"); const EMAIL_STAGE_TYPE = "m.login.email.identity"; +/** + * A base class for common functionality between Registration and Login e.g. + * storage of HS/IS URLs. + */ class Signup { constructor(hsUrl, isUrl) { this._hsUrl = hsUrl; @@ -29,13 +33,16 @@ class Signup { } } - +/** + * Registration logic class + */ class Register extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); this.setStep("START"); this.data = null; // from the server - this.params = {}; // random other stuff (e.g. query params, NOT params from the server) + // random other stuff (e.g. query params, NOT params from the server) + this.params = {}; this.credentials = null; this.activeStage = null; this.registrationPromise = null; @@ -104,12 +111,12 @@ class Register extends Signup { this._hsUrl, this._isUrl ); - console.log("register(formVals)"); + console.log("Starting registration process (form submission)"); return this._tryRegister(); } _tryRegister(authDict) { - console.log("_tryRegister %s", JSON.stringify(authDict)); + console.log("Trying to register with auth dict: %s", JSON.stringify(authDict)); var self = this; return MatrixClientPeg.get().register( this.username, this.password, this.params.sessionId, authDict @@ -163,6 +170,11 @@ class Register extends Signup { } } + hasCompletedStage(stageType) { + var completed = (this.data || {}).completed || []; + return completed.indexOf(stageType) !== -1; + } + startStage(stageName) { var self = this; this.setStep(`STEP_${stageName}`); @@ -175,8 +187,8 @@ class Register extends Signup { var stage = new StageClass(MatrixClientPeg.get(), this); this.activeStage = stage; return stage.complete().then(function(request) { - console.log("Stage %s completed with %s", stageName, JSON.stringify(request)); if (request.auth) { + console.log("Stage %s is returning an auth dict", stageName); return self._tryRegister(request.auth); } else { @@ -189,11 +201,6 @@ class Register extends Signup { }); } - hasCompletedStage(stageType) { - var completed = (this.data || {}).completed || []; - return completed.indexOf(stageType) !== -1; - } - chooseFlow(flows) { // If the user gave us an email then we want to pick an email // flow we can do, else any other flow. @@ -248,7 +255,6 @@ class Register extends Signup { ); if (this.params.hasEmailInfo) { - console.log("recheckState has email info.. starting email info.."); this.registrationPromise = this.startStage(EMAIL_STAGE_TYPE); } return this.registrationPromise; diff --git a/src/SignupStages.js b/src/SignupStages.js index 2a63ae6058..272a955d95 100644 --- a/src/SignupStages.js +++ b/src/SignupStages.js @@ -1,6 +1,9 @@ "use strict"; var q = require("q"); +/** + * An interface class which login types should abide by. + */ class Stage { constructor(type, matrixClient, signupInstance) { this.type = type; @@ -24,6 +27,9 @@ class Stage { Stage.TYPE = "NOT IMPLEMENTED"; +/** + * This stage requires no auth. + */ class DummyStage extends Stage { constructor(matrixClient, signupInstance) { super(DummyStage.TYPE, matrixClient, signupInstance); @@ -40,17 +46,24 @@ class DummyStage extends Stage { DummyStage.TYPE = "m.login.dummy"; +/** + * This stage uses Google's Recaptcha to do auth. + */ class RecaptchaStage extends Stage { constructor(matrixClient, signupInstance) { super(RecaptchaStage.TYPE, matrixClient, signupInstance); - this.defer = q.defer(); - this.publicKey = null; + this.defer = q.defer(); // resolved with the captcha response + this.publicKey = null; // from the HS + this.divId = null; // from the UI component } + // called when the UI component has loaded the recaptcha
so we can + // render to it. onReceiveData(data) { - if (data !== "loaded") { + if (!data || !data.divId) { return; } + this.divId = data.divId; this._attemptRender(); } @@ -81,9 +94,13 @@ class RecaptchaStage extends Stage { console.error("No public key for recaptcha!"); return; } + if (!this.divId) { + console.error("No div ID specified!"); + return; + } + console.log("Rendering to %s", this.divId); var self = this; - // FIXME: Tight coupling here and in CaptchaForm.js - global.grecaptcha.render('mx_recaptcha', { + global.grecaptcha.render(this.divId, { sitekey: this.publicKey, callback: function(response) { console.log("Received captcha response"); @@ -100,13 +117,16 @@ class RecaptchaStage extends Stage { RecaptchaStage.TYPE = "m.login.recaptcha"; +/** + * This state uses the IS to verify email addresses. + */ class EmailIdentityStage extends Stage { constructor(matrixClient, signupInstance) { super(EmailIdentityStage.TYPE, matrixClient, signupInstance); } _completeVerify() { - console.log("_completeVerify"); + // pull out the host of the IS URL by creating an anchor element var isLocation = document.createElement('a'); isLocation.href = this.signupInstance.getIdentityServerUrl(); @@ -130,20 +150,15 @@ class EmailIdentityStage extends Stage { * 2) When validating query parameters received from the link in the email */ complete() { - console.log("Email complete()"); + // TODO: The Registration class shouldn't really know this info. if (this.signupInstance.params.hasEmailInfo) { return this._completeVerify(); } - var config = { - clientSecret: this.client.generateClientSecret(), - sendAttempt: 1 - }; - this.signupInstance.params[EmailIdentityStage.TYPE] = config; - + var clientSecret = this.client.generateClientSecret(); var nextLink = this.signupInstance.params.registrationUrl + '?client_secret=' + - encodeURIComponent(config.clientSecret) + + encodeURIComponent(clientSecret) + "&hs_url=" + encodeURIComponent(this.signupInstance.getHomeserverUrl()) + "&is_url=" + @@ -153,8 +168,8 @@ class EmailIdentityStage extends Stage { return this.client.requestEmailToken( this.signupInstance.email, - config.clientSecret, - config.sendAttempt, + clientSecret, + 1, // TODO: Multiple send attempts? nextLink ).then(function(response) { return {}; // don't want to make a request From 23467de016463aa0e94c69862f71c75985fca62b Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 19 Nov 2015 16:47:28 +0000 Subject: [PATCH 10/13] Remove missed debug log --- src/Signup.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Signup.js b/src/Signup.js index 02a59ebe6e..bb74c58ac2 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -129,7 +129,6 @@ class Register extends Signup { console.error(error); if (error.httpStatus === 401 && error.data && error.data.flows) { self.data = error.data || {}; - console.log("RAW: %s", JSON.stringify(error.data)); var flow = self.chooseFlow(error.data.flows); if (flow) { From cad3afc7a47160046f6d8b6d3090bd863f41397d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 10:11:51 +0000 Subject: [PATCH 11/13] Remove unhelpful log lines --- src/Signup.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Signup.js b/src/Signup.js index bb74c58ac2..02ddaacc6d 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -111,22 +111,18 @@ class Register extends Signup { this._hsUrl, this._isUrl ); - console.log("Starting registration process (form submission)"); return this._tryRegister(); } _tryRegister(authDict) { - console.log("Trying to register with auth dict: %s", JSON.stringify(authDict)); var self = this; return MatrixClientPeg.get().register( this.username, this.password, this.params.sessionId, authDict ).then(function(result) { - console.log("Got a final response"); self.credentials = result; self.setStep("COMPLETE"); return result; // contains the credentials }, function(error) { - console.error(error); if (error.httpStatus === 401 && error.data && error.data.flows) { self.data = error.data || {}; var flow = self.chooseFlow(error.data.flows); From 030e2f0979e73de98d136fb6598309b5694bda98 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 10:14:00 +0000 Subject: [PATCH 12/13] Move CaptchaForm from Vector to React SDK --- src/components/login/CaptchaForm.js | 67 +++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/components/login/CaptchaForm.js diff --git a/src/components/login/CaptchaForm.js b/src/components/login/CaptchaForm.js new file mode 100644 index 0000000000..9b722f463b --- /dev/null +++ b/src/components/login/CaptchaForm.js @@ -0,0 +1,67 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +var React = require('react'); +var DIV_ID = 'mx_recaptcha'; + +/** + * A pure UI component which displays a captcha form. + */ +module.exports = React.createClass({ + displayName: 'CaptchaForm', + + propTypes: { + onCaptchaLoaded: React.PropTypes.func.isRequired // called with div id name + }, + + getDefaultProps: function() { + return { + onCaptchaLoaded: function() { + console.error("Unhandled onCaptchaLoaded"); + } + }; + }, + + componentDidMount: function() { + // Just putting a script tag into the returned jsx doesn't work, annoyingly, + // so we do this instead. + var self = this; + if (this.refs.recaptchaContainer) { + console.log("Loading recaptcha script..."); + var scriptTag = document.createElement('script'); + window.mx_on_recaptcha_loaded = function() { + console.log("Loaded recaptcha script."); + self.props.onCaptchaLoaded(DIV_ID); + }; + scriptTag.setAttribute( + 'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" + ); + this.refs.recaptchaContainer.appendChild(scriptTag); + } + }, + + render: function() { + // FIXME: Tight coupling with the div id and SignupStages.js + return ( +
+ This Home Server would like to make sure you are not a robot +
+
+ ); + } +}); \ No newline at end of file From 05a7d76785c029577e506e87dde1f0bb982e2920 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 20 Nov 2015 10:15:18 +0000 Subject: [PATCH 13/13] Remove old Register files --- src/controllers/templates/Register.js | 352 -------------------------- 1 file changed, 352 deletions(-) delete mode 100644 src/controllers/templates/Register.js diff --git a/src/controllers/templates/Register.js b/src/controllers/templates/Register.js deleted file mode 100644 index a3dac5b9d0..0000000000 --- a/src/controllers/templates/Register.js +++ /dev/null @@ -1,352 +0,0 @@ -/* -Copyright 2015 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -var MatrixClientPeg = require("../../MatrixClientPeg"); -var dis = require("../../dispatcher"); - -module.exports = { - FieldErrors: { - PasswordMismatch: 'PasswordMismatch', - TooShort: 'TooShort', - Missing: 'Missing', - InUse: 'InUse', - Length: 'Length' - }, - - getInitialState: function() { - return { - step: 'initial', - busy: false, - currentStep: 0, - totalSteps: 1 - }; - }, - - componentWillMount: function() { - this.savedParams = { - email: '', - username: '', - password: '', - confirmPassword: '' - }; - this.readNewProps(); - }, - - componentWillReceiveProps: function() { - this.readNewProps(); - }, - - readNewProps: function() { - if (this.props.clientSecret && this.props.hsUrl && - this.props.isUrl && this.props.sessionId && - this.props.idSid) { - this.authSessionId = this.props.sessionId; - MatrixClientPeg.replaceUsingUrls( - this.props.hsUrl, - this.props.isUrl - ); - this.setState({ - hs_url: this.props.hsUrl, - is_url: this.props.isUrl - }); - this.savedParams = {client_secret: this.props.clientSecret}; - this.setState({busy: true}); - - var isLocation = document.createElement('a'); - isLocation.href = this.props.isUrl; - - var auth = { - type: 'm.login.email.identity', - threepid_creds: { - sid: this.props.idSid, - client_secret: this.savedParams.client_secret, - id_server: isLocation.host - } - }; - this.tryRegister(auth); - } - }, - - componentDidUpdate: function() { - // Just putting a script tag into the returned jsx doesn't work, annoyingly, - // so we do this instead. - if (this.refs.recaptchaContainer) { - var scriptTag = document.createElement('script'); - window.mx_on_recaptcha_loaded = this.onCaptchaLoaded; - scriptTag.setAttribute('src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit"); - this.refs.recaptchaContainer.appendChild(scriptTag); - } - }, - - setStep: function(step) { - this.setState({ step: step, errorText: '', busy: false }); - }, - - getSupportedStageTypes: function() { - return ['m.login.email.identity', 'm.login.recaptcha']; - }, - - chooseFlow: function(flows) { - // this is fairly simple right now - var supportedTypes = this.getSupportedStageTypes(); - - var emailFlow = null; - var otherFlow = null; - for (var flowI = 0; flowI < flows.length; ++flowI) { - var flow = flows[flowI]; - var flowHasEmail = false; - var flowSupported = true; - for (var stageI = 0; stageI < flow.stages.length; ++stageI) { - var stage = flow.stages[stageI]; - - if (supportedTypes.indexOf(stage) == -1) { - flowSupported = false; - } - - if (stage == 'm.login.email.identity') { - flowHasEmail = true; - } - } - if (flowSupported) { - if (flowHasEmail) { - emailFlow = flow; - } else { - otherFlow = flow; - } - } - } - - if ( - this.savedParams.email != '' || - this.completedStages.indexOf('m.login.email.identity') > -1 - ) { - return emailFlow; - } else { - return otherFlow; - } - }, - - firstUncompletedStageIndex: function(flow) { - if (this.completedStages === undefined) return 0; - for (var i = 0; i < flow.stages.length; ++i) { - if (this.completedStages.indexOf(flow.stages[i]) == -1) { - return i; - } - } - }, - - numCompletedStages: function(flow) { - if (this.completedStages === undefined) return 0; - var nCompleted = 0; - for (var i = 0; i < flow.stages.length; ++i) { - if (this.completedStages.indexOf(flow.stages[i]) > -1) { - ++nCompleted; - } - } - return nCompleted; - }, - - onInitialStageSubmit: function(ev) { - ev.preventDefault(); - - var formVals = this.getRegFormVals(); - this.savedParams = formVals; - - var badFields = {}; - if (formVals.password != formVals.confirmPassword) { - badFields.confirmPassword = this.FieldErrors.PasswordMismatch; - } - if (formVals.password == '') { - badFields.password = this.FieldErrors.Missing; - } else if (formVals.password.length < 6) { - badFields.password = this.FieldErrors.Length; - } - if (formVals.username == '') { - badFields.username = this.FieldErrors.Missing; - } - if (formVals.email == '') { - badFields.email = this.FieldErrors.Missing; - } - if (Object.keys(badFields).length > 0) { - this.onBadFields(badFields); - return; - } - - MatrixClientPeg.replaceUsingUrls( - this.getHsUrl(), - this.getIsUrl() - ); - this.setState({ - hs_url: this.getHsUrl(), - is_url: this.getIsUrl() - }); - this.setState({busy: true}); - - this.tryRegister(); - }, - - startStage: function(stageName) { - var self = this; - this.setStep('stage_'+stageName); - switch(stageName) { - case 'm.login.email.identity': - self.setState({ - busy: true - }); - var cli = MatrixClientPeg.get(); - this.savedParams.client_secret = cli.generateClientSecret(); - this.savedParams.send_attempt = 1; - - var nextLink = this.props.registrationUrl + - '?client_secret=' + - encodeURIComponent(this.savedParams.client_secret) + - "&hs_url=" + - encodeURIComponent(this.state.hs_url) + - "&is_url=" + - encodeURIComponent(this.state.is_url) + - "&session_id=" + - encodeURIComponent(this.authSessionId); - - cli.requestEmailToken( - this.savedParams.email, - this.savedParams.client_secret, - this.savedParams.send_attempt, - nextLink - ).done(function(response) { - self.setState({ - busy: false, - }); - self.setStep('stage_m.login.email.identity'); - }, function(error) { - console.error(error); - self.setStep('initial'); - var newState = {busy: false}; - if (error.errcode == 'THREEPID_IN_USE') { - self.onBadFields({email: self.FieldErrors.InUse}); - } else { - newState.errorText = 'Unable to contact the given identity server'; - } - self.setState(newState); - }); - break; - case 'm.login.recaptcha': - if (!this.authParams || !this.authParams['m.login.recaptcha'].public_key) { - this.setState({ - errorText: "This server has not supplied enough information for Recaptcha authentication" - }); - } - break; - } - }, - - onRegistered: function(user_id, access_token) { - MatrixClientPeg.replaceUsingAccessToken( - this.state.hs_url, this.state.is_url, user_id, access_token - ); - if (this.props.onLoggedIn) { - this.props.onLoggedIn(); - } - }, - - onCaptchaLoaded: function() { - if (this.refs.recaptchaContainer) { - var sitekey = this.authParams['m.login.recaptcha'].public_key; - global.grecaptcha.render('mx_recaptcha', { - 'sitekey': sitekey, - 'callback': this.onCaptchaDone - }); - } - }, - - onCaptchaDone: function(captcha_response) { - this.tryRegister({ - type: 'm.login.recaptcha', - response: captcha_response - }); - }, - - tryRegister: function(auth) { - var self = this; - MatrixClientPeg.get().register( - this.savedParams.username, - this.savedParams.password, - this.authSessionId, - auth - ).done(function(result) { - self.onRegistered(result.user_id, result.access_token); - }, function(error) { - if (error.httpStatus == 401 && error.data.flows) { - self.authParams = error.data.params; - self.authSessionId = error.data.session; - - self.completedStages = error.data.completed || []; - - var flow = self.chooseFlow(error.data.flows); - - if (flow) { - var flowStage = self.firstUncompletedStageIndex(flow); - var numDone = self.numCompletedStages(flow); - - self.setState({ - busy: false, - flows: flow, - currentStep: 1+numDone, - totalSteps: flow.stages.length+1, - flowStage: flowStage - }); - self.startStage(flow.stages[flowStage]); - } - else { - self.setState({ - busy: false, - errorText: "Unable to register - missing email address?" - }); - } - } else { - console.log(error); - self.setStep("initial"); - var newState = { - busy: false, - errorText: "Unable to contact the given Home Server" - }; - if (error.name == 'M_USER_IN_USE') { - delete newState.errorText; - self.onBadFields({ - username: self.FieldErrors.InUse - }); - } else if (error.httpStatus == 401) { - newState.errorText = "Authorisation failed!"; - } else if (error.httpStatus >= 400 && error.httpStatus < 500) { - newState.errorText = "Registration failed!"; - } else if (error.httpStatus >= 500 && error.httpStatus < 600) { - newState.errorText = "Server error during registration!"; - } else if (error.name == "M_MISSING_PARAM") { - // The HS hasn't remembered the login params from - // the first try when the login email was sent. - newState.errorText = "This home server does not support resuming registration."; - } - self.setState(newState); - } - }); - }, - - showLogin: function(ev) { - ev.preventDefault(); - dis.dispatch({ - action: 'start_login' - }); - } -};