Updated email confirmation flow so confirmation is done via POST

To avoid non-user GET requests (Such as those from email scanners)
auto-triggering the confirm submission. Made auto-submit the form via
JavaScript in this extra added step with user-link backup to keep
existing user flow experience.

Closes #3797
This commit is contained in:
Dan Brown 2022-11-12 15:10:14 +00:00
parent 0e627a6e05
commit a1b1f8138a
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 72 additions and 4 deletions

View File

@ -51,14 +51,28 @@ class ConfirmEmailController extends Controller
return view('auth.user-unconfirmed', ['user' => $user]); return view('auth.user-unconfirmed', ['user' => $user]);
} }
/**
* Show the form for a user to provide their positive confirmation of their email.
*/
public function showAcceptForm(string $token)
{
return view('auth.register-confirm-accept', ['token' => $token]);
}
/** /**
* Confirms an email via a token and logs the user into the system. * Confirms an email via a token and logs the user into the system.
* *
* @throws ConfirmationEmailException * @throws ConfirmationEmailException
* @throws Exception * @throws Exception
*/ */
public function confirm(string $token) public function confirm(Request $request)
{ {
$validated = $this->validate($request, [
'token' => ['required', 'string']
]);
$token = $validated['token'];
try { try {
$userId = $this->emailConfirmationService->checkTokenAndGetUserId($token); $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
} catch (UserTokenNotFoundException $exception) { } catch (UserTokenNotFoundException $exception) {

View File

@ -0,0 +1,12 @@
class AutoSubmit {
setup() {
this.form = this.$el;
this.form.submit();
}
}
export default AutoSubmit;

View File

@ -4,6 +4,7 @@ import ajaxForm from "./ajax-form.js"
import attachments from "./attachments.js" import attachments from "./attachments.js"
import attachmentsList from "./attachments-list.js" import attachmentsList from "./attachments-list.js"
import autoSuggest from "./auto-suggest.js" import autoSuggest from "./auto-suggest.js"
import autoSubmit from "./auto-submit.js";
import backToTop from "./back-to-top.js" import backToTop from "./back-to-top.js"
import bookSort from "./book-sort.js" import bookSort from "./book-sort.js"
import chapterContents from "./chapter-contents.js" import chapterContents from "./chapter-contents.js"
@ -64,6 +65,7 @@ const componentMapping = {
"attachments": attachments, "attachments": attachments,
"attachments-list": attachmentsList, "attachments-list": attachmentsList,
"auto-suggest": autoSuggest, "auto-suggest": autoSuggest,
"auto-submit": autoSubmit,
"back-to-top": backToTop, "back-to-top": backToTop,
"book-sort": bookSort, "book-sort": bookSort,
"chapter-contents": chapterContents, "chapter-contents": chapterContents,

View File

@ -61,6 +61,8 @@ return [
'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.', 'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.', 'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.', 'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
'email_confirm_thanks' => 'Thanks for confirming!',
'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
'email_not_confirmed' => 'Email Address Not Confirmed', 'email_not_confirmed' => 'Email Address Not Confirmed',
'email_not_confirmed_text' => 'Your email address has not yet been confirmed.', 'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',

View File

@ -0,0 +1,27 @@
@extends('layouts.simple')
@section('content')
<div class="container very-small mt-xl">
<div class="card content-wrap auto-height">
<h1 class="list-heading">{{ trans('auth.email_confirm_thanks') }}</h1>
<p class="mb-none">{{ trans('auth.email_confirm_thanks_desc') }}</p>
<div class="flex-container-row items-center wrap">
<div class="flex min-width-s">
@include('common.loading-icon')
</div>
<div class="flex min-width-s text-s-right">
<form component="auto-submit" action="{{ url('/register/confirm/accept') }}" method="post">
{{ csrf_field() }}
<input type="hidden" name="token" value="{{ $token }}">
<button class="text-button">{{ trans('common.continue') }}</button>
</form>
</div>
</div>
</div>
</div>
@stop

View File

@ -316,7 +316,8 @@ Route::get('/register', [Auth\RegisterController::class, 'getRegister']);
Route::get('/register/confirm', [Auth\ConfirmEmailController::class, 'show']); Route::get('/register/confirm', [Auth\ConfirmEmailController::class, 'show']);
Route::get('/register/confirm/awaiting', [Auth\ConfirmEmailController::class, 'showAwaiting']); Route::get('/register/confirm/awaiting', [Auth\ConfirmEmailController::class, 'showAwaiting']);
Route::post('/register/confirm/resend', [Auth\ConfirmEmailController::class, 'resend']); Route::post('/register/confirm/resend', [Auth\ConfirmEmailController::class, 'resend']);
Route::get('/register/confirm/{token}', [Auth\ConfirmEmailController::class, 'confirm']); Route::get('/register/confirm/{token}', [Auth\ConfirmEmailController::class, 'showAcceptForm']);
Route::post('/register/confirm/accept', [Auth\ConfirmEmailController::class, 'confirm']);
Route::post('/register', [Auth\RegisterController::class, 'postRegister']); Route::post('/register', [Auth\RegisterController::class, 'postRegister']);
// SAML routes // SAML routes

View File

@ -46,8 +46,18 @@ class RegistrationTest extends TestCase
return $notification->token === $emailConfirmation->token; return $notification->token === $emailConfirmation->token;
}); });
// Check confirmation email confirmation activation. // Check confirmation email confirmation accept page.
$this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login'); $resp = $this->get('/register/confirm/' . $emailConfirmation->token);
$acceptPage = $this->withHtml($resp);
$resp->assertOk();
$resp->assertSee('Thanks for confirming!');
$acceptPage->assertElementExists('form[method="post"][action$="/register/confirm/accept"][component="auto-submit"] button');
$acceptPage->assertFieldHasValue('token', $emailConfirmation->token);
// Check acceptance confirm
$this->post('/register/confirm/accept', ['token' => $emailConfirmation->token])->assertRedirect('/login');
// Check state on login redirect
$this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.'); $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.');
$this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]); $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]);
$this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);