speakeasy
speakeasy copied to clipboard
API idea: add `expiry` option for longer-term tokens
Currently, the window option option as describing maximum acceptable the margin of error:
- (HOTP) the maximum acceptable difference between the authoritative counter and user-provided token. e.g.
0 <= delta <= +window - (TOTP) the maximum difference in seconds between the authoritative clock and the user-provided token. e.g.
-window <= delta <= +window
For TOTP, it may be useful to allow the user-generated token to be valid for a longer time in the future. For example, a site administrator may want to allow password reset tokens to remain valid for up to 24 hours. In another example, an e-commerce site may wish to send out coupon codes that have a limited window of use in the future. The current implementation of the window option does not easily allow for either of these scenarios as the programmer would need to adjust the time and window values manually.
I propose adding an expiry option valid solely for TOTP that sets the "lookahead window" such that a token is valid iff:
-window <= delta <= expiry + window
where delta represents the counter difference between the authoritative clock and the user-provided token.
Implementation of the aforementioned features would be possible by generating TOTP tokens using the following options:
| Scenario | expiry (seconds) |
time (seconds since UNIX epoch) |
|---|---|---|
| 24-hour password reset tokens | 24*60*60 |
(default) |
| coupon code valid for 8 hours starting in 7 days | 8*60*60 |
Date.now() + 7*24*60*60 |
| Festival ticket valid from December 1, 2016 at 10 AM PST until December 3, 2016 at 11:59 PM | (Date.parse('Dec 03 2016 23:59:00 PST') - Date.parse('Dec 01 2016 10:00:00 PST')) / 1000 |
Date.parse('Dec 01 2016 10:00:00 PST') / 1000 |
As with the secret option, the user will need to provide the expiry and time options on verification as well to achieve the desired effect. Thus, the library agrees to only validate the token within the provided time window and marks all tokens as invalid outside the time window.
The server still only computes ± window + 1 tokens at each comparison because we know that the starting time for which the token was generated is fixed. As both the expiry and time options are provided at validation, the token is valid iff both these conditions hold:
time - window <= now() <= time + expiry + window-window <= delta@time <= +window
where delta@time represents the calculated delta for the given time option.
As an alternative to storing the time and expiry values in a database, developers may wish to encode these values into a signed string (e.g. using HMAC). On decoding, the developer verifies that the string has not been tampered with and passes the appropriate options to speakeasy for validation.
Additional thoughts:
tolerancecould be a better name for thewindowoption.untilcould be used to accept the absolute time version ofexpiry.
Please let me know what you think!
Cheers.
Really neat idea, thank you for writing this up. How will the interaction between window and expiry be reconciled if both are specified?
Both window and expiry may be used together, though after going through this algorithm-writing exercise, I think that accepting an absolute until time option is better API design. Similarly, accepting an absolute from time option and keeping the current semantics of time would allow cleaner separation of concerns in code and likely will reduce the number of issues later on.
Revised API proposal
- Add
fromas the number of seconds since the UNIX epoch after which tokens will be verified. - Add
untilas the number of seconds since the UNIX epoch after which tokens will no longer be verified.
To achieve the desired effect, from and optionally until must be provided for both generation and verification. It is illegal for the until option to be provided without the from option.
The from option overrides the time option for both token generation and verification. The rest of the token generation algorithm is the same as without the from option. The until option is unused for token generation.
For verification, the from and until options are used with the time option to disable verification outside before the from time and after the until time. As with token generation, the until option overrides the time option.
Given the number of things that can go wrong if the developer forgets to provide the appropriate options for both generation and validation, I propose that this functionality be implemented as new API methods. The methods would require the from option and allow the until option. They would also wrap the existing methods to allow implementing the feature with clean separation of concerns. I don't have any appropriate names jumping out at me right now though.
generateFoo & validateFoo!
Algorithms for previously proposed design
TOTP generate algorithm with window and expiry
Same as without expiry except that time is a required option:
windowis unused.expiryis unused.timeis the seconds since the UNIX epoch after which the token will be valid.
TOTP verify algorithm
Note: it is invalid to use expiry without a time component. Requiring an absolute until value instead of expiry would disallow this scenario. Instead until would signify the time after which no token will be validated.
Given:
tokenas the token generated using the options below, except fornowwhich is not an option.windowas number of seconds.expiryas number of seconds.timeas number of seconds since the UNIX epoch for whichtokenwas generated.now = Date.now() / 1000as number of seconds since the UNIX epoch.
Note: In the current design time has the same semantics as now. With the expiry design as described previously, time has dual semantics: the current time and/or the time for which the token is generated.
- Calculate the counter values for:
nowCounter = counterValueFor(now)minNowCounter = counterValueFor(time - window)maxNowCounter = counterValueFor(time + expiry + window)minCounter = counterValueFor(time - window)maxCounter = counterValueFor(time + window)
- Fail validation if expired:
- If
nowCounter < minNowCounter || maxNowCounter < nowCounterthen fail validation.
- If
- Fail if token is invalid:
- For each
tokengenerated fromminCounteruntilmaxCounterinclusive, if no token match then fail validation.
- For each
I was wondering if the expiry was applied to this library or any other library as it would be a more useful idea. Right now, I'm struggling with making TOTPs with a fixed validity. I have tried various combinations of step and window and can't seem to make an OTP with the validity I want. Any ideas on how to do this?
The simplest solution I can think of is:
- choose a time step of 1 hour
- set the window to half your desired validity duration
- generate codes for the middle of your absolute validity period
Example 1: September 1, 2016 until September 3, 2016
var HOUR = 60 * 60
var DAY = 24 * HOUR
var assert = require('assert')
var speakeasy = require('speakeasy')
var secret = 'mysecret'
var window = 3 * DAY / (2 * HOUR)
var token = speakeasy.totp({
secret: secret,
step: HOUR,
window: window,
time: Date.parse('2016-09-01T00:00:00-07:00') / 1000,
})
function testValidate(timeStr) {
return speakeasy.totp.verify({
token: token,
secret: secret,
step: HOUR,
window: window,
time: Date.parse(timeStr) / 1000,
})
}
// These times validate fine
assert(testValidate('2016-09-01T00:00:00-07:00'))
assert(testValidate('2016-09-02T00:00:00-07:00'))
assert(testValidate('2016-09-02T00:11:59-07:00'))
// These times fail to validate
assert(!testValidate('2000-01-01T00:00:00-07:00'))
assert(!testValidate('2016-08-30T00:00:00-07:00'))
assert(!testValidate('2016-08-30T00:11:59-07:00'))
assert(!testValidate('2016-09-03T00:00:00-07:00'))
assert(!testValidate('2100-01-01T00:00:00-07:00'))
On Jul 14, 2016, at 5:45 AM, Abhyudit Jain [email protected] wrote:
I was wondering if the expiry was applied to this library or any other library as it would be a more useful idea. Right now, I'm struggling with making TOTPs with a fixed validity. I have tried various combinations of step and window and can't seem to make an OTP with the validity I want. Any ideas on how to do this?
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/speakeasyjs/speakeasy/issues/51#issuecomment-232655175, or mute the thread https://github.com/notifications/unsubscribe/AAm4b0SEiBmhf1G2RuC1Wazc_Yq7Cnx-ks5qVi9-gaJpZM4HsOUU.