Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

Provide generation, period, and expiration for TOTP codes #31684

@johnnybubonic

Description

@johnnybubonic

Is your feature request related to a problem? Please describe.

When generating a TOTP code, all that's returned in the data container is just

{
  "code": 000000
}

This is particularly unhelpful in definitively knowing how long that code is valid for.

Sure, one may do a GET/read on /<totp>/keys/:name and get the period, BUT:

  1. That's an unnecessary round-trip
  2. It requires permission to read the TOTP configuration, which is a separate ACL grant from simply generating a code
  3. It only gives a general idea of length of validity; for something time-sensitive like TOTP (...it being the first T), this is generally tricky - especially for shorter periods.
  4. It is ambiguous. The requester can base it against local UNIX time, but that doesn't account for neither the duration of the request/processing/response nor the length of code generation factored in, which are unknown to the client. The client can fetch the period and make an assumption based on the date response header, but this is going to lead to some wildly wrong assumptions in some cases. (e.g. Vault generates a code at time step TN(+29s), it takes 2 seconds to get to the client, assuming a 30s period the client is now going to determine validity based on TN+1(+1s). That's an entirely wrong time step. Some verifiers are pretty strict.)

Describe the solution you'd like

Instead of:

{
  "code": 000000
}

do e.g.:

{
  "code": 000000,
  "generated": 1766084776.3819246,
  "expiry": 1766084790.0,
  "period": "30s"
}

Where:

  • generated: A UNIX Epoch timestamp in seconds as a float64, with nanosecond precision[0][1] of right before (or right after) the code was generated.
  • expiry: A UNIX Epoch timestamp[1] in seconds as a float64, with nanosecond precision of the moment the code expires (essentially the end of the current time step, in RFC 6238 lingo).
  • period: A time.Duration.String() string of the TOTP configuration's period. (A time.Duration.String() output is directly parseable by time.ParseDuration.)

[0] The quick and dirty would be

something like this.
var t time.Time
var epochNano float64

t = time.Now()
epochNano = float64(t.UnixNano()) / float64(time.Second)

But don't do that. Do [1] instead.

[1] Of course, the JSON encoder will truncate a direct float64to ten-millionths - in other words, in this case a tenth of a microsecond.

Easily fixable (and actually gets us more accurate nanoseconds than [0] does).
import (
	"strconv"
	"strings"
	"time"
)

type (
	NanoEpoch time.Time
)

func (n *NanoEpoch) MarshalJSON() (b []byte, err error) {

        var t time.Time

        if n == nil {
                return
        }
        t = time.Time(*n)

        b = []byte(fmt.Sprintf("%d.%d", t.Unix(), t.Nanosecond()))

        return
}

func (n *NanoEpoch) UnmarshalJSON(b []byte) (err error) {

        var spl []string
        var sec int64
        var nsec int64
        var t time.Time

        if len(b) == 0 {
                return
        }

        spl = strings.SplitN(string(b), ".", 2)
        // Theoretically should always be true. But alas, better safe than sorry.
        if spl[0] != "" {
                if sec, err = strconv.ParseInt(spl[0], 10, 64); err != nil {
                        return
                }
        }
        if len(spl) == 2 {
                if nsec, err = strconv.ParseInt(spl[1], 10, 64); err != nil {
                        return
                }
        }

        t = time.Unix(sec, nsec)
        *n = NanoEpoch(t)

        return
}

Describe alternatives you've considered

As laid out above, currently the only way of doing this is/are:

  1. Fetch the period from the TOTP's configuration (which is even assuming the entity has permission to read it)
  2. Deriving the time step on the client's end using the Date header
  3. Hope and pray it's not near/on a time step boundary and your network latency is less than the remaining time in the time step, because you don't even know if you need to get another TOTP in the new time step or not currently

Explain any additional use-cases

Giving indication of the "lifetime" of a TOTP is pretty standard for all TOTP generators. Hell, the web UI does it; I was frankly floored that the API didn't whatsoever.

Additional context

The RFC doesn't specify any sort of recommendation or requirement for this case, because the assumption is always:

<PROVER> =(TOTP CODE)=> <VERIFIER>

and not

<PROVER> =(REQUEST)=> <GENERATOR>
<GENERATOR> =(TOTP CODE)=> <PROVER>
<PROVER> =(TOTP CODE)=> <VERIFIER>

so there's a lot of additional considerations that need to be considered in addition to RFC 6238 § 5.2 and RFC 6238 § 6.

Obligatory legal

I attest that any and all source code provided in this post was written by myself, Brent Saner.
Any and all source code shared in this post is released by the author to the public under an

MIT License

Copyright 2025 Brent Saner

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

and may be used in this or any other project/software with no obligations, provisos, or the like other than what the above license specifies.
Explicit contribution release/agreement and/or relicensing is available upon request if required.

(INTERNAL TRACKING X-REF: openbao/openbao#2233)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions