summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormrw1593 <botahamec@outlook.com>2023-05-29 10:51:10 -0400
committermrw1593 <botahamec@outlook.com>2023-05-29 10:51:10 -0400
commite048d7d050f87e9e5bf06f01e39fd417d6868c7e (patch)
tree8312a39234b7f78699914cba06ce2328b047625d
parente38854c7db0fe6f006304d7f638b6aa190fc2d87 (diff)
Create a Client struct
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml1
-rw-r--r--src/api/users.rs2
-rw-r--r--src/models/client.rs111
-rw-r--r--src/models/mod.rs5
-rw-r--r--src/services/db.rs2
6 files changed, 118 insertions, 5 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ffe67bd..4d05ec5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1628,6 +1628,7 @@ dependencies = [
"tera",
"thiserror",
"unic-langid",
+ "url",
"uuid",
]
@@ -2309,6 +2310,7 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
+ "serde",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index f56f5fb..5195d8b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,7 @@ serde = "1"
thiserror = "1"
rust-argon2 = "1"
uuid = { version = "1", features = [ "v4", "fast-rng", "serde" ] }
+url = { version = "2", features = ["serde"] }
raise = "2"
exun = "0.1"
rust-ini = "0.18"
diff --git a/src/api/users.rs b/src/api/users.rs
index 353f8ff..2b67663 100644
--- a/src/api/users.rs
+++ b/src/api/users.rs
@@ -6,7 +6,7 @@ use sqlx::MySqlPool;
use thiserror::Error;
use uuid::Uuid;
-use crate::models::User;
+use crate::models::user::User;
use crate::services::crypto::PasswordHash;
use crate::services::{db, id};
diff --git a/src/models/client.rs b/src/models/client.rs
new file mode 100644
index 0000000..a7df936
--- /dev/null
+++ b/src/models/client.rs
@@ -0,0 +1,111 @@
+use std::{hash::Hash, marker::PhantomData};
+
+use exun::{Expect, RawUnexpected};
+use raise::yeet;
+use thiserror::Error;
+use url::Url;
+use uuid::Uuid;
+
+use crate::services::crypto::PasswordHash;
+
+/// There are two types of clients, based on their ability to maintain the
+/// security of their client credentials.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type)]
+#[sqlx(rename_all = "lowercase")]
+pub enum ClientType {
+ /// A client that is capable of maintaining the confidentiality of their
+ /// credentials, or capable of secure client authentication using other
+ /// means. An example would be a secure server with restricted access to
+ /// the client credentials.
+ Confidential,
+ /// A client that is incapable of maintaining the confidentiality of their
+ /// credentials and cannot authenticate securely by any other means, such
+ /// as an installed application, or a web-browser based application.
+ Public,
+}
+
+#[derive(Debug, Clone)]
+pub struct Client {
+ ty: ClientType,
+ id: Uuid,
+ secret: Option<PasswordHash>,
+ redirect_uris: Box<[Url]>,
+}
+
+impl PartialEq for Client {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ }
+}
+
+impl Eq for Client {}
+
+impl Hash for Client {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ state.write_u128(self.id.as_u128())
+ }
+}
+
+#[derive(Debug, Clone, Copy, Error)]
+#[error("Confidential clients must have a secret, but it was not provided")]
+pub struct NoSecretError {
+ _phantom: PhantomData<()>,
+}
+
+impl NoSecretError {
+ fn new() -> Self {
+ Self {
+ _phantom: PhantomData,
+ }
+ }
+}
+
+impl Client {
+ pub fn new_public(
+ id: Uuid,
+ ty: ClientType,
+ secret: Option<&str>,
+ redirect_uris: &[Url],
+ ) -> Result<Self, Expect<NoSecretError>> {
+ let secret = if let Some(secret) = secret {
+ Some(PasswordHash::new(secret)?)
+ } else {
+ None
+ };
+
+ if ty == ClientType::Confidential && secret.is_none() {
+ yeet!(NoSecretError::new().into());
+ }
+
+ Ok(Self {
+ id,
+ ty: ClientType::Public,
+ secret,
+ redirect_uris: redirect_uris.into_iter().cloned().collect(),
+ })
+ }
+
+ pub fn id(&self) -> Uuid {
+ self.id
+ }
+
+ pub fn client_type(&self) -> ClientType {
+ self.ty
+ }
+
+ pub fn secret_hash(&self) -> Option<&[u8]> {
+ self.secret.as_ref().map(|s| s.hash())
+ }
+
+ pub fn secret_salt(&self) -> Option<&[u8]> {
+ self.secret.as_ref().map(|s| s.salt())
+ }
+
+ pub fn secret_version(&self) -> Option<u8> {
+ self.secret.as_ref().map(|s| s.version())
+ }
+
+ pub fn check_secret(&self, secret: &str) -> Option<Result<bool, RawUnexpected>> {
+ self.secret.as_ref().map(|s| s.check_password(secret))
+ }
+}
diff --git a/src/models/mod.rs b/src/models/mod.rs
index 4a9be81..633f846 100644
--- a/src/models/mod.rs
+++ b/src/models/mod.rs
@@ -1,3 +1,2 @@
-mod user;
-
-pub use user::User;
+pub mod client;
+pub mod user;
diff --git a/src/services/db.rs b/src/services/db.rs
index f4da004..79df260 100644
--- a/src/services/db.rs
+++ b/src/services/db.rs
@@ -2,7 +2,7 @@ use exun::*;
use sqlx::{mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, MySql, MySqlPool};
use uuid::Uuid;
-use crate::models::User;
+use crate::models::user::User;
use super::crypto::PasswordHash;