0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added 2fa token verification error handling

closes https://linear.app/tryghost/issue/ENG-1635

# Conflicts:
#	ghost/admin/app/controllers/signin-verify.js
This commit is contained in:
Kevin Ansfield 2024-10-09 14:44:29 +01:00
parent 5f192344f8
commit 1a05652b50
4 changed files with 104 additions and 37 deletions

View file

@ -1,30 +1,58 @@
import Controller from '@ember/controller';
// eslint-disable-next-line
import DS from 'ember-data';
import {TrackedArray} from 'tracked-built-ins';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
const {Errors} = DS;
// eslint-disable-next-line ghost/ember/alias-model-in-controller
class VerifyData {
@tracked token;
@tracked hasValidated = new TrackedArray();
errors = Errors.create();
validate() {
this.errors.clear();
this.hasValidated = new TrackedArray();
if (!this.token?.trim()) {
this.errors.add('token', 'Token is required');
this.hasValidated.push('token');
}
}
}
export default class SigninVerifyController extends Controller {
@service ajax;
@service session;
@tracked flowErrors = '';
@tracked token = '';
@tracked verifyData = new VerifyData();
@action
resetData() {
this.verifyData = new VerifyData();
}
@action
validateToken() {
return true;
this.verifyData.validate();
}
@action
handleTokenInput(event) {
this.token = event.target.value;
this.verifyData.token = event.target.value;
}
@task
*verifyTokenTask() {
try {
yield this.session.authenticate('authenticator:cookie', {token: this.token});
yield this.session.authenticate('authenticator:cookie', {token: this.verifyData.token});
} catch (error) {
if (error && error.payload && error.payload.errors) {
this.flowErrors = error.payload.errors[0].message;

View file

@ -0,0 +1,7 @@
import UnauthenticatedRoute from 'ghost-admin/routes/unauthenticated';
export default class SigninVerifyRoute extends UnauthenticatedRoute {
setupController(controller) {
controller.resetData();
}
}

View file

@ -5,40 +5,43 @@
<header>
<div class="gh-site-icon" style={{site-icon-style}}></div>
<h1>New browser detected</h1>
<p>
For security, you need to verify your sign-in.
An email has been sent to you with a 6-digit code, please enter it below.
</p>
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="token">
<label for="token">Verification code</label>
<span class="gh-input-icon">
<input
id="token"
name="token"
type="text"
inputmode="numeric"
pattern="[0-9]*"
placeholder="123456"
autocomplete="one-time-code"
class="gh-input email"
value={{this.token}}
data-test-input="token"
{{on "input" this.handleTokenInput}}
{{on "blur" this.validateToken}}
/>
</span>
</GhFormGroup>
<GhTaskButton
@buttonText="Verify sign-in &rarr;"
@task={{this.verifyTokenTask}}
@showSuccess={{false}}
@class="login gh-btn gh-btn-login gh-btn-block gh-btn-icon"
@type="submit"
@useAccentColor={{true}}
data-test-button="verify" />
</header>
<p>
For security, you need to verify your sign-in.
An email has been sent to you with a 6-digit code, please enter it below.
</p>
<GhFormGroup @errors={{this.verifyData.errors}} @hasValidated={{this.verifyData.hasValidated}} @property="token">
<label for="token">Verification code</label>
<span class="gh-input-icon">
<input
id="token"
name="token"
type="text"
inputmode="numeric"
pattern="[0-9]*"
placeholder="123456"
autocomplete="one-time-code"
class="gh-input email"
value={{this.token}}
data-test-input="token"
{{on "input" this.handleTokenInput}}
{{on "blur" this.validateToken}}
/>
</span>
</GhFormGroup>
<GhTaskButton
@buttonText="Verify sign-in &rarr;"
@task={{this.verifyTokenTask}}
@showSuccess={{false}}
@class="login gh-btn gh-btn-login gh-btn-block gh-btn-icon"
@type="submit"
@useAccentColor={{true}}
data-test-button="verify" />
</form>
<p class="{{if this.flowErrors "main-error" "main-notification"}}" data-test-flow-notification>{{if this.flowErrors this.flowErrors this.flowNotification}}&nbsp;</p>
</section>
</div>
</div>

View file

@ -3,7 +3,7 @@ import windowProxy from 'ghost-admin/utils/window-proxy';
import {Response} from 'miragejs';
import {afterEach, beforeEach, describe, it} from 'mocha';
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {click, currentRouteName, currentURL, fillIn, findAll, triggerKeyEvent, visit, waitFor} from '@ember/test-helpers';
import {click, currentRouteName, currentURL, fillIn, find, findAll, triggerKeyEvent, visit, waitFor} from '@ember/test-helpers';
import {expect} from 'chai';
import {run} from '@ember/runloop';
import {setupApplicationTest} from 'ember-mocha';
@ -144,6 +144,35 @@ describe('Acceptance: Authentication', function () {
expect(currentURL()).to.equal('/dashboard');
});
it('handles 2fa code verification errors', async function () {
this.server.post('/session', function () {
return new Response(403, {}, {
errors: {
code: '2FA_TOKEN_REQUIRED'
}
});
});
this.server.put('/session/verify', function () {
return new Response(401, {}, {
errors: {
message: 'Invalid or expired token'
}
});
});
await invalidateSession();
await visit('/signin');
await fillIn('[data-test-input="email"]', 'my@email.com');
await fillIn('[data-test-input="password"]', 'password');
await click('[data-test-button="sign-in"]');
await fillIn('[data-test-input="token"]', 123456);
await click('[data-test-button="verify"]');
expect(find('[data-test-flow-notification]')).to.have.trimmed.text('Invalid or expired token');
});
});
describe('editor', function () {