1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
use argon2::Argon2;
use password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use rand_core::OsRng;
use sqlx::{sqlite::Sqlite, SqliteConnection, Transaction};
use crate::error::BoxedError;
use crate::id::Id as BaseId;
pub trait Provider {
fn logins(&mut self) -> Logins;
}
impl<'c> Provider for Transaction<'c, Sqlite> {
fn logins(&mut self) -> Logins {
Logins(self)
}
}
pub struct Logins<'t>(&'t mut SqliteConnection);
#[derive(Debug)]
pub struct Login {
pub id: Id,
// Field unused (as of this writing), omitted to avoid warnings.
// Feel free to add it:
//
// pub name: String,
// However, the omission of the hashed password is deliberate, to minimize
// the chance that it ends up tangled up in debug output or in some other
// chunk of logic elsewhere.
}
impl<'c> Logins<'c> {
/// Create a new login, if the name is not already taken. Returns a [Login]
/// if a new login has actually been created, or `None` if an existing login
/// was found.
pub async fn create(
&mut self,
name: &str,
password: &str,
) -> Result<Option<Login>, BoxedError> {
let id = Id::generate();
let password_hash = StoredHash::new(password)?;
let insert_res = sqlx::query_as!(
Login,
r#"
insert or fail
into login (id, name, password_hash)
values ($1, $2, $3)
returning id as "id: Id"
"#,
id,
name,
password_hash,
)
.fetch_one(&mut *self.0)
.await;
let result = match insert_res {
Ok(id) => Ok(Some(id)),
Err(err) => {
if let Some(true) = err
.as_database_error()
.map(|db_err| db_err.is_unique_violation())
{
// Login with the same username (or, very rarely, same ID) already
// exists.
Ok(None)
} else {
Err(err)
}
}
}?;
Ok(result)
}
/// Authenticates `name` and `password` against an existing [Login]. Returns
/// that [Login] if one was found and the password was correct, or `None` if
/// either condition does not hold.
pub async fn authenticate(
&mut self,
name: &str,
password: &str,
) -> Result<Option<Login>, BoxedError> {
let found = self.for_name(name).await?;
let login = if let Some((login, stored_hash)) = found {
if stored_hash.verify(password)? {
// User found and password validation succeeded.
Some(login)
} else {
// Password validation failed.
None
}
} else {
// User not found.
None
};
Ok(login)
}
async fn for_name(&mut self, name: &str) -> Result<Option<(Login, StoredHash)>, BoxedError> {
let found = sqlx::query!(
r#"
select
id as "id: Id",
password_hash as "password_hash: StoredHash"
from login
where name = $1
"#,
name,
)
.map(|rec| (Login { id: rec.id }, rec.password_hash))
.fetch_optional(&mut *self.0)
.await?;
Ok(found)
}
}
/// Stable identifier for a [Login]. Prefixed with `L`.
#[derive(Debug, sqlx::Type)]
#[sqlx(transparent)]
pub struct Id(BaseId);
impl From<BaseId> for Id {
fn from(id: BaseId) -> Self {
Self(id)
}
}
impl Id {
pub fn generate() -> Self {
BaseId::generate("L")
}
}
#[derive(Debug, sqlx::Type)]
#[sqlx(transparent)]
struct StoredHash(String);
impl StoredHash {
fn new(password: &str) -> Result<Self, password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2
.hash_password(password.as_bytes(), &salt)?
.to_string();
Ok(Self(hash))
}
fn verify(&self, password: &str) -> Result<bool, password_hash::Error> {
let hash = PasswordHash::new(&self.0)?;
match Argon2::default().verify_password(password.as_bytes(), &hash) {
// Successful authentication, not an error
Ok(()) => Ok(true),
// Unsuccessful authentication, also not an error
Err(password_hash::errors::Error::Password) => Ok(false),
// Password validation failed for some other reason, treat as an error
Err(err) => Err(err),
}
}
}
|