Skip to content

Commit

Permalink
adds 2FA in front-end (#3264)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcrespov authored Aug 16, 2022
1 parent 339596a commit b17ac3e
Show file tree
Hide file tree
Showing 8 changed files with 440 additions and 28 deletions.
33 changes: 33 additions & 0 deletions services/web/client/source/class/osparc/auth/LoginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,17 @@ qx.Class.define("osparc.auth.LoginPage", {

const login = new osparc.auth.ui.LoginView();
const register = new osparc.auth.ui.RegistrationView();
const verifyPhoneNumber = new osparc.auth.ui.VerifyPhoneNumberView();
const resetRequest = new osparc.auth.ui.ResetPassRequestView();
const reset = new osparc.auth.ui.ResetPassView();
const loginSMSCode = new osparc.auth.ui.LoginSMSCodeView();

pages.add(login);
pages.add(register);
pages.add(verifyPhoneNumber);
pages.add(resetRequest);
pages.add(reset);
pages.add(loginSMSCode);

const page = osparc.auth.core.Utils.findParameterInFragment("page");
const code = osparc.auth.core.Utils.findParameterInFragment("code");
Expand Down Expand Up @@ -157,11 +161,40 @@ qx.Class.define("osparc.auth.LoginPage", {
login.resetValues();
}, this);

login.addListener("toVerifyPhone", e => {
verifyPhoneNumber.set({
userEmail: e.getData()
});
pages.setSelection([verifyPhoneNumber]);
login.resetValues();
}, this);

login.addListener("toSMSCode", e => {
const msg = e.getData();
const startIdx = msg.indexOf("+");
loginSMSCode.set({
userEmail: login.getEmail(),
userPhoneNumber: msg.substring(startIdx, msg.length)
});
pages.setSelection([loginSMSCode]);
login.resetValues();
}, this);

loginSMSCode.addListener("done", msg => {
login.resetValues();
this.fireDataEvent("done", msg);
}, this);

register.addListener("done", msg => {
osparc.utils.Utils.cookie.deleteCookie("user");
this.fireDataEvent("done", msg);
});

verifyPhoneNumber.addListener("done", msg => {
login.resetValues();
this.fireDataEvent("done", msg);
}, this);

[resetRequest, reset].forEach(srcPage => {
srcPage.addListener("done", msg => {
pages.setSelection([login]);
Expand Down
94 changes: 77 additions & 17 deletions services/web/client/source/class/osparc/auth/Manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,61 @@ qx.Class.define("osparc.auth.Manager", {
*/

members: {
register: function(userData) {
const params = {
data: userData
};
return osparc.data.Resources.fetch("auth", "postRegister", params);
},

verifyPhoneNumber: function(email, phoneNumber) {
const params = {
data: {
email,
phone: phoneNumber
}
};
return osparc.data.Resources.fetch("auth", "postVerifyPhoneNumber", params);
},

validateCodeRegister: function(email, phone, code, loginCbk, failCbk, context) {
const params = {
data: {
email,
phone,
code
}
};
osparc.data.Resources.fetch("auth", "postValidationCodeRegister", params)
.then(data => {
osparc.data.Resources.getOne("profile", {}, null, false)
.then(profile => {
this.__loginUser(profile);
loginCbk.call(context, data);
})
.catch(err => failCbk.call(context, err.message));
})
.catch(err => failCbk.call(context, err.message));
},

validateCodeLogin: function(email, code, loginCbk, failCbk, context) {
const params = {
data: {
email,
code
}
};
osparc.data.Resources.fetch("auth", "postValidationCodeLogin", params)
.then(data => {
osparc.data.Resources.getOne("profile", {}, null, false)
.then(profile => {
this.__loginUser(profile);
loginCbk.call(context, data);
})
.catch(err => failCbk.call(context, err.message));
})
.catch(err => failCbk.call(context, err.message));
},

isLoggedIn: function() {
// TODO: how to store this localy?? See http://www.qooxdoo.org/devel/pages/data_binding/stores.html#offline-store
Expand Down Expand Up @@ -73,23 +128,35 @@ qx.Class.define("osparc.auth.Manager", {
});
},

login: function(email, password, successCbk, failCbk, context) {
login: function(email, password, loginCbk, verifyPhoneCbk, twoFactorAuthCbk, failCbk, context) {
const params = {
data: {
email,
password
}
email,
password
};
osparc.data.Resources.fetch("auth", "postLogin", params)
.then(data => {
const url = osparc.data.Resources.resources["auth"].endpoints["postLogin"].url;
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 202) {
const resp = JSON.parse(xhr.responseText);
const data = resp.data;
if (data["code"] === "PHONE_NUMBER_REQUIRED") {
verifyPhoneCbk.call(context);
} else if (data["code"] === "SMS_CODE_REQUIRED") {
twoFactorAuthCbk.call(context, data["reason"]);
}
} else if (xhr.status === 200) {
const resp = JSON.parse(xhr.responseText);
osparc.data.Resources.getOne("profile", {}, null, false)
.then(profile => {
this.__loginUser(profile);
successCbk.call(context, data);
loginCbk.call(context, resp.data);
})
.catch(err => failCbk.call(context, err.message));
})
.catch(err => failCbk.call(context, err.message));
}
};
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(params));
},

logout: function() {
Expand All @@ -106,13 +173,6 @@ qx.Class.define("osparc.auth.Manager", {
.finally(this.__logoutUser());
},

register: function(userData) {
const params = {
data: userData
};
return osparc.data.Resources.fetch("auth", "postRegister", params);
},

resetPasswordRequest: function(email, successCbk, failCbk, context) {
const params = {
data: {
Expand Down
13 changes: 12 additions & 1 deletion services/web/client/source/class/osparc/auth/core/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ qx.Class.define("osparc.auth.core.Utils", {
return isValid;
},

// https://en.wikipedia.org/wiki/E.164 '^\+[1-9]\d{4,14}$'
phoneNumberValidator: function(phoneNumber, item) {
const regEx = /^\+[1-9]\d{4,14}$/;
const isValid = regEx.test(phoneNumber);
item.set({
invalidMessage: isValid ? "" : qx.locale.Manager.tr("Invalid phone number. Please, [+][country code][phone number plus area code]"),
valid: isValid
});
return isValid;
},

/** Finds parameters in the fragment
*
* Expected fragment format as https://osparc.io#page=reset-password;code=123546
Expand All @@ -51,7 +62,7 @@ qx.Class.define("osparc.auth.core.Utils", {
const params = window.location.hash.substr(1).split(";");
params.forEach(function(item) {
const tmp = item.split("=");
if (tmp[0] === parameterName) {
if (tmp[0].includes(parameterName)) {
result = decodeURIComponent(tmp[1]);
}
});
Expand Down
115 changes: 115 additions & 0 deletions services/web/client/source/class/osparc/auth/ui/LoginSMSCodeView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/* ************************************************************************
osparc - the simcore frontend
https://osparc.io
Copyright:
2022 IT'IS Foundation, https://itis.swiss
License:
MIT: https://opensource.org/licenses/MIT
Authors:
* Odei Maiz (odeimaz)
************************************************************************ */


qx.Class.define("osparc.auth.ui.LoginSMSCodeView", {
extend: osparc.auth.core.BaseAuthPage,

properties: {
userPhoneNumber: {
check: "String",
init: "+41-XXXXXXXXX",
nullable: false,
event: "changeUserPhoneNumber"
},

userEmail: {
check: "String",
init: "[email protected]",
nullable: false
}
},

members: {
__validateCodeTF: null,
__validateCodeBtn: null,
__resendCodeBtn: null,

_buildPage: function() {
const smsCodeDesc = new qx.ui.basic.Label();
this.bind("userPhoneNumber", smsCodeDesc, "value", {
converter: pNumber => this.tr("We just sent a 4-digit code to ") + pNumber
});
this.add(smsCodeDesc);

const validateCodeTF = this.__validateCodeTF = new qx.ui.form.TextField().set({
placeholder: this.tr("Type code"),
required: true
});
this.add(validateCodeTF);
this.addListener("appear", () => {
validateCodeTF.focus();
validateCodeTF.activate();
osparc.auth.ui.VerifyPhoneNumberView.restartResendTimer(this.__resendCodeBtn, this.tr("Resend code"));
});

const validateCodeBtn = this.__validateCodeBtn = new osparc.ui.form.FetchButton(this.tr("Validate")).set({
center: true,
appearance: "strong-button"
});
validateCodeBtn.addListener("execute", () => this.__validateCodeLogin(), this);
this.add(validateCodeBtn);

const resendLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(5).set({
alignY: "middle"
}));
const resendCodeDesc = new qx.ui.basic.Label().set({
value: this.tr("Didn't receive the code?")
});
resendLayout.add(resendCodeDesc, {
flex: 1
});

this.add(new qx.ui.core.Spacer(null, 20));
const resendCodeBtn = this.__resendCodeBtn = new qx.ui.form.Button().set({
label: this.tr("Resend code") + ` (60)`,
enabled: false
});
resendLayout.add(resendCodeBtn, {
flex: 1
});
resendCodeBtn.addListener("execute", () => {
osparc.auth.ui.VerifyPhoneNumberView.restartResendTimer(this.__resendCodeBtn, this.tr("Resend code"));
}, this);
this.add(resendLayout);
},

__validateCodeLogin: function() {
this.__validateCodeBtn.setFetching(true);

const loginFun = log => {
this.__validateCodeBtn.setFetching(false);
this.fireDataEvent("done", log.message);
};

const failFun = msg => {
this.__validateCodeBtn.setFetching(false);
// TODO: can get field info from response here
msg = String(msg) || this.tr("Invalid code");
this.__validateCodeTF.set({
invalidMessage: msg,
valid: false
});

osparc.component.message.FlashMessenger.getInstance().logAs(msg, "ERROR");
};

const manager = osparc.auth.Manager.getInstance();
manager.validateCodeLogin(this.getUserEmail(), this.__validateCodeTF.getValue(), loginFun, failFun, this);
}
}
});
34 changes: 30 additions & 4 deletions services/web/client/source/class/osparc/auth/ui/LoginView.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ qx.Class.define("osparc.auth.ui.LoginView", {

events: {
"toRegister": "qx.event.type.Event",
"toReset": "qx.event.type.Event"
"toReset": "qx.event.type.Event",
"toVerifyPhone": "qx.event.type.Data",
"toSMSCode": "qx.event.type.Data"
},

/*
Expand Down Expand Up @@ -144,6 +146,11 @@ qx.Class.define("osparc.auth.ui.LoginView", {
return grp;
},

getEmail: function() {
const email = this.__form.getItems().email;
return email.getValue();
},

__login: function() {
if (!this.__form.validate()) {
return;
Expand All @@ -154,7 +161,7 @@ qx.Class.define("osparc.auth.ui.LoginView", {
const email = this.__form.getItems().email;
const pass = this.__form.getItems().password;

const successFun = function(log) {
const loginFun = function(log) {
this.__loginBtn.setFetching(false);
this.fireDataEvent("done", log.message);
// we don't need the form any more, so remove it and mock-navigate-away
Expand All @@ -163,7 +170,26 @@ qx.Class.define("osparc.auth.ui.LoginView", {
window.history.replaceState(null, window.document.title, window.location.pathname);
};

const failFun = function(msg) {
const verifyPhoneCbk = () => {
this.__loginBtn.setFetching(false);
this.fireDataEvent("toVerifyPhone", email.getValue());
// we don't need the form any more, so remove it and mock-navigate-away
// and thus tell the password manager to save the content
this._formElement.dispose();
window.history.replaceState(null, window.document.title, window.location.pathname);
};

const twoFactorAuthCbk = log => {
this.__loginBtn.setFetching(false);
osparc.component.message.FlashMessenger.getInstance().logAs(log, "INFO");
this.fireDataEvent("toSMSCode", log);
// we don't need the form any more, so remove it and mock-navigate-away
// and thus tell the password manager to save the content
this._formElement.dispose();
window.history.replaceState(null, window.document.title, window.location.pathname);
};

const failFun = msg => {
this.__loginBtn.setFetching(false);
// TODO: can get field info from response here
msg = String(msg) || this.tr("Typed an invalid email or password");
Expand All @@ -178,7 +204,7 @@ qx.Class.define("osparc.auth.ui.LoginView", {
};

const manager = osparc.auth.Manager.getInstance();
manager.login(email.getValue(), pass.getValue(), successFun, failFun, this);
manager.login(email.getValue(), pass.getValue(), loginFun, verifyPhoneCbk, twoFactorAuthCbk, failFun, this);
},

resetValues: function() {
Expand Down
Loading

0 comments on commit b17ac3e

Please sign in to comment.