diff --git a/auth-new/auth.go b/auth-new/auth.go index 6bc9576..12a7257 100644 --- a/auth-new/auth.go +++ b/auth-new/auth.go @@ -9,6 +9,7 @@ package auth import ( "time" + "git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/sliceutils" "github.com/go-webauthn/webauthn/webauthn" @@ -18,7 +19,8 @@ import ( ) type Authenticator struct { - webauthn *webauthn.WebAuthn + webauthn *webauthn.WebAuthn + recentlyUsedTotpTokens map[string]time.Time } type LoginNextState uint8 @@ -33,6 +35,17 @@ const ( LoginStartPasskey // Login starts with a passkey ) +func New(webauthnConfig *webauthn.Config) (*Authenticator, error) { + webauthn, err := webauthn.New(webauthnConfig) + if err != nil { + return nil, other.Error("auth", "failed to create webauthn handler", err) + } + return &Authenticator{ + webauthn: webauthn, + recentlyUsedTotpTokens: make(map[string]time.Time), + }, nil +} + func calcAccessExpirationTimestamp() time.Time { // For now, the default expiration is one month after creation // though "never" might also be a good option diff --git a/auth-new/errors.go b/auth-new/errors.go index 5e8fe14..ffad91a 100644 --- a/auth-new/errors.go +++ b/auth-new/errors.go @@ -12,6 +12,7 @@ var ( // A user may not login, for whatever reason ErrCantLogin = errors.New("user can't login") ErrDecryptionFailure = errors.New("failed to decrypt content") + ErrTotpRecentlyUsed = errors.New("totp token was used too recently") ) type CombinedError struct { diff --git a/auth-new/totp.go b/auth-new/totp.go index a08061b..a8a7eac 100644 --- a/auth-new/totp.go +++ b/auth-new/totp.go @@ -1,5 +1,7 @@ package auth +// Some helpful comments from: https://waters.me/internet/google-authenticator-implementation-note-key-length-token-reuse/ + import ( "time" @@ -15,11 +17,20 @@ import ( ) const totpUnverifiedSuffix = "-NOT_VERIFIED" +const totpTokenNoLongerRecentlyUsed = time.Second * 90 func (a *Authenticator) PerformTotpLogin( username string, totpToken string, ) (LoginNextState, string, error) { + // First check if that token has been seen recently for that user + if timestamp, found := a.recentlyUsedTotpTokens[totpToken+"+"+username]; found { + if timestamp.Add(totpTokenNoLongerRecentlyUsed).After(time.Now()) { + return LoginNextFailure, "", ErrTotpRecentlyUsed + } else { + delete(a.recentlyUsedTotpTokens, totpToken+"+"+username) + } + } if ok, err := a.canUsernameLogin(username); !ok { return 0, "", other.Error("auth", "user may not login", err) } @@ -61,6 +72,9 @@ func (a *Authenticator) PerformTotpLogin( ErrInvalidCombination, ) } + + a.recentlyUsedTotpTokens[totpToken+"+"+username] = time.Now() + token := models.AccessToken{ User: *acc, UserId: acc.ID,