diff options
| author | Mica White <botahamec@outlook.com> | 2025-12-08 20:08:21 -0500 |
|---|---|---|
| committer | Mica White <botahamec@outlook.com> | 2025-12-08 20:08:21 -0500 |
| commit | 608ce1d9910cd68ce825838ea313e02c598f908e (patch) | |
| tree | 0bd4ad26f86e5c873f97308983112b0ffe593df3 /src/services | |
| parent | 93fd2e82e8fdc5ee62739053385f8ccffc660f02 (diff) | |
Diffstat (limited to 'src/services')
| -rw-r--r-- | src/services/authorization.rs | 164 | ||||
| -rw-r--r-- | src/services/config.rs | 148 | ||||
| -rw-r--r-- | src/services/crypto.rs | 194 | ||||
| -rw-r--r-- | src/services/db.rs | 30 | ||||
| -rw-r--r-- | src/services/db/client.rs | 784 | ||||
| -rw-r--r-- | src/services/db/jwt.rs | 398 | ||||
| -rw-r--r-- | src/services/db/user.rs | 472 | ||||
| -rw-r--r-- | src/services/id.rs | 54 | ||||
| -rw-r--r-- | src/services/jwt.rs | 582 | ||||
| -rw-r--r-- | src/services/mod.rs | 14 | ||||
| -rw-r--r-- | src/services/secrets.rs | 48 |
11 files changed, 1444 insertions, 1444 deletions
diff --git a/src/services/authorization.rs b/src/services/authorization.rs index bfbbb5a..4e6ef35 100644 --- a/src/services/authorization.rs +++ b/src/services/authorization.rs @@ -1,82 +1,82 @@ -use actix_web::{ - error::ParseError, - http::header::{self, Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue}, -}; -use base64::Engine; -use raise::yeet; - -#[derive(Clone)] -pub struct BasicAuthorization { - username: Box<str>, - password: Box<str>, -} - -impl TryIntoHeaderValue for BasicAuthorization { - type Error = InvalidHeaderValue; - - fn try_into_value(self) -> Result<HeaderValue, Self::Error> { - let username = self.username; - let password = self.password; - let utf8 = format!("{username}:{password}"); - let b64 = base64::engine::general_purpose::STANDARD.encode(utf8); - let value = format!("Basic {b64}"); - HeaderValue::from_str(&value) - } -} - -impl Header for BasicAuthorization { - fn name() -> HeaderName { - header::AUTHORIZATION - } - - fn parse<M: actix_web::HttpMessage>(msg: &M) -> Result<Self, actix_web::error::ParseError> { - let Some(value) = msg.headers().get(Self::name()) else { - yeet!(ParseError::Header) - }; - - let Ok(value) = value.to_str() else { - yeet!(ParseError::Header) - }; - - if !value.starts_with("Basic") { - yeet!(ParseError::Header); - } - - let value: String = value - .chars() - .skip(5) - .skip_while(|ch| ch.is_whitespace()) - .collect(); - - if value.is_empty() { - yeet!(ParseError::Header); - } - - let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(value) else { - yeet!(ParseError::Header) - }; - - let Ok(value) = String::from_utf8(bytes) else { - yeet!(ParseError::Header) - }; - - let mut parts = value.split(':'); - let username = Box::from(parts.next().unwrap()); - let Some(password) = parts.next() else { - yeet!(ParseError::Header) - }; - let password = Box::from(password); - - Ok(Self { username, password }) - } -} - -impl BasicAuthorization { - pub fn username(&self) -> &str { - &self.username - } - - pub fn password(&self) -> &str { - &self.password - } -} +use actix_web::{
+ error::ParseError,
+ http::header::{self, Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue},
+};
+use base64::Engine;
+use raise::yeet;
+
+#[derive(Clone)]
+pub struct BasicAuthorization {
+ username: Box<str>,
+ password: Box<str>,
+}
+
+impl TryIntoHeaderValue for BasicAuthorization {
+ type Error = InvalidHeaderValue;
+
+ fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
+ let username = self.username;
+ let password = self.password;
+ let utf8 = format!("{username}:{password}");
+ let b64 = base64::engine::general_purpose::STANDARD.encode(utf8);
+ let value = format!("Basic {b64}");
+ HeaderValue::from_str(&value)
+ }
+}
+
+impl Header for BasicAuthorization {
+ fn name() -> HeaderName {
+ header::AUTHORIZATION
+ }
+
+ fn parse<M: actix_web::HttpMessage>(msg: &M) -> Result<Self, actix_web::error::ParseError> {
+ let Some(value) = msg.headers().get(Self::name()) else {
+ yeet!(ParseError::Header)
+ };
+
+ let Ok(value) = value.to_str() else {
+ yeet!(ParseError::Header)
+ };
+
+ if !value.starts_with("Basic") {
+ yeet!(ParseError::Header);
+ }
+
+ let value: String = value
+ .chars()
+ .skip(5)
+ .skip_while(|ch| ch.is_whitespace())
+ .collect();
+
+ if value.is_empty() {
+ yeet!(ParseError::Header);
+ }
+
+ let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(value) else {
+ yeet!(ParseError::Header)
+ };
+
+ let Ok(value) = String::from_utf8(bytes) else {
+ yeet!(ParseError::Header)
+ };
+
+ let mut parts = value.split(':');
+ let username = Box::from(parts.next().unwrap());
+ let Some(password) = parts.next() else {
+ yeet!(ParseError::Header)
+ };
+ let password = Box::from(password);
+
+ Ok(Self { username, password })
+ }
+}
+
+impl BasicAuthorization {
+ pub fn username(&self) -> &str {
+ &self.username
+ }
+
+ pub fn password(&self) -> &str {
+ &self.password
+ }
+}
diff --git a/src/services/config.rs b/src/services/config.rs index 6468126..932f38f 100644 --- a/src/services/config.rs +++ b/src/services/config.rs @@ -1,74 +1,74 @@ -use std::{ - fmt::{self, Display}, - str::FromStr, -}; - -use exun::RawUnexpected; -use parking_lot::RwLock; -use serde::Deserialize; -use thiserror::Error; -use url::Url; - -static ENVIRONMENT: RwLock<Environment> = RwLock::new(Environment::Local); - -#[derive(Debug, Clone, Deserialize)] -pub struct Config { - pub id: Box<str>, - pub url: Url, -} - -pub fn get_config() -> Result<Config, RawUnexpected> { - let env = get_environment(); - let path = format!("static/config/{env}.toml"); - let string = std::fs::read_to_string(path)?; - let config = toml::from_str(&string)?; - Ok(config) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Environment { - Local, - Dev, - Staging, - Production, -} - -impl Display for Environment { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Local => f.write_str("local"), - Self::Dev => f.write_str("dev"), - Self::Staging => f.write_str("staging"), - Self::Production => f.write_str("prod"), - } - } -} - -#[derive(Debug, Clone, Error)] -#[error("Expected one of the following environments: local, dev, staging, prod. Found {string}")] -pub struct ParseEnvironmentError { - string: Box<str>, -} - -impl FromStr for Environment { - type Err = ParseEnvironmentError; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "local" => Ok(Self::Local), - "dev" => Ok(Self::Dev), - "staging" => Ok(Self::Staging), - "prod" => Ok(Self::Production), - _ => Err(ParseEnvironmentError { string: s.into() }), - } - } -} - -pub fn set_environment(env: Environment) { - let mut env_ptr = ENVIRONMENT.write(); - *env_ptr = env; -} - -fn get_environment() -> Environment { - ENVIRONMENT.read().clone() -} +use std::{
+ fmt::{self, Display},
+ str::FromStr,
+};
+
+use exun::RawUnexpected;
+use parking_lot::RwLock;
+use serde::Deserialize;
+use thiserror::Error;
+use url::Url;
+
+static ENVIRONMENT: RwLock<Environment> = RwLock::new(Environment::Local);
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct Config {
+ pub id: Box<str>,
+ pub url: Url,
+}
+
+pub fn get_config() -> Result<Config, RawUnexpected> {
+ let env = get_environment();
+ let path = format!("static/config/{env}.toml");
+ let string = std::fs::read_to_string(path)?;
+ let config = toml::from_str(&string)?;
+ Ok(config)
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Environment {
+ Local,
+ Dev,
+ Staging,
+ Production,
+}
+
+impl Display for Environment {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Local => f.write_str("local"),
+ Self::Dev => f.write_str("dev"),
+ Self::Staging => f.write_str("staging"),
+ Self::Production => f.write_str("prod"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Error)]
+#[error("Expected one of the following environments: local, dev, staging, prod. Found {string}")]
+pub struct ParseEnvironmentError {
+ string: Box<str>,
+}
+
+impl FromStr for Environment {
+ type Err = ParseEnvironmentError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "local" => Ok(Self::Local),
+ "dev" => Ok(Self::Dev),
+ "staging" => Ok(Self::Staging),
+ "prod" => Ok(Self::Production),
+ _ => Err(ParseEnvironmentError { string: s.into() }),
+ }
+ }
+}
+
+pub fn set_environment(env: Environment) {
+ let mut env_ptr = ENVIRONMENT.write();
+ *env_ptr = env;
+}
+
+fn get_environment() -> Environment {
+ ENVIRONMENT.read().clone()
+}
diff --git a/src/services/crypto.rs b/src/services/crypto.rs index 5fce403..0107374 100644 --- a/src/services/crypto.rs +++ b/src/services/crypto.rs @@ -1,97 +1,97 @@ -use std::hash::Hash; - -use argon2::{hash_raw, verify_raw}; -use exun::RawUnexpected; - -use crate::services::secrets::pepper; - -/// The configuration used for hashing and verifying passwords -/// -/// # Example -/// -/// ``` -/// use crate::services::secrets; -/// -/// let pepper = secrets::pepper(); -/// let config = config(&pepper); -/// ``` -fn config<'a>(pepper: &'a [u8]) -> argon2::Config<'a> { - argon2::Config { - hash_length: 32, - lanes: 4, - mem_cost: 5333, - time_cost: 4, - secret: pepper, - - ad: &[], - thread_mode: argon2::ThreadMode::Sequential, - variant: argon2::Variant::Argon2i, - version: argon2::Version::Version13, - } -} - -/// A password hash and salt for a user -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PasswordHash { - hash: Box<[u8]>, - salt: Box<[u8]>, - version: u8, -} - -impl Hash for PasswordHash { - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { - state.write(&self.hash) - } -} - -impl PasswordHash { - /// Hash a password using Argon2 - pub fn new(password: &str) -> Result<Self, RawUnexpected> { - let password = password.as_bytes(); - - let salt: [u8; 32] = rand::random(); - let salt = Box::from(salt); - let pepper = pepper()?; - let hash = hash_raw(password, &salt, &config(&pepper))?.into_boxed_slice(); - - Ok(Self { - hash, - salt, - version: 0, - }) - } - - /// Create this structure from a given hash and salt - pub fn from_fields(hash: &[u8], salt: &[u8], version: u8) -> Self { - Self { - hash: Box::from(hash), - salt: Box::from(salt), - version, - } - } - - /// Get the password hash - pub fn hash(&self) -> &[u8] { - &self.hash - } - - /// Get the salt used for the hash - pub fn salt(&self) -> &[u8] { - &self.salt - } - - pub fn version(&self) -> u8 { - self.version - } - - /// Check if the given password is the one that was hashed - pub fn check_password(&self, password: &str) -> Result<bool, RawUnexpected> { - let pepper = pepper()?; - Ok(verify_raw( - password.as_bytes(), - &self.salt, - &self.hash, - &config(&pepper), - )?) - } -} +use std::hash::Hash;
+
+use argon2::{hash_raw, verify_raw};
+use exun::RawUnexpected;
+
+use crate::services::secrets::pepper;
+
+/// The configuration used for hashing and verifying passwords
+///
+/// # Example
+///
+/// ```
+/// use crate::services::secrets;
+///
+/// let pepper = secrets::pepper();
+/// let config = config(&pepper);
+/// ```
+fn config<'a>(pepper: &'a [u8]) -> argon2::Config<'a> {
+ argon2::Config {
+ hash_length: 32,
+ lanes: 4,
+ mem_cost: 5333,
+ time_cost: 4,
+ secret: pepper,
+
+ ad: &[],
+ thread_mode: argon2::ThreadMode::Sequential,
+ variant: argon2::Variant::Argon2i,
+ version: argon2::Version::Version13,
+ }
+}
+
+/// A password hash and salt for a user
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PasswordHash {
+ hash: Box<[u8]>,
+ salt: Box<[u8]>,
+ version: u8,
+}
+
+impl Hash for PasswordHash {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ state.write(&self.hash)
+ }
+}
+
+impl PasswordHash {
+ /// Hash a password using Argon2
+ pub fn new(password: &str) -> Result<Self, RawUnexpected> {
+ let password = password.as_bytes();
+
+ let salt: [u8; 32] = rand::random();
+ let salt = Box::from(salt);
+ let pepper = pepper()?;
+ let hash = hash_raw(password, &salt, &config(&pepper))?.into_boxed_slice();
+
+ Ok(Self {
+ hash,
+ salt,
+ version: 0,
+ })
+ }
+
+ /// Create this structure from a given hash and salt
+ pub fn from_fields(hash: &[u8], salt: &[u8], version: u8) -> Self {
+ Self {
+ hash: Box::from(hash),
+ salt: Box::from(salt),
+ version,
+ }
+ }
+
+ /// Get the password hash
+ pub fn hash(&self) -> &[u8] {
+ &self.hash
+ }
+
+ /// Get the salt used for the hash
+ pub fn salt(&self) -> &[u8] {
+ &self.salt
+ }
+
+ pub fn version(&self) -> u8 {
+ self.version
+ }
+
+ /// Check if the given password is the one that was hashed
+ pub fn check_password(&self, password: &str) -> Result<bool, RawUnexpected> {
+ let pepper = pepper()?;
+ Ok(verify_raw(
+ password.as_bytes(),
+ &self.salt,
+ &self.hash,
+ &config(&pepper),
+ )?)
+ }
+}
diff --git a/src/services/db.rs b/src/services/db.rs index f811d79..e3cb48b 100644 --- a/src/services/db.rs +++ b/src/services/db.rs @@ -1,15 +1,15 @@ -use exun::{RawUnexpected, ResultErrorExt}; -use sqlx::MySqlPool; - -mod client; -mod jwt; -mod user; - -pub use self::jwt::*; -pub use client::*; -pub use user::*; - -/// Intialize the connection pool -pub async fn initialize(db_url: &str) -> Result<MySqlPool, RawUnexpected> { - MySqlPool::connect(db_url).await.unexpect() -} +use exun::{RawUnexpected, ResultErrorExt};
+use sqlx::MySqlPool;
+
+mod client;
+mod jwt;
+mod user;
+
+pub use self::jwt::*;
+pub use client::*;
+pub use user::*;
+
+/// Intialize the connection pool
+pub async fn initialize(db_url: &str) -> Result<MySqlPool, RawUnexpected> {
+ MySqlPool::connect(db_url).await.unexpect()
+}
diff --git a/src/services/db/client.rs b/src/services/db/client.rs index b8942e9..1ad97b1 100644 --- a/src/services/db/client.rs +++ b/src/services/db/client.rs @@ -1,392 +1,392 @@ -use std::str::FromStr; - -use exun::{RawUnexpected, ResultErrorExt}; -use sqlx::{ - mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, FromRow, MySql, Transaction, -}; -use url::Url; -use uuid::Uuid; - -use crate::{ - models::client::{Client, ClientType}, - services::crypto::PasswordHash, -}; - -#[derive(Debug, Clone, FromRow)] -pub struct ClientRow { - pub id: Uuid, - pub alias: String, - pub client_type: ClientType, - pub allowed_scopes: String, - pub default_scopes: Option<String>, - pub is_trusted: bool, -} - -#[derive(Clone, FromRow)] -struct HashRow { - secret_hash: Option<Vec<u8>>, - secret_salt: Option<Vec<u8>>, - secret_version: Option<u32>, -} - -pub async fn client_id_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<bool, RawUnexpected> { - query_scalar!( - r"SELECT EXISTS(SELECT id FROM clients WHERE id = ?) as `e: bool`", - id - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn client_alias_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - alias: &str, -) -> Result<bool, RawUnexpected> { - query_scalar!( - "SELECT EXISTS(SELECT alias FROM clients WHERE alias = ?) as `e: bool`", - alias - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn get_client_id_by_alias<'c>( - executor: impl Executor<'c, Database = MySql>, - alias: &str, -) -> Result<Option<Uuid>, RawUnexpected> { - query_scalar!( - "SELECT id as `id: Uuid` FROM clients WHERE alias = ?", - alias - ) - .fetch_optional(executor) - .await - .unexpect() -} - -pub async fn get_client_response<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<Option<ClientRow>, RawUnexpected> { - let record = query_as!( - ClientRow, - r"SELECT id as `id: Uuid`, - alias, - type as `client_type: ClientType`, - allowed_scopes, - default_scopes, - trusted as `is_trusted: bool` - FROM clients WHERE id = ?", - id - ) - .fetch_optional(executor) - .await?; - - Ok(record) -} - -pub async fn get_client_alias<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<Option<Box<str>>, RawUnexpected> { - let alias = query_scalar!("SELECT alias FROM clients WHERE id = ?", id) - .fetch_optional(executor) - .await - .unexpect()?; - - Ok(alias.map(String::into_boxed_str)) -} - -pub async fn get_client_type<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<Option<ClientType>, RawUnexpected> { - let ty = query_scalar!( - "SELECT type as `type: ClientType` FROM clients WHERE id = ?", - id - ) - .fetch_optional(executor) - .await - .unexpect()?; - - Ok(ty) -} - -pub async fn get_client_allowed_scopes<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<Option<Box<str>>, RawUnexpected> { - let scopes = query_scalar!("SELECT allowed_scopes FROM clients WHERE id = ?", id) - .fetch_optional(executor) - .await?; - - Ok(scopes.map(Box::from)) -} - -pub async fn get_client_default_scopes<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<Option<Option<Box<str>>>, RawUnexpected> { - let scopes = query_scalar!("SELECT default_scopes FROM clients WHERE id = ?", id) - .fetch_optional(executor) - .await?; - - Ok(scopes.map(|s| s.map(Box::from))) -} - -pub async fn get_client_secret<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<Option<PasswordHash>, RawUnexpected> { - let hash = query_as!( - HashRow, - r"SELECT secret_hash, secret_salt, secret_version - FROM clients WHERE id = ?", - id - ) - .fetch_optional(executor) - .await?; - - let Some(hash) = hash else { return Ok(None) }; - let Some(version) = hash.secret_version else { return Ok(None) }; - let Some(salt) = hash.secret_hash else { return Ok(None) }; - let Some(hash) = hash.secret_salt else { return Ok(None) }; - - let hash = PasswordHash::from_fields(&hash, &salt, version as u8); - Ok(Some(hash)) -} - -pub async fn is_client_trusted<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<Option<bool>, RawUnexpected> { - query_scalar!("SELECT trusted as `t: bool` FROM clients WHERE id = ?", id) - .fetch_optional(executor) - .await - .unexpect() -} - -pub async fn get_client_redirect_uris<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<Box<[Url]>, RawUnexpected> { - let uris = query_scalar!( - "SELECT redirect_uri FROM client_redirect_uris WHERE client_id = ?", - id - ) - .fetch_all(executor) - .await - .unexpect()?; - - uris.into_iter() - .map(|s| Url::from_str(&s).unexpect()) - .collect() -} - -pub async fn client_has_redirect_uri<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - url: &Url, -) -> Result<bool, RawUnexpected> { - query_scalar!( - r"SELECT EXISTS( - SELECT redirect_uri - FROM client_redirect_uris - WHERE client_id = ? AND redirect_uri = ? - ) as `e: bool`", - id, - url.to_string() - ) - .fetch_one(executor) - .await - .unexpect() -} - -async fn delete_client_redirect_uris<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<(), sqlx::Error> { - query!("DELETE FROM client_redirect_uris WHERE client_id = ?", id) - .execute(executor) - .await?; - Ok(()) -} - -async fn create_client_redirect_uris<'c>( - mut transaction: Transaction<'c, MySql>, - client_id: Uuid, - uris: &[Url], -) -> Result<(), sqlx::Error> { - for uri in uris { - query!( - r"INSERT INTO client_redirect_uris (client_id, redirect_uri) - VALUES ( ?, ?)", - client_id, - uri.to_string() - ) - .execute(&mut transaction) - .await?; - } - - transaction.commit().await?; - - Ok(()) -} - -pub async fn create_client<'c>( - mut transaction: Transaction<'c, MySql>, - client: &Client, -) -> Result<(), sqlx::Error> { - query!( - r"INSERT INTO clients (id, alias, type, secret_hash, secret_salt, secret_version, allowed_scopes, default_scopes) - VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)", - client.id(), - client.alias(), - client.client_type(), - client.secret_hash(), - client.secret_salt(), - client.secret_version(), - client.allowed_scopes(), - client.default_scopes() - ) - .execute(&mut transaction) - .await?; - - create_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?; - - Ok(()) -} - -pub async fn update_client<'c>( - mut transaction: Transaction<'c, MySql>, - client: &Client, -) -> Result<(), sqlx::Error> { - query!( - r"UPDATE clients SET - alias = ?, - type = ?, - secret_hash = ?, - secret_salt = ?, - secret_version = ?, - allowed_scopes = ?, - default_scopes = ? - WHERE id = ?", - client.client_type(), - client.alias(), - client.secret_hash(), - client.secret_salt(), - client.secret_version(), - client.allowed_scopes(), - client.default_scopes(), - client.id() - ) - .execute(&mut transaction) - .await?; - - update_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?; - - Ok(()) -} - -pub async fn update_client_alias<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - alias: &str, -) -> Result<MySqlQueryResult, sqlx::Error> { - query!("UPDATE clients SET alias = ? WHERE id = ?", alias, id) - .execute(executor) - .await -} - -pub async fn update_client_type<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - ty: ClientType, -) -> Result<MySqlQueryResult, sqlx::Error> { - query!("UPDATE clients SET type = ? WHERE id = ?", ty, id) - .execute(executor) - .await -} - -pub async fn update_client_allowed_scopes<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - allowed_scopes: &str, -) -> Result<MySqlQueryResult, sqlx::Error> { - query!( - "UPDATE clients SET allowed_scopes = ? WHERE id = ?", - allowed_scopes, - id - ) - .execute(executor) - .await -} - -pub async fn update_client_default_scopes<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - default_scopes: Option<String>, -) -> Result<MySqlQueryResult, sqlx::Error> { - query!( - "UPDATE clients SET default_scopes = ? WHERE id = ?", - default_scopes, - id - ) - .execute(executor) - .await -} - -pub async fn update_client_trusted<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - is_trusted: bool, -) -> Result<MySqlQueryResult, sqlx::Error> { - query!( - "UPDATE clients SET trusted = ? WHERE id = ?", - is_trusted, - id - ) - .execute(executor) - .await -} - -pub async fn update_client_redirect_uris<'c>( - mut transaction: Transaction<'c, MySql>, - id: Uuid, - uris: &[Url], -) -> Result<(), sqlx::Error> { - delete_client_redirect_uris(&mut transaction, id).await?; - create_client_redirect_uris(transaction, id, uris).await?; - Ok(()) -} - -pub async fn update_client_secret<'c>( - executor: impl Executor<'c, Database = MySql>, - id: Uuid, - secret: Option<PasswordHash>, -) -> Result<MySqlQueryResult, sqlx::Error> { - if let Some(secret) = secret { - query!( - "UPDATE clients SET secret_hash = ?, secret_salt = ?, secret_version = ? WHERE id = ?", - secret.hash(), - secret.salt(), - secret.version(), - id - ) - .execute(executor) - .await - } else { - query!( - r"UPDATE clients - SET secret_hash = NULL, secret_salt = NULL, secret_version = NULL - WHERE id = ?", - id - ) - .execute(executor) - .await - } -} +use std::str::FromStr;
+
+use exun::{RawUnexpected, ResultErrorExt};
+use sqlx::{
+ mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, FromRow, MySql, Transaction,
+};
+use url::Url;
+use uuid::Uuid;
+
+use crate::{
+ models::client::{Client, ClientType},
+ services::crypto::PasswordHash,
+};
+
+#[derive(Debug, Clone, FromRow)]
+pub struct ClientRow {
+ pub id: Uuid,
+ pub alias: String,
+ pub client_type: ClientType,
+ pub allowed_scopes: String,
+ pub default_scopes: Option<String>,
+ pub is_trusted: bool,
+}
+
+#[derive(Clone, FromRow)]
+struct HashRow {
+ secret_hash: Option<Vec<u8>>,
+ secret_salt: Option<Vec<u8>>,
+ secret_version: Option<u32>,
+}
+
+pub async fn client_id_exists<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<bool, RawUnexpected> {
+ query_scalar!(
+ r"SELECT EXISTS(SELECT id FROM clients WHERE id = ?) as `e: bool`",
+ id
+ )
+ .fetch_one(executor)
+ .await
+ .unexpect()
+}
+
+pub async fn client_alias_exists<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ alias: &str,
+) -> Result<bool, RawUnexpected> {
+ query_scalar!(
+ "SELECT EXISTS(SELECT alias FROM clients WHERE alias = ?) as `e: bool`",
+ alias
+ )
+ .fetch_one(executor)
+ .await
+ .unexpect()
+}
+
+pub async fn get_client_id_by_alias<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ alias: &str,
+) -> Result<Option<Uuid>, RawUnexpected> {
+ query_scalar!(
+ "SELECT id as `id: Uuid` FROM clients WHERE alias = ?",
+ alias
+ )
+ .fetch_optional(executor)
+ .await
+ .unexpect()
+}
+
+pub async fn get_client_response<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<Option<ClientRow>, RawUnexpected> {
+ let record = query_as!(
+ ClientRow,
+ r"SELECT id as `id: Uuid`,
+ alias,
+ type as `client_type: ClientType`,
+ allowed_scopes,
+ default_scopes,
+ trusted as `is_trusted: bool`
+ FROM clients WHERE id = ?",
+ id
+ )
+ .fetch_optional(executor)
+ .await?;
+
+ Ok(record)
+}
+
+pub async fn get_client_alias<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<Option<Box<str>>, RawUnexpected> {
+ let alias = query_scalar!("SELECT alias FROM clients WHERE id = ?", id)
+ .fetch_optional(executor)
+ .await
+ .unexpect()?;
+
+ Ok(alias.map(String::into_boxed_str))
+}
+
+pub async fn get_client_type<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<Option<ClientType>, RawUnexpected> {
+ let ty = query_scalar!(
+ "SELECT type as `type: ClientType` FROM clients WHERE id = ?",
+ id
+ )
+ .fetch_optional(executor)
+ .await
+ .unexpect()?;
+
+ Ok(ty)
+}
+
+pub async fn get_client_allowed_scopes<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<Option<Box<str>>, RawUnexpected> {
+ let scopes = query_scalar!("SELECT allowed_scopes FROM clients WHERE id = ?", id)
+ .fetch_optional(executor)
+ .await?;
+
+ Ok(scopes.map(Box::from))
+}
+
+pub async fn get_client_default_scopes<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<Option<Option<Box<str>>>, RawUnexpected> {
+ let scopes = query_scalar!("SELECT default_scopes FROM clients WHERE id = ?", id)
+ .fetch_optional(executor)
+ .await?;
+
+ Ok(scopes.map(|s| s.map(Box::from)))
+}
+
+pub async fn get_client_secret<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<Option<PasswordHash>, RawUnexpected> {
+ let hash = query_as!(
+ HashRow,
+ r"SELECT secret_hash, secret_salt, secret_version
+ FROM clients WHERE id = ?",
+ id
+ )
+ .fetch_optional(executor)
+ .await?;
+
+ let Some(hash) = hash else { return Ok(None) };
+ let Some(version) = hash.secret_version else { return Ok(None) };
+ let Some(salt) = hash.secret_hash else { return Ok(None) };
+ let Some(hash) = hash.secret_salt else { return Ok(None) };
+
+ let hash = PasswordHash::from_fields(&hash, &salt, version as u8);
+ Ok(Some(hash))
+}
+
+pub async fn is_client_trusted<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<Option<bool>, RawUnexpected> {
+ query_scalar!("SELECT trusted as `t: bool` FROM clients WHERE id = ?", id)
+ .fetch_optional(executor)
+ .await
+ .unexpect()
+}
+
+pub async fn get_client_redirect_uris<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<Box<[Url]>, RawUnexpected> {
+ let uris = query_scalar!(
+ "SELECT redirect_uri FROM client_redirect_uris WHERE client_id = ?",
+ id
+ )
+ .fetch_all(executor)
+ .await
+ .unexpect()?;
+
+ uris.into_iter()
+ .map(|s| Url::from_str(&s).unexpect())
+ .collect()
+}
+
+pub async fn client_has_redirect_uri<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+ url: &Url,
+) -> Result<bool, RawUnexpected> {
+ query_scalar!(
+ r"SELECT EXISTS(
+ SELECT redirect_uri
+ FROM client_redirect_uris
+ WHERE client_id = ? AND redirect_uri = ?
+ ) as `e: bool`",
+ id,
+ url.to_string()
+ )
+ .fetch_one(executor)
+ .await
+ .unexpect()
+}
+
+async fn delete_client_redirect_uris<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<(), sqlx::Error> {
+ query!("DELETE FROM client_redirect_uris WHERE client_id = ?", id)
+ .execute(executor)
+ .await?;
+ Ok(())
+}
+
+async fn create_client_redirect_uris<'c>(
+ mut transaction: Transaction<'c, MySql>,
+ client_id: Uuid,
+ uris: &[Url],
+) -> Result<(), sqlx::Error> {
+ for uri in uris {
+ query!(
+ r"INSERT INTO client_redirect_uris (client_id, redirect_uri)
+ VALUES ( ?, ?)",
+ client_id,
+ uri.to_string()
+ )
+ .execute(&mut transaction)
+ .await?;
+ }
+
+ transaction.commit().await?;
+
+ Ok(())
+}
+
+pub async fn create_client<'c>(
+ mut transaction: Transaction<'c, MySql>,
+ client: &Client,
+) -> Result<(), sqlx::Error> {
+ query!(
+ r"INSERT INTO clients (id, alias, type, secret_hash, secret_salt, secret_version, allowed_scopes, default_scopes)
+ VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)",
+ client.id(),
+ client.alias(),
+ client.client_type(),
+ client.secret_hash(),
+ client.secret_salt(),
+ client.secret_version(),
+ client.allowed_scopes(),
+ client.default_scopes()
+ )
+ .execute(&mut transaction)
+ .await?;
+
+ create_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?;
+
+ Ok(())
+}
+
+pub async fn update_client<'c>(
+ mut transaction: Transaction<'c, MySql>,
+ client: &Client,
+) -> Result<(), sqlx::Error> {
+ query!(
+ r"UPDATE clients SET
+ alias = ?,
+ type = ?,
+ secret_hash = ?,
+ secret_salt = ?,
+ secret_version = ?,
+ allowed_scopes = ?,
+ default_scopes = ?
+ WHERE id = ?",
+ client.client_type(),
+ client.alias(),
+ client.secret_hash(),
+ client.secret_salt(),
+ client.secret_version(),
+ client.allowed_scopes(),
+ client.default_scopes(),
+ client.id()
+ )
+ .execute(&mut transaction)
+ .await?;
+
+ update_client_redirect_uris(transaction, client.id(), client.redirect_uris()).await?;
+
+ Ok(())
+}
+
+pub async fn update_client_alias<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+ alias: &str,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ query!("UPDATE clients SET alias = ? WHERE id = ?", alias, id)
+ .execute(executor)
+ .await
+}
+
+pub async fn update_client_type<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+ ty: ClientType,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ query!("UPDATE clients SET type = ? WHERE id = ?", ty, id)
+ .execute(executor)
+ .await
+}
+
+pub async fn update_client_allowed_scopes<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+ allowed_scopes: &str,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ query!(
+ "UPDATE clients SET allowed_scopes = ? WHERE id = ?",
+ allowed_scopes,
+ id
+ )
+ .execute(executor)
+ .await
+}
+
+pub async fn update_client_default_scopes<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+ default_scopes: Option<String>,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ query!(
+ "UPDATE clients SET default_scopes = ? WHERE id = ?",
+ default_scopes,
+ id
+ )
+ .execute(executor)
+ .await
+}
+
+pub async fn update_client_trusted<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+ is_trusted: bool,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ query!(
+ "UPDATE clients SET trusted = ? WHERE id = ?",
+ is_trusted,
+ id
+ )
+ .execute(executor)
+ .await
+}
+
+pub async fn update_client_redirect_uris<'c>(
+ mut transaction: Transaction<'c, MySql>,
+ id: Uuid,
+ uris: &[Url],
+) -> Result<(), sqlx::Error> {
+ delete_client_redirect_uris(&mut transaction, id).await?;
+ create_client_redirect_uris(transaction, id, uris).await?;
+ Ok(())
+}
+
+pub async fn update_client_secret<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+ secret: Option<PasswordHash>,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ if let Some(secret) = secret {
+ query!(
+ "UPDATE clients SET secret_hash = ?, secret_salt = ?, secret_version = ? WHERE id = ?",
+ secret.hash(),
+ secret.salt(),
+ secret.version(),
+ id
+ )
+ .execute(executor)
+ .await
+ } else {
+ query!(
+ r"UPDATE clients
+ SET secret_hash = NULL, secret_salt = NULL, secret_version = NULL
+ WHERE id = ?",
+ id
+ )
+ .execute(executor)
+ .await
+ }
+}
diff --git a/src/services/db/jwt.rs b/src/services/db/jwt.rs index b2f1367..73d6902 100644 --- a/src/services/db/jwt.rs +++ b/src/services/db/jwt.rs @@ -1,199 +1,199 @@ -use chrono::{DateTime, Utc}; -use exun::{RawUnexpected, ResultErrorExt}; -use sqlx::{query, query_scalar, Executor, MySql}; -use uuid::Uuid; - -use crate::services::jwt::RevokedRefreshTokenReason; - -pub async fn auth_code_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result<bool, RawUnexpected> { - query_scalar!( - "SELECT EXISTS(SELECT jti FROM auth_codes WHERE jti = ?) as `e: bool`", - jti - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn access_token_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result<bool, RawUnexpected> { - query_scalar!( - "SELECT EXISTS(SELECT jti FROM access_tokens WHERE jti = ?) as `e: bool`", - jti - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn refresh_token_exists<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result<bool, RawUnexpected> { - query_scalar!( - "SELECT EXISTS(SELECT jti FROM refresh_tokens WHERE jti = ?) as `e: bool`", - jti - ) - .fetch_one(executor) - .await - .unexpect() -} - -pub async fn refresh_token_revoked<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result<bool, RawUnexpected> { - let result = query_scalar!( - r"SELECT EXISTS( - SELECT revoked_reason FROM refresh_tokens WHERE jti = ? and revoked_reason IS NOT NULL - ) as `e: bool`", - jti - ) - .fetch_one(executor) - .await? - .unwrap_or(true); - - Ok(result) -} - -pub async fn create_auth_code<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, - exp: DateTime<Utc>, -) -> Result<(), sqlx::Error> { - query!( - r"INSERT INTO auth_codes (jti, exp) - VALUES ( ?, ?)", - jti, - exp - ) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn create_access_token<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, - auth_code: Option<Uuid>, - exp: DateTime<Utc>, -) -> Result<(), sqlx::Error> { - query!( - r"INSERT INTO access_tokens (jti, auth_code, exp) - VALUES ( ?, ?, ?)", - jti, - auth_code, - exp - ) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn create_refresh_token<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, - auth_code: Option<Uuid>, - exp: DateTime<Utc>, -) -> Result<(), sqlx::Error> { - query!( - r"INSERT INTO access_tokens (jti, auth_code, exp) - VALUES ( ?, ?, ?)", - jti, - auth_code, - exp - ) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn delete_auth_code<'c>( - executor: impl Executor<'c, Database = MySql>, - auth_code: Uuid, -) -> Result<bool, RawUnexpected> { - let result = query!("DELETE FROM auth_codes WHERE jti = ?", auth_code) - .execute(executor) - .await?; - - Ok(result.rows_affected() != 0) -} - -pub async fn delete_expired_auth_codes<'c>( - executor: impl Executor<'c, Database = MySql>, -) -> Result<(), RawUnexpected> { - query!("DELETE FROM auth_codes WHERE exp < ?", Utc::now()) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn delete_access_tokens_with_auth_code<'c>( - executor: impl Executor<'c, Database = MySql>, - auth_code: Uuid, -) -> Result<bool, RawUnexpected> { - let result = query!("DELETE FROM access_tokens WHERE auth_code = ?", auth_code) - .execute(executor) - .await?; - - Ok(result.rows_affected() != 0) -} - -pub async fn delete_expired_access_tokens<'c>( - executor: impl Executor<'c, Database = MySql>, -) -> Result<(), RawUnexpected> { - query!("DELETE FROM access_tokens WHERE exp < ?", Utc::now()) - .execute(executor) - .await?; - - Ok(()) -} - -pub async fn revoke_refresh_token<'c>( - executor: impl Executor<'c, Database = MySql>, - jti: Uuid, -) -> Result<bool, RawUnexpected> { - let result = query!( - "UPDATE refresh_tokens SET revoked_reason = ? WHERE jti = ?", - RevokedRefreshTokenReason::NewRefreshToken, - jti - ) - .execute(executor) - .await?; - - Ok(result.rows_affected() != 0) -} - -pub async fn revoke_refresh_tokens_with_auth_code<'c>( - executor: impl Executor<'c, Database = MySql>, - auth_code: Uuid, -) -> Result<bool, RawUnexpected> { - let result = query!( - "UPDATE refresh_tokens SET revoked_reason = ? WHERE auth_code = ?", - RevokedRefreshTokenReason::ReusedAuthorizationCode, - auth_code - ) - .execute(executor) - .await?; - - Ok(result.rows_affected() != 0) -} - -pub async fn delete_expired_refresh_tokens<'c>( - executor: impl Executor<'c, Database = MySql>, -) -> Result<(), RawUnexpected> { - query!("DELETE FROM refresh_tokens WHERE exp < ?", Utc::now()) - .execute(executor) - .await?; - - Ok(()) -} +use chrono::{DateTime, Utc};
+use exun::{RawUnexpected, ResultErrorExt};
+use sqlx::{query, query_scalar, Executor, MySql};
+use uuid::Uuid;
+
+use crate::services::jwt::RevokedRefreshTokenReason;
+
+pub async fn auth_code_exists<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ jti: Uuid,
+) -> Result<bool, RawUnexpected> {
+ query_scalar!(
+ "SELECT EXISTS(SELECT jti FROM auth_codes WHERE jti = ?) as `e: bool`",
+ jti
+ )
+ .fetch_one(executor)
+ .await
+ .unexpect()
+}
+
+pub async fn access_token_exists<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ jti: Uuid,
+) -> Result<bool, RawUnexpected> {
+ query_scalar!(
+ "SELECT EXISTS(SELECT jti FROM access_tokens WHERE jti = ?) as `e: bool`",
+ jti
+ )
+ .fetch_one(executor)
+ .await
+ .unexpect()
+}
+
+pub async fn refresh_token_exists<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ jti: Uuid,
+) -> Result<bool, RawUnexpected> {
+ query_scalar!(
+ "SELECT EXISTS(SELECT jti FROM refresh_tokens WHERE jti = ?) as `e: bool`",
+ jti
+ )
+ .fetch_one(executor)
+ .await
+ .unexpect()
+}
+
+pub async fn refresh_token_revoked<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ jti: Uuid,
+) -> Result<bool, RawUnexpected> {
+ let result = query_scalar!(
+ r"SELECT EXISTS(
+ SELECT revoked_reason FROM refresh_tokens WHERE jti = ? and revoked_reason IS NOT NULL
+ ) as `e: bool`",
+ jti
+ )
+ .fetch_one(executor)
+ .await?
+ .unwrap_or(true);
+
+ Ok(result)
+}
+
+pub async fn create_auth_code<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ jti: Uuid,
+ exp: DateTime<Utc>,
+) -> Result<(), sqlx::Error> {
+ query!(
+ r"INSERT INTO auth_codes (jti, exp)
+ VALUES ( ?, ?)",
+ jti,
+ exp
+ )
+ .execute(executor)
+ .await?;
+
+ Ok(())
+}
+
+pub async fn create_access_token<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ jti: Uuid,
+ auth_code: Option<Uuid>,
+ exp: DateTime<Utc>,
+) -> Result<(), sqlx::Error> {
+ query!(
+ r"INSERT INTO access_tokens (jti, auth_code, exp)
+ VALUES ( ?, ?, ?)",
+ jti,
+ auth_code,
+ exp
+ )
+ .execute(executor)
+ .await?;
+
+ Ok(())
+}
+
+pub async fn create_refresh_token<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ jti: Uuid,
+ auth_code: Option<Uuid>,
+ exp: DateTime<Utc>,
+) -> Result<(), sqlx::Error> {
+ query!(
+ r"INSERT INTO access_tokens (jti, auth_code, exp)
+ VALUES ( ?, ?, ?)",
+ jti,
+ auth_code,
+ exp
+ )
+ .execute(executor)
+ .await?;
+
+ Ok(())
+}
+
+pub async fn delete_auth_code<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ auth_code: Uuid,
+) -> Result<bool, RawUnexpected> {
+ let result = query!("DELETE FROM auth_codes WHERE jti = ?", auth_code)
+ .execute(executor)
+ .await?;
+
+ Ok(result.rows_affected() != 0)
+}
+
+pub async fn delete_expired_auth_codes<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+) -> Result<(), RawUnexpected> {
+ query!("DELETE FROM auth_codes WHERE exp < ?", Utc::now())
+ .execute(executor)
+ .await?;
+
+ Ok(())
+}
+
+pub async fn delete_access_tokens_with_auth_code<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ auth_code: Uuid,
+) -> Result<bool, RawUnexpected> {
+ let result = query!("DELETE FROM access_tokens WHERE auth_code = ?", auth_code)
+ .execute(executor)
+ .await?;
+
+ Ok(result.rows_affected() != 0)
+}
+
+pub async fn delete_expired_access_tokens<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+) -> Result<(), RawUnexpected> {
+ query!("DELETE FROM access_tokens WHERE exp < ?", Utc::now())
+ .execute(executor)
+ .await?;
+
+ Ok(())
+}
+
+pub async fn revoke_refresh_token<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ jti: Uuid,
+) -> Result<bool, RawUnexpected> {
+ let result = query!(
+ "UPDATE refresh_tokens SET revoked_reason = ? WHERE jti = ?",
+ RevokedRefreshTokenReason::NewRefreshToken,
+ jti
+ )
+ .execute(executor)
+ .await?;
+
+ Ok(result.rows_affected() != 0)
+}
+
+pub async fn revoke_refresh_tokens_with_auth_code<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+ auth_code: Uuid,
+) -> Result<bool, RawUnexpected> {
+ let result = query!(
+ "UPDATE refresh_tokens SET revoked_reason = ? WHERE auth_code = ?",
+ RevokedRefreshTokenReason::ReusedAuthorizationCode,
+ auth_code
+ )
+ .execute(executor)
+ .await?;
+
+ Ok(result.rows_affected() != 0)
+}
+
+pub async fn delete_expired_refresh_tokens<'c>(
+ executor: impl Executor<'c, Database = MySql>,
+) -> Result<(), RawUnexpected> {
+ query!("DELETE FROM refresh_tokens WHERE exp < ?", Utc::now())
+ .execute(executor)
+ .await?;
+
+ Ok(())
+}
diff --git a/src/services/db/user.rs b/src/services/db/user.rs index 09a09da..f85047a 100644 --- a/src/services/db/user.rs +++ b/src/services/db/user.rs @@ -1,236 +1,236 @@ -use exun::RawUnexpected; -use sqlx::{mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, MySql}; -use uuid::Uuid; - -use crate::{models::user::User, services::crypto::PasswordHash}; - -struct UserRow { - id: Uuid, - username: String, - password_hash: Vec<u8>, - password_salt: Vec<u8>, - password_version: u32, -} - -impl TryFrom<UserRow> for User { - type Error = RawUnexpected; - - fn try_from(row: UserRow) -> Result<Self, Self::Error> { - let password = PasswordHash::from_fields( - &row.password_hash, - &row.password_salt, - row.password_version as u8, - ); - let user = User { - id: row.id, - username: row.username.into_boxed_str(), - password, - }; - Ok(user) - } -} - -/// Check if a user with a given user ID exists -pub async fn user_id_exists<'c>( - conn: impl Executor<'c, Database = MySql>, - id: Uuid, -) -> Result<bool, RawUnexpected> { - let exists = query_scalar!( - r#"SELECT EXISTS(SELECT id FROM users WHERE id = ?) as `e: bool`"#, - id - ) - .fetch_one(conn) - .await?; - - Ok(exists) -} - -/// Check if a given username is taken -pub async fn username_is_used<'c>( - conn: impl Executor<'c, Database = MySql>, - username: &str, -) -> Result<bool, RawUnexpected> { - let exists = query_scalar!( - r#"SELECT EXISTS(SELECT id FROM users WHERE username = ?) as "e: bool""#, - username - ) - .fetch_one(conn) - .await?; - - Ok(exists) -} - -/// Get a user from their ID -pub async fn get_user<'c>( - conn: impl Executor<'c, Database = MySql>, - user_id: Uuid, -) -> Result<Option<User>, RawUnexpected> { - let record = query_as!( - UserRow, - r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version - FROM users WHERE id = ?", - user_id - ) - .fetch_optional(conn) - .await?; - - let Some(record) = record else { return Ok(None) }; - - Ok(Some(record.try_into()?)) -} - -/// Get a user from their username -pub async fn get_user_by_username<'c>( - conn: impl Executor<'c, Database = MySql>, - username: &str, -) -> Result<Option<User>, RawUnexpected> { - let record = query_as!( - UserRow, - r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version - FROM users WHERE username = ?", - username - ) - .fetch_optional(conn) - .await?; - - let Some(record) = record else { return Ok(None) }; - - Ok(Some(record.try_into()?)) -} - -/// Search the list of users for a given username -pub async fn search_users<'c>( - conn: impl Executor<'c, Database = MySql>, - username: &str, -) -> Result<Box<[User]>, RawUnexpected> { - let records = query_as!( - UserRow, - r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version - FROM users - WHERE LOCATE(?, username) != 0", - username, - ) - .fetch_all(conn) - .await?; - - Ok(records - .into_iter() - .map(|u| u.try_into()) - .collect::<Result<Box<[User]>, RawUnexpected>>()?) -} - -/// Search the list of users, only returning a certain range of results -pub async fn search_users_limit<'c>( - conn: impl Executor<'c, Database = MySql>, - username: &str, - offset: u32, - limit: u32, -) -> Result<Box<[User]>, RawUnexpected> { - let records = query_as!( - UserRow, - r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version - FROM users - WHERE LOCATE(?, username) != 0 - LIMIT ? - OFFSET ?", - username, - offset, - limit - ) - .fetch_all(conn) - .await?; - - Ok(records - .into_iter() - .map(|u| u.try_into()) - .collect::<Result<Box<[User]>, RawUnexpected>>()?) -} - -/// Get the username of a user with a certain ID -pub async fn get_username<'c>( - conn: impl Executor<'c, Database = MySql>, - user_id: Uuid, -) -> Result<Option<Box<str>>, RawUnexpected> { - let username = query_scalar!(r"SELECT username FROM users where id = ?", user_id) - .fetch_optional(conn) - .await? - .map(String::into_boxed_str); - - Ok(username) -} - -/// Create a new user -pub async fn create_user<'c>( - conn: impl Executor<'c, Database = MySql>, - user: &User, -) -> Result<MySqlQueryResult, sqlx::Error> { - query!( - r"INSERT INTO users (id, username, password_hash, password_salt, password_version) - VALUES ( ?, ?, ?, ?, ?)", - user.id, - user.username(), - user.password_hash(), - user.password_salt(), - user.password_version() - ) - .execute(conn) - .await -} - -/// Update a user -pub async fn update_user<'c>( - conn: impl Executor<'c, Database = MySql>, - user: &User, -) -> Result<MySqlQueryResult, sqlx::Error> { - query!( - r"UPDATE users SET - username = ?, - password_hash = ?, - password_salt = ?, - password_version = ? - WHERE id = ?", - user.username(), - user.password_hash(), - user.password_salt(), - user.password_version(), - user.id - ) - .execute(conn) - .await -} - -/// Update the username of a user with the given ID -pub async fn update_username<'c>( - conn: impl Executor<'c, Database = MySql>, - user_id: Uuid, - username: &str, -) -> Result<MySqlQueryResult, sqlx::Error> { - query!( - r"UPDATE users SET username = ? WHERE id = ?", - username, - user_id - ) - .execute(conn) - .await -} - -/// Update the password of a user with the given ID -pub async fn update_password<'c>( - conn: impl Executor<'c, Database = MySql>, - user_id: Uuid, - password: &PasswordHash, -) -> Result<MySqlQueryResult, sqlx::Error> { - query!( - r"UPDATE users SET - password_hash = ?, - password_salt = ?, - password_version = ? - WHERE id = ?", - password.hash(), - password.salt(), - password.version(), - user_id - ) - .execute(conn) - .await -} +use exun::RawUnexpected;
+use sqlx::{mysql::MySqlQueryResult, query, query_as, query_scalar, Executor, MySql};
+use uuid::Uuid;
+
+use crate::{models::user::User, services::crypto::PasswordHash};
+
+struct UserRow {
+ id: Uuid,
+ username: String,
+ password_hash: Vec<u8>,
+ password_salt: Vec<u8>,
+ password_version: u32,
+}
+
+impl TryFrom<UserRow> for User {
+ type Error = RawUnexpected;
+
+ fn try_from(row: UserRow) -> Result<Self, Self::Error> {
+ let password = PasswordHash::from_fields(
+ &row.password_hash,
+ &row.password_salt,
+ row.password_version as u8,
+ );
+ let user = User {
+ id: row.id,
+ username: row.username.into_boxed_str(),
+ password,
+ };
+ Ok(user)
+ }
+}
+
+/// Check if a user with a given user ID exists
+pub async fn user_id_exists<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ id: Uuid,
+) -> Result<bool, RawUnexpected> {
+ let exists = query_scalar!(
+ r#"SELECT EXISTS(SELECT id FROM users WHERE id = ?) as `e: bool`"#,
+ id
+ )
+ .fetch_one(conn)
+ .await?;
+
+ Ok(exists)
+}
+
+/// Check if a given username is taken
+pub async fn username_is_used<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ username: &str,
+) -> Result<bool, RawUnexpected> {
+ let exists = query_scalar!(
+ r#"SELECT EXISTS(SELECT id FROM users WHERE username = ?) as "e: bool""#,
+ username
+ )
+ .fetch_one(conn)
+ .await?;
+
+ Ok(exists)
+}
+
+/// Get a user from their ID
+pub async fn get_user<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ user_id: Uuid,
+) -> Result<Option<User>, RawUnexpected> {
+ let record = query_as!(
+ UserRow,
+ r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version
+ FROM users WHERE id = ?",
+ user_id
+ )
+ .fetch_optional(conn)
+ .await?;
+
+ let Some(record) = record else { return Ok(None) };
+
+ Ok(Some(record.try_into()?))
+}
+
+/// Get a user from their username
+pub async fn get_user_by_username<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ username: &str,
+) -> Result<Option<User>, RawUnexpected> {
+ let record = query_as!(
+ UserRow,
+ r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version
+ FROM users WHERE username = ?",
+ username
+ )
+ .fetch_optional(conn)
+ .await?;
+
+ let Some(record) = record else { return Ok(None) };
+
+ Ok(Some(record.try_into()?))
+}
+
+/// Search the list of users for a given username
+pub async fn search_users<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ username: &str,
+) -> Result<Box<[User]>, RawUnexpected> {
+ let records = query_as!(
+ UserRow,
+ r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version
+ FROM users
+ WHERE LOCATE(?, username) != 0",
+ username,
+ )
+ .fetch_all(conn)
+ .await?;
+
+ Ok(records
+ .into_iter()
+ .map(|u| u.try_into())
+ .collect::<Result<Box<[User]>, RawUnexpected>>()?)
+}
+
+/// Search the list of users, only returning a certain range of results
+pub async fn search_users_limit<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ username: &str,
+ offset: u32,
+ limit: u32,
+) -> Result<Box<[User]>, RawUnexpected> {
+ let records = query_as!(
+ UserRow,
+ r"SELECT id as `id: Uuid`, username, password_hash, password_salt, password_version
+ FROM users
+ WHERE LOCATE(?, username) != 0
+ LIMIT ?
+ OFFSET ?",
+ username,
+ offset,
+ limit
+ )
+ .fetch_all(conn)
+ .await?;
+
+ Ok(records
+ .into_iter()
+ .map(|u| u.try_into())
+ .collect::<Result<Box<[User]>, RawUnexpected>>()?)
+}
+
+/// Get the username of a user with a certain ID
+pub async fn get_username<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ user_id: Uuid,
+) -> Result<Option<Box<str>>, RawUnexpected> {
+ let username = query_scalar!(r"SELECT username FROM users where id = ?", user_id)
+ .fetch_optional(conn)
+ .await?
+ .map(String::into_boxed_str);
+
+ Ok(username)
+}
+
+/// Create a new user
+pub async fn create_user<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ user: &User,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ query!(
+ r"INSERT INTO users (id, username, password_hash, password_salt, password_version)
+ VALUES ( ?, ?, ?, ?, ?)",
+ user.id,
+ user.username(),
+ user.password_hash(),
+ user.password_salt(),
+ user.password_version()
+ )
+ .execute(conn)
+ .await
+}
+
+/// Update a user
+pub async fn update_user<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ user: &User,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ query!(
+ r"UPDATE users SET
+ username = ?,
+ password_hash = ?,
+ password_salt = ?,
+ password_version = ?
+ WHERE id = ?",
+ user.username(),
+ user.password_hash(),
+ user.password_salt(),
+ user.password_version(),
+ user.id
+ )
+ .execute(conn)
+ .await
+}
+
+/// Update the username of a user with the given ID
+pub async fn update_username<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ user_id: Uuid,
+ username: &str,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ query!(
+ r"UPDATE users SET username = ? WHERE id = ?",
+ username,
+ user_id
+ )
+ .execute(conn)
+ .await
+}
+
+/// Update the password of a user with the given ID
+pub async fn update_password<'c>(
+ conn: impl Executor<'c, Database = MySql>,
+ user_id: Uuid,
+ password: &PasswordHash,
+) -> Result<MySqlQueryResult, sqlx::Error> {
+ query!(
+ r"UPDATE users SET
+ password_hash = ?,
+ password_salt = ?,
+ password_version = ?
+ WHERE id = ?",
+ password.hash(),
+ password.salt(),
+ password.version(),
+ user_id
+ )
+ .execute(conn)
+ .await
+}
diff --git a/src/services/id.rs b/src/services/id.rs index 0c665ed..e1227e4 100644 --- a/src/services/id.rs +++ b/src/services/id.rs @@ -1,27 +1,27 @@ -use std::future::Future; - -use exun::RawUnexpected; -use sqlx::{Executor, MySql}; -use uuid::Uuid; - -/// Create a unique id, handling duplicate ID's. -/// -/// The given `unique_check` parameter returns `true` if the ID is used and -/// `false` otherwise. -pub async fn new_id< - 'c, - E: Executor<'c, Database = MySql> + Clone, - F: Future<Output = Result<bool, RawUnexpected>>, ->( - conn: E, - unique_check: impl Fn(E, Uuid) -> F, -) -> Result<Uuid, RawUnexpected> { - let uuid = loop { - let uuid = Uuid::new_v4(); - if !unique_check(conn.clone(), uuid).await? { - break uuid; - } - }; - - Ok(uuid) -} +use std::future::Future;
+
+use exun::RawUnexpected;
+use sqlx::{Executor, MySql};
+use uuid::Uuid;
+
+/// Create a unique id, handling duplicate ID's.
+///
+/// The given `unique_check` parameter returns `true` if the ID is used and
+/// `false` otherwise.
+pub async fn new_id<
+ 'c,
+ E: Executor<'c, Database = MySql> + Clone,
+ F: Future<Output = Result<bool, RawUnexpected>>,
+>(
+ conn: E,
+ unique_check: impl Fn(E, Uuid) -> F,
+) -> Result<Uuid, RawUnexpected> {
+ let uuid = loop {
+ let uuid = Uuid::new_v4();
+ if !unique_check(conn.clone(), uuid).await? {
+ break uuid;
+ }
+ };
+
+ Ok(uuid)
+}
diff --git a/src/services/jwt.rs b/src/services/jwt.rs index 16f5fa6..863eb83 100644 --- a/src/services/jwt.rs +++ b/src/services/jwt.rs @@ -1,291 +1,291 @@ -use chrono::{serde::ts_milliseconds, serde::ts_milliseconds_option, DateTime, Duration, Utc}; -use exun::{Expect, RawUnexpected, ResultErrorExt}; -use jwt::{SignWithKey, VerifyWithKey}; -use raise::yeet; -use serde::{Deserialize, Serialize}; -use sqlx::{Executor, MySql, MySqlPool}; -use thiserror::Error; -use url::Url; -use uuid::Uuid; - -use super::{db, id::new_id, secrets}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum TokenType { - Authorization, - Access, - Refresh, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Claims { - iss: Url, - sub: Uuid, - aud: Box<[String]>, - #[serde(with = "ts_milliseconds")] - exp: DateTime<Utc>, - #[serde(with = "ts_milliseconds_option")] - nbf: Option<DateTime<Utc>>, - #[serde(with = "ts_milliseconds")] - iat: DateTime<Utc>, - jti: Uuid, - scope: Box<str>, - client_id: Uuid, - token_type: TokenType, - auth_code_id: Option<Uuid>, - redirect_uri: Option<Url>, -} - -#[derive(Debug, Clone, Copy, sqlx::Type)] -#[sqlx(rename_all = "kebab-case")] -pub enum RevokedRefreshTokenReason { - ReusedAuthorizationCode, - NewRefreshToken, -} - -impl Claims { - pub async fn auth_code<'c>( - db: &MySqlPool, - self_id: Url, - client_id: Uuid, - sub: Uuid, - scopes: &str, - redirect_uri: &Url, - ) -> Result<Self, RawUnexpected> { - let five_minutes = Duration::minutes(5); - - let id = new_id(db, db::auth_code_exists).await?; - let iat = Utc::now(); - let exp = iat + five_minutes; - - db::create_auth_code(db, id, exp).await?; - - let aud = [self_id.to_string(), client_id.to_string()].into(); - - Ok(Self { - iss: self_id, - sub, - aud, - exp, - nbf: None, - iat, - jti: id, - scope: scopes.into(), - client_id, - auth_code_id: Some(id), - token_type: TokenType::Authorization, - redirect_uri: Some(redirect_uri.clone()), - }) - } - - pub async fn access_token<'c>( - db: &MySqlPool, - auth_code_id: Option<Uuid>, - self_id: Url, - client_id: Uuid, - sub: Uuid, - duration: Duration, - scopes: &str, - ) -> Result<Self, RawUnexpected> { - let id = new_id(db, db::access_token_exists).await?; - let iat = Utc::now(); - let exp = iat + duration; - - db::create_access_token(db, id, auth_code_id, exp) - .await - .unexpect()?; - - let aud = [self_id.to_string(), client_id.to_string()].into(); - - Ok(Self { - iss: self_id, - sub, - aud, - exp, - nbf: None, - iat, - jti: id, - scope: scopes.into(), - client_id, - auth_code_id, - token_type: TokenType::Access, - redirect_uri: None, - }) - } - - pub async fn refresh_token( - db: &MySqlPool, - other_token: &Claims, - ) -> Result<Self, RawUnexpected> { - let one_day = Duration::days(1); - - let id = new_id(db, db::refresh_token_exists).await?; - let iat = Utc::now(); - let exp = other_token.exp + one_day; - - db::create_refresh_token(db, id, other_token.auth_code_id, exp).await?; - - let mut claims = other_token.clone(); - claims.exp = exp; - claims.iat = iat; - claims.jti = id; - claims.token_type = TokenType::Refresh; - - Ok(claims) - } - - pub async fn refreshed_access_token( - db: &MySqlPool, - refresh_token: &Claims, - exp_time: Duration, - ) -> Result<Self, RawUnexpected> { - let id = new_id(db, db::access_token_exists).await?; - let iat = Utc::now(); - let exp = iat + exp_time; - - db::create_access_token(db, id, refresh_token.auth_code_id, exp).await?; - - let mut claims = refresh_token.clone(); - claims.exp = exp; - claims.iat = iat; - claims.jti = id; - claims.token_type = TokenType::Access; - - Ok(claims) - } - - pub fn id(&self) -> Uuid { - self.jti - } - - pub fn subject(&self) -> Uuid { - self.sub - } - - pub fn expires_in(&self) -> i64 { - (self.exp - Utc::now()).num_seconds() - } - - pub fn scopes(&self) -> &str { - &self.scope - } - - pub fn to_jwt(&self) -> Result<Box<str>, RawUnexpected> { - let key = secrets::signing_key()?; - let jwt = self.sign_with_key(&key)?.into_boxed_str(); - Ok(jwt) - } -} - -#[derive(Debug, Error)] -pub enum VerifyJwtError { - #[error("{0}")] - ParseJwtError(#[from] jwt::Error), - #[error("The issuer for this token is incorrect")] - IncorrectIssuer, - #[error("This bearer token was intended for a different client")] - WrongClient, - #[error("The given audience parameter does not contain this issuer")] - BadAudience, - #[error("The redirect URI doesn't match what's in the token")] - IncorrectRedirectUri, - #[error("The token is expired")] - ExpiredToken, - #[error("The token cannot be used yet")] - NotYet, - #[error("The bearer token has been revoked")] - JwtRevoked, -} - -fn verify_jwt( - token: &str, - self_id: &Url, - client_id: Option<Uuid>, -) -> Result<Claims, Expect<VerifyJwtError>> { - let key = secrets::signing_key()?; - let claims: Claims = token - .verify_with_key(&key) - .map_err(|e| VerifyJwtError::from(e))?; - - if &claims.iss != self_id { - yeet!(VerifyJwtError::IncorrectIssuer.into()) - } - - if let Some(client_id) = client_id { - if claims.client_id != client_id { - yeet!(VerifyJwtError::WrongClient.into()) - } - } - - if !claims.aud.contains(&self_id.to_string()) { - yeet!(VerifyJwtError::BadAudience.into()) - } - - let now = Utc::now(); - - if now > claims.exp { - yeet!(VerifyJwtError::ExpiredToken.into()) - } - - if let Some(nbf) = claims.nbf { - if now < nbf { - yeet!(VerifyJwtError::NotYet.into()) - } - } - - Ok(claims) -} - -pub async fn verify_auth_code<'c>( - db: &MySqlPool, - token: &str, - self_id: &Url, - client_id: Uuid, - redirect_uri: Url, -) -> Result<Claims, Expect<VerifyJwtError>> { - let claims = verify_jwt(token, self_id, Some(client_id))?; - - if let Some(claimed_uri) = &claims.redirect_uri { - if claimed_uri.clone() != redirect_uri { - yeet!(VerifyJwtError::IncorrectRedirectUri.into()); - } - } - - if db::delete_auth_code(db, claims.jti).await? { - db::delete_access_tokens_with_auth_code(db, claims.jti).await?; - db::revoke_refresh_tokens_with_auth_code(db, claims.jti).await?; - yeet!(VerifyJwtError::JwtRevoked.into()); - } - - Ok(claims) -} - -pub async fn verify_access_token<'c>( - db: impl Executor<'c, Database = MySql>, - token: &str, - self_id: &Url, - client_id: Uuid, -) -> Result<Claims, Expect<VerifyJwtError>> { - let claims = verify_jwt(token, self_id, Some(client_id))?; - - if !db::access_token_exists(db, claims.jti).await? { - yeet!(VerifyJwtError::JwtRevoked.into()) - } - - Ok(claims) -} - -pub async fn verify_refresh_token<'c>( - db: impl Executor<'c, Database = MySql>, - token: &str, - self_id: &Url, - client_id: Option<Uuid>, -) -> Result<Claims, Expect<VerifyJwtError>> { - let claims = verify_jwt(token, self_id, client_id)?; - - if db::refresh_token_revoked(db, claims.jti).await? { - yeet!(VerifyJwtError::JwtRevoked.into()) - } - - Ok(claims) -} +use chrono::{serde::ts_milliseconds, serde::ts_milliseconds_option, DateTime, Duration, Utc};
+use exun::{Expect, RawUnexpected, ResultErrorExt};
+use jwt::{SignWithKey, VerifyWithKey};
+use raise::yeet;
+use serde::{Deserialize, Serialize};
+use sqlx::{Executor, MySql, MySqlPool};
+use thiserror::Error;
+use url::Url;
+use uuid::Uuid;
+
+use super::{db, id::new_id, secrets};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum TokenType {
+ Authorization,
+ Access,
+ Refresh,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Claims {
+ iss: Url,
+ sub: Uuid,
+ aud: Box<[String]>,
+ #[serde(with = "ts_milliseconds")]
+ exp: DateTime<Utc>,
+ #[serde(with = "ts_milliseconds_option")]
+ nbf: Option<DateTime<Utc>>,
+ #[serde(with = "ts_milliseconds")]
+ iat: DateTime<Utc>,
+ jti: Uuid,
+ scope: Box<str>,
+ client_id: Uuid,
+ token_type: TokenType,
+ auth_code_id: Option<Uuid>,
+ redirect_uri: Option<Url>,
+}
+
+#[derive(Debug, Clone, Copy, sqlx::Type)]
+#[sqlx(rename_all = "kebab-case")]
+pub enum RevokedRefreshTokenReason {
+ ReusedAuthorizationCode,
+ NewRefreshToken,
+}
+
+impl Claims {
+ pub async fn auth_code<'c>(
+ db: &MySqlPool,
+ self_id: Url,
+ client_id: Uuid,
+ sub: Uuid,
+ scopes: &str,
+ redirect_uri: &Url,
+ ) -> Result<Self, RawUnexpected> {
+ let five_minutes = Duration::minutes(5);
+
+ let id = new_id(db, db::auth_code_exists).await?;
+ let iat = Utc::now();
+ let exp = iat + five_minutes;
+
+ db::create_auth_code(db, id, exp).await?;
+
+ let aud = [self_id.to_string(), client_id.to_string()].into();
+
+ Ok(Self {
+ iss: self_id,
+ sub,
+ aud,
+ exp,
+ nbf: None,
+ iat,
+ jti: id,
+ scope: scopes.into(),
+ client_id,
+ auth_code_id: Some(id),
+ token_type: TokenType::Authorization,
+ redirect_uri: Some(redirect_uri.clone()),
+ })
+ }
+
+ pub async fn access_token<'c>(
+ db: &MySqlPool,
+ auth_code_id: Option<Uuid>,
+ self_id: Url,
+ client_id: Uuid,
+ sub: Uuid,
+ duration: Duration,
+ scopes: &str,
+ ) -> Result<Self, RawUnexpected> {
+ let id = new_id(db, db::access_token_exists).await?;
+ let iat = Utc::now();
+ let exp = iat + duration;
+
+ db::create_access_token(db, id, auth_code_id, exp)
+ .await
+ .unexpect()?;
+
+ let aud = [self_id.to_string(), client_id.to_string()].into();
+
+ Ok(Self {
+ iss: self_id,
+ sub,
+ aud,
+ exp,
+ nbf: None,
+ iat,
+ jti: id,
+ scope: scopes.into(),
+ client_id,
+ auth_code_id,
+ token_type: TokenType::Access,
+ redirect_uri: None,
+ })
+ }
+
+ pub async fn refresh_token(
+ db: &MySqlPool,
+ other_token: &Claims,
+ ) -> Result<Self, RawUnexpected> {
+ let one_day = Duration::days(1);
+
+ let id = new_id(db, db::refresh_token_exists).await?;
+ let iat = Utc::now();
+ let exp = other_token.exp + one_day;
+
+ db::create_refresh_token(db, id, other_token.auth_code_id, exp).await?;
+
+ let mut claims = other_token.clone();
+ claims.exp = exp;
+ claims.iat = iat;
+ claims.jti = id;
+ claims.token_type = TokenType::Refresh;
+
+ Ok(claims)
+ }
+
+ pub async fn refreshed_access_token(
+ db: &MySqlPool,
+ refresh_token: &Claims,
+ exp_time: Duration,
+ ) -> Result<Self, RawUnexpected> {
+ let id = new_id(db, db::access_token_exists).await?;
+ let iat = Utc::now();
+ let exp = iat + exp_time;
+
+ db::create_access_token(db, id, refresh_token.auth_code_id, exp).await?;
+
+ let mut claims = refresh_token.clone();
+ claims.exp = exp;
+ claims.iat = iat;
+ claims.jti = id;
+ claims.token_type = TokenType::Access;
+
+ Ok(claims)
+ }
+
+ pub fn id(&self) -> Uuid {
+ self.jti
+ }
+
+ pub fn subject(&self) -> Uuid {
+ self.sub
+ }
+
+ pub fn expires_in(&self) -> i64 {
+ (self.exp - Utc::now()).num_seconds()
+ }
+
+ pub fn scopes(&self) -> &str {
+ &self.scope
+ }
+
+ pub fn to_jwt(&self) -> Result<Box<str>, RawUnexpected> {
+ let key = secrets::signing_key()?;
+ let jwt = self.sign_with_key(&key)?.into_boxed_str();
+ Ok(jwt)
+ }
+}
+
+#[derive(Debug, Error)]
+pub enum VerifyJwtError {
+ #[error("{0}")]
+ ParseJwtError(#[from] jwt::Error),
+ #[error("The issuer for this token is incorrect")]
+ IncorrectIssuer,
+ #[error("This bearer token was intended for a different client")]
+ WrongClient,
+ #[error("The given audience parameter does not contain this issuer")]
+ BadAudience,
+ #[error("The redirect URI doesn't match what's in the token")]
+ IncorrectRedirectUri,
+ #[error("The token is expired")]
+ ExpiredToken,
+ #[error("The token cannot be used yet")]
+ NotYet,
+ #[error("The bearer token has been revoked")]
+ JwtRevoked,
+}
+
+fn verify_jwt(
+ token: &str,
+ self_id: &Url,
+ client_id: Option<Uuid>,
+) -> Result<Claims, Expect<VerifyJwtError>> {
+ let key = secrets::signing_key()?;
+ let claims: Claims = token
+ .verify_with_key(&key)
+ .map_err(|e| VerifyJwtError::from(e))?;
+
+ if &claims.iss != self_id {
+ yeet!(VerifyJwtError::IncorrectIssuer.into())
+ }
+
+ if let Some(client_id) = client_id {
+ if claims.client_id != client_id {
+ yeet!(VerifyJwtError::WrongClient.into())
+ }
+ }
+
+ if !claims.aud.contains(&self_id.to_string()) {
+ yeet!(VerifyJwtError::BadAudience.into())
+ }
+
+ let now = Utc::now();
+
+ if now > claims.exp {
+ yeet!(VerifyJwtError::ExpiredToken.into())
+ }
+
+ if let Some(nbf) = claims.nbf {
+ if now < nbf {
+ yeet!(VerifyJwtError::NotYet.into())
+ }
+ }
+
+ Ok(claims)
+}
+
+pub async fn verify_auth_code<'c>(
+ db: &MySqlPool,
+ token: &str,
+ self_id: &Url,
+ client_id: Uuid,
+ redirect_uri: Url,
+) -> Result<Claims, Expect<VerifyJwtError>> {
+ let claims = verify_jwt(token, self_id, Some(client_id))?;
+
+ if let Some(claimed_uri) = &claims.redirect_uri {
+ if claimed_uri.clone() != redirect_uri {
+ yeet!(VerifyJwtError::IncorrectRedirectUri.into());
+ }
+ }
+
+ if db::delete_auth_code(db, claims.jti).await? {
+ db::delete_access_tokens_with_auth_code(db, claims.jti).await?;
+ db::revoke_refresh_tokens_with_auth_code(db, claims.jti).await?;
+ yeet!(VerifyJwtError::JwtRevoked.into());
+ }
+
+ Ok(claims)
+}
+
+pub async fn verify_access_token<'c>(
+ db: impl Executor<'c, Database = MySql>,
+ token: &str,
+ self_id: &Url,
+ client_id: Uuid,
+) -> Result<Claims, Expect<VerifyJwtError>> {
+ let claims = verify_jwt(token, self_id, Some(client_id))?;
+
+ if !db::access_token_exists(db, claims.jti).await? {
+ yeet!(VerifyJwtError::JwtRevoked.into())
+ }
+
+ Ok(claims)
+}
+
+pub async fn verify_refresh_token<'c>(
+ db: impl Executor<'c, Database = MySql>,
+ token: &str,
+ self_id: &Url,
+ client_id: Option<Uuid>,
+) -> Result<Claims, Expect<VerifyJwtError>> {
+ let claims = verify_jwt(token, self_id, client_id)?;
+
+ if db::refresh_token_revoked(db, claims.jti).await? {
+ yeet!(VerifyJwtError::JwtRevoked.into())
+ }
+
+ Ok(claims)
+}
diff --git a/src/services/mod.rs b/src/services/mod.rs index de08b58..4c69367 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,7 +1,7 @@ -pub mod authorization; -pub mod config; -pub mod crypto; -pub mod db; -pub mod id; -pub mod jwt; -pub mod secrets; +pub mod authorization;
+pub mod config;
+pub mod crypto;
+pub mod db;
+pub mod id;
+pub mod jwt;
+pub mod secrets;
diff --git a/src/services/secrets.rs b/src/services/secrets.rs index 241b2c5..e1d4992 100644 --- a/src/services/secrets.rs +++ b/src/services/secrets.rs @@ -1,24 +1,24 @@ -use std::env; - -use exun::*; -use hmac::{Hmac, Mac}; -use sha2::Sha256; - -/// This is a secret salt, needed for creating passwords. It's used as an extra -/// layer of security, on top of the salt that's already used. -pub fn pepper() -> Result<Box<[u8]>, RawUnexpected> { - let pepper = env::var("SECRET_SALT")?; - let pepper = hex::decode(pepper)?; - Ok(pepper.into_boxed_slice()) -} - -/// The URL to the MySQL database -pub fn database_url() -> Result<String, RawUnexpected> { - env::var("DATABASE_URL").unexpect() -} - -pub fn signing_key() -> Result<Hmac<Sha256>, RawUnexpected> { - let key = env::var("PRIVATE_KEY")?; - let key = Hmac::<Sha256>::new_from_slice(key.as_bytes())?; - Ok(key) -} +use std::env;
+
+use exun::*;
+use hmac::{Hmac, Mac};
+use sha2::Sha256;
+
+/// This is a secret salt, needed for creating passwords. It's used as an extra
+/// layer of security, on top of the salt that's already used.
+pub fn pepper() -> Result<Box<[u8]>, RawUnexpected> {
+ let pepper = env::var("SECRET_SALT")?;
+ let pepper = hex::decode(pepper)?;
+ Ok(pepper.into_boxed_slice())
+}
+
+/// The URL to the MySQL database
+pub fn database_url() -> Result<String, RawUnexpected> {
+ env::var("DATABASE_URL").unexpect()
+}
+
+pub fn signing_key() -> Result<Hmac<Sha256>, RawUnexpected> {
+ let key = env::var("PRIVATE_KEY")?;
+ let key = Hmac::<Sha256>::new_from_slice(key.as_bytes())?;
+ Ok(key)
+}
|
