From db071ec363c29242792f1eda5edd7d084f1f27d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20Bouli=C3=A8re?= <108473857+tbouliere-datasolution@users.noreply.github.com> Date: Wed, 1 Feb 2023 13:33:54 +0100 Subject: [PATCH] fix: handle google recaptcha v3 expiration (#1347) * fix: handle google recaptcha v3 expiration * fix: fetch captcha token after a REST error failed reCAPTCHA tokens expire after two minutes. It is recommended in the documentation to make sure to call execute when the user takes the action rather than on page load. An automatic refresh has been set up after the form becomes valid. --------- Co-authored-by: Silke --- .../shared/captcha-v3/captcha-v3.component.ts | 45 +++++++++++++++---- .../registration-page.component.ts | 16 +++++-- ...registration-form-configuration.service.ts | 2 +- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/app/extensions/captcha/shared/captcha-v3/captcha-v3.component.ts b/src/app/extensions/captcha/shared/captcha-v3/captcha-v3.component.ts index 772fb86f87..0c7a184efa 100644 --- a/src/app/extensions/captcha/shared/captcha-v3/captcha-v3.component.ts +++ b/src/app/extensions/captcha/shared/captcha-v3/captcha-v3.component.ts @@ -2,10 +2,11 @@ import { ChangeDetectionStrategy, Component, Input, NgModule, OnDestroy, OnInit import { FormGroup, Validators } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; import { RECAPTCHA_V3_SITE_KEY, ReCaptchaV3Service, RecaptchaV3Module } from 'ng-recaptcha'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { Subject, timer } from 'rxjs'; +import { filter, switchMap, take, takeUntil } from 'rxjs/operators'; import { DirectivesModule } from 'ish-core/directives.module'; +import { whenTruthy } from 'ish-core/utils/operators'; import { SitekeyProviderService, @@ -32,13 +33,39 @@ export class CaptchaV3Component implements OnInit, OnDestroy { ngOnInit() { this.parentForm.get('captchaAction').setValidators([Validators.required]); - this.recaptchaV3Service - .execute(this.parentForm.get('captchaAction').value) - .pipe(takeUntil(this.destroy$)) - .subscribe(token => { - this.parentForm.get('captcha').setValue(token); - this.parentForm.get('captcha').updateValueAndValidity(); - }); + // as soon as the form gets valid request a captcha token every 2 minutes + if (!SSR) { + this.parentForm.statusChanges + .pipe( + filter(status => status === 'VALID'), + take(1), + switchMap(() => + timer(0, 2 * (60 - 3) * 1000).pipe( + switchMap(() => this.recaptchaV3Service.execute(this.parentForm.get('captchaAction').value)) + ) + ), + whenTruthy(), + takeUntil(this.destroy$) + ) + .subscribe(token => { + this.parentForm.get('captcha').setValue(token); + this.parentForm.get('captcha').updateValueAndValidity(); + }); + + // if the captcha is set to undefined from outside request a captcha token + this.parentForm + .get('captcha') + .valueChanges.pipe( + filter(token => token === undefined), + switchMap(() => this.recaptchaV3Service.execute(this.parentForm.get('captchaAction').value)), + whenTruthy(), + takeUntil(this.destroy$) + ) + .subscribe(token => { + this.parentForm.get('captcha').setValue(token); + this.parentForm.get('captcha').updateValueAndValidity(); + }); + } } ngOnDestroy() { diff --git a/src/app/pages/registration/registration-page.component.ts b/src/app/pages/registration/registration-page.component.ts index 89da4ceb54..d6423208d6 100644 --- a/src/app/pages/registration/registration-page.component.ts +++ b/src/app/pages/registration/registration-page.component.ts @@ -1,12 +1,13 @@ /* eslint-disable ish-custom-rules/no-intelligence-in-artifacts */ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; -import { Observable } from 'rxjs'; +import { Observable, tap } from 'rxjs'; import { AccountFacade } from 'ish-core/facades/account.facade'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; +import { whenTruthy } from 'ish-core/utils/operators'; import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils'; import { @@ -39,10 +40,13 @@ export class RegistrationPageComponent implements OnInit { fields: FormlyFieldConfig[]; model: Record; options: FormlyFormOptions; - form = new FormGroup({}); + form = new UntypedFormGroup({}); ngOnInit() { - this.error$ = this.registrationFormConfiguration.getErrorSources(); + this.error$ = this.registrationFormConfiguration.getErrorSources().pipe( + whenTruthy(), + tap(() => this.clearCaptchaToken()) + ); const snapshot = this.route.snapshot; this.model = this.registrationFormConfiguration.extractModel(snapshot); @@ -70,4 +74,8 @@ export class RegistrationPageComponent implements OnInit { get submitDisabled(): boolean { return this.form.invalid && this.submitted; } + + private clearCaptchaToken() { + this.form.get('captcha')?.setValue(undefined); + } } diff --git a/src/app/pages/registration/services/registration-form-configuration/registration-form-configuration.service.ts b/src/app/pages/registration/services/registration-form-configuration/registration-form-configuration.service.ts index 87ec276ba0..68f7ab23eb 100644 --- a/src/app/pages/registration/services/registration-form-configuration/registration-form-configuration.service.ts +++ b/src/app/pages/registration/services/registration-form-configuration/registration-form-configuration.service.ts @@ -201,7 +201,7 @@ export class RegistrationFormConfigurationService { getErrorSources() { return merge( - this.accountFacade.userError$.pipe(filter(error => error.status !== 404)), + this.accountFacade.userError$.pipe(filter(error => error?.status !== 404)), this.accountFacade.ssoRegistrationError$ ); }