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:
		
							parent
							
								
									0e627a6e05
								
							
						
					
					
						commit
						a1b1f8138a
					
				| 
						 | 
					@ -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) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AutoSubmit {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setup() {
 | 
				
			||||||
 | 
					        this.form = this.$el;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.form.submit();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default AutoSubmit;
 | 
				
			||||||
| 
						 | 
					@ -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,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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.',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue