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// ~~
5// This fork re-introduces the `ip` field of each token for additional security,
6// along with delete_tokens_for_ip
7// ~~
8// IP has been removed since IPs can change randomly and it causes you to need
9// to relog wayyyy more often. I'm keeping this fork just in case I do need to
10// change the auth system in the future.
11
12module auth
13
14import rand
15import crypto.rand as crypto_rand
16import crypto.hmac
17import crypto.sha256
18
19const max_safe_unsigned_integer = u32(4_294_967_295)
20
21pub struct Auth[T] {
22 db T
23}
24
25pub struct Token {
26pub:
27 id int @[primary; sql: serial]
28 user_id int
29 value string
30}
31
32pub fn new[T](db T) Auth[T] {
33 set_rand_crypto_safe_seed()
34 sql db {
35 create table Token
36 } or { eprintln('veb.auth: failed to create table Token') }
37 return Auth[T]{
38 db: db
39 }
40}
41
42pub fn (mut app Auth[T]) add_token(user_id int) !string {
43 mut uuid := rand.uuid_v4()
44 token := Token{
45 user_id: user_id
46 value: uuid
47 }
48 sql app.db {
49 insert token into Token
50 }!
51 return uuid
52}
53
54pub fn (app &Auth[T]) find_token(value string) ?Token {
55 tokens := sql app.db {
56 select from Token where value == value limit 1
57 } or { []Token{} }
58 if tokens.len == 0 {
59 return none
60 }
61 return tokens.first()
62}
63
64// logs out of all devices
65pub fn (mut app Auth[T]) delete_tokens_for_user(user_id int) ! {
66 sql app.db {
67 delete from Token where user_id == user_id
68 }!
69}
70
71// logs out of one device
72pub fn (mut app Auth[T]) delete_tokens_for_value(value string) ! {
73 sql app.db {
74 delete from Token where value == value
75 }!
76}
77
78pub fn set_rand_crypto_safe_seed() {
79 first_seed := generate_crypto_safe_int_u32()
80 second_seed := generate_crypto_safe_int_u32()
81 rand.seed([first_seed, second_seed])
82}
83
84fn generate_crypto_safe_int_u32() u32 {
85 return u32(crypto_rand.int_u64(max_safe_unsigned_integer) or { 0 })
86}
87
88pub fn generate_salt() string {
89 return rand.i64().str()
90}
91
92pub fn hash_password_with_salt(plain_text_password string, salt string) string {
93 salted_password := '${plain_text_password}${salt}'
94 return sha256.sum(salted_password.bytes()).hex().str()
95}
96
97pub fn compare_password_with_hash(plain_text_password string, salt string, hashed string) bool {
98 digest := hash_password_with_salt(plain_text_password, salt)
99 // constant time comparison
100 // I know this is operating on the hex-encoded strings, but it's still constant time
101 // and better than not doing it at all
102 return hmac.equal(digest.bytes(), hashed.bytes())
103}