resolveAccount($identifier); if ($account === null) { throw new InvalidCredentialsException(); } $appPassword = null; if (!$this->passwordHasher->verify($password, $account->getPasswordScrypt())) { $appPassword = $this->findMatchingAppPassword($account->getDid(), $password); if ($appPassword === null) { throw new InvalidCredentialsException(); } } $actor = $this->resolveActorOrSynthesize($account); if ($actor->getTakedownRef() !== null) { throw new AccountTakedownException(); } return new AuthenticatedAccount($account, $actor, $appPassword); } private function resolveAccount(string $identifier): ?Account { $identifier = trim($identifier); if ($identifier === '') { return null; } try { if (str_starts_with($identifier, 'did:')) { return $this->accounts->findAccountByDid($identifier); } if (str_contains($identifier, '@')) { return $this->accounts->findAccountByEmail($identifier); } return $this->accounts->findAccountByHandle($identifier); } catch (AccountNotFoundException) { return null; } } private function findMatchingAppPassword(string $did, string $password): ?AppPassword { foreach ($this->appPasswords->findAllForDid($did) as $candidate) { if ($this->passwordHasher->verify($password, $candidate->getPasswordScrypt())) { return $candidate; } } return null; } /** * Build a synthetic, repo-inactive {@see Actor} when the account row * exists but no matching actor row does. That state shouldn't happen * in practice. */ private function resolveActorOrSynthesize(Account $account): Actor { try { return $this->actors->findActorByDid($account->getDid()); } catch (ActorNotFoundException) { return new Actor( did: $account->getDid(), handle: null, createdAt: new DateTimeImmutable(), ); } } }