Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,101 @@
|
||||
## Description
|
||||
|
||||
`veb.auth` is a module that helps with common logic required for authentication.
|
||||
|
||||
It allows to easily generate hashed and salted passwords and to compare password hashes.
|
||||
|
||||
It also handles authentication tokens, including DB table creation and insertion.
|
||||
All DBs are supported.
|
||||
|
||||
## Usage
|
||||
|
||||
```v
|
||||
import veb
|
||||
import db.pg
|
||||
import veb.auth
|
||||
|
||||
pub struct App {
|
||||
veb.StaticHandler
|
||||
pub mut:
|
||||
db pg.DB
|
||||
auth auth.Auth[pg.DB] // or auth.Auth[sqlite.DB] etc
|
||||
}
|
||||
|
||||
const port = 8081
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
current_user User
|
||||
}
|
||||
|
||||
struct User {
|
||||
id int @[primary; sql: serial]
|
||||
name string
|
||||
password_hash string
|
||||
salt string
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut app := &App{
|
||||
// Use your actual local PostgreSQL password here.
|
||||
db: pg.connect(host: 'localhost', user: 'postgres', password: 'password', dbname: 'postgres')!
|
||||
}
|
||||
app.auth = auth.new(app.db)
|
||||
veb.run[App, Context](mut app, port)
|
||||
}
|
||||
|
||||
@[post]
|
||||
pub fn (mut app App) register_user(mut ctx Context, name string, password string) veb.Result {
|
||||
salt := auth.generate_salt()
|
||||
new_user := User{
|
||||
name: name
|
||||
password_hash: auth.hash_password_with_salt(password, salt)
|
||||
salt: salt
|
||||
}
|
||||
user_id := sql app.db {
|
||||
insert new_user into User
|
||||
} or { 0 }
|
||||
|
||||
// Generate and insert the token using user ID
|
||||
token := app.auth.add_token(user_id) or { '' }
|
||||
// Authenticate the user by adding the token to the cookies
|
||||
ctx.set_cookie(name: 'token', value: token)
|
||||
|
||||
return ctx.redirect('/')
|
||||
}
|
||||
|
||||
@[post]
|
||||
pub fn (mut app App) login_post(mut ctx Context, name string, password string) veb.Result {
|
||||
user := app.find_user_by_name(name) or {
|
||||
ctx.error('Bad credentials')
|
||||
return ctx.redirect('/login')
|
||||
}
|
||||
// Verify user password using veb.auth
|
||||
if !auth.compare_password_with_hash(password, user.salt, user.password_hash) {
|
||||
ctx.error('Bad credentials')
|
||||
return ctx.redirect('/login')
|
||||
}
|
||||
// Find the user token in the Token table
|
||||
token := app.auth.add_token(user.id) or { '' }
|
||||
// Authenticate the user by adding the token to the cookies
|
||||
ctx.set_cookie(name: 'token', value: token)
|
||||
return ctx.redirect('/')
|
||||
}
|
||||
|
||||
pub fn (mut app App) find_user_by_name(name string) ?User {
|
||||
// ... db query
|
||||
return User{}
|
||||
}
|
||||
```
|
||||
|
||||
On macOS, Postgres.app rejects passwordless `trust` connections from unknown
|
||||
processes. Use a real password in the example above instead of `password: ''`.
|
||||
|
||||
## Security considerations
|
||||
|
||||
`hash_password_with_salt` and its related functions use `sha256` for hashing with a single
|
||||
iteration. This is not secure for production use, and you should use a more secure hashing
|
||||
algorithm and multiple iterations.
|
||||
|
||||
See also:
|
||||
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module auth
|
||||
|
||||
import rand
|
||||
import crypto.rand as crypto_rand
|
||||
import crypto.hmac
|
||||
import crypto.sha256
|
||||
|
||||
const max_safe_unsigned_integer = u32(4_294_967_295)
|
||||
|
||||
pub struct Auth[T] {
|
||||
db T
|
||||
// pub:
|
||||
// salt string
|
||||
}
|
||||
|
||||
pub struct Token {
|
||||
pub:
|
||||
id int @[primary; sql: serial]
|
||||
user_id int
|
||||
value string
|
||||
// ip string
|
||||
}
|
||||
|
||||
pub fn new[T](db T) Auth[T] {
|
||||
set_rand_crypto_safe_seed()
|
||||
sql db {
|
||||
create table Token
|
||||
} or { eprintln('veb.auth: failed to create table Token') }
|
||||
return Auth[T]{
|
||||
db: db
|
||||
// salt: generate_salt()
|
||||
}
|
||||
}
|
||||
|
||||
// fn (mut app App) add_token(user_id int, ip string) !string {
|
||||
pub fn (mut app Auth[T]) add_token(user_id int) !string {
|
||||
mut uuid := rand.uuid_v4()
|
||||
token := Token{
|
||||
user_id: user_id
|
||||
value: uuid
|
||||
// ip: ip
|
||||
}
|
||||
sql app.db {
|
||||
insert token into Token
|
||||
}!
|
||||
return uuid
|
||||
}
|
||||
|
||||
pub fn (app &Auth[T]) find_token(value string) ?Token {
|
||||
tokens := sql app.db {
|
||||
select from Token where value == value limit 1
|
||||
} or { []Token{} }
|
||||
if tokens.len == 0 {
|
||||
return none
|
||||
}
|
||||
return tokens.first()
|
||||
}
|
||||
|
||||
pub fn (mut app Auth[T]) delete_tokens(user_id int) ! {
|
||||
sql app.db {
|
||||
delete from Token where user_id == user_id
|
||||
}!
|
||||
}
|
||||
|
||||
pub fn set_rand_crypto_safe_seed() {
|
||||
first_seed := generate_crypto_safe_int_u32()
|
||||
second_seed := generate_crypto_safe_int_u32()
|
||||
rand.seed([first_seed, second_seed])
|
||||
}
|
||||
|
||||
fn generate_crypto_safe_int_u32() u32 {
|
||||
return u32(crypto_rand.int_u64(max_safe_unsigned_integer) or { 0 })
|
||||
}
|
||||
|
||||
pub fn generate_salt() string {
|
||||
return rand.i64().str()
|
||||
}
|
||||
|
||||
pub fn hash_password_with_salt(plain_text_password string, salt string) string {
|
||||
salted_password := '${plain_text_password}${salt}'
|
||||
return sha256.sum(salted_password.bytes()).hex().str()
|
||||
}
|
||||
|
||||
pub fn compare_password_with_hash(plain_text_password string, salt string, hashed string) bool {
|
||||
digest := hash_password_with_salt(plain_text_password, salt)
|
||||
// constant time comparison
|
||||
// I know this is operating on the hex-encoded strings, but it's still constant time
|
||||
// and better than not doing it at all
|
||||
return hmac.equal(digest.bytes(), hashed.bytes())
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module auth
|
||||
|
||||
pub struct Request {
|
||||
pub:
|
||||
client_id string
|
||||
client_secret string
|
||||
code string
|
||||
state string
|
||||
}
|
||||
Reference in new issue
Block a user