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:
parent
5f192344f8
commit
1a05652b50
4 changed files with 104 additions and 37 deletions
|
@ -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;
|
||||
|
|
7
ghost/admin/app/routes/signin-verify.js
Normal file
7
ghost/admin/app/routes/signin-verify.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import UnauthenticatedRoute from 'ghost-admin/routes/unauthenticated';
|
||||
|
||||
export default class SigninVerifyRoute extends UnauthenticatedRoute {
|
||||
setupController(controller) {
|
||||
controller.resetData();
|
||||
}
|
||||
}
|
|
@ -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 →"
|
||||
@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 →"
|
||||
@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}} </p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
|
@ -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 () {
|
||||
|
|
Loading…
Add table
Reference in a new issue