1use std::sync::Arc;
7
8use arcly_http_core::auth::JwtService;
9use rand::RngCore;
10
11use crate::consent::ConsentStore;
12use crate::error::{IdentityError, Result};
13use crate::federation::{
14 FederatedIdentity, FederatedIdentityStore, FederatedOutcome, FederatedProfile, FederationAudit,
15 FederationDecision, FederationEvent, LinkingConfig,
16};
17use crate::lockout::LockoutPolicy;
18use crate::mfa::MfaVerifier;
19use crate::model::{AccountStatus, Identity, LoginOutcome, RegisterRequest, TokenResponse};
20use crate::password::{BreachChecker, PasswordHasher, PasswordPolicy};
21use crate::risk::{RiskEngine, RiskSignal, RiskVerdict};
22use crate::session::{ChallengeStore, RefreshStore};
23use crate::store::{CredentialStore, UserStore};
24
25#[derive(Debug, Clone)]
27pub struct IdentityConfig {
28 pub require_email_verification: bool,
31 pub bind_tenant: bool,
34 pub invite_only: bool,
37 pub mfa_challenge_ttl_secs: u64,
39}
40
41impl Default for IdentityConfig {
42 fn default() -> Self {
43 Self {
44 require_email_verification: false,
45 bind_tenant: true,
46 invite_only: false,
47 mfa_challenge_ttl_secs: 300,
48 }
49 }
50}
51
52#[derive(Clone)]
55pub struct IdentityService {
56 users: Arc<dyn UserStore>,
57 creds: Arc<dyn CredentialStore>,
58 hasher: Arc<dyn PasswordHasher>,
59 jwt: Arc<JwtService>,
60 refresh: Arc<dyn RefreshStore>,
61 challenges: Arc<dyn ChallengeStore>,
62 lockout: Option<Arc<LockoutPolicy>>,
63 mfa: Option<Arc<dyn MfaVerifier>>,
64 federated: Option<Arc<dyn FederatedIdentityStore>>,
65 risk: Option<Arc<dyn RiskEngine>>,
66 fed_audit: Option<Arc<dyn FederationAudit>>,
67 linking: LinkingConfig,
68 password_policy: PasswordPolicy,
69 breach: Option<Arc<dyn BreachChecker>>,
70 consent: Option<Arc<dyn ConsentStore>>,
71 required_consents: Vec<String>,
72 config: IdentityConfig,
73}
74
75pub struct IdentityServiceBuilder {
77 users: Arc<dyn UserStore>,
78 creds: Arc<dyn CredentialStore>,
79 hasher: Arc<dyn PasswordHasher>,
80 jwt: Arc<JwtService>,
81 refresh: Arc<dyn RefreshStore>,
82 challenges: Arc<dyn ChallengeStore>,
83 lockout: Option<Arc<LockoutPolicy>>,
84 mfa: Option<Arc<dyn MfaVerifier>>,
85 federated: Option<Arc<dyn FederatedIdentityStore>>,
86 risk: Option<Arc<dyn RiskEngine>>,
87 fed_audit: Option<Arc<dyn FederationAudit>>,
88 linking: LinkingConfig,
89 password_policy: PasswordPolicy,
90 breach: Option<Arc<dyn BreachChecker>>,
91 consent: Option<Arc<dyn ConsentStore>>,
92 required_consents: Vec<String>,
93 config: IdentityConfig,
94}
95
96impl IdentityService {
97 pub fn builder(
100 users: Arc<dyn UserStore>,
101 creds: Arc<dyn CredentialStore>,
102 hasher: Arc<dyn PasswordHasher>,
103 jwt: Arc<JwtService>,
104 refresh: Arc<dyn RefreshStore>,
105 challenges: Arc<dyn ChallengeStore>,
106 ) -> IdentityServiceBuilder {
107 IdentityServiceBuilder {
108 users,
109 creds,
110 hasher,
111 jwt,
112 refresh,
113 challenges,
114 lockout: None,
115 mfa: None,
116 federated: None,
117 risk: None,
118 fed_audit: None,
119 linking: LinkingConfig::default(),
120 password_policy: PasswordPolicy::default(),
121 breach: None,
122 consent: None,
123 required_consents: Vec::new(),
124 config: IdentityConfig::default(),
125 }
126 }
127
128 pub async fn register(&self, req: RegisterRequest) -> Result<Identity> {
132 if self.config.invite_only {
133 return Err(IdentityError::Forbidden);
134 }
135 if self
137 .users
138 .find_by_email(req.tenant.as_deref(), &req.email)
139 .await?
140 .is_some()
141 {
142 return Err(IdentityError::AlreadyExists);
143 }
144
145 let status = if self.config.require_email_verification {
146 AccountStatus::Pending
147 } else {
148 AccountStatus::Active
149 };
150 let identity = Identity {
151 id: new_id(),
152 tenant: req.tenant.clone(),
153 email: Some(req.email.clone()),
154 email_verified: false,
155 phone: None,
156 phone_verified: false,
157 status,
158 roles: vec!["user".to_owned()],
159 perms: Vec::new(),
160 mfa: Default::default(),
161 attributes: req.attributes.clone(),
162 };
163 if let Some(password) = &req.password {
166 self.password_policy.validate(password)?;
167 if let Some(checker) = &self.breach {
168 if checker.is_breached(password).await? {
169 return Err(IdentityError::PasswordRejected(
170 "this password has appeared in a known data breach".into(),
171 ));
172 }
173 }
174 }
175
176 self.users.insert(&identity).await?;
177
178 if let Some(password) = req.password {
179 let phc = self.hasher.hash(password).await?;
180 self.creds.set_password(&identity.id, &phc).await?;
181 }
182 Ok(identity)
183 }
184
185 pub async fn login(
188 &self,
189 tenant: Option<&str>,
190 email: &str,
191 password: &str,
192 ) -> Result<LoginOutcome> {
193 self.login_bound(tenant, email, password, None).await
194 }
195
196 pub async fn login_bound(
201 &self,
202 tenant: Option<&str>,
203 email: &str,
204 password: &str,
205 cnf_jkt: Option<&str>,
206 ) -> Result<LoginOutcome> {
207 if let Some(lock) = &self.lockout {
210 let principal = format!("{}::{}", tenant.unwrap_or("-"), email.to_ascii_lowercase());
211 lock.check(&principal).await?;
212 }
213
214 let user = self.users.find_by_email(tenant, email).await?;
215
216 let Some(user) = user else {
219 return Err(IdentityError::InvalidCredentials);
220 };
221
222 if !user.status.can_authenticate() {
223 return Err(IdentityError::InvalidCredentials);
225 }
226
227 let phc = self
228 .creds
229 .get_password(&user.id)
230 .await?
231 .ok_or(IdentityError::InvalidCredentials)?;
232
233 let ok = self.hasher.verify(password.to_owned(), phc.clone()).await?;
234 if !ok {
235 return Err(IdentityError::InvalidCredentials);
236 }
237
238 if self.hasher.needs_rehash(&phc) {
240 if let Ok(new_phc) = self.hasher.hash(password.to_owned()).await {
241 let _ = self.creds.set_password(&user.id, &new_phc).await;
242 }
243 }
244
245 if user.mfa.is_enrolled() {
246 let challenge_id = new_id();
247 self.challenges
248 .put(&challenge_id, &user.id, self.config.mfa_challenge_ttl_secs)
249 .await?;
250 return Ok(LoginOutcome::MfaChallenge {
251 mfa_required: true,
252 challenge_id,
253 methods: self.enrolled_methods(&user),
254 });
255 }
256
257 Ok(LoginOutcome::Authenticated(
258 self.mint_bound(&user, cnf_jkt).await?,
259 ))
260 }
261
262 pub async fn verify_mfa(
265 &self,
266 challenge_id: &str,
267 method: &str,
268 code: &str,
269 ) -> Result<TokenResponse> {
270 let challenge = self
271 .challenges
272 .take(challenge_id)
273 .await?
274 .ok_or(IdentityError::InvalidToken)?;
275
276 let verifier = self.mfa.as_ref().ok_or(IdentityError::MfaFailed)?;
277 let ok = verifier.verify(&challenge.user_id, method, code).await?;
278 if !ok {
279 return Err(IdentityError::MfaFailed);
280 }
281 let user = self
282 .users
283 .find_by_id(&challenge.user_id)
284 .await?
285 .ok_or(IdentityError::NotFound)?;
286 self.mint(&user).await
287 }
288
289 pub async fn login_federated(
296 &self,
297 tenant: Option<&str>,
298 profile: &FederatedProfile,
299 signal: Option<RiskSignal>,
300 ) -> Result<FederatedOutcome> {
301 let fed = self
302 .federated
303 .as_ref()
304 .ok_or_else(|| IdentityError::Internal("federated store not configured".into()))?;
305
306 if let Some(uid) = fed
308 .find_user(&profile.provider, &profile.provider_id)
309 .await?
310 {
311 let user = self
312 .users
313 .find_by_id(&uid)
314 .await?
315 .ok_or(IdentityError::NotFound)?;
316 self.audit_fed(
317 profile,
318 Some(&user.id),
319 tenant,
320 FederationDecision::ReturningLogin,
321 &signal,
322 )
323 .await;
324 return self.finish_federated(user, signal).await;
325 }
326
327 let (user, decision) = match self.resolve_account(tenant, profile).await? {
329 Resolution::LinkRequired(email) => {
330 self.audit_fed(
331 profile,
332 None,
333 tenant,
334 FederationDecision::LinkRequired,
335 &signal,
336 )
337 .await;
338 return Ok(FederatedOutcome::LinkRequired { email });
339 }
340 Resolution::Denied => {
341 self.audit_fed(profile, None, tenant, FederationDecision::Denied, &signal)
342 .await;
343 return Err(IdentityError::Forbidden);
344 }
345 Resolution::Existing(user) => (user, FederationDecision::AutoLinked),
346 Resolution::Provisioned(user) => (user, FederationDecision::Provisioned),
347 };
348
349 fed.link(FederatedIdentity {
351 provider: profile.provider.clone(),
352 provider_id: profile.provider_id.clone(),
353 user_id: user.id.clone(),
354 linked_at: JwtService::unix_now(),
355 })
356 .await?;
357 self.audit_fed(profile, Some(&user.id), tenant, decision, &signal)
358 .await;
359 self.finish_federated(user, signal).await
360 }
361
362 pub async fn link_federated(&self, user_id: &str, profile: &FederatedProfile) -> Result<()> {
367 let fed = self
368 .federated
369 .as_ref()
370 .ok_or_else(|| IdentityError::Internal("federated store not configured".into()))?;
371 if let Some(existing) = fed
373 .find_user(&profile.provider, &profile.provider_id)
374 .await?
375 {
376 if existing != user_id {
377 return Err(IdentityError::Forbidden);
378 }
379 return Ok(());
380 }
381 fed.link(FederatedIdentity {
382 provider: profile.provider.clone(),
383 provider_id: profile.provider_id.clone(),
384 user_id: user_id.to_owned(),
385 linked_at: JwtService::unix_now(),
386 })
387 .await
388 }
389
390 async fn resolve_account(
392 &self,
393 tenant: Option<&str>,
394 profile: &FederatedProfile,
395 ) -> Result<Resolution> {
396 if let Some(email) = profile.email.as_deref() {
399 if let Some(existing) = self.users.find_by_email(tenant, email).await? {
400 let both_verified = profile.email_verified && existing.email_verified;
406 return Ok(if both_verified && self.linking.auto_link_verified_email {
407 Resolution::Existing(existing)
408 } else {
409 Resolution::LinkRequired(email.to_owned())
410 });
411 }
412 }
413 if self.linking.jit_provisioning && !self.config.invite_only {
415 Ok(Resolution::Provisioned(
416 self.provision_from_profile(tenant, profile).await?,
417 ))
418 } else {
419 Ok(Resolution::Denied)
420 }
421 }
422
423 async fn provision_from_profile(
425 &self,
426 tenant: Option<&str>,
427 profile: &FederatedProfile,
428 ) -> Result<Identity> {
429 let identity = Identity {
430 id: new_id(),
431 tenant: tenant.map(str::to_owned),
432 email: profile.email.clone(),
433 email_verified: profile.email_verified,
436 phone: None,
437 phone_verified: false,
438 status: AccountStatus::Active,
439 roles: vec!["user".to_owned()],
440 perms: Vec::new(),
441 mfa: Default::default(),
442 attributes: profile.attributes.clone(),
443 };
444 self.users.insert(&identity).await?;
445 Ok(identity)
446 }
447
448 async fn finish_federated(
451 &self,
452 user: Identity,
453 signal: Option<RiskSignal>,
454 ) -> Result<FederatedOutcome> {
455 if let (Some(engine), Some(sig)) = (&self.risk, signal.as_ref()) {
456 match engine.assess(sig).await {
457 RiskVerdict::Deny => return Err(IdentityError::Forbidden),
458 RiskVerdict::StepUp | RiskVerdict::Allow => {}
462 }
463 }
464 if user.mfa.is_enrolled() {
466 let challenge_id = new_id();
467 self.challenges
468 .put(&challenge_id, &user.id, self.config.mfa_challenge_ttl_secs)
469 .await?;
470 return Ok(FederatedOutcome::MfaChallenge {
471 mfa_required: true,
472 challenge_id,
473 methods: self.enrolled_methods(&user),
474 });
475 }
476 Ok(FederatedOutcome::Authenticated(self.mint(&user).await?))
477 }
478
479 async fn audit_fed(
480 &self,
481 profile: &FederatedProfile,
482 user_id: Option<&str>,
483 tenant: Option<&str>,
484 decision: FederationDecision,
485 signal: &Option<RiskSignal>,
486 ) {
487 if let Some(sink) = &self.fed_audit {
488 sink.record(FederationEvent {
489 provider: profile.provider.clone(),
490 provider_id: profile.provider_id.clone(),
491 user_id: user_id.map(str::to_owned),
492 email: profile.email.clone(),
493 tenant: tenant.map(str::to_owned),
494 decision,
495 ip: signal.as_ref().and_then(|s| s.ip.clone()),
496 trace_id: signal.as_ref().and_then(|s| s.trace_id.clone()),
497 })
498 .await;
499 }
500 }
501
502 pub async fn refresh(&self, refresh_token: &str) -> Result<TokenResponse> {
506 let (sub, jti) = self
507 .jwt
508 .validate_refresh(refresh_token)
509 .ok_or(IdentityError::InvalidToken)?;
510
511 match self.refresh.take(&jti).await? {
514 Some(uid) if uid == sub => {}
515 Some(_) => return Err(IdentityError::InvalidToken),
516 None => {
517 self.refresh.revoke_all(&sub).await.ok();
518 return Err(IdentityError::InvalidToken);
519 }
520 }
521
522 let user = self
523 .users
524 .find_by_id(&sub)
525 .await?
526 .ok_or(IdentityError::NotFound)?;
527 self.mint(&user).await
528 }
529
530 pub async fn logout(&self, refresh_token: &str) -> Result<()> {
532 if let Some((_, jti)) = self.jwt.validate_refresh(refresh_token) {
533 self.refresh.revoke(&jti).await?;
534 }
535 Ok(())
536 }
537
538 pub async fn logout_all(&self, user_id: &str) -> Result<()> {
540 self.refresh.revoke_all(user_id).await
541 }
542
543 pub async fn me(&self, user_id: &str) -> Result<Identity> {
545 self.users
546 .find_by_id(user_id)
547 .await?
548 .ok_or(IdentityError::NotFound)
549 }
550
551 pub async fn mint(&self, user: &Identity) -> Result<TokenResponse> {
556 self.mint_bound(user, None).await
557 }
558
559 pub async fn mint_bound(
564 &self,
565 user: &Identity,
566 cnf_jkt: Option<&str>,
567 ) -> Result<TokenResponse> {
568 if !self.required_consents.is_empty() {
572 if let Some(consent) = &self.consent {
573 let mut missing = Vec::new();
574 for purpose in &self.required_consents {
575 if !consent.has_consent(&user.id, purpose).await? {
576 missing.push(purpose.clone());
577 }
578 }
579 if !missing.is_empty() {
580 return Err(IdentityError::ConsentRequired(missing));
581 }
582 }
583 }
584
585 let tenant = if self.config.bind_tenant {
586 user.tenant.as_deref()
587 } else {
588 None
589 };
590 let email = user.email.clone().unwrap_or_default();
591 let access = self
592 .jwt
593 .issue_access_bound_cnf(
594 &user.id,
595 user.primary_role(),
596 &email,
597 &user.perms,
598 tenant,
599 cnf_jkt,
600 )
601 .map_err(|e| IdentityError::Internal(e.to_string()))?;
602 let (refresh, jti) = self
603 .jwt
604 .issue_refresh(&user.id)
605 .map_err(|e| IdentityError::Internal(e.to_string()))?;
606 self.refresh
607 .save(&jti, &user.id, self.jwt.refresh_ttl_secs())
608 .await?;
609 Ok(TokenResponse::bearer(
610 access,
611 refresh,
612 self.jwt.access_ttl_secs(),
613 ))
614 }
615
616 fn enrolled_methods(&self, user: &Identity) -> Vec<String> {
617 let mut m = Vec::new();
618 if user.mfa.totp_enrolled {
619 m.push("totp".to_owned());
620 }
621 if user.mfa.passkeys > 0 {
622 m.push("passkey".to_owned());
623 }
624 if user.mfa.recovery_codes_remaining > 0 {
625 m.push("recovery".to_owned());
626 }
627 m
628 }
629}
630
631impl IdentityServiceBuilder {
632 pub fn lockout(mut self, lockout: Arc<LockoutPolicy>) -> Self {
633 self.lockout = Some(lockout);
634 self
635 }
636 pub fn mfa(mut self, verifier: Arc<dyn MfaVerifier>) -> Self {
637 self.mfa = Some(verifier);
638 self
639 }
640 pub fn federated(mut self, store: Arc<dyn FederatedIdentityStore>) -> Self {
642 self.federated = Some(store);
643 self
644 }
645 pub fn risk(mut self, engine: Arc<dyn RiskEngine>) -> Self {
647 self.risk = Some(engine);
648 self
649 }
650 pub fn federation_audit(mut self, sink: Arc<dyn FederationAudit>) -> Self {
652 self.fed_audit = Some(sink);
653 self
654 }
655 pub fn linking(mut self, linking: LinkingConfig) -> Self {
658 self.linking = linking;
659 self
660 }
661 pub fn password_policy(mut self, policy: PasswordPolicy) -> Self {
663 self.password_policy = policy;
664 self
665 }
666 pub fn breach_checker(mut self, checker: Arc<dyn BreachChecker>) -> Self {
668 self.breach = Some(checker);
669 self
670 }
671 pub fn require_consents(mut self, store: Arc<dyn ConsentStore>, purposes: Vec<String>) -> Self {
674 self.consent = Some(store);
675 self.required_consents = purposes;
676 self
677 }
678 pub fn config(mut self, config: IdentityConfig) -> Self {
679 self.config = config;
680 self
681 }
682 pub fn build(self) -> IdentityService {
683 IdentityService {
684 users: self.users,
685 creds: self.creds,
686 hasher: self.hasher,
687 jwt: self.jwt,
688 refresh: self.refresh,
689 challenges: self.challenges,
690 lockout: self.lockout,
691 mfa: self.mfa,
692 federated: self.federated,
693 risk: self.risk,
694 fed_audit: self.fed_audit,
695 linking: self.linking,
696 password_policy: self.password_policy,
697 breach: self.breach,
698 consent: self.consent,
699 required_consents: self.required_consents,
700 config: self.config,
701 }
702 }
703}
704
705enum Resolution {
707 Existing(Identity),
709 Provisioned(Identity),
711 LinkRequired(String),
713 Denied,
715}
716
717pub fn new_id() -> String {
720 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
721 use base64::Engine;
722 let mut b = [0u8; 16];
723 rand::thread_rng().fill_bytes(&mut b);
724 URL_SAFE_NO_PAD.encode(b)
725}