Allow passwordless registration

This commit is contained in:
silverpill 2022-02-15 19:26:06 +00:00
parent 6ddfb5b52d
commit 227e3d3729
7 changed files with 43 additions and 13 deletions

View file

@ -20,6 +20,7 @@ paths:
- password - password
- ethereum - ethereum
- eip4361 - eip4361
example: eip4361
username: username:
description: User name (required if grant type is "password"). description: User name (required if grant type is "password").
type: string type: string
@ -75,21 +76,23 @@ paths:
description: The desired username for the account. description: The desired username for the account.
type: string type: string
password: password:
description: The password to be used for login. description: The password to be used for login. Either password or EIP-4361 message must be provided.
type: string type: string
example: null
message: message:
description: EIP-4361 message description: EIP-4361 message
type: string type: string
example: "example.com wants you to sign in with your Ethereum account:"
signature: signature:
description: EIP-4361 signature (required if message is present) description: EIP-4361 signature (required if message is present)
type: string type: string
example: 0x905...
invite_code: invite_code:
description: Invite code description: Invite code
type: string type: string
example: 9b288bfa7dc75fff53e98aa4d76e77d5 example: 9b288bfa7dc75fff53e98aa4d76e77d5
required: required:
- username - username
- password
responses: responses:
201: 201:
description: Successful operation description: Successful operation

View file

@ -0,0 +1 @@
ALTER TABLE user_account ALTER COLUMN password_hash DROP NOT NULL;

View file

