summaryrefslogtreecommitdiffstats
path: root/internal/storage
diff options
context:
space:
mode:
Diffstat (limited to 'internal/storage')
-rw-r--r--internal/storage/auth.go88
1 files changed, 88 insertions, 0 deletions
diff --git a/internal/storage/auth.go b/internal/storage/auth.go
new file mode 100644
index 0000000..6eefa41
--- /dev/null
+++ b/internal/storage/auth.go
@@ -0,0 +1,88 @@
1package storage
2
3import (
4 "context"
5 "crypto/rand"
6 "encoding/hex"
7 "fmt"
8 "time"
9)
10
11const (
12 ChallengeLength = 32 // bytes
13 ChallengeTTL = 10 * time.Minute
14)
15
16func generateChallenge() (string, error) {
17 bytes := make([]byte, ChallengeLength)
18 if _, err := rand.Read(bytes); err != nil {
19 return "", fmt.Errorf("failed to generate challenge: %w", err)
20 }
21 return hex.EncodeToString(bytes), nil
22}
23
24func (s *Storage) CreateAuthChallenge(ctx context.Context) (string, error) {
25 challenge, err := generateChallenge()
26 if err != nil {
27 return "", err
28 }
29
30 now := time.Now().Unix()
31 expiresAt := time.Now().Add(ChallengeTTL).Unix()
32
33 _, err = s.db.ExecContext(ctx,
34 "INSERT INTO auth_challenges (challenge, created_at, expires_at, used) VALUES (?, ?, ?, 0)",
35 challenge, now, expiresAt,
36 )
37 if err != nil {
38 return "", fmt.Errorf("failed to store challenge: %w", err)
39 }
40
41 return challenge, nil
42}
43
44func (s *Storage) ValidateAndConsumeChallenge(ctx context.Context, challenge string) error {
45 tx, err := s.db.BeginTx(ctx, nil)
46 if err != nil {
47 return fmt.Errorf("failed to begin transaction: %w", err)
48 }
49 defer tx.Rollback()
50
51 var expiresAt int64
52 var used int
53 err = tx.QueryRowContext(ctx,
54 "SELECT expires_at, used FROM auth_challenges WHERE challenge = ?",
55 challenge,
56 ).Scan(&expiresAt, &used)
57
58 if err != nil {
59 return fmt.Errorf("challenge not found or invalid")
60 }
61
62 if used != 0 {
63 return fmt.Errorf("challenge already used")
64 }
65
66 if time.Now().Unix() > expiresAt {
67 return fmt.Errorf("challenge expired")
68 }
69
70 _, err = tx.ExecContext(ctx,
71 "UPDATE auth_challenges SET used = 1 WHERE challenge = ?",
72 challenge,
73 )
74 if err != nil {
75 return fmt.Errorf("failed to mark challenge as used: %w", err)
76 }
77
78 return tx.Commit()
79}
80
81func (s *Storage) CleanupExpiredChallenges(ctx context.Context) error {
82 now := time.Now().Unix()
83 _, err := s.db.ExecContext(ctx,
84 "DELETE FROM auth_challenges WHERE expires_at < ?",
85 now,
86 )
87 return err
88}