Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions common/verification.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package common

import (
"crypto/subtle"
"strings"
"sync"
"time"

"github.com/go-redis/redis/v8"
"github.com/google/uuid"
)

Expand All @@ -18,6 +20,8 @@ const (
PasswordResetPurpose = "r"
)

const verificationRedisPrefix = "verify_code:"

var verificationMutex sync.Mutex
var verificationMap map[string]verificationValue
var verificationMapMaxSize = 10
Expand All @@ -32,7 +36,18 @@ func GenerateVerificationCode(length int) string {
return code[:length]
}

func verificationRedisKey(key, purpose string) string {
return verificationRedisPrefix + purpose + ":" + key
}

func RegisterVerificationCodeWithKey(key string, code string, purpose string) {
if RedisEnabled {
ttl := time.Duration(VerificationValidMinutes) * time.Minute
if err := RedisSet(verificationRedisKey(key, purpose), code, ttl); err != nil {
SysError("RegisterVerificationCodeWithKey: redis set failed: " + err.Error())
}
return
}
Comment thread
jr-ni marked this conversation as resolved.
verificationMutex.Lock()
defer verificationMutex.Unlock()
verificationMap[purpose+key] = verificationValue{
Expand All @@ -45,6 +60,16 @@ func RegisterVerificationCodeWithKey(key string, code string, purpose string) {
}

func VerifyCodeWithKey(key string, code string, purpose string) bool {
if RedisEnabled {
stored, err := RedisGet(verificationRedisKey(key, purpose))
if err != nil {
if err != redis.Nil {
SysError("VerifyCodeWithKey: redis get failed: " + err.Error())
}
return false
}
return subtle.ConstantTimeCompare([]byte(stored), []byte(code)) == 1
Comment thread
jr-ni marked this conversation as resolved.
}
verificationMutex.Lock()
defer verificationMutex.Unlock()
value, okay := verificationMap[purpose+key]
Expand All @@ -56,6 +81,12 @@ func VerifyCodeWithKey(key string, code string, purpose string) bool {
}

func DeleteKey(key string, purpose string) {
if RedisEnabled {
if err := RedisDelKey(verificationRedisKey(key, purpose)); err != nil {
SysError("DeleteKey: redis del failed: " + err.Error())
}
return
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
verificationMutex.Lock()
defer verificationMutex.Unlock()
delete(verificationMap, purpose+key)
Expand Down
38 changes: 38 additions & 0 deletions common/verification_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package common

import "testing"

// Exercise the in-process map fallback path that's used when Redis is
// disabled. The Redis path is covered by live testing.
func TestVerifyCodeWithKey_MemoryFallback(t *testing.T) {
prevRedis := RedisEnabled
RedisEnabled = false
t.Cleanup(func() { RedisEnabled = prevRedis })

const key = "user@example.com"
const code = "abc123"

if VerifyCodeWithKey(key, code, EmailVerificationPurpose) {
t.Fatalf("expected false before code is registered")
}

RegisterVerificationCodeWithKey(key, code, EmailVerificationPurpose)

if !VerifyCodeWithKey(key, code, EmailVerificationPurpose) {
t.Fatalf("expected true after registration")
}

if VerifyCodeWithKey(key, "wrong", EmailVerificationPurpose) {
t.Fatalf("expected false for wrong code")
}

// Different purpose namespace must not collide.
if VerifyCodeWithKey(key, code, PasswordResetPurpose) {
t.Fatalf("purposes must be isolated")
}

DeleteKey(key, EmailVerificationPurpose)
if VerifyCodeWithKey(key, code, EmailVerificationPurpose) {
t.Fatalf("expected false after DeleteKey")
}
}