Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
Aravinth Manivannan
6f029d2945
fix: associate challenges with usernames 2023-06-17 16:13:48 +05:30
Aravinth Manivannan
e7b01a5b06
feat: impl auth challenges interfaces for pg 2023-06-13 19:26:21 +05:30
Aravinth Manivannan
0cfffed52e
feat: impl auth challenges interfaces for mariadb 2023-06-13 19:24:03 +05:30
Aravinth Manivannan
c53fe2e3ff
feat: define internfaces to create,fetch and rm auth challenges 2023-06-13 19:23:23 +05:30
10 changed files with 491 additions and 0 deletions

11
Cargo.lock generated
View file

@ -873,6 +873,7 @@ dependencies = [
"serde_json",
"thiserror",
"url",
"uuid",
]
[[package]]
@ -3392,6 +3393,16 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2"
dependencies = [
"getrandom",
"serde 1.0.143",
]
[[package]]
name = "validator"
version = "0.15.0"

View file

@ -15,6 +15,7 @@ serde = { version = "1", features = ["derive"]}
url = { version = "2.2.2", features = ["serde"] }
#libmcaptcha = { version = "0.2.2", git = "https://github.com/mCaptcha/libmcaptcha", features = ["minimal"], default-features = false, tag = "0.2.2"}
libmcaptcha = { branch = "master", git = "https://github.com/mCaptcha/libmcaptcha", features = ["full"] }
uuid = { version = "1.3.3", features = ["v4", "serde"] }
[features]
default = []

View file

@ -31,7 +31,10 @@
//! - [errors](crate::auth): error data structures used in this crate
//! - [ops](crate::ops): meta operations like connection pool creation, migrations and getting
//! connection from pool
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use libmcaptcha::defense::Level;
@ -97,6 +100,73 @@ pub struct NameHash {
pub hash: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
/// Email challenge reason
pub enum ChallengeReason {
/// challenge created to verify a newly registered user
EmailVerification,
/// Challenge created to verify a password reset request
PasswordReset,
}
impl ChallengeReason {
pub fn to_str(&self) -> &'static str {
match self {
Self::EmailVerification => "email_verification",
Self::PasswordReset => "password_resset",
}
}
}
impl ToString for ChallengeReason {
fn to_string(&self) -> String {
self.to_str().into()
}
}
impl FromStr for ChallengeReason {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
for reason in [Self::PasswordReset, Self::EmailVerification].iter() {
if s == reason.to_str() {
return Ok(reason.clone());
}
}
Err(())
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
/// Minimal user representation for use in challenge verification
pub struct ChallengeUser {
/// username of the user
pub username: String,
/// email ID of the user
pub email: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
/// Email challenge
pub struct Challenge {
/// challenge unique identifier
pub challenge: Uuid,
/// reason why the challenge was create
pub reason: ChallengeReason,
}
impl Challenge {
/// create new Challenge instance for a given reason. Challenge text is auto-generated
pub fn new(reason: ChallengeReason) -> Self {
let challenge = Uuid::new_v4();
Self { challenge, reason }
}
/// Generate new ID (useful when ID clashes)
pub fn new_id(&mut self) {
self.challenge = Uuid::new_v4();
}
}
#[async_trait]
/// mCaptcha's database requirements. To implement support for $Database, kindly implement this
/// trait.
@ -250,6 +320,19 @@ pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
/// fetch PoWConfig confirms
async fn fetch_confirm(&self, user: &str, key: &str) -> DBResult<Vec<i64>>;
/// Record challenge in database
async fn new_challenge(&self, user: &str, challenge: &mut Challenge)
-> DBResult<()>;
/// Record challenge in database
async fn fetch_challenge_user(
&self,
challenge: &Challenge,
) -> DBResult<ChallengeUser>;
/// Delete a challenge from database
async fn delete_challenge(&self, challenge: &Challenge) -> DBResult<()>;
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]

View file

@ -295,4 +295,13 @@ pub async fn database_works<'a, T: MCDatabase>(
// delete captcha; updated key = p.username so invoke delete with it
db.delete_captcha(p.username, p.username).await.unwrap();
assert!(!db.captcha_exists(Some(p.username), c.key).await.unwrap());
let mut challenge = Challenge::new(ChallengeReason::PasswordReset);
db.new_challenge(p.username, &mut challenge).await.unwrap();
db.new_challenge(p.username, &mut challenge).await.unwrap();
let c = db.fetch_challenge_user(&challenge).await.unwrap();
assert_eq!(c.username, p.username);
assert_eq!(&c.email, p.email.as_ref().unwrap());
db.delete_challenge(&challenge).await.unwrap();
assert!(db.fetch_challenge_user(&challenge).await.is_err())
}

