mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-06-20 11:47:53 +02:00
Fix #11 : Set TOTP loop duration on remaining time instead of remaining dots
This commit is contained in:
parent
02798a05f3
commit
8253d28102
@ -40,7 +40,7 @@ class TwoFAccount extends Model implements Sortable
|
|||||||
*
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $appends = ['token', 'isConsistent', 'otpType', 'secret', 'algorithm', 'digits', 'totpPeriod', 'totpPosition', 'hotpCounter', 'imageLink'];
|
protected $appends = ['token', 'isConsistent', 'otpType', 'secret', 'algorithm', 'digits', 'totpPeriod', 'totpTimestamp', 'hotpCounter', 'imageLink'];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,7 +56,7 @@ class TwoFAccount extends Model implements Sortable
|
|||||||
*
|
*
|
||||||
* @var OTPHP/TOTP || OTPHP/HOTP
|
* @var OTPHP/TOTP || OTPHP/HOTP
|
||||||
*/
|
*/
|
||||||
protected $otp;
|
protected $otp, $timestamp;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -348,29 +348,6 @@ class TwoFAccount extends Model implements Sortable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate where is now() in the totp current period
|
|
||||||
* @return mixed The position
|
|
||||||
*/
|
|
||||||
private function getTotpPosition()
|
|
||||||
{
|
|
||||||
// For memo :
|
|
||||||
// $nextOtpAt = ($PeriodCount+1)*$period
|
|
||||||
// $remainingTime = $nextOtpAt - time()
|
|
||||||
if( $this->otpType === 'totp' ) {
|
|
||||||
|
|
||||||
$currentPosition = time();
|
|
||||||
$PeriodCount = floor($currentPosition / $this->totpPeriod); //nombre de période de x s depuis T0 (x=30 par défaut)
|
|
||||||
$currentPeriodStartAt = $PeriodCount * $this->totpPeriod;
|
|
||||||
$positionInCurrentPeriod = $currentPosition - $currentPeriodStartAt;
|
|
||||||
|
|
||||||
return $positionInCurrentPeriod;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the uri attribute using the OTP object
|
* Update the uri attribute using the OTP object
|
||||||
* @return void
|
* @return void
|
||||||
@ -382,12 +359,15 @@ class TwoFAccount extends Model implements Sortable
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a token which is valid at the current time (now)
|
* Generate a token which is valid at the current time
|
||||||
* @return string The generated token
|
* @return string The generated token
|
||||||
*/
|
*/
|
||||||
public function generateToken() : string
|
public function generateToken() : string
|
||||||
{
|
{
|
||||||
return $this->otpType === 'totp' ? $this->otp->now() : $this->otp->at($this->otp->getCounter());
|
$this->timestamp = time();
|
||||||
|
$token = $this->otpType === 'totp' ? $this->otp->at($this->timestamp) : $this->otp->at($this->otp->getCounter());
|
||||||
|
|
||||||
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -405,6 +385,17 @@ class TwoFAccount extends Model implements Sortable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get totpTimestamp attribute
|
||||||
|
*
|
||||||
|
* @return int The timestamp
|
||||||
|
*/
|
||||||
|
public function getTotpTimestampAttribute()
|
||||||
|
{
|
||||||
|
return $this->timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get token attribute
|
* get token attribute
|
||||||
*
|
*
|
||||||
@ -416,17 +407,6 @@ class TwoFAccount extends Model implements Sortable
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get totpPosition attribute
|
|
||||||
*
|
|
||||||
* @return int The position
|
|
||||||
*/
|
|
||||||
public function getTotpPositionAttribute()
|
|
||||||
{
|
|
||||||
return $this->getTotpPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get OTP Type attribute
|
* get OTP Type attribute
|
||||||
*
|
*
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<p class="is-size-6 has-text-grey has-ellipsis">{{ internal_account }}</p>
|
<p class="is-size-6 has-text-grey has-ellipsis">{{ internal_account }}</p>
|
||||||
<p class="is-size-1 has-text-white is-clickable" :title="$t('commons.copy_to_clipboard')" v-clipboard="() => token.replace(/ /g, '')" v-clipboard:success="clipboardSuccessHandler">{{ displayedToken }}</p>
|
<p class="is-size-1 has-text-white is-clickable" :title="$t('commons.copy_to_clipboard')" v-clipboard="() => token.replace(/ /g, '')" v-clipboard:success="clipboardSuccessHandler">{{ displayedToken }}</p>
|
||||||
<ul class="dots" v-if="internal_otpType === 'totp'">
|
<ul class="dots" v-if="internal_otpType === 'totp'">
|
||||||
<li v-for="n in 30"></li>
|
<li v-for="n in 10"></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul v-else-if="internal_otpType === 'hotp'">
|
<ul v-else-if="internal_otpType === 'hotp'">
|
||||||
<li>counter: {{ internal_hotpCounter }}</li>
|
<li>counter: {{ internal_hotpCounter }}</li>
|
||||||
@ -23,13 +23,18 @@
|
|||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
token : '',
|
token : '',
|
||||||
timerID: null,
|
remainingTimeout: null,
|
||||||
|
firstDotToNextOneTimeout: null,
|
||||||
|
dotToDotInterval: null,
|
||||||
position: null,
|
position: null,
|
||||||
|
totpTimestamp: null,
|
||||||
internal_otpType: '',
|
internal_otpType: '',
|
||||||
internal_account: '',
|
internal_account: '',
|
||||||
internal_service: '',
|
internal_service: '',
|
||||||
internal_icon: '',
|
internal_icon: '',
|
||||||
internal_hotpCounter: null,
|
internal_hotpCounter: null,
|
||||||
|
lastActiveDot: null,
|
||||||
|
dotToDotCounter: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -121,42 +126,62 @@
|
|||||||
|
|
||||||
getTOTP: function() {
|
getTOTP: function() {
|
||||||
|
|
||||||
|
this.dotToDotCounter = 0
|
||||||
|
|
||||||
this.axios.post('/api/twofaccounts/otp', { id: this.id, otp: this.$props }).then(response => {
|
this.axios.post('/api/twofaccounts/otp', { id: this.id, otp: this.$props }).then(response => {
|
||||||
|
|
||||||
let spacePosition = Math.ceil(response.data.token.length / 2);
|
let spacePosition = Math.ceil(response.data.token.length / 2);
|
||||||
|
|
||||||
this.token = response.data.token.substr(0, spacePosition) + " " + response.data.token.substr(spacePosition);
|
this.token = response.data.token.substr(0, spacePosition) + " " + response.data.token.substr(spacePosition);
|
||||||
this.position = response.data.totpPosition;
|
this.totpTimestamp = response.data.totpTimestamp; // the timestamp used to generate the token
|
||||||
|
this.position = this.totpTimestamp % response.data.totpPeriod // The position of the totp timestamp in the current period
|
||||||
|
|
||||||
|
// Hide all dots
|
||||||
let dots = this.$el.querySelector('.dots');
|
let dots = this.$el.querySelector('.dots');
|
||||||
|
|
||||||
// clear active dots
|
|
||||||
while (dots.querySelector('[data-is-active]')) {
|
while (dots.querySelector('[data-is-active]')) {
|
||||||
dots.querySelector('[data-is-active]').removeAttribute('data-is-active');
|
dots.querySelector('[data-is-active]').removeAttribute('data-is-active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// set dot at given position as the active one
|
// Activate the dot at the totp position
|
||||||
let active = dots.querySelector('li:nth-child(' + (this.position + 1 ) + ')');
|
let relativePosition = (this.position * 10) / response.data.totpPeriod
|
||||||
active.setAttribute('data-is-active', true);
|
let dotNumber = (Math.floor(relativePosition) +1)
|
||||||
|
|
||||||
let self = this;
|
this.lastActiveDot = dots.querySelector('li:nth-child(' + dotNumber + ')');
|
||||||
|
this.lastActiveDot.setAttribute('data-is-active', true);
|
||||||
|
|
||||||
this.timerID = setInterval(function() {
|
// Main timeout which run all over the totpPeriod.
|
||||||
|
|
||||||
let sibling = active.nextSibling;
|
let remainingTimeBeforeEndOfPeriod = response.data.totpPeriod - this.position
|
||||||
|
let self = this; // because of the setInterval/setTimeout closures
|
||||||
|
|
||||||
if(active.nextSibling === null) {
|
this.remainingTimeout = setTimeout(function() {
|
||||||
console.log('no more sibling to activate, we refresh the OTP')
|
|
||||||
self.stopLoop()
|
self.stopLoop()
|
||||||
self.getTOTP();
|
self.getTOTP();
|
||||||
}
|
}, remainingTimeBeforeEndOfPeriod*1000);
|
||||||
else
|
|
||||||
{
|
// During the remainingTimeout countdown we have to show a next dot every durationBetweenTwoDots seconds
|
||||||
active.removeAttribute('data-is-active');
|
// except for the first next dot
|
||||||
sibling.setAttribute('data-is-active', true);
|
|
||||||
active = sibling
|
let durationBetweenTwoDots = response.data.totpPeriod / 10 // we have 10 dots
|
||||||
|
let firstDotTimeout = (Math.ceil(this.position / durationBetweenTwoDots) * durationBetweenTwoDots) - this.position
|
||||||
|
|
||||||
|
this.firstDotToNextOneTimeout = setTimeout(function() {
|
||||||
|
|
||||||
|
if( firstDotTimeout > 0 ) {
|
||||||
|
self.activeNextDot()
|
||||||
|
dotNumber += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 1000);
|
self.dotToDotInterval = setInterval(function() {
|
||||||
|
|
||||||
|
self.dotToDotCounter += 1
|
||||||
|
self.activeNextDot()
|
||||||
|
dotNumber += 1
|
||||||
|
|
||||||
|
}, durationBetweenTwoDots*1000)
|
||||||
|
|
||||||
|
}, firstDotTimeout*1000)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.$router.push({ name: 'genericError', params: { err: error.response } });
|
this.$router.push({ name: 'genericError', params: { err: error.response } });
|
||||||
@ -183,7 +208,7 @@
|
|||||||
|
|
||||||
clearOTP: function() {
|
clearOTP: function() {
|
||||||
this.stopLoop()
|
this.stopLoop()
|
||||||
this.id = this.timerID = this.position = this.internal_hotpCounter = null
|
this.id = this.remainingTimeout = this.dotToDotInterval = this.firstDotToNextOneTimeout = this.position = this.internal_hotpCounter = null
|
||||||
this.internal_service = this.internal_account = this.internal_icon = this.internal_otpType = ''
|
this.internal_service = this.internal_account = this.internal_icon = this.internal_otpType = ''
|
||||||
this.token = '... ...'
|
this.token = '... ...'
|
||||||
|
|
||||||
@ -199,7 +224,19 @@
|
|||||||
|
|
||||||
stopLoop: function() {
|
stopLoop: function() {
|
||||||
if( this.internal_otpType === 'totp' ) {
|
if( this.internal_otpType === 'totp' ) {
|
||||||
clearInterval(this.timerID)
|
clearTimeout(this.remainingTimeout)
|
||||||
|
clearTimeout(this.firstDotToNextOneTimeout)
|
||||||
|
clearInterval(this.dotToDotInterval)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
activeNextDot: function() {
|
||||||
|
if(this.lastActiveDot.nextSibling !== null) {
|
||||||
|
|
||||||
|
this.lastActiveDot.removeAttribute('data-is-active')
|
||||||
|
this.lastActiveDot.nextSibling.setAttribute('data-is-active', true)
|
||||||
|
this.lastActiveDot = this.lastActiveDot.nextSibling
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
16
resources/sass/app.scss
vendored
16
resources/sass/app.scss
vendored
@ -310,23 +310,17 @@ figure.no-icon {
|
|||||||
background: hsl(0, 0%, 7%); /* grey */
|
background: hsl(0, 0%, 7%); /* grey */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dots li:nth-child(-n+27) {
|
.dots li:nth-child(-n+9) {
|
||||||
background: hsl(48, 100%, 67%); /* yellow */
|
background: hsl(48, 100%, 67%); /* yellow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dots li:nth-child(-n+18) {
|
.dots li:nth-child(-n+6) {
|
||||||
background: hsl(141, 71%, 48%); /* green */
|
background: hsl(141, 71%, 48%); /* green */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dots li:nth-child(3n+2), .dots li:nth-child(3n+3) {
|
// .dots li:nth-child(3n+2), .dots li:nth-child(3n+3) {
|
||||||
//background-color: black;
|
// display:none;
|
||||||
display:none;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
.dots li:last-child() {
|
|
||||||
// background-color: black;
|
|
||||||
//display:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input, .select select, .textarea {
|
.input, .select select, .textarea {
|
||||||
background-color: hsl(0, 0%, 21%);
|
background-color: hsl(0, 0%, 21%);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user