Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5d14e86b authored by Akhil's avatar Akhil 🙂
Browse files

Merge branch 'dev/hcaptcha' into 'main'

HCaptcha as the captcha provider

See merge request !147
parents 6a6fe81e 64235a29
Loading
Loading
Loading
Loading
Loading
+13 −1
Original line number Diff line number Diff line
@@ -14,6 +14,18 @@

- This plugin creates an endpoint `/apps/ecloud-accounts/api/set_account_data` that is to be used to set user's email, quota,recovery email and create the user's folder if necessary

### Captcha Configuration for user account creation

- Simple image based captcha is the default for human verification
- To change the value, set `ecloud-accounts.captcha_provider` 
  - Allowed values are `image` (default) and `hcaptcha` (https://hcaptcha.com)

#### HCaptcha Configuration

- For hcaptcha provider to work, set the following values correctly:
  - `ecloud-accounts.hcaptcha_site_key`
  - `ecloud-accounts.hcaptcha_secret`

## Drop account

- The drop account functionality plugin works in conjunction with the drop_account plugin : https://apps.nextcloud.com/apps/drop_account
@@ -25,7 +37,7 @@

Please open issues here : https://gitlab.e.foundation/e/backlog/issues

## Dependancies
## Dependencies

This plugin works in cunjunction with the drop_account plugin : https://apps.nextcloud.com/apps/drop_account

+1 −1
Original line number Diff line number Diff line
@@ -10,7 +10,7 @@ return ['routes' => [
	['name' => 'shop_account#check_shop_email_post_delete', 'url' => '/shop-accounts/check_shop_email_post_delete', 'verb' => 'GET'],
	[
		'name' => 'user#preflighted_cors', 'url' => '/api/{path}',
		'verb' => 'OPTIONS', 'requirements' => array('path' => '.+')
		'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']
	],
	[
		'name' => 'beta_user#remove_user_in_group',
+73 −14
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ use OCA\EcloudAccounts\Exception\AddUsernameToCommonStoreException;
use OCA\EcloudAccounts\Exception\LDAPUserCreationException;
use OCA\EcloudAccounts\Exception\RecoveryEmailValidationException;
use OCA\EcloudAccounts\Service\CaptchaService;
use OCA\EcloudAccounts\Service\HCaptchaService;
use OCA\EcloudAccounts\Service\NewsLetterService;
use OCA\EcloudAccounts\Service\UserService;
use OCP\AppFramework\Controller;
@@ -34,6 +35,7 @@ class AccountController extends Controller {
	private $userService;
	private $newsletterService;
	private $captchaService;
	private HCaptchaService $hCaptchaService;
	protected $l10nFactory;
	private $session;
	private $userSession;
@@ -43,6 +45,11 @@ class AccountController extends Controller {
	private IInitialState $initialState;
	private const SESSION_USERNAME_CHECK = 'username_check_passed';
	private const CAPTCHA_VERIFIED_CHECK = 'captcha_verified';
	private const ALLOWED_CAPTCHA_PROVIDERS = ['image', 'hcaptcha'];
	private const DEFAULT_CAPTCHA_PROVIDER = 'image';
	private const HCAPTCHA_PROVIDER = 'hcaptcha';
	private const HCAPTCHA_DOMAINS = ['https://hcaptcha.com', 'https://*.hcaptcha.com'];

	private ILogger $logger;
	public function __construct(
		$AppName,
@@ -50,19 +57,21 @@ class AccountController extends Controller {
		UserService $userService,
		NewsLetterService $newsletterService,
		CaptchaService $captchaService,
		HCaptchaService $hCaptchaService,
		IFactory $l10nFactory,
		IUserSession $userSession,
		IURLGenerator $urlGenerator,
		ISession $session,
		IConfig $config,
		IInitialState $initialState,
		ILogger $logger
		ILogger $logger,
		IInitialState $initialState
	) {
		parent::__construct($AppName, $request);
		$this->appName = $AppName;
		$this->userService = $userService;
		$this->newsletterService = $newsletterService;
		$this->captchaService = $captchaService;
		$this->hCaptchaService = $hCaptchaService;
		$this->l10nFactory = $l10nFactory;
		$this->session = $session;
		$this->userSession = $userSession;
@@ -88,12 +97,30 @@ class AccountController extends Controller {

		$_SERVER['HTTP_ACCEPT_LANGUAGE'] = $lang;
		$this->initialState->provideInitialState('lang', $lang);
		return new TemplateResponse(
		
		$response = new TemplateResponse(
			Application::APP_ID,
			'signup',
			['appName' => Application::APP_ID, 'lang' => $lang],
			TemplateResponse::RENDER_AS_GUEST
		);

		$captchaProvider = $this->getCaptchaProvider();
		$this->initialState->provideInitialState('captchaProvider', $captchaProvider);
		
		if ($captchaProvider === self::HCAPTCHA_PROVIDER) {
			$csp = $response->getContentSecurityPolicy();
			foreach (self::HCAPTCHA_DOMAINS as $domain) {
				$csp->addAllowedScriptDomain($domain);
				$csp->addAllowedFrameDomain($domain);
				$csp->addAllowedStyleDomain($domain);
				$csp->addAllowedConnectDomain($domain);
			}
			$response->setContentSecurityPolicy($csp);
			$hcaptchaSiteKey = $this->config->getSystemValue(Application::APP_ID . '.hcaptcha_site_key');
			$this->initialState->provideInitialState('hCaptchaSiteKey', $hcaptchaSiteKey);
		}
		return $response;
	}
	
	/**
@@ -198,7 +225,7 @@ class AccountController extends Controller {
	 *
	 * @return string|null If validation fails, a string describing the error; otherwise, null.
	 */
	public function validateInput(string $inputName, string $value, int $maxLength = null) : ?string {
	public function validateInput(string $inputName, string $value, ?int $maxLength = null) : ?string {
		if ($value === '') {
			return "$inputName is required.";
		}
@@ -249,10 +276,15 @@ class AccountController extends Controller {
	 * @NoCSRFRequired
	 */
	public function captcha(): Http\DataDisplayResponse {
		$captchaValue = $this->captchaService->generateCaptcha();
		// Don't allow requests to image captcha if different provider is set
		if ($this->getCaptchaProvider() !== self::DEFAULT_CAPTCHA_PROVIDER) {
			$response = new DataResponse();
			$response->setStatus(400);
			return $response;
		}

		$captchaValue = $this->captchaService->generateCaptcha();
		$response = new Http\DataDisplayResponse($captchaValue, Http::STATUS_OK, ['Content-Type' => 'image/png']);

		return $response;
	}
	/**
@@ -262,24 +294,51 @@ class AccountController extends Controller {
	 * @PublicPage
	 * @NoCSRFRequired
	 *
	 * @param string $captchaInput The user-provided human verification input.
	 * @param string $token The user-provided human verification input.
	 * @param string $bypassToken Token to bypass captcha for automation testing
	 *
	 * @return \OCP\AppFramework\Http\DataResponse
	 */
	public function verifyCaptcha(string $captchaInput = '', string $bypassToken = '') : DataResponse {
	public function verifyCaptcha(string $userToken = '', string $bypassToken = '') : DataResponse {
		$response = new DataResponse();
		$captchaToken = $this->config->getSystemValue('bypass_captcha_token', '');
		// Initialize the default status to 400 (Bad Request)
		$response->setStatus(400);
		// Check if the input matches the bypass token or the stored captcha result
		$captchaResult = (string) $this->session->get(CaptchaService::CAPTCHA_RESULT_KEY, '');
		if ((!empty($captchaToken) && $bypassToken === $captchaToken) || (!empty($captchaResult) && $captchaInput === $captchaResult)) {

		// Check if the input matches the bypass token
		$bypassTokenInConfig = $this->config->getSystemValue('bypass_captcha_token', '');
		if ((!empty($bypassTokenInConfig) && $bypassToken === $bypassTokenInConfig)) {
			$this->session->set(self::CAPTCHA_VERIFIED_CHECK, true);
			$response->setStatus(200);
		}

		$response->setStatus(400);
		$captchaProvider = $this->getCaptchaProvider();

		// Check for default captcha provider
		if ($captchaProvider === self::DEFAULT_CAPTCHA_PROVIDER && $this->verifyImageCaptcha($userToken)) {
			$this->session->set(self::CAPTCHA_VERIFIED_CHECK, true);
			$this->session->remove(CaptchaService::CAPTCHA_RESULT_KEY);
			$response->setStatus(200);
		}

		// Check for hcaptcha provider
		if ($captchaProvider === self::HCAPTCHA_PROVIDER && $this->hCaptchaService->verify($userToken)) {
			$this->session->set(self::CAPTCHA_VERIFIED_CHECK, true);
			$response->setStatus(200);
		}
		return $response;
	}

	private function verifyImageCaptcha(string $captchaInput = '') : bool {
		$captchaResult = (string) $this->session->get(CaptchaService::CAPTCHA_RESULT_KEY, '');
		return (!empty($captchaResult) && $captchaInput === $captchaResult);
	}

	private function getCaptchaProvider() : string {
		$captchaProvider = $this->config->getSystemValue('ecloud-accounts.captcha_provider', self::DEFAULT_CAPTCHA_PROVIDER);

		if (!in_array($captchaProvider, self::ALLOWED_CAPTCHA_PROVIDERS)) {
			$captchaProvider = self::DEFAULT_CAPTCHA_PROVIDER;
		}
		return $captchaProvider;
	}

}
+1 −1
Original line number Diff line number Diff line
@@ -46,7 +46,7 @@ class BeforeUserDeletedListener implements IEventListener {
		$isUserOnLDAP = $this->LDAPConnectionService->isUserOnLDAPBackend($user);

		try {
			$this->logger->info("PostDelete user {user}", array('user' => $uid));
			$this->logger->info("PostDelete user {user}", ['user' => $uid]);
			$this->userService->deleteEmailAccount($email);
		} catch (Exception $e) {
			$this->logger->error('Error deleting mail folder for user '. $uid . ' :' . $e->getMessage());
+5 −5
Original line number Diff line number Diff line
@@ -25,7 +25,7 @@ class CurlService {
	 * @param array $userOptions
	 * @return mixed
	 */
	public function get($url, $params = array(), $headers = array(), $userOptions = array()) {
	public function get($url, $params = [], $headers = [], $userOptions = []) {
		return $this->request('GET', $url, $params, $headers, $userOptions);
	}

@@ -38,7 +38,7 @@ class CurlService {
	 * @param array $userOptions
	 * @return mixed
	 */
	public function post($url, $params = array(), $headers = array(), $userOptions = array()) {
	public function post($url, $params = [], $headers = [], $userOptions = []) {
		return $this->request('POST', $url, $params, $headers, $userOptions);
	}

@@ -88,13 +88,13 @@ class CurlService {
	 * @return mixed
	 * @throws Exception
	 */
	private function request($method, $url, $params = array(), $headers = array(), $userOptions = array()) {
	private function request($method, $url, $params = [], $headers = [], $userOptions = []) {
		$ch = curl_init();
		$method = strtoupper($method);
		$options = array(
		$options = [
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_HTTPHEADER => $headers
		);
		];
		foreach ($userOptions as $key => $value) {
			$options[$key] = $value;
		}
Loading