speakeasy icon indicating copy to clipboard operation
speakeasy copied to clipboard

API idea: add `expiry` option for longer-term tokens

Open mikepb opened this issue 9 years ago • 4 comments

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:

  • tolerance could be a better name for the window option.
  • until could be used to accept the absolute time version of expiry.

Please let me know what you think!

Cheers.

mikepb avatar Mar 08 '16 22:03 mikepb

Really neat idea, thank you for writing this up. How will the interaction between window and expiry be reconciled if both are specified?

markbao avatar Mar 09 '16 03:03 markbao

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 from as the number of seconds since the UNIX epoch after which tokens will be verified.
  • Add until as 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:

  • window is unused.
  • expiry is unused.
  • time is 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:

  • token as the token generated using the options below, except for now which is not an option.
  • window as number of seconds.
  • expiry as number of seconds.
  • time as number of seconds since the UNIX epoch for which token was generated.
  • now = Date.now() / 1000 as 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.

  1. 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)
  2. Fail validation if expired:
    • If nowCounter < minNowCounter || maxNowCounter < nowCounter then fail validation.
  3. Fail if token is invalid:
    • For each token generated from minCounter until maxCounter inclusive, if no token match then fail validation.

mikepb avatar Mar 09 '16 04:03 mikepb

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?

abhyuditjain avatar Jul 14 '16 12:07 abhyuditjain

The simplest solution I can think of is:

  1. choose a time step of 1 hour
  2. set the window to half your desired validity duration
  3. 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.

mikepb avatar Jul 22 '16 06:07 mikepb