View file

@ -0,0 +1,27 @@
CREATE TABLE IF NOT EXISTS mcaptcha_challenge_reason (
id INT auto_increment,
PRIMARY KEY(id),
name VARCHAR(40) NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS mcaptcha_challenge (
id INT auto_increment,
PRIMARY KEY(id),
reason INT NOT NULL,
challenge_id varchar(40) NOT NULL UNIQUE,
received timestamp NOT NULL DEFAULT now(),
user_id INT NOT NULL,
CONSTRAINT `fk_mcaptcha_challenge_user`
FOREIGN KEY (user_id)
REFERENCES mcaptcha_users (ID)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `fk_mcaptcha_mcaptcha_challenge_reason`
FOREIGN KEY (reason)
REFERENCES mcaptcha_challenge_reason (id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

View file

@ -1,5 +1,53 @@
{
"db": "MySQL",
"04e79a67bc8c1b18eca95fc4d2602ed5dd41b6d864796f034540efec3da05fa8": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 1
}
},
"query": "INSERT IGNORE INTO\n mcaptcha_challenge_reason (name)\n VALUES (?)"
},
"12a7d765fb683c8134d032563f2d101e2fd70c261e71696e7a90387507e0ef43": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": {
"char_set": 224,
"flags": {
"bits": 4101
},
"max_size": 400,
"type": "VarString"
}
},
{
"name": "email",
"ordinal": 1,
"type_info": {
"char_set": 224,
"flags": {
"bits": 4
},
"max_size": 400,
"type": "VarString"
}
}
],
"nullable": [
false,
true
],
"parameters": {
"Right": 2
}
},
"query": "SELECT name, email\n FROM mcaptcha_users\n WHERE ID = (SELECT user_id \n FROM mcaptcha_challenge\n WHERE\n challenge_id = ?\n AND reason = (\n SELECT id FROM mcaptcha_challenge_reason WHERE name = ?\n )\n );"
},
"1367dceb151a766a901b5dd771d0b75d0bc61d2fef17a94a90c8ffa0065e2c44": {
"describe": {
"columns": [
@ -247,6 +295,16 @@
},
"query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config where captcha_key= (?)\n ) ORDER BY difficulty_factor ASC;"
},
"740ed2dab8c07c718c1b0e8e4262251bbf2501cdebfc4872fb903f70ec3d0dc8": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 4
}
},
"query": "INSERT INTO mcaptcha_challenge (challenge_id, received, reason, user_id)\n VALUES (?, ?,\n (SELECT id FROM mcaptcha_challenge_reason WHERE name = ?),\n (SELECT id FROM mcaptcha_users WHERE name = ?)\n );\n "
},
"74d68a86f852d3d85957e94ed04e8acd8e6144744f7b13e383ebcb2bcf3360ae": {
"describe": {
"columns": [],
@ -873,6 +931,16 @@
},
"query": "SELECT name, password FROM mcaptcha_users WHERE email = ?"
},
"f47c05c0a7da41a2176f08a44c6c945dabb84558a4d09369b6108bfce8b9d2bf": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 2
}
},
"query": "DELETE\n FROM mcaptcha_challenge\n WHERE\n challenge_id = ?\n AND reason = (SELECT id FROM mcaptcha_challenge_reason WHERE name = ?);"
},
"fc717ff0827ccfaa1cc61a71cc7f71c348ebb03d35895c54b011c03121ad2385": {
"describe": {
"columns": [],

View file

@ -95,6 +95,22 @@ impl Migrate for Database {
.run(&self.pool)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?;
for reason in [
ChallengeReason::EmailVerification,
ChallengeReason::PasswordReset,
] {
sqlx::query!(
"INSERT IGNORE INTO
mcaptcha_challenge_reason (name)
VALUES (?)",
reason.to_str()
)
.execute(&self.pool)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?;
}
Ok(())
}
}
@ -895,6 +911,93 @@ impl MCDatabase for Database {
Ok(Date::dates_to_unix(records))
}
/// Record challenge in database
async fn new_challenge(
&self,
user: &str,
challenge: &mut Challenge,
) -> DBResult<()> {
let now = now_unix_time_stamp();
loop {
let res = sqlx::query!(
"INSERT INTO mcaptcha_challenge (challenge_id, received, reason, user_id)
VALUES (?, ?,
(SELECT id FROM mcaptcha_challenge_reason WHERE name = ?),
(SELECT id FROM mcaptcha_users WHERE name = ?)
);
",
&challenge.challenge.to_string(),
now,
challenge.reason.to_str(),
user
)
.execute(&self.pool)
.await;
if let Err(Error::Database(err)) = res {
use std::borrow::Cow;
if err.code() == Some(Cow::from("23505")) {
let msg = err.message();
if msg.contains("for key 'challenge_id'") {
challenge.new_id();
continue;
}
}
}
break;
}
Ok(())
}
/// Record challenge in database
async fn fetch_challenge_user(
&self,
challenge: &Challenge,
) -> DBResult<ChallengeUser> {
struct C {
name: String,
email: Option<String>,
}
let res = sqlx::query_as!(
C,
"SELECT name, email
FROM mcaptcha_users
WHERE ID = (SELECT user_id
FROM mcaptcha_challenge
WHERE
challenge_id = ?
AND reason = (
SELECT id FROM mcaptcha_challenge_reason WHERE name = ?
)
);",
&challenge.challenge.to_string(),
challenge.reason.to_str(),
)
.fetch_one(&self.pool)
.await
.map_err(map_register_err)?;
Ok(ChallengeUser {
username: res.name,
email: res.email.unwrap(),
})
}
/// Delete a challenge from database
async fn delete_challenge(&self, challenge: &Challenge) -> DBResult<()> {
let _ = sqlx::query!(
"DELETE
FROM mcaptcha_challenge
WHERE
challenge_id = ?
AND reason = (SELECT id FROM mcaptcha_challenge_reason WHERE name = ?);",
&challenge.challenge.to_string(),
challenge.reason.to_str(),
)
.execute(&self.pool)
.await;
Ok(())
}
}
#[derive(Clone)]

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS mcaptcha_challenge_reason (
id SERIAL PRIMARY KEY NOT NULL,
name VARCHAR(40) NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS mcaptcha_challenge (
id SERIAL PRIMARY KEY NOT NULL,
reason INTEGER NOT NULL references mcaptcha_challenge_reason(ID) ON DELETE CASCADE,
user_id INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
challenge_id varchar(40) NOT NULL UNIQUE,
received timestamptz NOT NULL DEFAULT now()
);

View file

@ -81,6 +81,33 @@
},
"query": "DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic\n WHERE config_id = (\n SELECT config_id \n FROM \n mcaptcha_config \n WHERE\n key = ($1) \n AND \n user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)\n );"
},
"0fe29ca10e9a83f2064b1b98f570161d339891a74c637077b94d138a4360340e": {
"describe": {
"columns": [
{
"name": "email",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "name",
"ordinal": 1,
"type_info": "Varchar"
}
],
"nullable": [
true,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT\n email, name\n FROM\n mcaptcha_users\n WHERE\n ID = (\n SELECT\n user_id\n FROM\n mcaptcha_challenge\n WHERE\n challenge_id = $1\n AND reason = (SELECT ID FROM mcaptcha_challenge_reason WHERE name = $2)\n );"
},
"16864df9cf9a69c299d9ab68bac559c48f4fc433541a10f7c1b60717df2b820e": {
"describe": {
"columns": [
@ -119,6 +146,21 @@
},
"query": "SELECT key, name, config_id, duration FROM mcaptcha_config WHERE\n user_id = (SELECT ID FROM mcaptcha_users WHERE name = $1) "
},
"1e08fab612b17ab3cf3f76cd1543fb4d4006f7c20e09ecb58e1a1cfd5a7e70a2": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Timestamptz",
"Text",
"Text"
]
}
},
"query": "INSERT INTO mcaptcha_challenge (challenge_id, received, reason, user_id)\n VALUES ($1, $2, \n (SELECT ID FROM mcaptcha_challenge_reason WHERE name = $3),\n (SELECT ID FROM mcaptcha_users WHERE name = $4)\n );\n "
},
"1e9fe69b23e4bfa7bb369455753100307e334e8dbaf02ff37cda08992fe95910": {
"describe": {
"columns": [],
@ -427,6 +469,19 @@
},
"query": "SELECT time FROM mcaptcha_pow_solved_stats \n WHERE config_id = (\n SELECT config_id FROM mcaptcha_config \n WHERE \n key = $1\n AND\n user_id = (\n SELECT \n ID FROM mcaptcha_users WHERE name = $2)) \n ORDER BY time DESC"
},
"8a624372ec26200acdbc1c6c330dad841581e9abad586fa7f5a117a7cd289bd9": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "DELETE\n FROM mcaptcha_challenge\n WHERE\n challenge_id = $1\n AND reason = (SELECT ID FROM mcaptcha_challenge_reason WHERE name = $2);"
},
"9753721856a47438c5e72f28fd9d149db10c48e677b4613bf3f1e8487908aac8": {
"describe": {
"columns": [
@ -453,6 +508,18 @@
},
"query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n ) ORDER BY difficulty_factor ASC;"
},
"a209d14eb2c2eba8a750d66f74f8edcdbb02cf7c6c5249b226db30f52541a79b": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar"
]
}
},
"query": "INSERT INTO\n mcaptcha_challenge_reason (name)\n VALUES ($1) ON CONFLICT DO NOTHING\n "
},
"ad196ab3ef9dc32f6de2313577ccd6c26eae9ab19df5f71ce182651983efb99a": {
"describe": {
"columns": [

View file

@ -95,6 +95,23 @@ impl Migrate for Database {
.run(&self.pool)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?;
for reason in [
ChallengeReason::EmailVerification,
ChallengeReason::PasswordReset,
] {
sqlx::query!(
"INSERT INTO
mcaptcha_challenge_reason (name)
VALUES ($1) ON CONFLICT DO NOTHING
",
reason.to_str()
)
.execute(&self.pool)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?;
}
Ok(())
}
}
@ -901,6 +918,99 @@ impl MCDatabase for Database {
Ok(Date::dates_to_unix(records))
}
/// Record challenge in database
async fn new_challenge(
&self,
user: &str,
challenge: &mut Challenge,
) -> DBResult<()> {
let now = now_unix_time_stamp();
loop {
let res = sqlx::query!(
"INSERT INTO mcaptcha_challenge (challenge_id, received, reason, user_id)
VALUES ($1, $2,
(SELECT ID FROM mcaptcha_challenge_reason WHERE name = $3),
(SELECT ID FROM mcaptcha_users WHERE name = $4)
);
",
&challenge.challenge.to_string(),
now,
challenge.reason.to_str(),
user
)
.execute(&self.pool)
.await;
if let Err(Error::Database(err)) = res {
use std::borrow::Cow;
if err.code() == Some(Cow::from("23505")) {
let msg = err.message();
if msg.contains("mcaptcha_challenge_challenge_id_key") {
challenge.new_id();
continue;
}
}
}
break;
}
Ok(())
}
/// Record challenge in database
async fn fetch_challenge_user(
&self,
challenge: &Challenge,
) -> DBResult<ChallengeUser> {
struct U {
name: String,
email: Option<String>,
}
let res = sqlx::query_as!(
U,
"SELECT
email, name
FROM
mcaptcha_users
WHERE
ID = (
SELECT
user_id
FROM
mcaptcha_challenge
WHERE
challenge_id = $1
AND reason = (SELECT ID FROM mcaptcha_challenge_reason WHERE name = $2)
);",
challenge.challenge.to_string(),
challenge.reason.to_str(),
)
.fetch_one(&self.pool)
.await
.map_err(map_register_err)?;
Ok(ChallengeUser {
username: res.name,
email: res.email.unwrap(),
})
}
/// Delete a challenge from database
async fn delete_challenge(&self, challenge: &Challenge) -> DBResult<()> {
let _ = sqlx::query!(
"DELETE
FROM mcaptcha_challenge
WHERE
challenge_id = $1
AND reason = (SELECT ID FROM mcaptcha_challenge_reason WHERE name = $2);",
&challenge.challenge.to_string(),
challenge.reason.to_str(),
)
.execute(&self.pool)
.await;
Ok(())
}
}
#[derive(Clone)]