Skip to content

Commit

Permalink
validate TOTP before activation
Browse files Browse the repository at this point in the history
  • Loading branch information
floriannari committed Feb 19, 2024
1 parent a8c6425 commit 0a8093a
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 107 deletions.
5 changes: 4 additions & 1 deletion properties/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"totp": {
"name": "Timed password (TOTP)",
"description": "This method generate 6 numerals codes",
"howTo": "Push 'Generate QrCode ', then scan the code using Esup Auth app (iOS and Android) or enter the code directly into the application"
"howTo": "Push 'Scan the code using Esup Auth app (iOS and Android) or enter the code directly into the application",
"howToActivate": "Then, to validate the TOTP activation, enter below the 6-digit code displayed on your application",
"enter_code": "Enter the 6-digit code"
},
"random_code": {
"name": "One time password",
Expand Down Expand Up @@ -62,6 +64,7 @@
"action":{
"connection" : "Login",
"activate" : "Activate",
"validate": "Validate",
"deactivate" : "Deactivate",
"verify" : "Verify",
"save" : "Save",
Expand Down
5 changes: 4 additions & 1 deletion properties/messages_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"totp": {
"name": "Timed password (TOTP)",
"description": "This method generate 6 numerals codes",
"howTo": "Push 'Generate QrCode ', then scan the code using your Esup Auth app or Google Authenticator app or enter the code directly into your TOTP application"
"howTo": "Scan the code using your Esup Auth app or Google Authenticator app or enter the code directly into your TOTP application",
"howToActivate": "Then, to validate the TOTP activation, enter below the 6-digit code displayed on your application",
"enter_code": "Enter the 6-digit code"
},
"random_code": {
"name": "One time password",
Expand Down Expand Up @@ -68,6 +70,7 @@
"action":{
"connection" : "Login",
"activate" : "Activate",
"validate": "Validate",
"deactivate" : "Deactivate",
"verify" : "Verify",
"save" : "Save",
Expand Down
5 changes: 4 additions & 1 deletion properties/messages_fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"totp": {
"name": "Code temporel (TOTP)",
"description": "Méthode permettant de générer des codes uniques de 6 chiffres.",
"howTo": "Appuyez sur le bouton 'Générer un QrCode', puis scannez le code à l'aide de votre application mobile Esup Auth ou Google Authentificator ou entrez le code directement dans votre application TOTP favori"
"howTo": "Scannez le code à l'aide de votre application mobile Esup Auth ou Google Authentificator ou entrez le code directement dans votre application TOTP favori",
"howToActivate": "Ensuite, pour valider l'activation du TOTP, saisissez ci-dessous le code à 6 chiffres affiché sur votre application",
"enter_code": "Saisissez le code de 6 chiffres"
},
"random_code": {
"name": "Code par SMS",
Expand Down Expand Up @@ -69,6 +71,7 @@
"action":{
"connection" : "Se connecter",
"activate" : "Activer",
"validate": "Valider",
"deactivate" : "Désactiver",
"verify" : "Vérifier",
"save" : "Enregistrer",
Expand Down
197 changes: 109 additions & 88 deletions public/javascripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,103 +88,122 @@ var BypassMethod = Vue.extend({
template: '#bypass-method'
});

var TotpMethod = Vue.extend({
const TotpMethod = Vue.extend({
props: {
'user': Object,
'generate_totp': Function,
'activate': Function,
'deactivate': Function,
'messages': Object,
'formatApiUrl': Function,
},
methods: {
validate: function() {
const totpCode = this.user.methods.totp.validation_code;
this.user.methods.totp.validation_code = '';
$.ajax({
method: "POST",
url: this.formatApiUrl("totp/activate/confirm/" + totpCode),
dataType: 'json',
cache: false,
success: function (data) {
if (data.code != "Ok") {
Materialize.toast('Erreur, veuillez réessayer.', 3000, 'red darken-1');
} else {
this.user.methods.totp.active = true;
this.user.methods.totp.qrCode = '';
this.user.methods.totp.message = '';
Materialize.toast('Code validé', 3000, 'green darken-1');
}
}.bind(this),
error: function (xhr, status, err) {
console.error("/api/totp/activate/confirm", status, err.toString());
}.bind(this)
});
}
},
template: '#totp-method'
});

function randomCodeMethod(isManagerDashboard) {
const urlPrefix = '/api/' + (isManagerDashboard ? 'admin/' : '');
function urlSufix(context) {
return isManagerDashboard ? '/' + context.user.uid : '';
}
return Vue.extend({
props: {
'user': Object,
'messages': Object,
'activate': Function,
'deactivate': Function,
},
methods: {
saveTransport: function(transport) {
var new_transport = document.getElementById(transport + '-input').value;
var reg;
if (transport == 'sms') reg = new RegExp("^0[6-7]([-. ]?[0-9]{2}){4}$");
else reg = new RegExp(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/);
if (reg.test(new_transport)) {
var oldTransport = this.user.transports[transport];
this.user.transports[transport]= new_transport;
document.getElementById(transport + '-input').value = '';
$.ajax({
method: 'PUT',
url: urlPrefix + 'transport/' + transport + '/' + new_transport + urlSufix(this),
dataType: 'json',
cache: false,
success: function(data) {
if (data.code != "Ok") {
this.user.transports[transport]= oldTransport;
document.getElementById(transport + '-input').value = oldTransport;
Materialize.toast('Erreur interne, veuillez réessayer plus tard.', 3000, 'red darken-1');
}else Materialize.toast('Transport vérifié', 3000, 'green darken-1');
}.bind(this),
error: function(xhr, status, err) {
this.user.transports[transport]= oldTransport;
document.getElementById(transport + '-input').value = oldTransport;
Materialize.toast(err, 3000, 'red darken-1');
console.error('/api/transport/' + transport + '/' + new_transport, status, err.toString());
}.bind(this)
});
}else Materialize.toast('Format invalide.', 3000, 'red darken-1');
},
deleteTransport: function(transport) {
const RandomCodeMethod = Vue.extend({
props: {
'user': Object,
'messages': Object,
'activate': Function,
'deactivate': Function,
'formatApiUrl': Function,
},
methods: {
saveTransport: function(transport) {
var new_transport = document.getElementById(transport + '-input').value;
var reg;
if (transport == 'sms') reg = new RegExp("^0[6-7]([-. ]?[0-9]{2}){4}$");
else reg = new RegExp(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/);
if (reg.test(new_transport)) {
var oldTransport = this.user.transports[transport];
this.user.transports[transport]= null;
this.user.transports[transport]= new_transport;
document.getElementById(transport + '-input').value = '';
$.ajax({
method: 'DELETE',
url: urlPrefix + 'transport/' + transport + urlSufix(this),
method: 'PUT',
url: this.formatApiUrl('transport/' + transport + '/' + new_transport),
dataType: 'json',
cache: false,
success: function(data) {
if (data.code != "Ok") this.user.transports[transport]= oldTransport;
if (data.code != "Ok") {
this.user.transports[transport]= oldTransport;
document.getElementById(transport + '-input').value = oldTransport;
Materialize.toast('Erreur interne, veuillez réessayer plus tard.', 3000, 'red darken-1');
}else Materialize.toast('Transport vérifié', 3000, 'green darken-1');
}.bind(this),
error: function(xhr, status, err) {
this.user.transports[transport]= oldTransport;
document.getElementById(transport + '-input').value = oldTransport;
Materialize.toast(err, 3000, 'red darken-1');
console.error("/data/deactivate.json", status, err.toString());
console.error('/api/transport/' + transport + '/' + new_transport, status, err.toString());
}.bind(this)
});
},
testTransport: function(transport) {
$.ajax({
url: urlPrefix + 'transport/' + transport + '/test' + urlSufix(this),
dataType: 'json',
cache: false,
success: function(data) {
if (data.code != "Ok") Materialize.toast(data.message, 3000, 'red darken-1');
else Materialize.toast('Transport vérifié', 3000, 'green darken-1');
}.bind(this),
error: function(xhr, status, err) {
Materialize.toast(err, 3000, 'red darken-1');
console.error('/api/transport/' + transport + '/test', status, err.toString());
}.bind(this)
});
},
}else Materialize.toast('Format invalide.', 3000, 'red darken-1');
},
template: '#random_code-method'
});
}
deleteTransport: function(transport) {
var oldTransport = this.user.transports[transport];
this.user.transports[transport]= null;
$.ajax({
method: 'DELETE',
url: this.formatApiUrl('transport/' + transport),
dataType: 'json',
cache: false,
success: function(data) {
if (data.code != "Ok") this.user.transports[transport]= oldTransport;
}.bind(this),
error: function(xhr, status, err) {
this.user.transports[transport]= oldTransport;
Materialize.toast(err, 3000, 'red darken-1');
console.error("/data/deactivate.json", status, err.toString());
}.bind(this)
});
},
testTransport: function(transport) {
$.ajax({
url: this.formatApiUrl('transport/' + transport + '/test'),
dataType: 'json',
cache: false,
success: function(data) {
if (data.code != "Ok") Materialize.toast(data.message, 3000, 'red darken-1');
else Materialize.toast('Transport vérifié', 3000, 'green darken-1');
}.bind(this),
error: function(xhr, status, err) {
Materialize.toast(err, 3000, 'red darken-1');
console.error('/api/transport/' + transport + '/test', status, err.toString());
}.bind(this)
});
},
},
template: '#random_code-method'
});

function randomCodeMailMethod(isManagerDashboard) {
return randomCodeMethod(isManagerDashboard).extend({
template: '#random_code_mail-method'
});
}
const RandomCodeMailMethod = RandomCodeMethod.extend({
template: '#random_code_mail-method'
});

var Esupnfc = Vue.extend({
template:'#esupnfc-method'
Expand All @@ -202,14 +221,17 @@ var UserDashboard = Vue.extend({
"push": PushMethod,
"totp": TotpMethod,
"bypass": BypassMethod,
"random_code": randomCodeMethod(false),
"random_code_mail": randomCodeMailMethod(false),
"random_code": RandomCodeMethod,
"random_code_mail": RandomCodeMailMethod,
"esupnfc":Esupnfc
},
template: "#user-dashboard",
created: function () {
},
methods: {
formatApiUrl: function(url) {
return '/api/' + url;
},
activate: function (method) {
switch (method) {
case 'push':
Expand All @@ -228,10 +250,7 @@ var UserDashboard = Vue.extend({
this.standardActivate(method);
break;
case 'totp':
this.standardActivate(method);
this.generateTotp(function () {
this.user.methods.totp.active = false;
});
this.generateTotp();
break;
case 'esupnfc':
this.standardActivate(method);
Expand Down Expand Up @@ -327,11 +346,12 @@ var UserDashboard = Vue.extend({
generateTotp: function (onError) {
$.ajax({
method: "POST",
url: "/api/generate/totp",
url: "/api/generate/totp?require_method_validation=true",
dataType: 'json',
cache: false,
success: function (data) {
if (data.code == "Ok") {
this.user.methods.totp.active = true;
this.user.methods.totp.message = data.message;
this.user.methods.totp.qrCode = data.qrCode;
this.user.methods.totp.uid = data.uid;
Expand Down Expand Up @@ -359,8 +379,8 @@ var UserView = Vue.extend({
"push": PushMethod,
"totp": TotpMethod,
"bypass": BypassMethod,
"random_code": randomCodeMethod(true),
"random_code_mail": randomCodeMailMethod(true),
"random_code": RandomCodeMethod,
"random_code_mail": RandomCodeMailMethod,
"esupnfc":Esupnfc
},
data: function () {
Expand All @@ -370,6 +390,9 @@ var UserView = Vue.extend({
},
template: '#user-view',
methods: {
formatApiUrl: function(url) {
return '/api/admin/' + url + '/' + this.user.uid;
},
activate: function (method) {
switch (method) {
case 'push':
Expand All @@ -388,10 +411,7 @@ var UserView = Vue.extend({
this.standardActivate(method);
break;
case 'totp':
this.standardActivate(method);
this.generateTotp(function () {
this.user.methods.totp.active = false;
});
this.generateTotp();
break;
case 'esupnfc':
this.standardActivate(method);
Expand Down Expand Up @@ -492,11 +512,12 @@ var UserView = Vue.extend({
generateTotp: function (onError) {
$.ajax({
method: "POST",
url: "/api/admin/generate/totp/" + this.user.uid,
url: "/api/admin/generate/totp/" + this.user.uid + "?require_method_validation=true",
dataType: 'json',
cache: false,
success: function (data) {
if (data.code == "Ok") {
this.user.methods.totp.active = true;
this.user.methods.totp.message = data.message;
this.user.methods.totp.qrCode = data.qrCode;
this.user.methods.totp.uid = data.uid;
Expand Down
26 changes: 24 additions & 2 deletions server/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,20 @@ function routing() {
relUrl: 'users/'+req.session.passport.user.uid+'/methods/'+req.params.method+'/activate/' + getHash(req),
});
});

router.post('/api/:method/activate/confirm/:activation_code', isUser, function(req, res) {
request_otp_api(req, res, {
method: 'POST',
relUrl: 'users/' + req.session.passport.user.uid + '/methods/' + req.params.method + '/activate/' + req.params.activation_code + '/' + getHash(req),
});
});

router.post('/api/admin/:method/activate/confirm/:activation_code/:uid', isManager, function(req, res) {
request_otp_api(req, res, {
method: 'POST',
relUrl: 'protected/users/' + req.params.uid + '/methods/' + req.params.method + '/activate/' + req.params.activation_code, bearerAuth: true
});
});

router.put('/api/:method/deactivate', isUser, function(req, res) {
request_otp_api(req, res, {
Expand Down Expand Up @@ -223,9 +237,13 @@ function routing() {
});

router.post('/api/generate/:method', isUser, function(req, res) {
var uri = 'users/'+ req.session.passport.user.uid + '/methods/' + req.params.method + '/secret/' + getHash(req);
if(req.query.require_method_validation === 'true') {
uri += '?require_method_validation=true';
}
request_otp_api(req, res, {
method: 'POST',
relUrl: 'users/'+ req.session.passport.user.uid + '/methods/' + req.params.method + '/secret/' + getHash(req),
relUrl: uri,
});
});

Expand Down Expand Up @@ -284,9 +302,13 @@ function routing() {
});

router.post('/api/admin/generate/:method/:uid', isManager, function(req, res) {
var uri = 'protected/users/'+ req.params.uid + '/methods/' + req.params.method + '/secret/';
if(req.query.require_method_validation === 'true') {
uri += '?require_method_validation=true';
}
request_otp_api(req, res, {
method: 'POST',
relUrl: 'protected/users/'+ req.params.uid + '/methods/' + req.params.method + '/secret/', bearerAuth: true,
relUrl: uri, bearerAuth: true,
});
});

Expand Down
Loading

0 comments on commit 0a8093a

Please sign in to comment.