use crate::authn::service::AuthnService;
use crate::authn::service::outcomes::SignupOutcome;
use crate::authn::{
error::AuthnError,
event::{AuthEventBuilder, AuthEventType},
store::{FactorStore, IdentityStore},
};
use crate::session::{
data::{WorkflowKind, WorkflowState},
extractor::AuthSession,
};
impl<I, F> AuthnService<I, F>
where
I: IdentityStore,
F: FactorStore<Error = I::Error>,
{
#[tracing::instrument(skip(self, user, session), fields(tenant = %tenant_identifier))]
pub async fn begin_signup(
&self,
user: crate::authn::types::User,
tenant_identifier: &str,
session: &AuthSession,
) -> Result<SignupOutcome, AuthnError<I::Error>> {
use crate::validation::{MAX_DISPLAY_NAME_BYTES, MAX_IDENTIFIER_BYTES, is_printable};
if user.identifier.is_empty()
|| user.identifier.len() > MAX_IDENTIFIER_BYTES
|| user.display_name.len() > MAX_DISPLAY_NAME_BYTES
|| tenant_identifier.is_empty()
|| tenant_identifier.len() > MAX_IDENTIFIER_BYTES
|| !is_printable(&user.identifier)
|| !is_printable(&user.display_name)
{
return Err(AuthnError::InvalidAssertion);
}
let tenant = self
.identity
.find_tenant(tenant_identifier)
.await
.map_err(AuthnError::Store)?;
let tenant = match tenant {
Some(t) if t.status.is_active() => t,
Some(_) => return Ok(SignupOutcome::TenantNotActive),
None => return Ok(SignupOutcome::TenantNotActive),
};
let existing = self
.identity
.find_user(&user.identifier, &tenant.id)
.await
.map_err(AuthnError::Store)?;
if let Some(existing_user) = existing {
if matches!(
existing_user.status,
crate::authn::types::EntityState::Candidate
) {
let now = self.clock.now();
let workflow = WorkflowState::new(WorkflowKind::Signup, 1, now);
session
.set_pending_workflow(existing_user.id, existing_user.tenant_id, workflow)
.await;
tracing::info!(
user_id = %existing_user.id,
tenant_id = %existing_user.tenant_id,
"resumed orphan Candidate signup"
);
return Ok(SignupOutcome::Started);
}
return Ok(SignupOutcome::AlreadyExists);
}
let user_id = user.id;
let tenant_id = tenant.id;
self.identity
.create_user(user)
.await
.map_err(AuthnError::Store)?;
let now = self.clock.now();
let workflow = WorkflowState::new(WorkflowKind::Signup, 1, now);
session
.set_pending_workflow(user_id, tenant_id, workflow)
.await;
self.emit_audit_at(
AuthEventBuilder::success(AuthEventType::SignupStarted)
.attributed_to(&user_id, &tenant_id),
now,
)
.await;
Ok(SignupOutcome::Started)
}
#[tracing::instrument(skip(self, session))]
pub async fn complete_signup(&self, session: &AuthSession) -> Result<(), AuthnError<I::Error>> {
let state = session.auth_state().await;
let (user_id, tenant_id) = match &state {
crate::session::data::AuthState::PendingWorkflow {
user_id,
tenant_id,
workflow,
} if workflow.kind == WorkflowKind::Signup => (*user_id, *tenant_id),
_ => return Err(AuthnError::NoFlow),
};
self.identity
.activate_user(&user_id)
.await
.map_err(AuthnError::Store)?;
let now = self.clock.now();
session.set_authenticated(user_id, tenant_id, now).await;
let sid = session.session_id().await;
if let Some(reg) = &self.registry
&& !reg.register(&user_id, &sid).await
{
tracing::error!(
user_id = %user_id,
tenant_id = %tenant_id,
"complete_signup register failed; clearing session and refusing"
);
session.clear().await;
return Err(AuthnError::NoFlow);
}
self.emit_audit_at(
AuthEventBuilder::success(AuthEventType::SignupCompleted)
.attributed_to(&user_id, &tenant_id)
.with_session(sid),
now,
)
.await;
Ok(())
}
}