backend signup
This commit is contained in:
+61
-56
@@ -7,10 +7,9 @@ use backend::auth::passkeys::{
|
||||
PasskeyLoginFinishPayload, PasskeyLoginStartPayload, PasskeyRegistrationFinishPayload,
|
||||
RegistrationChallengeResponse,
|
||||
};
|
||||
use backend::jobs::JOB_PROVISION_TENANT;
|
||||
use backend::models::{Job, NewUserMembership, TenantStatus, UserPasskey};
|
||||
use backend::models::{NewUserMembership, TenantStatus, UserPasskey};
|
||||
use backend::openapi::schemas::PasskeySummary;
|
||||
use backend::schema::{jobs, tenants, user_memberships, users};
|
||||
use backend::schema::{tenants, user_memberships, users};
|
||||
use common::{acquire_db_lock, body_to_vec, TestApp};
|
||||
use diesel::prelude::*;
|
||||
use serde::Deserialize;
|
||||
@@ -43,6 +42,12 @@ struct LoginResponse {
|
||||
tenant: LoginTenant,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SignupStartResponse {
|
||||
signup_token: String,
|
||||
challenge: RegistrationChallengeResponse,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TenantSelectionResponse {
|
||||
access_token: String,
|
||||
@@ -93,72 +98,40 @@ async fn login_rejects_unknown_user() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn signup_creates_user_tenant_and_membership() -> Result<()> {
|
||||
async fn signup_start_and_finish_require_valid_passkey() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let payload = json!({
|
||||
"username": "signup-user",
|
||||
"password": "super-secret",
|
||||
});
|
||||
let payload = json!({ "username": "signup-user" });
|
||||
|
||||
let response = app.post_json("/api/auth/signup", &payload, None).await?;
|
||||
let response = app
|
||||
.post_json("/api/auth/signup/start", &payload, None)
|
||||
.await?;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = body_to_vec(response.into_body()).await?;
|
||||
let login: LoginResponse = serde_json::from_slice(&body)?;
|
||||
let start: SignupStartResponse = serde_json::from_slice(&body)?;
|
||||
assert!(!start.signup_token.is_empty());
|
||||
assert_ne!(start.challenge.challenge_id, Uuid::nil());
|
||||
|
||||
// session works immediately
|
||||
let me = app.get("/api/auth/me", Some(&login.access_token)).await?;
|
||||
assert_eq!(me.status(), StatusCode::OK);
|
||||
|
||||
app.with_conn(move |conn| {
|
||||
let user: backend::models::User = users::table
|
||||
.filter(users::username.eq("signup-user"))
|
||||
.first(conn)?;
|
||||
|
||||
let tenant: backend::models::Tenant = tenants::table
|
||||
.filter(tenants::name.eq("signup-user"))
|
||||
.first(conn)?;
|
||||
|
||||
assert_eq!(tenant.status, TenantStatus::Creating);
|
||||
assert_eq!(tenant.created_by, Some(user.id));
|
||||
|
||||
let membership_exists: bool = diesel::select(diesel::dsl::exists(
|
||||
user_memberships::table
|
||||
.filter(user_memberships::user_id.eq(user.id))
|
||||
.filter(user_memberships::tenant_id.eq(tenant.id)),
|
||||
app.with_conn(|conn| {
|
||||
let exists: bool = diesel::select(diesel::dsl::exists(
|
||||
users::table.filter(users::username.eq("signup-user")),
|
||||
))
|
||||
.get_result(conn)?;
|
||||
assert!(
|
||||
!membership_exists,
|
||||
"membership should be enqueued, not created synchronously"
|
||||
);
|
||||
|
||||
let job: Job = jobs::table
|
||||
.filter(jobs::tenant_id.eq(tenant.id))
|
||||
.filter(jobs::job_type.eq(JOB_PROVISION_TENANT))
|
||||
.first(conn)
|
||||
.context("provision job missing")?;
|
||||
|
||||
let members = job
|
||||
.payload
|
||||
.get("members")
|
||||
.and_then(|value| value.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
assert!(members.iter().any(|value| {
|
||||
value
|
||||
.as_str()
|
||||
.and_then(|id| Uuid::parse_str(id).ok())
|
||||
.map(|parsed| parsed == user.id)
|
||||
.unwrap_or(false)
|
||||
}));
|
||||
|
||||
assert!(!exists);
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
|
||||
let finish_payload = json!({
|
||||
"signup_token": start.signup_token,
|
||||
"credential": fake_register_credential(),
|
||||
});
|
||||
let finish_response = app
|
||||
.post_json("/api/auth/signup/finish", &finish_payload, None)
|
||||
.await?;
|
||||
assert_eq!(finish_response.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -329,6 +302,7 @@ async fn delete_passkey_soft_revokes() -> Result<()> {
|
||||
let password = "secret";
|
||||
let user_id = app.insert_user("passkey-delete", password, "admin").await?;
|
||||
let passkey_id = app.insert_passkey(user_id, Some("Phone")).await?;
|
||||
app.insert_passkey(user_id, Some("Backup")).await?;
|
||||
let (session, _) = login_with_session(&app, "passkey-delete", password).await?;
|
||||
|
||||
let response = app
|
||||
@@ -355,6 +329,37 @@ async fn delete_passkey_soft_revokes() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_passkey_prevents_last() -> Result<()> {
|
||||
let _lock = acquire_db_lock().await;
|
||||
let app = TestApp::new().await?;
|
||||
|
||||
let password = "secret";
|
||||
let user_id = app.insert_user("passkey-guard", password, "admin").await?;
|
||||
let first_id = app.insert_passkey(user_id, Some("Key A")).await?;
|
||||
let last_id = app.insert_passkey(user_id, Some("Key B")).await?;
|
||||
let (session, _) = login_with_session(&app, "passkey-guard", password).await?;
|
||||
|
||||
let response = app
|
||||
.delete(
|
||||
&format!("/api/profile/passkeys/{}", first_id),
|
||||
Some(&session.access_token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let block_response = app
|
||||
.delete(
|
||||
&format!("/api/profile/passkeys/{}", last_id),
|
||||
Some(&session.access_token),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(block_response.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
app.cleanup().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fake_register_credential() -> RegisterPublicKeyCredential {
|
||||
RegisterPublicKeyCredential {
|
||||
id: "fake-passkey".to_string(),
|
||||
|
||||
@@ -240,12 +240,14 @@ impl TestApp {
|
||||
let passkey_id = Uuid::new_v4();
|
||||
let nickname = nickname.map(|value| value.to_string());
|
||||
self.with_conn(move |conn| {
|
||||
let credential_id = passkey_id.as_bytes().to_vec();
|
||||
let public_key = passkey_id.as_bytes().iter().copied().collect::<Vec<u8>>();
|
||||
let passkey = NewUserPasskey {
|
||||
id: passkey_id,
|
||||
user_id,
|
||||
credential_id: vec![1, 2, 3],
|
||||
public_key: vec![4, 5, 6],
|
||||
credential: json!({ "dummy": true }),
|
||||
credential_id,
|
||||
public_key,
|
||||
credential: json!({ "dummy": passkey_id.to_string() }),
|
||||
sign_count: 0,
|
||||
transports: vec![Some("usb".to_string())],
|
||||
aaguid: None,
|
||||
|
||||
Reference in New Issue
Block a user