@ -23,7 +23,7 @@ CREATE TABLE user_invite_code (
CREATE TABLE user_account ( CREATE TABLE user_account (
id UUID PRIMARY KEY REFERENCES actor_profile (id) ON DELETE CASCADE, id UUID PRIMARY KEY REFERENCES actor_profile (id) ON DELETE CASCADE,
wallet_address VARCHAR(100) UNIQUE, wallet_address VARCHAR(100) UNIQUE,
password_hash VARCHAR(200) NOT NULL, password_hash VARCHAR(200),
private_key TEXT NOT NULL, private_key TEXT NOT NULL,
invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL, invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()

View file

@ -104,7 +104,7 @@ impl Account {
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct AccountCreateData { pub struct AccountCreateData {
pub username: String, pub username: String,
pub password: String, pub password: Option<String>,
pub message: Option<String>, pub message: Option<String>,
pub signature: Option<String>, pub signature: Option<String>,
@ -117,6 +117,9 @@ impl AccountCreateData {
pub fn clean(&self) -> Result<(), ValidationError> { pub fn clean(&self) -> Result<(), ValidationError> {
validate_username(&self.username)?; validate_username(&self.username)?;
validate_local_username(&self.username)?; validate_local_username(&self.username)?;
if self.password.is_none() && self.message.is_none() {
return Err(ValidationError("password or EIP-4361 message is required"));
};
Ok(()) Ok(())
} }
} }
@ -212,6 +215,19 @@ mod tests {
const INSTANCE_URL: &str = "https://example.com"; const INSTANCE_URL: &str = "https://example.com";
#[test]
fn test_validate_account_create_data() {
let account_data = AccountCreateData {
username: "test".to_string(),
password: None,
message: None,
signature: Some("test".to_string()),
invite_code: None,
};
let error = account_data.clean().unwrap_err();
assert_eq!(error.to_string(), "password or EIP-4361 message is required");
}
#[test] #[test]
fn test_create_account_from_profile() { fn test_create_account_from_profile() {
let profile = DbActorProfile { let profile = DbActorProfile {

View file

@ -72,6 +72,14 @@ pub async fn create_account(
return Err(ValidationError("invalid invite code").into()); return Err(ValidationError("invalid invite code").into());
} }
} }
let password_hash = if let Some(password) = account_data.password.as_ref() {
let password_hash = hash_password(password)
.map_err(|_| HttpError::InternalError)?;
Some(password_hash)
} else {
None
};
let wallet_address = if let Some(message) = account_data.message.as_ref() { let wallet_address = if let Some(message) = account_data.message.as_ref() {
let signature = account_data.signature.as_ref() let signature = account_data.signature.as_ref()
.ok_or(ValidationError("signature is required"))?; .ok_or(ValidationError("signature is required"))?;
@ -85,8 +93,10 @@ pub async fn create_account(
} else { } else {
None None
}; };
assert!(password_hash.is_some() || wallet_address.is_some());
if let Some(blockchain_config) = config.blockchain.as_ref() { if let Some(blockchain_config) = config.blockchain.as_ref() {
// Wallet address is required only if blockchain integration is enabled // Wallet address is required if blockchain integration is enabled
let wallet_address = wallet_address.as_ref() let wallet_address = wallet_address.as_ref()
.ok_or(ValidationError("wallet address is required"))?; .ok_or(ValidationError("wallet address is required"))?;
let is_allowed = is_allowed_user(blockchain_config, wallet_address).await let is_allowed = is_allowed_user(blockchain_config, wallet_address).await
@ -95,9 +105,7 @@ pub async fn create_account(
return Err(ValidationError("not allowed to sign up").into()); return Err(ValidationError("not allowed to sign up").into());
} }
} }
// Hash password and generate private key // Generate RSA private key for actor
let password_hash = hash_password(&account_data.password)
.map_err(|_| HttpError::InternalError)?;
let private_key = match web::block(generate_private_key).await { let private_key = match web::block(generate_private_key).await {
Ok(private_key) => private_key, Ok(private_key) => private_key,
Err(_) => return Err(HttpError::InternalError), Err(_) => return Err(HttpError::InternalError),

View file

@ -58,9 +58,11 @@ async fn token_view(
if request_data.grant_type == "password" || request_data.grant_type == "ethereum" { if request_data.grant_type == "password" || request_data.grant_type == "ethereum" {
let password = request_data.password.as_ref() let password = request_data.password.as_ref()
.ok_or(ValidationError("password is required"))?; .ok_or(ValidationError("password is required"))?;
let password_hash = user.password_hash.as_ref()
.ok_or(ValidationError("password auth is disabled"))?;
let password_correct = verify_password( let password_correct = verify_password(
&user.password_hash, password_hash,
&password, password,
).map_err(|_| HttpError::InternalError)?; ).map_err(|_| HttpError::InternalError)?;
if !password_correct { if !password_correct {
return Err(ValidationError("incorrect password").into()); return Err(ValidationError("incorrect password").into());

View file

@ -11,7 +11,7 @@ use crate::models::profiles::types::DbActorProfile;
pub struct DbUser { pub struct DbUser {
pub id: Uuid, pub id: Uuid,
pub wallet_address: Option<String>, pub wallet_address: Option<String>,
pub password_hash: String, pub password_hash: Option<String>,
pub private_key: String, pub private_key: String,
pub invite_code: Option<String>, pub invite_code: Option<String>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
@ -23,7 +23,7 @@ pub struct DbUser {
pub struct User { pub struct User {
pub id: Uuid, pub id: Uuid,
pub wallet_address: Option<String>, pub wallet_address: Option<String>,
pub password_hash: String, pub password_hash: Option<String>,
pub private_key: String, pub private_key: String,
pub profile: DbActorProfile, pub profile: DbActorProfile,
} }
@ -47,7 +47,7 @@ impl User {
#[cfg_attr(test, derive(Default))] #[cfg_attr(test, derive(Default))]
pub struct UserCreateData { pub struct UserCreateData {
pub username: String, pub username: String,
pub password_hash: String, pub password_hash: Option<String>,
pub private_key_pem: String, pub private_key_pem: String,
pub wallet_address: Option<String>, pub wallet_address: Option<String>,
pub invite_code: Option<String>, pub invite_code: Option<String>,