Commit a1290818 authored by Yadd's avatar Yadd
Browse files

Full TOTP (#1359)

parent 10c06332
Pipeline #826 passed with stage
in 2 minutes and 15 seconds
......@@ -243,6 +243,7 @@ sub defaultValues {
'timeoutActivity' => 0,
'timeoutActivityInterval' => 60,
'totp2fActivation' => 0,
'totp2fDigits' => 6,
'totp2fInterval' => 30,
'totp2fRange' => 1,
'trustedProxies' => '',
......
......@@ -12,14 +12,14 @@ our $VERSION = '2.0.0';
# Verify that TOTP $code match with $secret
sub verifyCode {
my ( $self, $interval, $range, $secret, $code ) = @_;
my ( $self, $interval, $range, $digits, $secret, $code ) = @_;
my $s = eval { decode_base32($secret) };
if ($@) {
$self->logger->error('Bad characters in TOTP secret');
return -1;
}
for ( 0 .. $range ) {
if ( $code eq $self->_code( $s, $_, $interval ) ) {
if ( $code eq $self->_code( $s, $_, $interval, $digits ) ) {
return 1;
}
}
......@@ -28,7 +28,8 @@ sub verifyCode {
# Internal subroutine that calculates TOTP code using $secret and $interval
sub _code {
my ( $self, $secret, $r, $interval ) = @_;
my ( $self, $secret, $r, $interval, $digits ) = @_;
$digits ||= 6;
my $hmac = hmac_sha1_hex(
pack( 'H*',
sprintf( '%016x', int( ( time + $r * $interval ) / $interval ) ) ),
......@@ -36,7 +37,7 @@ sub _code {
);
return sprintf(
'%06d',
'%0' . $digits . 'd',
(
hex( substr( $hmac, hex( substr( $hmac, -1 ) ) * 2, 8 ) ) &
0x7fffffff
......
......@@ -3130,10 +3130,17 @@ qr/^(?:(?:(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][-a-
'totp2fAuthnLevel' => {
'type' => 'int'
},
'totp2fDigits' => {
'default' => 6,
'type' => 'int'
},
'totp2fInterval' => {
'default' => 30,
'type' => 'int'
},
'totp2fIssuer' => {
'type' => 'text'
},
'totp2fRange' => {
'default' => 1,
'type' => 'int'
......
......@@ -1046,6 +1046,10 @@ sub attributes {
documentation =>
'Authentication level for users authentified by password+TOTP'
},
totp2fIssuer => {
type => 'text',
documentation => 'TOTP Issuer',
},
totp2fInterval => {
type => 'int',
default => 30,
......@@ -1056,6 +1060,11 @@ sub attributes {
default => 1,
documentation => 'TOTP range (number of interval to test)',
},
totp2fDigits => {
type => 'int',
default => 6,
documentation => 'Number of digits for TOTP code',
},
# External second factor
ext2fActivation => {
......
......@@ -664,8 +664,9 @@ sub tree {
form => 'simpleInputContainer',
nodes => [
'totp2fActivation', 'totpSelfRegistration',
'totp2fAuthnLevel', 'totp2fInterval',
'totp2fRange',
'totp2fAuthnLevel', 'totp2fIssuer',
'totp2fInterval', 'totp2fRange',
'totp2fDigits',
]
},
{
......
......@@ -703,7 +703,9 @@
"totp":"TOTP",
"totp2fActivation":"Activation",
"totp2fAuthnLevel":"TOTP authentication level",
"totp2fDigits":"Number of digits",
"totp2fInterval":"Interval",
"totp2fIssuer":"TOTP Issuer name",
"totp2fRange":"Range of attempts",
"totpSelfRegistration":"Self registration",
"trustedDomains": "النطاقات الموثوق بها",
......
......@@ -703,7 +703,9 @@
"totp":"TOTP",
"totp2fActivation":"Activation",
"totp2fAuthnLevel":"TOTP authentication level",
"totp2fDigits":"Number of digits",
"totp2fInterval":"Interval",
"totp2fIssuer":"TOTP Issuer name",
"totp2fRange":"Range of attempts",
"totpSelfRegistration":"Self registration",
"trustedDomains": "Trusted domains",
......
......@@ -702,8 +702,10 @@
"tokenUseGlobalStorage": "Utiliser le cache global",
"totp":"TOTP",
"totp2fActivation":"Activation",
"totp2fAuthnLevel":"Niceau d'authentification TOTP",
"totp2fAuthnLevel":"Niveau d'authentification TOTP",
"totp2fDigits":"Nombre de chiffres",
"totp2fInterval":"Intervalle",
"totp2fIssuer":"Nom du fournisseur TOTP",
"totp2fRange":"Nombre d'intervalles à tester",
"totpSelfRegistration":"Auto-enregistrement",
"trustedDomains": "Domaines approuvés",
......
......@@ -703,7 +703,9 @@
"totp":"TOTP",
"totp2fActivation":"Activation",
"totp2fAuthnLevel":"TOTP authentication level",
"totp2fDigits":"Number of digits",
"totp2fInterval":"Interval",
"totp2fIssuer":"TOTP Issuer name",
"totp2fRange":"Range of attempts",
"totpSelfRegistration":"Self registration",
"trustedDomains": "Domini attendibili",
......
......@@ -703,7 +703,9 @@
"totp":"TOTP",
"totp2fActivation":"Activation",
"totp2fAuthnLevel":"TOTP authentication level",
"totp2fDigits":"Number of digits",
"totp2fInterval":"Interval",
"totp2fIssuer":"TOTP Issuer name",
"totp2fRange":"Range of attempts",
"totpSelfRegistration":"Self registration",
"trustedDomains": "Miền tin cậy",
......
......@@ -71,6 +71,7 @@ sub selfRegister {
my $r = $self->verifyCode(
$self->conf->{totp2fInterval},
$self->conf->{totp2fRange},
$self->conf->{totp2fDigits},
$token->{_totp2fSecret}, $code
);
if ( $r == -1 ) {
......@@ -114,18 +115,23 @@ sub selfRegister {
}
);
my $portal = $self->conf->{portal};
$portal =~ s#^https?://([^/:]+).*$#$1#;
my $issuer;
unless ( $issuer = $self->conf->{totp2fIssuer} ) {
$issuer = $self->conf->{portal};
$issuer =~ s#^https?://([^/:]+).*$#$1#;
}
# QR-code will be generated by a javascript, here we just send data
return $self->p->sendJSONresponse(
$req,
{
secret => $secret,
token => $token,
portal => $portal,
user => $user,
newkey => $nk,
secret => $secret,
token => $token,
portal => $issuer,
user => $user,
newkey => $nk,
digits => $self->conf->{totp2fDigits},
interval => $self->conf->{totp2fInterval}
}
);
}
......
......@@ -67,6 +67,7 @@ sub verify {
my $r = $self->verifyCode(
$self->conf->{totp2fInterval},
$self->conf->{totp2fRange},
$self->conf->{totp2fDigits},
$session->{_totp2fSecret}, $code
);
if ( $r == -1 ) { return PE_ERROR; }
......
......@@ -31,6 +31,10 @@ getKey = (reset) ->
success: (data) ->
# Generate OTP url
s = "otpauth://totp/#{escape(data.portal)}:#{escape(data.user)}?secret=#{data.secret}&issuer=#{escape(data.portal)}"
if data.digits != 6
s += "&digits=#{data.digits}"
if data.interval != 30
s += "&period=#{data.interval}"
# Generate QR code
qr = new QRious
element: document.getElementById('qr'),
......
......@@ -42,6 +42,12 @@ LemonLDAP::NG TOTP registration script
success: function(data) {
var qr, s;
s = "otpauth://totp/" + (escape(data.portal)) + ":" + (escape(data.user)) + "?secret=" + data.secret + "&issuer=" + (escape(data.portal));
if (data.digits !== 6) {
s += "&digits=" + data.digits;
}
if (data.interval !== 30) {
s += "&period=" + data.interval;
}
qr = new QRious({
element: document.getElementById('qr'),
value: s,
......
(function(){var a,b,d,c,e;d=function(f,g){$("#msg").html(window.translate(f));$("#color").removeClass("message-positive message-warning alert-success alert-warning");$("#color").addClass("message-"+g);if(g==="positive"){g="success"}return $("#color").addClass("alert-"+g)};a=function(g,f,i){var h;console.log("Error",i);h=JSON.parse(g.responseText);if(h&&h.error){h=h.error.replace(/.* /,"");console.log("Returned error",h);return d(h,"warning")}};c="";b=function(f){return $.ajax({type:"POST",url:portal+"/totpregister/getkey",dataType:"json",data:{newkey:f},error:a,success:function(i){var g,h;h="otpauth://totp/"+(escape(i.portal))+":"+(escape(i.user))+"?secret="+i.secret+"&issuer="+(escape(i.portal));g=new QRious({element:document.getElementById("qr"),value:h,size:150});$("#serialized").text(h);if(i.newkey){d("yourNewTotpKey","warning")}else{d("yourTotpKey","success")}return c=i.token}})};e=function(){var f;f=$("#code").val();if(!f){return d("fillTheForm","danger")}else{return $.ajax({type:"POST",url:portal+"/totpregister/verify",dataType:"json",data:{token:c,code:f},error:a,success:function(g){if(g.error){if(g.error.match(/badCode/)){return d("badCode","warning")}else{return d(g.error,"danger")}}else{return d("yourKeyIsRegistered","success")}}})}};$(document).ready(function(){b(0);$("#changekey").on("click",function(){return b(1)});return $("#verify").on("click",function(){return e()})})}).call(this);
\ No newline at end of file
(function(){var a,b,d,c,e;d=function(f,g){$("#msg").html(window.translate(f));$("#color").removeClass("message-positive message-warning alert-success alert-warning");$("#color").addClass("message-"+g);if(g==="positive"){g="success"}return $("#color").addClass("alert-"+g)};a=function(g,f,i){var h;console.log("Error",i);h=JSON.parse(g.responseText);if(h&&h.error){h=h.error.replace(/.* /,"");console.log("Returned error",h);return d(h,"warning")}};c="";b=function(f){return $.ajax({type:"POST",url:portal+"/totpregister/getkey",dataType:"json",data:{newkey:f},error:a,success:function(i){var g,h;h="otpauth://totp/"+(escape(i.portal))+":"+(escape(i.user))+"?secret="+i.secret+"&issuer="+(escape(i.portal));if(i.digits!==6){h+="&digits="+i.digits}if(i.interval!==30){h+="&period="+i.interval}g=new QRious({element:document.getElementById("qr"),value:h,size:150});$("#serialized").text(h);if(i.newkey){d("yourNewTotpKey","warning")}else{d("yourTotpKey","success")}return c=i.token}})};e=function(){var f;f=$("#code").val();if(!f){return d("fillTheForm","danger")}else{return $.ajax({type:"POST",url:portal+"/totpregister/verify",dataType:"json",data:{token:c,code:f},error:a,success:function(g){if(g.error){if(g.error.match(/badCode/)){return d("badCode","warning")}else{return d(g.error,"danger")}}else{return d("yourKeyIsRegistered","success")}}})}};$(document).ready(function(){b(0);$("#changekey").on("click",function(){return b(1)});return $("#verify").on("click",function(){return e()})})}).call(this);
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment