feat(signin): Reduce the need to enter a password, better expired session handling

Do not ask for a password for users signing into a non-key
requesting RP if they already have an FxA session of any sort.

I removed a lot of tests that now seem duplicate, since there is
no real distinction between a "Sync" signin and an "OAuth" signin
when it comes to whether to display the password field.

I also added onto a bunch of the "cached session" tests
to actually click the "submit" button to ensure the user
is redirected where they are supposed to be. The idea
is to follow on with trying expired cached credentials
targeting the problems uncovered in #999.

fixes #1371
This commit is contained in:
Shane Tomlinson 2019-07-29 16:13:38 +01:00
Родитель 27533baba5
Коммит 587acdb2a9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 09D4F897B87A2D19
15 изменённых файлов: 247 добавлений и 484 удалений

Просмотреть файл

@ -0,0 +1,109 @@
# Minimizing password entry
- Deciders: Shane Tomlinson, Alex Davis, Ryan Feeley, Ryan Kelly
- Date: 2019-08-07
## Context and Problem Statement
See [Github Issue 1371][#gh-issue-1371]. The FxA authorization flow sometimes asks already authenticated users to enter their password, sometimes it does not. Password entry, especially on mobile devices, is difficult and a source of user dropoff. Minimizing the need for a password in an authorization flow should increase flow completion rates.
When and where passwords are asked for has been a repeated source of confusion amongst both users and Firefox Accounts developers. If a user is signed into Sync, passwords are only _supposed_ to be required for authorization flows for RPs that require encryption keys. However, there is a bug in the state management logic that forces users to enter their password more often than expected.
Technically, we _must always_ ask the user to enter their password any time encryption keys are needed by an RP, e.g., Sync, Lockwise, and Send. For RPs that do not require encryption keys, e.g., Monitor and AMO, there is no technical reason why authenticated users must enter their password again, the existing sessionToken is capable of requesting new OAuth tokens.
## Decision Drivers
- User happiness via fewer keystrokes, less confusion
- Improved signin rates
## Considered Options
1. Keep the existing flow
2. Only ask authenticated users for a password if encryption keys are required
## Decision Outcome
Chosen option: "option 2", because it minimizes the number of places the user must enter their password.
### Positive Consequences
- User will need to type their password in fewer places.
- Signin completion rates should increase.
### Negative Consequences
- There may be user confusion around what it means to sign out.
### [option 1] Keep the existing flow
If a user signs in to Sync first and is not signing into an OAuth
RP that requires encryption keys, then no password is required.
If a user does not sign into Sync and instead signs into an
OAuth RP, e.g., Send, and then visits a 2nd OAuth RP that does not
require encryption keys, e.g., Monitor, then they must enter their password.
**example 1** User performs the initial authorization flow for an OAuth RP, e.g., Send, and then visits a 2nd OAuth RP that does not require encryption keys, e.g., Monitor, then _ask_ for the password.
**example 2** User performs the initial authorization flow for Sync, then a subsequent authorization flow for an OAuth RP that does not require encryption keys, e.g., Monitor, _do not_ ask for the password.
**example 3** User performs the initial authorization flow for an OAuth RP, e.g., Monitor, and then a subsequent authorization flow for an OAuth RP that _does_ require encryption keys, e.g., Send, then _ask_ for the password.
**example 4** User performs the initial authorization flow for Sync, then a subsequent authorization flow for an OAuth RP that _does_ require encryption keys, e.g., Send, then _ask_ for the password.
**example 5** User performs the initial authorization flow for an OAuth RP that does not require keys, e.g., Monitor, and then performs an authorization flow for Sync, then _ask_ for the password.
**example 6** User performs the initial authorization flow for an OAuth RP that does does require keys, e.g., Send, and then performs an authorization flow for Sync, then _ask_ for the password.
- Good, because we already have it and no effort is required to keep it.
- Bad because there is no technical reason why we cannot re-use existing sessionTokens created when signing into OAuth RPs to generate OAuth tokens for other non-key requesting OAuth RPs.
- Bad, because users need to enter their password more than they need to.
- Bad, because due to a bug in the code, users that are currently signed into Sync are sometimes asked for their password to sign into services such as Monitor that do not require keys.
### [option 2] Only ask authenticated users for a password if encryption keys are required
**example 1** User performs the initial authorization flow for an OAuth RP, e.g., Send, and then visits a 2nd OAuth RP that does not require encryption keys, e.g., Monitor, then _do not_ ask for the password.
**example 2** User performs the initial authorization flow for Sync, then a subsequent authorization flow for an OAuth RP that does not require encryption keys, e.g., Monitor, _do not_ ask for the password.
**example 3** User performs the initial authorization flow for an OAuth RP, e.g., Monitor, and then a subsequent authorization flow for an OAuth RP that _does_ require encryption keys, e.g., Send, then _ask_ for the password.
**example 4** User performs the initial authorization flow for Sync, then a subsequent authorization flow for an OAuth RP that _does_ require encryption keys, e.g., Send, then _ask_ for the password.
**example 5** User performs the initial authorization flow for an OAuth RP that does not require keys, e.g., Monitor, and then performs an authorization flow for Sync, then _ask_ for the password.
**example 6** User performs the initial authorization flow for an OAuth RP that does does require keys, e.g., Send, and then performs an authorization flow for Sync, then _ask_ for the password.
- Good, because case 1 _does not_ ask for a password whereas it _does_ with option 1.
- Bad, because there is potential for user confusion about expected behavior when destroying the sessionToken - should destroying the sessionToken sign the user out of the RP too? See [Github issue 640][#gh-issue-640].
- Support for [RP initiated logout][#gh-issue-1979] will largely mitigate this.
## Is a password needed for service <X>?
| Service | Password needed if already authenticated to FxA? |
| ----------------------- | ------------------------------------------------ |
| Lockwise | yes |
| Notes | yes |
| Send | yes |
| Sync | yes |
| AMO | no |
| Email preferences | no |
| Firefox Private Network | no |
| Monitor | no |
| Mozilla IAM | no? |
| Mozilla Support | no |
| Pocket | no |
| Pontoon | no |
## Expired sessions
Not mentioned above is how invalid sessions are handled. Sessions become invalid under a number of scenarios, including password change, password reset, and session revocation from the [FxA Devices & Apps panel][#fxa-devices-apps-panel]. FxA only knows when previously authenticated sessions are invalid when the user attempts to use the previously authenticated session. If a previously authenticated user attempts to sign into Monitor, FxA will not initially ask for their password. Once the user clicks "Submit", FxA will learn the session is invalid and ask the user for their password.
## Future
In the future some sort of "session freshness" heuristic may be used to force users who have not recently authenticated must re-enter their password. Support for [RP initiated logout][#gh-issue-1979] will largely mitigate user confusion around what it means to sign out of a service and whether destroying a sessionToken should also sign the user out of an RP.
[#gh-issue-1371]: https://github.com/mozilla/fxa/issues/1371
[#gh-issue-640]: https://github.com/mozilla/fxa/issues/640
[#gh-issue-1979]: https://github.com/mozilla/fxa/issues/1979
[#fxa-devices-apps-panel]: https://accounts.firefox.com/settings/clients

Просмотреть файл

@ -9,6 +9,7 @@ This log lists the architectural decisions for [project name].
- [ADR-0002](0002-use-react-redux-and-typescript-for-subscription-management-pages.md) - Use React, Redux, and Typescript for subscription management pages
- [ADR-0003](0003-event-broker-for-subscription-platform.md) - Event Broker for Subscription Platform
- [ADR-0004](0004-product-capabilities-for-subscription-services.md) - Product Capabilities for Subscription Services
- [ADR-0005](0005-minimize-password-entry.md) - Minimizing password entry
<!-- adrlogstop -->

Просмотреть файл

@ -32,6 +32,11 @@ export default {
* @returns {Boolean}
*/
isPasswordNeededForAccount(account) {
// If the account doesn't have a sessionToken, we'll need a password
if (!account.get('sessionToken')) {
return true;
}
// If the account doesn't yet have an email address, we'll need a password too.
if (!account.get('email')) {
return true;
@ -43,13 +48,6 @@ export default {
return true;
}
// We need to ask the user again for their password unless the credentials came from Sync.
// Otherwise they aren't able to "fully" log out. Only Sync has a clear path to disconnect/log out
// your account that invalidates your sessionToken.
if (!this.user.isSyncAccount(account)) {
return true;
}
// Ask when 'chooserAskForPassword' is explicitly set.
// This happens in response to an expired session token.
if (this.model.get('chooserAskForPassword') === true) {

Просмотреть файл

@ -52,11 +52,8 @@ const View = FormView.extend({
// should be re-rendered with the default avatar.
const account = this.getAccount();
this.listenTo(account, 'change:accessToken', () => {
// if no access token and password is not visible we need to show the password field.
if (
!account.has('accessToken') &&
this.$(PASSWORD_SELECTOR).is(':hidden')
) {
// if no access token we need to show the password field.
if (!account.has('accessToken')) {
this.model.set('chooserAskForPassword', true);
return this.render().then(() => this.setDefaultPlaceholderAvatar());
}

Просмотреть файл

@ -27,7 +27,11 @@ describe('views/mixins/cached-credentials-mixin', () => {
let view;
beforeEach(() => {
account = new Model();
account = new Model({
email: 'testuser@testuser.com',
sessionToken: 'session-token',
});
formPrefill = new Model();
model = new Model();
relier = new Relier();
@ -52,10 +56,18 @@ describe('views/mixins/cached-credentials-mixin', () => {
});
describe('isPasswordNeededForAccount', () => {
it('asks for a password if the account has no sessionToken', () => {
account.unset('sessionToken');
sinon.stub(relier, 'wantsKeys').callsFake(() => false);
model.unset('chooserAskForPassword');
sinon.stub(view, 'getPrefillEmail').callsFake(() => '');
assert.isTrue(view.isPasswordNeededForAccount(account));
});
it('asks for password if no email', () => {
account.unset('email');
sinon.stub(relier, 'wantsKeys').callsFake(() => false);
sinon.stub(user, 'isSyncAccount').callsFake(() => true);
model.unset('chooserAskForPassword');
sinon.stub(view, 'getPrefillEmail').callsFake(() => '');
@ -63,21 +75,7 @@ describe('views/mixins/cached-credentials-mixin', () => {
});
it('asks for password if the relier wants keys (Sync)', () => {
account.set('email', 'testuser@testuser.com');
sinon.stub(relier, 'wantsKeys').callsFake(() => true);
sinon.stub(user, 'isSyncAccount').callsFake(() => true);
model.unset('chooserAskForPassword');
sinon
.stub(view, 'getPrefillEmail')
.callsFake(() => 'testuser@testuser.com');
assert.isTrue(view.isPasswordNeededForAccount(account));
});
it('asks for the password if the stored session is not from sync', () => {
account.set('email', 'testuser@testuser.com');
sinon.stub(relier, 'wantsKeys').callsFake(() => false);
sinon.stub(user, 'isSyncAccount').callsFake(() => false);
model.unset('chooserAskForPassword');
sinon
.stub(view, 'getPrefillEmail')
@ -87,9 +85,7 @@ describe('views/mixins/cached-credentials-mixin', () => {
});
it('asks for the password if forced', () => {
account.set('email', 'testuser@testuser.com');
sinon.stub(relier, 'wantsKeys').callsFake(() => false);
sinon.stub(user, 'isSyncAccount').callsFake(() => true);
model.set('chooserAskForPassword', true);
sinon
.stub(view, 'getPrefillEmail')
@ -99,9 +95,7 @@ describe('views/mixins/cached-credentials-mixin', () => {
});
it('asks for the password if the prefill email is different', () => {
account.set('email', 'testuser@testuser.com');
sinon.stub(relier, 'wantsKeys').callsFake(() => false);
sinon.stub(user, 'isSyncAccount').callsFake(() => true);
model.unset('chooserAskForPassword');
sinon
.stub(view, 'getPrefillEmail')
@ -109,6 +103,16 @@ describe('views/mixins/cached-credentials-mixin', () => {
assert.isTrue(view.isPasswordNeededForAccount(account));
});
it('does not ask for a password if none of the above are met', () => {
sinon.stub(relier, 'wantsKeys').callsFake(() => false);
model.set('chooserAskForPassword', false);
sinon
.stub(view, 'getPrefillEmail')
.callsFake(() => 'testuser@testuser.com');
assert.isFalse(view.isPasswordNeededForAccount(account));
});
});
describe('suggestedAccount', () => {

Просмотреть файл

@ -176,11 +176,9 @@ describe('views/sign_in', () => {
});
});
it('re-renders, keeps the original email, forces user to enter password', () => {
it('re-renders, forces user to enter password', () => {
assert.equal(view.render.callCount, 2);
assert.equal(view.$('.prefillEmail').text(), 'a@a.com');
assert.equal(view.$('input[type=email]').val(), 'a@a.com');
assert.lengthOf(view.$('input[type=password]'), 1);
assert.isTrue(view.model.get('chooserAskForPassword'));
});
});
});

Просмотреть файл

@ -246,7 +246,8 @@ registerSuite('Firefox desktop user info handshake', {
)
)
// User can sign in with cached credentials, no password needed.
.then(noSuchElement(selectors.SIGNIN.PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors.SETTINGS.HEADER))
);
},
@ -285,9 +286,8 @@ registerSuite('Firefox desktop user info handshake', {
otherEmail
)
)
// normal email element is in the DOM to help password managers.
.then(testElementValueEquals(selectors.SIGNIN.EMAIL, otherEmail))
.then(testElementExists(selectors.SIGNIN.PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors.SETTINGS.HEADER))
);
},

Просмотреть файл

@ -259,6 +259,8 @@ module.exports = {
},
SIGNIN_PASSWORD: {
EMAIL: 'input[type=email]',
EMAIL_NOT_EDITABLE: '.prefillEmail',
ERROR: '.error',
HEADER: '#fxa-signin-password-header',
LINK_FORGOT_PASSWORD: 'a[href^="/reset_password"]',
LINK_MISTYPED_EMAIL: '.use-different',
@ -267,6 +269,7 @@ module.exports = {
SHOW_PASSWORD: '#password ~ .show-password-label',
SUB_HEADER: '#fxa-signin-password-header .service',
SUBMIT: 'button[type="submit"]',
SUBMIT_USE_SIGNED_IN: '.use-logged-in',
},
SIGNIN_RECOVERY_CODE: {
DONE_BUTTON: '.two-step-authentication-done',

Просмотреть файл

@ -12,8 +12,6 @@ const config = intern._config;
const OAUTH_APP = config.fxaOAuthApp;
const selectors = require('./lib/selectors');
const SYNC_SIGNIN_URL = `${config.fxaContentRoot}signin?context=fx_desktop_v3&service=sync&action=email`;
const PASSWORD = 'passwordzxcv';
let email;
@ -21,9 +19,7 @@ const {
clearBrowserState,
click,
createUser,
noSuchElement,
openFxaFromRp,
openPage,
openVerificationLinkInSameTab,
testElementExists,
testElementTextEquals,
@ -279,245 +275,7 @@ registerSuite('oauth email first', {
);
},
'cached Sync credentials': function() {
return (
this.remote
.then(createUser(email, PASSWORD, { preVerified: true }))
.then(
openPage(SYNC_SIGNIN_URL, selectors.ENTER_EMAIL.HEADER, {
webChannelResponses: {
'fxaccounts:can_link_account': { ok: true },
'fxaccounts:fxa_status': {
capabilities: null,
signedInUser: null,
},
},
})
)
.then(type(selectors.ENTER_EMAIL.EMAIL, email))
.then(
click(
selectors.ENTER_EMAIL.SUBMIT,
selectors.SIGNIN_PASSWORD.HEADER
)
)
.then(type(selectors.SIGNIN_PASSWORD.PASSWORD, PASSWORD))
.then(
click(
selectors.SIGNIN_PASSWORD.SUBMIT,
selectors.CONNECT_ANOTHER_DEVICE.HEADER
)
)
// user is signed into Sync, now try to sign into OAuth w/o entering password.
.then(
openFxaFromRp('email-first', {
header: selectors.SIGNIN_PASSWORD.HEADER,
})
)
.then(testElementValueEquals(selectors.SIGNIN_PASSWORD.EMAIL, email))
.then(noSuchElement(selectors.SIGNIN_PASSWORD.PASSWORD))
.then(click(selectors.SIGNIN_PASSWORD.SUBMIT))
.then(testAtOAuthApp())
);
},
'cached Sync credentials, user changes email': function() {
const oAuthEmail = createEmail();
return (
this.remote
.then(createUser(email, PASSWORD, { preVerified: true }))
.then(createUser(oAuthEmail, PASSWORD, { preVerified: true }))
.then(
openPage(SYNC_SIGNIN_URL, selectors.ENTER_EMAIL.HEADER, {
webChannelResponses: {
'fxaccounts:can_link_account': { ok: true },
'fxaccounts:fxa_status': {
capabilities: null,
signedInUser: null,
},
},
})
)
.then(type(selectors.ENTER_EMAIL.EMAIL, email))
.then(
click(
selectors.ENTER_EMAIL.SUBMIT,
selectors.SIGNIN_PASSWORD.HEADER
)
)
.then(type(selectors.SIGNIN_PASSWORD.PASSWORD, PASSWORD))
.then(
click(
selectors.SIGNIN_PASSWORD.SUBMIT,
selectors.CONNECT_ANOTHER_DEVICE.HEADER
)
)
.then(
openFxaFromRp('email-first', {
header: selectors.SIGNIN_PASSWORD.HEADER,
})
)
.then(testElementValueEquals(selectors.SIGNIN_PASSWORD.EMAIL, email))
// user realizes they want to use a different account.
.then(
click(
selectors.SIGNIN_PASSWORD.LINK_MISTYPED_EMAIL,
selectors.ENTER_EMAIL.HEADER
)
)
.then(type(selectors.ENTER_EMAIL.EMAIL, oAuthEmail))
.then(
click(
selectors.ENTER_EMAIL.SUBMIT,
selectors.SIGNIN_PASSWORD.HEADER
)
)
.then(type(selectors.SIGNIN_PASSWORD.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN_PASSWORD.SUBMIT))
.then(testAtOAuthApp())
);
},
'cached Sync credentials, login_hint specified by relier': function() {
const loginHintEmail = createEmail();
return this.remote
.then(createUser(email, PASSWORD, { preVerified: true }))
.then(createUser(loginHintEmail, PASSWORD, { preVerified: true }))
.then(
openPage(SYNC_SIGNIN_URL, selectors.ENTER_EMAIL.HEADER, {
webChannelResponses: {
'fxaccounts:can_link_account': { ok: true },
'fxaccounts:fxa_status': {
capabilities: null,
signedInUser: null,
},
},
})
)
.then(type(selectors.ENTER_EMAIL.EMAIL, email))
.then(
click(selectors.ENTER_EMAIL.SUBMIT, selectors.SIGNIN_PASSWORD.HEADER)
)
.then(type(selectors.SIGNIN_PASSWORD.PASSWORD, PASSWORD))
.then(
click(
selectors.SIGNIN_PASSWORD.SUBMIT,
selectors.CONNECT_ANOTHER_DEVICE.HEADER
)
)
.then(
openFxaFromRp('email-first', {
header: selectors.SIGNIN_PASSWORD.HEADER,
query: {
login_hint: loginHintEmail,
},
})
)
.then(
testElementValueEquals(
selectors.SIGNIN_PASSWORD.EMAIL,
loginHintEmail
)
)
.then(type(selectors.SIGNIN_PASSWORD.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN_PASSWORD.SUBMIT))
.then(testAtOAuthApp());
},
'cached Sync credentials, login_hint specified by relier, user changes email': function() {
const loginHintEmail = createEmail();
const oAuthEmail = createEmail();
return (
this.remote
.then(createUser(email, PASSWORD, { preVerified: true }))
.then(createUser(oAuthEmail, PASSWORD, { preVerified: true }))
.then(createUser(loginHintEmail, PASSWORD, { preVerified: true }))
.then(
openPage(SYNC_SIGNIN_URL, selectors.ENTER_EMAIL.HEADER, {
webChannelResponses: {
'fxaccounts:can_link_account': { ok: true },
'fxaccounts:fxa_status': {
capabilities: null,
signedInUser: null,
},
},
})
)
.then(type(selectors.ENTER_EMAIL.EMAIL, email))
.then(
click(
selectors.ENTER_EMAIL.SUBMIT,
selectors.SIGNIN_PASSWORD.HEADER
)
)
.then(type(selectors.SIGNIN_PASSWORD.PASSWORD, PASSWORD))
.then(
click(
selectors.SIGNIN_PASSWORD.SUBMIT,
selectors.CONNECT_ANOTHER_DEVICE.HEADER
)
)
.then(
openFxaFromRp('email-first', {
header: selectors.SIGNIN_PASSWORD.HEADER,
query: {
login_hint: loginHintEmail,
},
})
)
.then(
testElementValueEquals(
selectors.SIGNIN_PASSWORD.EMAIL,
loginHintEmail
)
)
// user realizes they want to use a different account.
.then(
click(
selectors.SIGNIN_PASSWORD.LINK_MISTYPED_EMAIL,
selectors.ENTER_EMAIL.HEADER
)
)
.then(type(selectors.ENTER_EMAIL.EMAIL, oAuthEmail))
.then(
click(
selectors.ENTER_EMAIL.SUBMIT,
selectors.SIGNIN_PASSWORD.HEADER
)
)
.then(type(selectors.SIGNIN_PASSWORD.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN_PASSWORD.SUBMIT))
.then(testAtOAuthApp())
);
},
'cached OAuth credentials': function() {
'cached credentials': function() {
return (
this.remote
.then(createUser(email, PASSWORD, { preVerified: true }))
@ -540,21 +298,20 @@ registerSuite('oauth email first', {
.then(testAtOAuthApp())
.then(click(selectors['123DONE'].LINK_LOGOUT))
// user is signed in, use cached credentials but a password is needed
// user is signed in, use cached credentials no password is needed
.then(
openFxaFromRp('email-first', {
header: selectors.SIGNIN_PASSWORD.HEADER,
})
)
.then(testElementValueEquals(selectors.SIGNIN_PASSWORD.EMAIL, email))
.then(type(selectors.SIGNIN_PASSWORD.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN_PASSWORD.SUBMIT))
.then(testAtOAuthApp())
);
},
'cached OAuth credentials, login_hint specified by relier': function() {
'cached credentials, login_hint specified by relier': function() {
const loginHintEmail = createEmail();
const oAuthEmail = createEmail();
@ -582,7 +339,7 @@ registerSuite('oauth email first', {
.then(testAtOAuthApp())
.then(click(selectors['123DONE'].LINK_LOGOUT))
// user is signed in, use cached credentials but a password is needed
// login_hint takes precedence over the signed in user
.then(
openFxaFromRp('email-first', {
header: selectors.SIGNIN_PASSWORD.HEADER,
@ -605,7 +362,7 @@ registerSuite('oauth email first', {
);
},
'cached OAuth credentials, login_hint specified by relier, user changes email': function() {
'cached credentials, login_hint specified by relier, user changes email': function() {
const loginHintEmail = createEmail();
const oAuthEmail = createEmail();

Просмотреть файл

@ -20,17 +20,18 @@ let otherAccount;
const PASSWORD = '12345678';
const click = FunctionalHelpers.click;
const clearBrowserState = FunctionalHelpers.clearBrowserState;
const createUser = FunctionalHelpers.createUser;
const fillOutSignIn = FunctionalHelpers.fillOutSignIn;
const openFxaFromRp = FunctionalHelpers.openFxaFromRp;
const noSuchElement = FunctionalHelpers.noSuchElement;
const testElementExists = FunctionalHelpers.testElementExists;
const testElementTextEquals = FunctionalHelpers.testElementTextEquals;
const testElementValueEquals = FunctionalHelpers.testElementValueEquals;
const thenify = FunctionalHelpers.thenify;
const visibleByQSA = FunctionalHelpers.visibleByQSA;
const {
click,
clearBrowserState,
createUser,
fillOutSignIn,
openFxaFromRp,
noSuchElement,
testElementExists,
testElementTextEquals,
thenify,
visibleByQSA,
} = FunctionalHelpers;
const ensureUsers = thenify(function() {
return this.parent
@ -103,6 +104,9 @@ registerSuite('Firefox desktop user info handshake - OAuth flows', {
)
// User can sign in with cached credentials, no password needed.
.then(noSuchElement(selectors.SIGNIN.PASSWORD))
.then(click(selectors.SIGNIN_PASSWORD.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors['123DONE'].AUTHENTICATED))
);
},
@ -157,9 +161,11 @@ registerSuite('Firefox desktop user info handshake - OAuth flows', {
otherEmail
)
)
// normal email element is in the DOM to help password managers.
.then(testElementValueEquals(selectors.SIGNIN.EMAIL, otherEmail))
.then(testElementExists(selectors.SIGNIN.PASSWORD))
// User can sign in with cached credentials, no password needed.
.then(noSuchElement(selectors.SIGNIN.PASSWORD))
.then(click(selectors.SIGNIN_PASSWORD.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors['123DONE'].AUTHENTICATED))
);
},
},

Просмотреть файл

@ -88,10 +88,9 @@ registerSuite('oauth permissions for untrusted reliers', {
.then(click(selectors['123DONE'].BUTTON_SIGNIN))
// user signed in previously and should not need to enter
// their email address.
// either their email address or password
.then(testElementExists(selectors.SIGNIN.HEADER))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
// no permissions additional asked for
.then(testElementExists(selectors['123DONE'].AUTHENTICATED))
@ -206,10 +205,9 @@ registerSuite('oauth permissions for untrusted reliers', {
.then(click(selectors['123DONE'].BUTTON_SIGNIN))
// user signed in previously and should not need to enter
// their email address.
// either their email address or password
.then(testElementExists(selectors.SIGNIN.HEADER))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors['123DONE'].AUTHENTICATED))
.then(testUrlEquals(UNTRUSTED_OAUTH_APP))
@ -282,10 +280,9 @@ registerSuite('oauth permissions for untrusted reliers', {
.then(closeCurrentWindow())
// user is already signed in, does not need to enter their password.
.then(click(selectors['123DONE'].BUTTON_SIGNIN))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
// display name is now available
.then(
@ -314,6 +311,7 @@ registerSuite('oauth permissions for untrusted reliers', {
'test user'
)
)
// user is already signed in, does not need to enter their password.
.then(click(selectors.SETTINGS_DISPLAY_NAME.SUBMIT))
.then(visibleByQSA(selectors.SETTINGS.SUCCESS))
@ -326,8 +324,7 @@ registerSuite('oauth permissions for untrusted reliers', {
})
)
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors.OAUTH_PERMISSIONS.HEADER))
// display name is not available because it's not requested
@ -346,8 +343,7 @@ registerSuite('oauth permissions for untrusted reliers', {
.then(click(selectors['123DONE'].LINK_LOGOUT))
.then(click(selectors['123DONE'].BUTTON_SIGNIN))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
// the second time through, profile:email, profile:uid, and
// profile:display_name will be asked for, so display_name is
@ -383,8 +379,7 @@ registerSuite('oauth permissions for untrusted reliers', {
.then(openFxaFromUntrustedRp('signin'))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(
testElementExists(selectors.OAUTH_PERMISSIONS.CHECKBOX_DISPLAY_NAME)
@ -404,9 +399,11 @@ registerSuite('oauth permissions for untrusted reliers', {
// display_name was de-selected last time.
.then(click(selectors['123DONE'].BUTTON_SIGNIN))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(
click(selectors.SIGNIN.SUBMIT, selectors['123DONE'].AUTHENTICATED)
click(
selectors.SIGNIN.SUBMIT_USE_SIGNED_IN,
selectors['123DONE'].AUTHENTICATED
)
)
);
},
@ -503,8 +500,7 @@ registerSuite('oauth permissions for trusted reliers', {
openFxaFromTrustedRp('signin', { query: { prompt: 'consent' } })
)
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
// since consent is now requested, user should see prompt
.then(testElementExists(selectors.OAUTH_PERMISSIONS.HEADER))

Просмотреть файл

@ -11,6 +11,7 @@ var config = intern._config;
var CONTENT_SERVER = config.fxaContentRoot;
var APPS_SETTINGS_URL = CONTENT_SERVER + 'settings/clients?forceDeviceList=1';
var UNTRUSTED_OAUTH_APP = config.fxaUntrustedOauthApp;
const selectors = require('./lib/selectors');
var PASSWORD = 'password';
@ -26,7 +27,6 @@ const {
pollUntilGoneByQSA,
switchToWindow,
testElementExists,
type,
} = FunctionalHelpers;
var email;
@ -51,9 +51,9 @@ registerSuite('oauth settings clients', {
this.remote
.then(openFxaFromRp('signup'))
.then(fillOutSignUp(email, PASSWORD))
.then(testElementExists('#fxa-confirm-header'))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(openVerificationLinkInSameTab(email, 0))
.then(testElementExists('#loggedin'))
.then(testElementExists(selectors['123DONE'].AUTHENTICATED))
// lists the first client
.then(openPage(APPS_SETTINGS_URL, '.client-disconnect'))
@ -65,24 +65,19 @@ registerSuite('oauth settings clients', {
// cannot use the helper method here, the helper method uses $ (jQuery)
// 123Done loads jQuery in the <body> this leads to '$ is undefined' error
// when running tests, because jQuery can be slow to load
.findByCssSelector('.ready')
.end()
.findByCssSelector('.signin')
.click()
.end()
.then(click(selectors['123DONE'].BUTTON_SIGNIN))
.then(type('#password', PASSWORD))
.then(click('#submit-btn'))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists('#fxa-permissions-header'))
.then(click('#accept'))
.then(testElementExists('#loggedin'))
.then(testElementExists(selectors.OAUTH_PERMISSIONS.HEADER))
.then(click(selectors.OAUTH_PERMISSIONS.SUBMIT))
.then(testElementExists(selectors['123DONE'].AUTHENTICATED))
.then(closeCurrentWindow())
// second app should show up using 'refresh'
.then(click('.clients-refresh'))
.then(click(selectors.SETTINGS_CLIENTS.BUTTON_REFRESH))
.then(testElementExists('li.client-oAuthApp[data-name^="321"]'))

Просмотреть файл

@ -18,7 +18,6 @@ const selectors = require('./lib/selectors');
otplib.authenticator.options = { encoding: 'hex' };
const SIGNUP_URL = `${config.fxaContentRoot}signup`;
const EMAIL_FIRST_SYNC_DESKTOP_URL = `${SIGNUP_URL}?context=fx_desktop_v3&service=sync&action=email`;
const SETTINGS_URL = `${config.fxaContentRoot}settings`;
const PASSWORD = 'passwordzxcv';
@ -29,7 +28,6 @@ let secret;
const thenify = FunctionalHelpers.thenify;
const {
cleanMemory,
clearBrowserState,
click,
closeCurrentWindow,
@ -43,7 +41,6 @@ const {
noSuchElement,
openFxaFromRp,
openPage,
openVerificationLinkInDifferentBrowser,
openVerificationLinkInNewTab,
openVerificationLinkInSameTab,
switchToWindow,
@ -81,12 +78,6 @@ registerSuite('oauth signin', {
);
},
tests: {
'clear memory': function() {
// tests fail on this suite very often on Circle because Firefox
// crashes here. Clear memory and hope that helps.
return this.remote.then(cleanMemory());
},
'with missing client_id': function() {
return this.remote.then(
openPage(SIGNIN_ROOT + '?scope=profile', selectors['400'].HEADER)
@ -127,8 +118,7 @@ registerSuite('oauth signin', {
.then(testAtOAuthApp());
},
'verified using a cached OAuth login': function() {
// verify account
'verified using a cached login': function() {
return (
this.remote
.then(openFxaFromRp('signin'))
@ -145,86 +135,36 @@ registerSuite('oauth signin', {
.then(click(selectors['123DONE'].BUTTON_SIGNIN))
.then(testElementExists(selectors.SIGNIN.HEADER))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testAtOAuthApp())
);
},
'verified using a cached Sync login': function() {
return this.remote
.then(
openPage(EMAIL_FIRST_SYNC_DESKTOP_URL, selectors.ENTER_EMAIL.HEADER, {
webChannelResponses: {
'fxaccounts:can_link_account': { ok: true },
},
})
)
.then(type(selectors.ENTER_EMAIL.EMAIL, email))
.then(click(selectors.ENTER_EMAIL.SUBMIT))
.then(type(selectors.SIGNUP_PASSWORD.PASSWORD, PASSWORD))
.then(type(selectors.SIGNUP_PASSWORD.VPASSWORD, PASSWORD))
.then(type(selectors.SIGNUP_PASSWORD.AGE, 21))
.then(click(selectors.SIGNUP_PASSWORD.SUBMIT))
.then(testElementExists(selectors.CHOOSE_WHAT_TO_SYNC.HEADER))
.then(click(selectors.CHOOSE_WHAT_TO_SYNC.SUBMIT))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(openVerificationLinkInDifferentBrowser(email, 0))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(openFxaFromRp('signin'))
.then(
testElementTextInclude(selectors.SIGNIN.EMAIL_NOT_EDITABLE, email)
)
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testAtOAuthApp());
},
'verified using a cached expired Sync login': function() {
'verified using a cached expired login': function() {
return (
this.remote
.then(
openPage(
EMAIL_FIRST_SYNC_DESKTOP_URL,
selectors.ENTER_EMAIL.HEADER,
{
webChannelResponses: {
'fxaccounts:can_link_account': { ok: true },
},
}
)
)
.then(type(selectors.ENTER_EMAIL.EMAIL, email))
.then(click(selectors.ENTER_EMAIL.SUBMIT))
.then(openFxaFromRp('signin'))
.then(createUser(email, PASSWORD, { preVerified: true }))
.then(type(selectors.SIGNUP_PASSWORD.PASSWORD, PASSWORD))
.then(type(selectors.SIGNUP_PASSWORD.VPASSWORD, PASSWORD))
.then(type(selectors.SIGNUP_PASSWORD.AGE, 21))
.then(click(selectors.SIGNUP_PASSWORD.SUBMIT))
// sign in with a verified account to cache credentials
.then(fillOutSignIn(email, PASSWORD))
.then(testElementExists(selectors.CHOOSE_WHAT_TO_SYNC.HEADER))
.then(click(selectors.CHOOSE_WHAT_TO_SYNC.SUBMIT))
.then(testAtOAuthApp())
.then(click(selectors['123DONE'].LINK_LOGOUT))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(openVerificationLinkInDifferentBrowser(email, 0))
.then(visibleByQSA(selectors['123DONE'].BUTTON_SIGNIN))
// round 2 - with the cached credentials
.then(click(selectors['123DONE'].BUTTON_SIGNIN))
.then(testElementExists(selectors.CONNECT_ANOTHER_DEVICE.HEADER))
.then(testElementExists(selectors.SIGNIN.HEADER))
.then(destroySessionForEmail(email))
// we only know the sessionToken is expired once the
// user submits the form.
.then(openFxaFromRp('signin'))
.then(
testElementTextInclude(selectors.SIGNIN.EMAIL_NOT_EDITABLE, email)
)
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
// we now know the sessionToken is expired. Allow the user to sign in
// with their password.
.then(testElementExists(selectors.SIGNIN.HEADER))
@ -270,8 +210,7 @@ registerSuite('oauth signin', {
.then(openFxaFromRp('signin'))
.then(testElementExists(selectors.SIGNIN.SUB_HEADER))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
// success is using a cached login and being redirected
// to a confirmation screen

Просмотреть файл

@ -28,7 +28,6 @@ let email;
let token;
const {
cleanMemory,
clearBrowserState,
click,
closeCurrentWindow,
@ -118,12 +117,6 @@ registerSuite('reset_password', {
.then(clearBrowserState());
},
tests: {
'clear memory': function() {
// tests fail on this suite very often on Circle because Firefox
// crashes here. Clear memory and hope that helps.
return this.remote.then(cleanMemory());
},
'visit confirmation screen without initiating reset_password, user is redirected to /reset_password': function() {
// user is immediately redirected to /reset_password if they have no
// sessionToken.

Просмотреть файл

@ -22,11 +22,6 @@ const PAGE_SIGNIN_SYNC_DESKTOP =
FX_DESKTOP_V3_CONTEXT +
'&service=sync&forceAboutAccounts=true';
const PAGE_SIGNUP = config.fxaContentRoot + 'signup';
const PAGE_SIGNUP_SYNC_DESKTOP =
config.fxaContentRoot +
'signup?context=' +
FX_DESKTOP_V3_CONTEXT +
'&service=sync';
const PASSWORD = 'password';
let email;
@ -43,7 +38,6 @@ const {
fillOutSignUp,
getStoredAccountByEmail,
openPage,
openVerificationLinkInDifferentBrowser,
respondToWebChannelMessage,
testElementExists,
testElementTextEquals,
@ -74,8 +68,7 @@ registerSuite('cached signin', {
.then(clearSessionStorage())
.then(openPage(PAGE_SIGNIN, selectors.SIGNIN.HEADER))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors.SETTINGS.HEADER))
);
@ -104,37 +97,19 @@ registerSuite('cached signin', {
// email is not yet denormalized :(
.then(
testElementValueEquals(selectors.SIGNIN.EMAIL, email.toUpperCase())
testElementTextEquals(
selectors.SIGNIN.EMAIL_NOT_EDITABLE,
email.toUpperCase()
)
)
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors.SETTINGS.HEADER))
// email is normalized!
.then(testElementTextEquals('.card-header', email))
.then(testElementTextEquals(selectors.SETTINGS.PROFILE_HEADER, email))
);
},
'sign in first in sync context, on second attempt credentials will be cached': function() {
return this.remote
.then(openPage(PAGE_SIGNIN_SYNC_DESKTOP, selectors.SIGNIN.HEADER))
.then(
respondToWebChannelMessage('fxaccounts:can_link_account', {
ok: true,
})
)
.then(fillOutSignIn(email, PASSWORD))
.then(testElementExists(selectors.CONFIRM_SIGNIN.HEADER))
.then(testIsBrowserNotified('fxaccounts:login'))
.then(openVerificationLinkInDifferentBrowser(email))
.then(openPage(PAGE_SIGNIN, selectors.SIGNIN.HEADER))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors.SETTINGS.HEADER));
},
'sign in once, use a different account': function() {
return (
this.remote
@ -165,18 +140,13 @@ registerSuite('cached signin', {
);
},
'sign in with cached credentials but with an expired session': function() {
'open signin page with expired cached credentials': function() {
return (
this.remote
.then(openPage(PAGE_SIGNIN_SYNC_DESKTOP, selectors.SIGNIN.HEADER))
.then(
respondToWebChannelMessage('fxaccounts:can_link_account', {
ok: true,
})
)
.then(openPage(PAGE_SIGNIN, selectors.SIGNIN.HEADER))
.then(fillOutSignIn(email, PASSWORD))
.then(testElementExists(selectors.CONFIRM_SIGNIN.HEADER))
.then(testIsBrowserNotified('fxaccounts:login'))
.then(testElementExists(selectors.SETTINGS.HEADER))
.then(destroySessionForEmail(email))
@ -184,40 +154,39 @@ registerSuite('cached signin', {
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
// Session expired error should show.
.then(visibleByQSA('.error'))
.then(visibleByQSA(selectors.SIGNIN_PASSWORD.ERROR))
.then(testElementValueEquals(selectors.SIGNIN.EMAIL, email))
.then(type('input.password', PASSWORD))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(testElementExists(selectors.SETTINGS.HEADER))
);
},
'unverified cached signin with sync context redirects to confirm email': function() {
const email = TestHelpers.createEmail();
'open signin page with valid cached credentials that expire': function() {
return (
this.remote
.then(openPage(PAGE_SIGNUP_SYNC_DESKTOP, selectors.SIGNUP.HEADER))
.then(
respondToWebChannelMessage('fxaccounts:can_link_account', {
ok: true,
})
)
.then(fillOutSignUp(email, PASSWORD))
.then(openPage(PAGE_SIGNIN, selectors.SIGNIN.HEADER))
.then(fillOutSignIn(email, PASSWORD))
.then(testElementExists(selectors.CHOOSE_WHAT_TO_SYNC.HEADER))
.then(click(selectors.SIGNIN.SUBMIT))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
// reset prefill and context
.then(clearSessionStorage())
.then(testElementExists(selectors.SETTINGS.HEADER))
.then(openPage(PAGE_SIGNIN, selectors.SIGNIN.HEADER))
// cached login should still go to email confirmation screen for unverified accounts
.then(testElementExists(selectors.SIGNIN.EMAIL_NOT_EDITABLE))
.then(destroySessionForEmail(email))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
// Session expired error should show.
.then(visibleByQSA(selectors.SIGNIN.ERROR))
.then(testElementValueEquals(selectors.SIGNIN.EMAIL, email))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(testElementExists(selectors.SETTINGS.HEADER))
);
},
@ -232,15 +201,14 @@ registerSuite('cached signin', {
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
.then(openPage(PAGE_SIGNIN, selectors.SIGNIN.HEADER))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
// cached login should still go to email confirmation screen for unverified accounts
.then(testElementExists(selectors.CONFIRM_SIGNUP.HEADER))
);
},
'sign in on desktop then sign in with prefill does not show picker': function() {
'sign in on desktop then specify a different email on query parameter continues to cache desktop signin': function() {
return (
this.remote
.then(openPage(PAGE_SIGNIN_SYNC_DESKTOP, selectors.SIGNIN.HEADER))
@ -256,7 +224,7 @@ registerSuite('cached signin', {
.then(
openPage(PAGE_SIGNIN + '?email=' + email2, selectors.SIGNIN.HEADER)
)
/*.then(testElementValueEquals('input.email.prefilled', email2))*/
.then(testElementValueEquals(selectors.SIGNIN.EMAIL, email2))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
@ -334,8 +302,7 @@ registerSuite('cached signin', {
})
.then(openPage(PAGE_SIGNIN, selectors.SIGNIN.HEADER))
.then(type(selectors.SIGNIN.PASSWORD, PASSWORD))
.then(click(selectors.SIGNIN.SUBMIT))
.then(click(selectors.SIGNIN.SUBMIT_USE_SIGNED_IN))
.then(testElementExists(selectors.SETTINGS.HEADER))
.then(getStoredAccountByEmail(email))