452 lines
15 KiB
JavaScript
452 lines
15 KiB
JavaScript
"use strict";
|
|
|
|
import Matrix from "matrix-js-sdk";
|
|
|
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
|
var SignupStages = require("./SignupStages");
|
|
var dis = require("./dispatcher");
|
|
var q = require("q");
|
|
var url = require("url");
|
|
|
|
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, opts) {
|
|
this._hsUrl = hsUrl;
|
|
this._isUrl = isUrl;
|
|
this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
|
}
|
|
|
|
getHomeserverUrl() {
|
|
return this._hsUrl;
|
|
}
|
|
|
|
getIdentityServerUrl() {
|
|
return this._isUrl;
|
|
}
|
|
|
|
setHomeserverUrl(hsUrl) {
|
|
this._hsUrl = hsUrl;
|
|
}
|
|
|
|
setIdentityServerUrl(isUrl) {
|
|
this._isUrl = isUrl;
|
|
}
|
|
|
|
/**
|
|
* Get a temporary MatrixClient, which can be used for login or register
|
|
* requests.
|
|
*/
|
|
_createTemporaryClient() {
|
|
return Matrix.createClient({
|
|
baseUrl: this._hsUrl,
|
|
idBaseUrl: this._isUrl,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registration logic class
|
|
* This exists for the lifetime of a user's attempt to register an account,
|
|
* so if their registration attempt fails for whatever reason and they
|
|
* try again, call register() on the same instance again.
|
|
*
|
|
* TODO: parts of this overlap heavily with InteractiveAuth in the js-sdk. It
|
|
* would be nice to make use of that rather than rolling our own version of it.
|
|
*/
|
|
class Register extends Signup {
|
|
constructor(hsUrl, isUrl, opts) {
|
|
super(hsUrl, isUrl, opts);
|
|
this.setStep("START");
|
|
this.data = null; // 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;
|
|
// 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) {
|
|
this.params.clientSecret = secret;
|
|
}
|
|
|
|
setSessionId(sessionId) {
|
|
this.params.sessionId = sessionId;
|
|
}
|
|
|
|
setRegistrationUrl(regUrl) {
|
|
this.params.registrationUrl = regUrl;
|
|
}
|
|
|
|
setIdSid(idSid) {
|
|
this.params.idSid = idSid;
|
|
}
|
|
|
|
setGuestAccessToken(token) {
|
|
this.guestAccessToken = token;
|
|
}
|
|
|
|
getStep() {
|
|
return this._step;
|
|
}
|
|
|
|
getCredentials() {
|
|
return this.credentials;
|
|
}
|
|
|
|
getServerData() {
|
|
return this.data || {};
|
|
}
|
|
|
|
getPromise() {
|
|
return this.registrationPromise;
|
|
}
|
|
|
|
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"
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Starts the registration process from the first stage
|
|
*/
|
|
register(formVals) {
|
|
var {username, password, email} = formVals;
|
|
this.email = email;
|
|
this.username = username;
|
|
this.password = password;
|
|
const client = this._createTemporaryClient();
|
|
this.activeStage = null;
|
|
|
|
// If there hasn't been a client secret set by this point,
|
|
// generate one for this session. It will only be used if
|
|
// we do email verification, but far simpler to just make
|
|
// sure we have one.
|
|
// We re-use this same secret over multiple calls to register
|
|
// so that the identity server can honour the sendAttempt
|
|
// parameter and not re-send email unless we actually want
|
|
// another mail to be sent.
|
|
if (!this.params.clientSecret) {
|
|
this.params.clientSecret = client.generateClientSecret();
|
|
}
|
|
return this._tryRegister(client);
|
|
}
|
|
|
|
_tryRegister(client, authDict, poll_for_success) {
|
|
var self = this;
|
|
|
|
var bindEmail;
|
|
|
|
if (this.username && this.password) {
|
|
// only need to bind_email when sending u/p - sending it at other
|
|
// times clobbers the u/p resulting in M_MISSING_PARAM (password)
|
|
bindEmail = true;
|
|
}
|
|
|
|
// TODO need to figure out how to send the device display name to /register.
|
|
return client.register(
|
|
this.username, this.password, this.params.sessionId, authDict, bindEmail,
|
|
this.guestAccessToken
|
|
).then(function(result) {
|
|
self.credentials = result;
|
|
self.setStep("COMPLETE");
|
|
return result; // contains the credentials
|
|
}, function(error) {
|
|
if (error.httpStatus === 401) {
|
|
if (error.data && error.data.flows) {
|
|
// Remember the session ID from the server:
|
|
// Either this is our first 401 in which case we need to store the
|
|
// session ID for future calls, or it isn't in which case this
|
|
// is just a no-op since it ought to be the same (or if it isn't,
|
|
// we should use the latest one from the server in any case).
|
|
self.params.sessionId = error.data.session;
|
|
self.data = error.data || {};
|
|
var flow = self.chooseFlow(error.data.flows);
|
|
|
|
if (flow) {
|
|
console.log("Active flow => %s", JSON.stringify(flow));
|
|
var flowStage = self.firstUncompletedStage(flow);
|
|
if (!self.activeStage || flowStage != self.activeStage.type) {
|
|
return self._startStage(client, flowStage).catch(function(err) {
|
|
self.setStep('START');
|
|
throw err;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (poll_for_success) {
|
|
return q.delay(2000).then(function() {
|
|
return self._tryRegister(client, authDict, poll_for_success);
|
|
});
|
|
} else {
|
|
throw new Error("Authorisation failed!");
|
|
}
|
|
} else {
|
|
if (error.errcode === 'M_USER_IN_USE') {
|
|
throw new Error("Username in use");
|
|
} else if (error.errcode == 'M_INVALID_USERNAME') {
|
|
throw new Error("User names may only contain alphanumeric characters, underscores or dots!");
|
|
} 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."
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
firstUncompletedStage(flow) {
|
|
for (var i = 0; i < flow.stages.length; ++i) {
|
|
if (!this.hasCompletedStage(flow.stages[i])) {
|
|
return flow.stages[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
hasCompletedStage(stageType) {
|
|
var completed = (this.data || {}).completed || [];
|
|
return completed.indexOf(stageType) !== -1;
|
|
}
|
|
|
|
_startStage(client, 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(client, this);
|
|
this.activeStage = stage;
|
|
return stage.complete().then(function(request) {
|
|
if (request.auth) {
|
|
console.log("Stage %s is returning an auth dict", stageName);
|
|
return self._tryRegister(client, request.auth, request.poll_for_success);
|
|
}
|
|
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.
|
|
console.log("Waiting for external action.");
|
|
return q.defer().promise;
|
|
}
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
recheckState() {
|
|
// 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) {
|
|
const client = this._createTemporaryClient();
|
|
this.registrationPromise = this._startStage(client, EMAIL_STAGE_TYPE);
|
|
}
|
|
return this.registrationPromise;
|
|
}
|
|
|
|
tellStage(stageName, data) {
|
|
if (this.activeStage && this.activeStage.type === stageName) {
|
|
console.log("Telling stage %s about something..", stageName);
|
|
this.activeStage.onReceiveData(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class Login extends Signup {
|
|
constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
|
|
super(hsUrl, isUrl, opts);
|
|
this._fallbackHsUrl = fallbackHsUrl;
|
|
this._currentFlowIndex = 0;
|
|
this._flows = [];
|
|
}
|
|
|
|
getFlows() {
|
|
var self = this;
|
|
var client = this._createTemporaryClient();
|
|
return client.loginFlows().then(function(result) {
|
|
self._flows = result.flows;
|
|
self._currentFlowIndex = 0;
|
|
// technically the UI should display options for all flows for the
|
|
// user to then choose one, so return all the flows here.
|
|
return self._flows;
|
|
});
|
|
}
|
|
|
|
chooseFlow(flowIndex) {
|
|
this._currentFlowIndex = flowIndex;
|
|
}
|
|
|
|
getCurrentFlowStep() {
|
|
// technically the flow can have multiple steps, but no one does this
|
|
// for login so we can ignore it.
|
|
var flowStep = this._flows[this._currentFlowIndex];
|
|
return flowStep ? flowStep.type : null;
|
|
}
|
|
|
|
loginAsGuest() {
|
|
var client = this._createTemporaryClient();
|
|
return client.registerGuest({
|
|
body: {
|
|
initial_device_display_name: this._defaultDeviceDisplayName,
|
|
},
|
|
}).then((creds) => {
|
|
return {
|
|
userId: creds.user_id,
|
|
deviceId: creds.device_id,
|
|
accessToken: creds.access_token,
|
|
homeserverUrl: this._hsUrl,
|
|
identityServerUrl: this._isUrl,
|
|
guest: true
|
|
};
|
|
}, (error) => {
|
|
if (error.httpStatus === 403) {
|
|
error.friendlyText = "Guest access is disabled on this Home Server.";
|
|
} else {
|
|
error.friendlyText = "Failed to register as guest: " + error.data;
|
|
}
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
loginViaPassword(username, pass) {
|
|
var self = this;
|
|
var isEmail = username.indexOf("@") > 0;
|
|
var loginParams = {
|
|
password: pass,
|
|
initial_device_display_name: this._defaultDeviceDisplayName,
|
|
};
|
|
if (isEmail) {
|
|
loginParams.medium = 'email';
|
|
loginParams.address = username;
|
|
} else {
|
|
loginParams.user = username;
|
|
}
|
|
|
|
var client = this._createTemporaryClient();
|
|
return client.login('m.login.password', loginParams).then(function(data) {
|
|
return q({
|
|
homeserverUrl: self._hsUrl,
|
|
identityServerUrl: self._isUrl,
|
|
userId: data.user_id,
|
|
deviceId: data.device_id,
|
|
accessToken: data.access_token
|
|
});
|
|
}, function(error) {
|
|
if (error.httpStatus == 400 && loginParams.medium) {
|
|
error.friendlyText = (
|
|
'This Home Server does not support login using email address.'
|
|
);
|
|
}
|
|
else if (error.httpStatus === 403) {
|
|
error.friendlyText = (
|
|
'Incorrect username and/or password.'
|
|
);
|
|
if (self._fallbackHsUrl) {
|
|
var fbClient = Matrix.createClient({
|
|
baseUrl: self._fallbackHsUrl,
|
|
idBaseUrl: this._isUrl,
|
|
});
|
|
|
|
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
|
return q({
|
|
homeserverUrl: self._fallbackHsUrl,
|
|
identityServerUrl: self._isUrl,
|
|
userId: data.user_id,
|
|
deviceId: data.device_id,
|
|
accessToken: data.access_token
|
|
});
|
|
}, function(fallback_error) {
|
|
// throw the original error
|
|
throw error;
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
error.friendlyText = (
|
|
'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
|
|
);
|
|
}
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
redirectToCas() {
|
|
var client = this._createTemporaryClient();
|
|
var parsedUrl = url.parse(window.location.href, true);
|
|
parsedUrl.query["homeserver"] = client.getHomeserverUrl();
|
|
parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
|
|
var casUrl = client.getCasLoginUrl(url.format(parsedUrl));
|
|
window.location.href = casUrl;
|
|
}
|
|
}
|
|
|
|
module.exports.Register = Register;
|
|
module.exports.Login = Login;
|