BastiaanJansen / otp-java

A small and easy-to-use one-time password generator library for Java implementing RFC 4226 (HOTP) and RFC 6238 (TOTP).
MIT License
186 stars 30 forks source link

How to ensure TOTP is one-time in a distributed system. #69

Open giamma opened 1 year ago

giamma commented 1 year ago

Consider a clustered RESTful application that generates and validates TOTP using this library.

Is it sufficient to use the same seed across all replicas of the application in order to produce a TOTP that would be validated by any other replica node? In other words, is it safe to assume that each replica node, given the same configuration, should produce the same TOTP and should be able to validate the TOTP produced by any other node?

If the above is true, how to deal with the fact that once the OTP is used on one node, being it a one-time password, no other replica node should accept it?

Is this something users would need to build on top of the library? How about defining a pluggable strategy that would allow your users to store the generated TOTP in a shared storage, for example a self-expiring distributed cache based on hazelcast? If a used token was stored in a shared map (user -> token) until it expires and is removed, no other node would be able to use it.

BastiaanJansen commented 1 year ago

In other words, is it safe to assume that each replica node, given the same configuration, should produce the same TOTP and should be able to validate the TOTP produced by any other node?

Yes, all nodes will generate the same TOTP when given the same configuration. Make sure to also synchronize the system clock.

If the above is true, how to deal with the fact that once the OTP is used on one node, being it a one-time password, no other replica node should accept it?

In contrary to the name, OTP's are not only valid for one time. They are valid for the time window they are generated in +- delay window. Otherwise a user could only sign in once every X seconds. The time window only ensures that when a token is compromised, the attacker only has the token period (30 seconds by default) + delay window (default 0) to coordinate a sign in attempt.

This means that all replica nodes should accept all TOTP's in a valid window, even when the token was already used. The library has no knowledge of used tokens. Of course if your requirements demand that a token can only be used once, you could as you suggested use shared storage to store the token for as long as it is deemed invalid. And before you verify if a token is valid, check if the token is already in the store. Redis (or Hazelcast) could be a good option. But this is not built in because this is not specified in RFC 6238.

giamma commented 1 year ago

Hi, thank you for the quick reply. I do believe that the one-time requirement is part of the RFC, see section 5.2,

Note that a prover may send the same OTP inside a given time-step window multiple times to a verifier. The verifier MUST NOT accept the second attempt of the OTP after the successful validation has been issued for the first OTP, which ensures one-time only use of an OTP.

As such Hazelcast/Redis or other similar solution seems necessary to me. Maybe you could consider as an improvement the possibility for your library to allow users to implement an interface to tell whether the OTP was already used or not.

Thank you.

BastiaanJansen commented 1 year ago

You are absolutely correct, I must have missed that :).

A pluggable system is a valid option. As you said, there are many ways the developer could make sure a token is only used once. I would rather not implement many different strategies for different stores in order to not bloat the library, but one default (in-memory) implementation could be useful. The user could switch strategies when necessary. Maybe developing multiple companion libraries for different strategies (Redis / Hazelcast) would be the way to go in order to not depend on external libraries for strategies you don't use.

I do accept PR's so if you have the time to think with me / open a PR, it will be appreciated!

Caltalys commented 1 year ago

In this case you need something like key manager. you can use an interface or an abstraction layer that provides a consistent interface for interacting with the key manager. Maybe database or 3rd party key manager, anh should have a in-memory manager as default.

svschouw-bb commented 1 year ago

The RFC suggests a delayWindow of 1 or 2 to allow for time drift. This means that at any one moment 3 or 5 tokens are valid. But given that the time drift between the user and server does not change wildly, we only need to store the actually used counter (as calculated in com.bastiaanjansen.otp.TOTPGenerator.verify(String, int)), and make sure for every subsequent validation the counter is strictly higher than the previous time. But this would require this method to return the actually used counter instead of just true/false. An external mechanism could then compare the counter against the previously used counter.