Skip to content

Commit

Permalink
Registration confirmation works!
Browse files Browse the repository at this point in the history
  • Loading branch information
marcua committed Aug 5, 2023
1 parent 227679d commit 0d4697f
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 70 deletions.
12 changes: 4 additions & 8 deletions src/ayb_db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,8 @@ impl AuthenticationMethodType {
)]
#[repr(i16)]
pub enum AuthenticationMethodStatus {
Unverified = 0,
Verified = 1,
Revoked = 2,
Verified = 0,
Revoked = 1,
}

impl fmt::Display for AuthenticationMethodStatus {
Expand All @@ -139,16 +138,14 @@ impl fmt::Display for AuthenticationMethodStatus {
impl AuthenticationMethodStatus {
pub fn from_i16(value: i16) -> AuthenticationMethodStatus {
match value {
0 => AuthenticationMethodStatus::Unverified,
1 => AuthenticationMethodStatus::Verified,
2 => AuthenticationMethodStatus::Revoked,
0 => AuthenticationMethodStatus::Verified,
1 => AuthenticationMethodStatus::Revoked,
_ => panic!("Unknown value: {}", value),
}
}

pub fn from_str(value: &str) -> AuthenticationMethodStatus {
match value {
"unverified" => AuthenticationMethodStatus::Unverified,
"verified" => AuthenticationMethodStatus::Verified,
"revoked" => AuthenticationMethodStatus::Revoked,
_ => panic!("Unknown value: {}", value),
Expand All @@ -157,7 +154,6 @@ impl AuthenticationMethodStatus {

pub fn to_str(&self) -> &str {
match self {
AuthenticationMethodStatus::Unverified => "unverified",
AuthenticationMethodStatus::Verified => "verified",
AuthenticationMethodStatus::Revoked => "revoked",
}
Expand Down
2 changes: 1 addition & 1 deletion src/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub async fn send_registration_email(
return send_email(
to,
"Your login credentials",
format!("To log in, type stacks client email-confirm email@example.com {token}"),
format!("To log in, type\n\tstacks client confirm {token}"),
config,
)
.await;
Expand Down
27 changes: 22 additions & 5 deletions src/http/client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::ayb_db::models::{DBType, EntityType};
use crate::error::AybError;
use crate::hosted_db::QueryResult;
use crate::http::structs::{Database, Entity};
use crate::http::structs::{APIKey, Database, EmptyResponse};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::de::DeserializeOwned;

Expand Down Expand Up @@ -37,6 +37,23 @@ impl AybClient {
}
}

pub async fn confirm(&self, authentication_token: &str) -> Result<APIKey, AybError> {
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("authentication-token"),
HeaderValue::from_str(authentication_token).unwrap(),
);

let response = reqwest::Client::new()
.post(self.make_url("confirm".to_owned()))
.headers(headers)
.send()
.await?;

self.handle_response(response, reqwest::StatusCode::OK)
.await
}

pub async fn create_database(
&self,
entity: &str,
Expand All @@ -50,7 +67,7 @@ impl AybClient {
);

let response = reqwest::Client::new()
.post(self.make_url(format!("{}/{}", entity, database)))
.post(self.make_url(format!("{}/{}/create", entity, database)))
.headers(headers)
.send()
.await?;
Expand Down Expand Up @@ -80,7 +97,7 @@ impl AybClient {
entity: &str,
email_address: &str,
entity_type: &EntityType,
) -> Result<Entity, AybError> {
) -> Result<EmptyResponse, AybError> {
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("email-address"),
Expand All @@ -92,12 +109,12 @@ impl AybClient {
);

let response = reqwest::Client::new()
.post(self.make_url(entity.to_owned()))
.post(self.make_url(format!("register/{}", entity)))
.headers(headers)
.send()
.await?;

self.handle_response(response, reqwest::StatusCode::CREATED)
self.handle_response(response, reqwest::StatusCode::OK)
.await
}
}
141 changes: 99 additions & 42 deletions src/http/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,90 @@ use crate::error::AybError;
use crate::hosted_db::paths::database_path;
use crate::hosted_db::{run_query, QueryResult};
use crate::http::structs::{
AuthenticationDetails, AuthenticationMode, AybConfig, Database as APIDatabase,
Entity as APIEntity, EntityDatabasePath, EntityPath,
APIKey as APIAPIKey, AuthenticationDetails, AuthenticationMode, AybConfig,
Database as APIDatabase, EmptyResponse, EntityDatabasePath, EntityPath,
};
use crate::http::tokens::create_token;
use crate::http::tokens::{decrypt_auth_token, encrypt_auth_token};
use crate::http::utils::get_header;
use actix_web::{post, web, HttpRequest, HttpResponse};

#[post("/v1/{entity}/{database}")]
#[post("/v1/confirm")]
async fn confirm(
req: HttpRequest,
ayb_db: web::Data<Box<dyn AybDb>>,
ayb_config: web::Data<AybConfig>,
) -> Result<HttpResponse, AybError> {
let auth_token = get_header(&req, "authentication-token")?;
let auth_details = decrypt_auth_token(auth_token, &ayb_config.authentication)?;

let created_entity = ayb_db
.get_or_create_entity(&Entity {
slug: auth_details.entity,
entity_type: auth_details.entity_type,
})
.await?;

// Ensure that there are no verified authentication methods, and
// check to see if this method has been previously attempted but
// not verified.
let auth_methods = ayb_db.list_authentication_methods(&created_entity).await?;
let mut already_verified = false;
let mut auth_method: Option<InstantiatedAuthenticationMethod> = None;
for method in auth_methods {
if method.status == (AuthenticationMethodStatus::Verified as i16) {
already_verified = true;
if method.method_type == (AuthenticationMethodType::Email as i16)
&& method.email_address == auth_details.email_address
{
auth_method = Some(method)
}
}
}

match AuthenticationMode::from_i16(auth_details.mode) {
AuthenticationMode::Register => {
// If registering, either accept this authentication
// method if it was previously created, or if there is no
// other verification method already verified.
if let None = auth_method {
if already_verified {
return Err(AybError {
message: format!("This entity has already been registered"),
});
}
ayb_db
.create_authentication_method(&AuthenticationMethod {
entity_id: created_entity.id,
method_type: AuthenticationMethodType::Email as i16,
status: AuthenticationMethodStatus::Verified as i16,
email_address: auth_details.email_address,
})
.await?;
}
}
AuthenticationMode::Login => {
// TODO(marcua): After creating the login endpoint,
// consider whether this code path is necessary, or if we
// can remove Register vs. Login mode. When doing that,
// think about entity that hasn't registered, has verified
// with the current authentication method, and has
// registered/verified with another authentication method.
if let None = auth_method {
return Err(AybError {
message: format!("Login failed due to unverified authentication method"),
});
}
}
}
// TODO(marcua): When we implement permissions, get_or_create default API keys.
// Ok(HttpResponse::Ok().json(APIAPIKey::from_persisted(&created_key)))
Ok(HttpResponse::Ok().json(APIAPIKey {
name: "default".to_string(),
key: "insecure, unimplemented".to_string(),
}))
}

#[post("/v1/{entity}/{database}/create")]
async fn create_database(
path: web::Path<EntityDatabasePath>,
req: HttpRequest,
Expand Down Expand Up @@ -49,7 +125,7 @@ async fn query(
Ok(web::Json(result))
}

#[post("/v1/{entity}")]
#[post("/v1/register/{entity}")]
async fn register(
path: web::Path<EntityPath>,
req: HttpRequest,
Expand All @@ -58,28 +134,22 @@ async fn register(
) -> Result<HttpResponse, AybError> {
let email_address = get_header(&req, "email-address")?;
let entity_type = get_header(&req, "entity-type")?;
let created_entity = ayb_db
.get_or_create_entity(&Entity {
slug: path.entity.clone(),
entity_type: EntityType::from_str(&entity_type) as i16,
})
.await?;
// Ensure that there are no verified authentication methods, and
// check to see if this method has been previously attempted but
// not verified.
let authentication_methods = ayb_db.list_authentication_methods(&created_entity).await?;
let desired_entity = ayb_db.get_entity(&path.entity).await;
// Ensure that there are no authentication methods aside from
// perhaps the currently requested one.
let mut already_verified = false;
let mut authentication_method: Option<InstantiatedAuthenticationMethod> = None;
for method in authentication_methods {
if method.status == (AuthenticationMethodStatus::Verified as i16) {
already_verified = true;
break;
}
if method.status == (AuthenticationMethodStatus::Unverified as i16)
&& method.method_type == (AuthenticationMethodType::Email as i16)
&& method.email_address == email_address
{
authentication_method = Some(method)
if let Ok(instantiated_entity) = desired_entity {
let auth_methods = ayb_db
.list_authentication_methods(&instantiated_entity)
.await?;
for method in auth_methods {
if AuthenticationMethodType::from_i16(method.method_type)
!= AuthenticationMethodType::Email
|| method.email_address != email_address
{
already_verified = true;
break;
}
}
}

Expand All @@ -89,29 +159,16 @@ async fn register(
});
}

if let None = authentication_method {
authentication_method = Some(
ayb_db
.create_authentication_method(&AuthenticationMethod {
entity_id: created_entity.id,
method_type: AuthenticationMethodType::Email as i16,
status: AuthenticationMethodStatus::Unverified as i16,
email_address: email_address.to_owned(),
})
.await?,
);
}

let token = create_token(
let token = encrypt_auth_token(
&AuthenticationDetails {
version: 1,
mode: AuthenticationMode::Register as i16,
entity: path.entity.clone(),
entity_type: created_entity.entity_type,
entity_type: EntityType::from_str(&entity_type) as i16,
email_address: email_address.to_owned(),
},
&ayb_config.authentication,
)?;
send_registration_email(&email_address, &token, &ayb_config.email).await?;
Ok(HttpResponse::Created().json(APIEntity::from_persisted(&created_entity)))
Ok(HttpResponse::Ok().json(EmptyResponse {}))
}
3 changes: 2 additions & 1 deletion src/http/server.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::ayb_db::db_interfaces::connect_to_ayb_db;
use crate::http::endpoints::{create_database, query, register};
use crate::http::endpoints::{confirm, create_database, query, register};
use crate::http::structs::AybConfig;
use actix_web::{middleware, web, App, HttpServer};
use dyn_clone::clone_box;
Expand All @@ -11,6 +11,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(create_database);
cfg.service(query);
cfg.service(register);
cfg.service(confirm);
}

pub async fn run_server(config_path: &PathBuf) -> std::io::Result<()> {
Expand Down
11 changes: 10 additions & 1 deletion src/http/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl fmt::Display for AuthenticationMode {
}

impl AuthenticationMode {
pub fn from_u16(value: u16) -> AuthenticationMode {
pub fn from_i16(value: i16) -> AuthenticationMode {
match value {
0 => AuthenticationMode::Register,
1 => AuthenticationMode::Login,
Expand Down Expand Up @@ -125,3 +125,12 @@ pub struct AuthenticationDetails {
pub entity_type: i16,
pub email_address: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct APIKey {
pub name: String,
pub key: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct EmptyResponse {}
32 changes: 21 additions & 11 deletions src/http/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,30 @@ use crate::http::structs::{AuthenticationDetails, AybConfigAuthentication};
use fernet::Fernet;
use serde_json;

pub fn create_token(
authentication_details: &AuthenticationDetails,
auth_config: &AybConfigAuthentication,
) -> Result<String, AybError> {
// println!("key: {}", fernet::Fernet::generate_key());
println!("key: {}", auth_config.fernet_key);
// TODO(marcua): Add `ayb server show_config` and `ayb server
// create_config` to make setting up keys easier.
fn get_fernet_generator(auth_config: &AybConfigAuthentication) -> Result<Fernet, AybError> {
match Fernet::new(&auth_config.fernet_key) {
Some(token_generator) => {
Ok(token_generator.encrypt(&serde_json::to_vec(&authentication_details)?))
}
Some(token_generator) => Ok(token_generator),
None => Err(AybError {
message: "Missing or invalid Fernet key".to_string(),
}),
}
}

pub fn encrypt_auth_token(
authentication_details: &AuthenticationDetails,
auth_config: &AybConfigAuthentication,
) -> Result<String, AybError> {
// TODO(marcua): Add `ayb server show_config` and `ayb server
// create_config` to make setting up keys easier.
// println!("key: {}", fernet::Fernet::generate_key());
let generator = get_fernet_generator(auth_config)?;
Ok(generator.encrypt(&serde_json::to_vec(&authentication_details)?))
}

pub fn decrypt_auth_token(
cyphertext: String,
auth_config: &AybConfigAuthentication,
) -> Result<AuthenticationDetails, AybError> {
let generator = get_fernet_generator(auth_config)?;
Ok(serde_json::from_slice(&generator.decrypt(&cyphertext)?)?)
}
Loading

0 comments on commit 0d4697f

Please sign in to comment.