a mini social media app for small communities
1// From: https://github.com/vlang/v/blob/1fae506900c79e3aafc00e08e1f861fc7cbf8012/vlib/veb/auth/auth.v
2// The original file's source is licensed under MIT.
3
4// This fork re-introduces the `ip` field of each token for additional security,
5// along with delete_tokens_for_ip
6
7module auth
8
9import rand
10import crypto.rand as crypto_rand
11import crypto.hmac
12import crypto.sha256
13
14const max_safe_unsigned_integer = u32(4_294_967_295)
15
16pub struct Auth[T] {
17 db T
18}
19
20pub struct Token {
21pub:
22 id int @[primary; sql: serial]
23 user_id int
24 value string
25 ip string
26}
27
28pub fn new[T](db T) Auth[T] {
29 set_rand_crypto_safe_seed()
30 sql db {
31 create table Token
32 } or { eprintln('veb.auth: failed to create table Token') }
33 return Auth[T]{
34 db: db
35 }
36}
37
38pub fn (mut app Auth[T]) add_token(user_id int, ip string) !string {
39 mut uuid := rand.uuid_v4()
40 token := Token{
41 user_id: user_id
42 value: uuid
43 ip: ip
44 }
45 sql app.db {
46 insert token into Token
47 }!
48 return uuid
49}
50
51pub fn (app &Auth[T]) find_token(value string, ip string) ?Token {
52 tokens := sql app.db {
53 select from Token where value == value && ip == ip limit 1
54 } or { []Token{} }
55 if tokens.len == 0 {
56 return none
57 }
58 return tokens.first()
59}
60
61pub fn (mut app Auth[T]) delete_tokens_for_user(user_id int) ! {
62 sql app.db {
63 delete from Token where user_id == user_id
64 }!
65}
66
67pub fn (mut app Auth[T]) delete_tokens_for_ip(ip string) ! {
68 sql app.db {
69 delete from Token where ip == ip
70 }!
71}
72
73pub fn set_rand_crypto_safe_seed() {
74 first_seed := generate_crypto_safe_int_u32()
75 second_seed := generate_crypto_safe_int_u32()
76 rand.seed([first_seed, second_seed])
77}
78
79fn generate_crypto_safe_int_u32() u32 {
80 return u32(crypto_rand.int_u64(max_safe_unsigned_integer) or { 0 })
81}
82
83pub fn generate_salt() string {
84 return rand.i64().str()
85}
86
87pub fn hash_password_with_salt(plain_text_password string, salt string) string {
88 salted_password := '${plain_text_password}${salt}'
89 return sha256.sum(salted_password.bytes()).hex().str()
90}
91
92pub fn compare_password_with_hash(plain_text_password string, salt string, hashed string) bool {
93 digest := hash_password_with_salt(plain_text_password, salt)
94 // constant time comparison
95 // I know this is operating on the hex-encoded strings, but it's still constant time
96 // and better than not doing it at all
97 return hmac.equal(digest.bytes(), hashed.bytes())
98}