-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial work on local password provider
- Loading branch information
1 parent
782623f
commit 88c5a22
Showing
7 changed files
with
263 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
CREATE TABLE user_credentials ( | ||
user_id VARCHAR(64) NOT NULL, | ||
password_hash VARCHAR(72) NOT NULL, | ||
PRIMARY KEY (user_id) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
use crate::authorization::{AuthorizationError, AuthorizationProvider, UserInformation}; | ||
use bcrypt::{hash_with_result, verify, Version}; | ||
use database::driver::Database; | ||
use database::user::User; | ||
use tap::TapOptional; | ||
use thiserror::Error; | ||
use tracing::{instrument, warn}; | ||
|
||
/// Credential provider utilizing the local database | ||
pub struct LocalCredentialsProvider<'a> { | ||
driver: &'a Database, | ||
} | ||
|
||
#[derive(Debug, Error)] | ||
pub enum LocalCredentialsProviderError { | ||
#[error(transparent)] | ||
Database(#[from] database::driver::Error), | ||
#[error(transparent)] | ||
Hashing(#[from] bcrypt::BcryptError), | ||
} | ||
|
||
impl<'a> LocalCredentialsProvider<'a> { | ||
/// Create a new local credentials provider. | ||
pub fn new(driver: &'a Database) -> Self { | ||
Self { driver } | ||
} | ||
} | ||
|
||
impl<'a> AuthorizationProvider for LocalCredentialsProvider<'a> { | ||
type Error = LocalCredentialsProviderError; | ||
|
||
#[instrument(skip(self, password))] | ||
async fn validate_credentials( | ||
&self, | ||
username: &str, | ||
password: &str, | ||
_: Option<&str>, | ||
) -> Result<UserInformation, AuthorizationError<Self::Error>> { | ||
// Fetch the user | ||
let user = User::get_by_email(self.driver, username) | ||
.await | ||
.map_err(|e| Self::Error::from(e))? | ||
.ok_or(AuthorizationError::InvalidCredentials)?; | ||
|
||
// Fetch the hash | ||
let stored_hash = user | ||
.get_password_hash(self.driver) | ||
.await | ||
.map_err(|e| Self::Error::from(e))? | ||
.tap_none(|| { | ||
warn!("No credentials stored for user with email {username}, but user does exist") | ||
}) | ||
.ok_or(AuthorizationError::InvalidCredentials)?; | ||
|
||
// Use bcrypt to verify the hash is correct | ||
let ok = verify(password, &stored_hash).map_err(|e| Self::Error::from(e))?; | ||
|
||
if ok { | ||
Ok(UserInformation { | ||
id: user.user_id, | ||
email: user.email, | ||
name: user.name, | ||
is_admin: user.is_admin, | ||
}) | ||
} else { | ||
Err(AuthorizationError::InvalidCredentials) | ||
} | ||
} | ||
|
||
fn supports_password_change(&self) -> bool { | ||
true | ||
} | ||
|
||
#[instrument(skip(self, new_password))] | ||
async fn set_password( | ||
&self, | ||
user_id: &str, | ||
new_password: &str, | ||
) -> Result<(), AuthorizationError<Self::Error>> { | ||
// Fetch the user | ||
let user = User::get_by_id(self.driver, user_id) | ||
.await | ||
.map_err(|e| Self::Error::from(e))? | ||
.ok_or(AuthorizationError::InvalidCredentials)?; | ||
|
||
// Generate the new password hash. | ||
// We are explicit with the format wanted, thus we use `hash_with_result`, rather than | ||
// `hash`. Although at the moment this block is identical to bcrypt's `hash` function, | ||
// this could change in the future. That would result in some rather annoying | ||
// differences in the way we hash. | ||
let new_password = hash_with_result(new_password, bcrypt::DEFAULT_COST) | ||
.map_err(|e| Self::Error::from(e))? | ||
.format_for_version(Version::TwoB); | ||
|
||
// Finally, update the database | ||
user.set_password_hash(self.driver, new_password) | ||
.await | ||
.map_err(|e| Self::Error::from(e))?; | ||
|
||
Ok(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
mod local_provider; | ||
|
||
use std::error::Error; | ||
use std::fmt::Debug; | ||
use thiserror::Error; | ||
|
||
#[derive(Debug, Error)] | ||
pub enum AuthorizationError<E: Error + Debug> { | ||
#[error("Invalid credentials provided")] | ||
InvalidCredentials, | ||
#[error("Two-factor authentication required")] | ||
TotpNeeded, | ||
#[error("Unsupported operation")] | ||
UnsupportedOperation, | ||
#[error(transparent)] | ||
Other(#[from] E), | ||
} | ||
|
||
/// Information about the authorized user | ||
#[derive(Debug)] | ||
pub struct UserInformation { | ||
/// The ID of the user. This ID should be used everywhere else, | ||
/// the authorization provider is the ultimate source of truth for which users | ||
/// can use the system. | ||
pub id: String, | ||
/// Whether the user is a global administrator. | ||
/// If true, all scope checks should be ignored. | ||
pub is_admin: bool, | ||
/// The name of the user. | ||
pub name: String, | ||
/// The email address of the user. | ||
pub email: String, | ||
} | ||
|
||
pub trait AuthorizationProvider { | ||
/// Error that can be returned by the authorization provider | ||
type Error: std::error::Error; | ||
|
||
/// Validate the given credentials. | ||
/// If the credentials are correct the associated user will be returned | ||
/// | ||
/// # Errors | ||
/// - If the credentials are invalid | ||
/// - If an underlying operaiton fails | ||
/// - If two-factor authentication is required | ||
async fn validate_credentials( | ||
&self, | ||
username: &str, | ||
password: &str, | ||
totp_code: Option<&str>, | ||
) -> Result<UserInformation, AuthorizationError<Self::Error>>; | ||
|
||
/// Whether the authorization provider supports changing the user's password. | ||
fn supports_password_change(&self) -> bool; | ||
|
||
/// Change the password of the user. | ||
/// Implementations do not have to support this operation, check this with [Self::supports_password_change]. | ||
/// | ||
/// # Errors | ||
/// - If the operation is not supported | ||
/// - If an underlying operation fails | ||
/// - If the user does not exist | ||
async fn set_password( | ||
&self, | ||
user_id: &str, | ||
new_password: &str, | ||
) -> Result<(), AuthorizationError<Self::Error>>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters