getFormData(); if (!is_array($body)) { throw XrpcException::invalidRequest('Request body must be a JSON object'); } $identifier = $this->requireString($body, 'identifier'); $password = $this->requireString($body, 'password'); $allowTakendown = $this->parseAllowTakendown($body['allowTakendown'] ?? null); if (strlen($password) > self::MAX_PASSWORD_LENGTH) { throw XrpcException::authRequired( 'Password too long. Consider resetting your password.' ); } $authenticated = $this->authenticate($identifier, $password, $allowTakendown); $tokens = $this->tokenIssuer->issue( $authenticated->account->getDid(), $this->scopeFor($authenticated->appPassword), $authenticated->appPassword?->getName() ); $this->refreshTokens->save(new RefreshToken( id: $tokens->getRefreshJti(), did: $authenticated->account->getDid(), expiresAt: $tokens->getRefreshExpiresAt()->format(DATE_ATOM), appPasswordName: $authenticated->appPassword?->getName(), nextId: null, )); $actor = $authenticated->actor; $didDoc = $this->didResolver->resolve($authenticated->account->getDid()); $response = new CreateSessionResponse( accessJwt: $tokens->getAccessJwt(), refreshJwt: $tokens->getRefreshJwt(), did: $authenticated->account->getDid(), didDoc: $didDoc, handle: $actor->getHandle() ?? CreateSessionResponse::HANDLE_INVALID, email: $authenticated->account->getEmail(), emailConfirmed: $authenticated->account->getEmailConfirmedAt() !== null, active: $actor->isRepoActive(), status: $actor->getRepoStatus(), ); return $this->respondWithData($response); } private function authenticate(string $identifier, string $password, bool $allowTakendown): AuthenticatedAccount { try { return $this->authenticator->login($identifier, $password); } catch (InvalidCredentialsException) { throw XrpcException::authRequired('Invalid identifier or password'); } catch (AccountTakedownException $e) { if (!$allowTakendown) { throw new XrpcException('AccountTakedown', $e->getMessage(), 401); } throw $e; } } private function scopeFor(?AppPassword $appPassword): string { if ($appPassword === null) { return AuthTokenIssuer::SCOPE_ACCESS; } return $appPassword->isPrivileged() ? AuthTokenIssuer::SCOPE_APP_PASS_PRIVILEGED : AuthTokenIssuer::SCOPE_APP_PASS; } private function parseAllowTakendown(mixed $raw): bool { if ($raw === null) { return false; } if (is_bool($raw)) { return $raw; } if ($raw === 'true' || $raw === 1 || $raw === '1') { return true; } if ($raw === 'false' || $raw === 0 || $raw === '0') { return false; } throw XrpcException::invalidParam( $this->actionName, 'allowTakendown must be a boolean', is_scalar($raw) ? (string) $raw : '' ); } }