TOTP self registration in progress (#1359)
This commit is contained in:
parent
d8dbf1a615
commit
560341ea51
|
@ -1,5 +1,8 @@
|
||||||
package Lemonldap::NG::Common::TOTP;
|
package Lemonldap::NG::Common::TOTP;
|
||||||
|
|
||||||
|
# This module is inspired by Auth::GoogleAuth written by Gryphon Shafer
|
||||||
|
# <gryphon@cpan.org>
|
||||||
|
|
||||||
use strict;
|
use strict;
|
||||||
use Mouse;
|
use Mouse;
|
||||||
use Convert::Base32 'decode_base32';
|
use Convert::Base32 'decode_base32';
|
||||||
|
@ -7,6 +10,7 @@ use Digest::HMAC_SHA1 'hmac_sha1_hex';
|
||||||
|
|
||||||
our $VERSION = '2.0.0';
|
our $VERSION = '2.0.0';
|
||||||
|
|
||||||
|
# Verify that TOTP $code match with $secret
|
||||||
sub verifyCode {
|
sub verifyCode {
|
||||||
my ( $self, $interval, $range, $secret, $code ) = @_;
|
my ( $self, $interval, $range, $secret, $code ) = @_;
|
||||||
my $s = eval { decode_base32($secret) };
|
my $s = eval { decode_base32($secret) };
|
||||||
|
@ -22,6 +26,7 @@ sub verifyCode {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Internal subroutine that calculates TOTP code using $secret and $interval
|
||||||
sub _code {
|
sub _code {
|
||||||
my ( $self, $secret, $r, $interval ) = @_;
|
my ( $self, $secret, $r, $interval ) = @_;
|
||||||
my $hmac = hmac_sha1_hex(
|
my $hmac = hmac_sha1_hex(
|
||||||
|
@ -39,4 +44,11 @@ sub _code {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Simply generate new base32 secret
|
||||||
|
sub newSecret {
|
||||||
|
my ($self) = @_;
|
||||||
|
my @chars = ( 'a' .. 'z', 2 .. 7 );
|
||||||
|
return join( '', @chars[ map { int( rand(32) ) } 1 .. 16 ] );
|
||||||
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
|
@ -6,7 +6,7 @@ use Mouse;
|
||||||
|
|
||||||
our $VERSION = '2.0.0';
|
our $VERSION = '2.0.0';
|
||||||
|
|
||||||
extends 'Lemonldap::NG::Common::Module', 'Lemonldap::NG::Common::TOTP';
|
extends 'Lemonldap::NG::Portal::Main::Plugin', 'Lemonldap::NG::Common::TOTP';
|
||||||
|
|
||||||
# INITIALIZATION
|
# INITIALIZATION
|
||||||
|
|
||||||
|
@ -23,14 +23,11 @@ has ott => (
|
||||||
|
|
||||||
sub init {
|
sub init {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
return 0 unless $self->SUPER::init;
|
$self->addAuthRoute(
|
||||||
if ( $self->conf->{totpSelfRegister} ) {
|
totpregister => { ':action' => 'selfRegister' },
|
||||||
$self->addAuthRoute(
|
['POST']
|
||||||
totpregister => { ':action' => 'selfRegister' },
|
);
|
||||||
['POST']
|
$self->addAuthRoute( 'totpregister.html' => undef, ['GET'] );
|
||||||
);
|
|
||||||
$self->addAuthRoute( 'totpregister.html' => undef, ['GET'] );
|
|
||||||
}
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +37,7 @@ sub selfRegister {
|
||||||
my $user = $req->userData->{ $self->conf->{whatToTrace} };
|
my $user = $req->userData->{ $self->conf->{whatToTrace} };
|
||||||
unless ($user) {
|
unless ($user) {
|
||||||
return $self->p->sendError( $req,
|
return $self->p->sendError( $req,
|
||||||
'No ' . $self->conf->{whatToTrace} . ' found in user datas', 500 );
|
'No ' . $self->conf->{whatToTrace} . ' found in user data', 500 );
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verification that user has a valid TOTP app
|
# Verification that user has a valid TOTP app
|
||||||
|
@ -89,15 +86,48 @@ sub selfRegister {
|
||||||
}
|
}
|
||||||
$self->logger->debug('TOTP code verified');
|
$self->logger->debug('TOTP code verified');
|
||||||
|
|
||||||
# Now code is verified, let's store the master key in persistent datas
|
# Now code is verified, let's store the master key in persistent data
|
||||||
$self->p->updatePersistentSession( $req,
|
$self->p->updatePersistentSession( $req,
|
||||||
{ _totp2fSecret => $token->{_totp2fSecret} } );
|
{ _totp2fSecret => $token->{_totp2fSecret} } );
|
||||||
$self->userLogger->logger('TOTP registration succeed');
|
$self->userLogger->logger('TOTP registration succeed');
|
||||||
return [ 200, [ 'Content-Type' => 'application/json' ],
|
return [ 200, [ 'Content-Type' => 'application/json' ],
|
||||||
['{"result":1}'] ];
|
['{"result":1}'] ];
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get or generate master key
|
# Get or generate master key
|
||||||
elsif($action eq 'getkey') {
|
elsif ( $action eq 'getkey' ) {
|
||||||
|
my $nk = 0;
|
||||||
|
my $secret;
|
||||||
|
if ( $req->param('newkey') or not $req->userData->{_totp2fSecret} ) {
|
||||||
|
$secret = $self->newSecret;
|
||||||
|
$nk = 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$secret = $req->userData->{_totp2fSecret};
|
||||||
|
}
|
||||||
|
|
||||||
|
# Secret is stored in a token: we choose to not accept secret returned
|
||||||
|
# by Ajax request to avoid some attacks
|
||||||
|
my $token = $self->ott->createToken(
|
||||||
|
{
|
||||||
|
_totp2fSecret => $secret,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
my $portal = $self->conf->{portal};
|
||||||
|
$portal =~ 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,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
49
lemonldap-ng-portal/site/coffee/totpregistration.coffee
Normal file
49
lemonldap-ng-portal/site/coffee/totpregistration.coffee
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
###
|
||||||
|
LemonLDAP::NG TOTP registration script
|
||||||
|
###
|
||||||
|
|
||||||
|
setMsg = (msg, level) ->
|
||||||
|
$('#msg').html window.translate msg
|
||||||
|
$('#color').removeClass 'message-positive message-warning alert-success alert-warning'
|
||||||
|
$('#color').addClass "message-#{level}"
|
||||||
|
level = 'success' if level == 'positive'
|
||||||
|
$('#color').addClass "alert-#{level}"
|
||||||
|
|
||||||
|
displayError = (j, status, err) ->
|
||||||
|
console.log 'Error', err
|
||||||
|
res = JSON.parse j.responseText
|
||||||
|
if res and res.error
|
||||||
|
res = res.error.replace /.* /, ''
|
||||||
|
console.log 'Returned error', res
|
||||||
|
setMsg res, 'warning'
|
||||||
|
|
||||||
|
token=''
|
||||||
|
|
||||||
|
getKey = (reset) ->
|
||||||
|
$.ajax
|
||||||
|
type: "POST",
|
||||||
|
url: "#{portal}/totpregister/getkey"
|
||||||
|
data:
|
||||||
|
newkey: reset
|
||||||
|
error: displayError
|
||||||
|
# Display key and QR code
|
||||||
|
success: (data) ->
|
||||||
|
console.log data
|
||||||
|
# Generate OTP url
|
||||||
|
s = "otpauth://totp/#{escape(data.portal)}:#{escape(data.user)}?secret=#{data.secret}&issuer=#{escape(data.portal)}"
|
||||||
|
# Generate QR code
|
||||||
|
qr = new QRious
|
||||||
|
element: document.getElementById('qr'),
|
||||||
|
value: s
|
||||||
|
size:150
|
||||||
|
# Display serialized key
|
||||||
|
$('#serialized').text(s)
|
||||||
|
# Show message (warning level if key is new)
|
||||||
|
if data.newkey
|
||||||
|
setMsg 'yourNewTotpKey', 'warning'
|
||||||
|
else
|
||||||
|
setMsg 'yourTotpKey', 'info'
|
||||||
|
token = data.token
|
||||||
|
|
||||||
|
$(document).ready ->
|
||||||
|
getKey(0)
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Generated by CoffeeScript 1.10.0
|
||||||
|
|
||||||
|
/*
|
||||||
|
LemonLDAP::NG TOTP registration script
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
var displayError, getKey, setMsg, token;
|
||||||
|
|
||||||
|
setMsg = function(msg, level) {
|
||||||
|
$('#msg').html(window.translate(msg));
|
||||||
|
$('#color').removeClass('message-positive message-warning alert-success alert-warning');
|
||||||
|
$('#color').addClass("message-" + level);
|
||||||
|
if (level === 'positive') {
|
||||||
|
level = 'success';
|
||||||
|
}
|
||||||
|
return $('#color').addClass("alert-" + level);
|
||||||
|
};
|
||||||
|
|
||||||
|
displayError = function(j, status, err) {
|
||||||
|
var res;
|
||||||
|
console.log('Error', err);
|
||||||
|
res = JSON.parse(j.responseText);
|
||||||
|
if (res && res.error) {
|
||||||
|
res = res.error.replace(/.* /, '');
|
||||||
|
console.log('Returned error', res);
|
||||||
|
return setMsg(res, 'warning');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
token = '';
|
||||||
|
|
||||||
|
getKey = function(reset) {
|
||||||
|
return $.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: portal + "/totpregister/getkey",
|
||||||
|
data: {
|
||||||
|
newkey: reset
|
||||||
|
},
|
||||||
|
error: displayError,
|
||||||
|
success: function(data) {
|
||||||
|
var qr, s;
|
||||||
|
console.log(data);
|
||||||
|
s = "otpauth://totp/" + (escape(data.portal)) + ":" + (escape(data.user)) + "?secret=" + data.secret + "&issuer=" + (escape(data.portal));
|
||||||
|
qr = new QRious({
|
||||||
|
element: document.getElementById('qr'),
|
||||||
|
value: s,
|
||||||
|
size: 150
|
||||||
|
});
|
||||||
|
$('#serialized').text(s);
|
||||||
|
if (data.newkey) {
|
||||||
|
setMsg('yourNewTotpKey', 'warning');
|
||||||
|
} else {
|
||||||
|
setMsg('yourTotpKey', 'info');
|
||||||
|
}
|
||||||
|
return token = data.token;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
return getKey(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
}).call(this);
|
1
lemonldap-ng-portal/site/htdocs/static/common/js/totpregistration.min.js
vendored
Normal file
1
lemonldap-ng-portal/site/htdocs/static/common/js/totpregistration.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(){var a,b,d,c;d=function(e,f){$("#msg").html(window.translate(e));$("#color").removeClass("message-positive message-warning alert-success alert-warning");$("#color").addClass("message-"+f);if(f==="positive"){f="success"}return $("#color").addClass("alert-"+f)};a=function(f,e,h){var g;console.log("Error",h);g=JSON.parse(f.responseText);if(g&&g.error){g=g.error.replace(/.* /,"");console.log("Returned error",g);return d(g,"warning")}};c="";b=function(e){return $.ajax({type:"POST",url:portal+"/totpregister/getkey",data:{newkey:e},error:a,success:function(h){var f,g;console.log(h);g="otpauth://totp/"+(escape(h.portal))+":"+(escape(h.user))+"?secret="+h.secret+"&issuer="+(escape(h.portal));f=new QRious({element:document.getElementById("qr"),value:g,size:150});$("#serialized").text(g);if(h.newkey){d("yourNewTotpKey","warning")}else{d("yourTotpKey","info")}return c=h.token}})};$(document).ready(function(){return b(0)})}).call(this);
|
|
@ -0,0 +1,46 @@
|
||||||
|
<TMPL_INCLUDE NAME="header.tpl">
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div id="color" class="message message-positive alert"><span id="msg"></span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main id="menucontent" class="container">
|
||||||
|
<div class="panel panel-info">
|
||||||
|
<div class="panel-body">
|
||||||
|
<div>
|
||||||
|
<canvas id="qr"></canvas>
|
||||||
|
<pre id="serialized">
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input name="code" type="number" />
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<span id="changekey" class="btn btn-success" role="button">
|
||||||
|
<span class="glyphicon glyphicon-check"></span>
|
||||||
|
<span trspan="changeKey">Change key</span>
|
||||||
|
</span>
|
||||||
|
<span id="verify" class="btn btn-success" role="button">
|
||||||
|
<span class="glyphicon glyphicon-check"></span>
|
||||||
|
<span trspan="verify">Verify</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<a id="goback" href="" class="btn btn-primary" role="button">
|
||||||
|
<span class="glyphicon glyphicon-home"></span>
|
||||||
|
<span trspan="goToPortal">Go to portal</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- //if:jsminified
|
||||||
|
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">bwr/qrious/dist/qrious.min.js"></script>
|
||||||
|
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/totpregistration.min.js"></script>
|
||||||
|
//else -->
|
||||||
|
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">bwr/qrious/dist/qrious.js"></script>
|
||||||
|
<script type="text/javascript" src="<TMPL_VAR NAME="STATIC_PREFIX">/common/js/totpregistration.js"></script>
|
||||||
|
<!-- //endif -->
|
||||||
|
<TMPL_INCLUDE NAME="footer.tpl">
|
Loading…
Reference in New Issue
Block a user