Skip to content
This repository has been archived by the owner. It is now read-only.

fix(oauth): Ensure we can derive relier keys during the signup flow. #2385

Merged
merged 1 commit into from May 13, 2015

Conversation

@rfk
Copy link
Member

@rfk rfk commented May 6, 2015

This is a rather brutal approach to ensuring that we can derive oauth relier keys during the signup flow. It persists the keys=true state in the oauth session data, and persists keyFetchToken and unwrapBKey in the account data so that they can be used on the complete_sign_up page.

I'm putting it up here as a proof of concept. We should find a way to do it without persisting the key-fetching material any more than necessary.

Fixes #2384. But not in a way that I'm happy about.

@rfk rfk force-pushed the rfk/relier-keys-signup-flow branch from 85529ef to 88dbdff May 7, 2015
@rfk rfk changed the title fix(oauth): Properly derive relier keys during the signup flow. fix(oauth): Ensure we can derive relier keys during the signup flow. May 7, 2015
@rfk rfk force-pushed the rfk/relier-keys-signup-flow branch 2 times, most recently from 4c95163 to b2ffb60 May 7, 2015
// If the relier wants keys, the signup verification tab will need
// to be able to fetch them in order to complete the flow.
// Send them as part of the oauth session data.
if (this.relier.wantsKeys()) {

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 7, 2015
Member

Maybe this should be done in persist so this same logic can be used for account unlock and password reset as well?

This comment has been minimized.

@rfk

rfk May 7, 2015
Author Member

Makes sense, although IIRC the account is not current passed into persist().

This comment has been minimized.

@rfk

rfk May 8, 2015
Author Member

Looking into this, I get the impression that:

  • the password reset case should work already, because of the existing cross-tab dance that goes on to coordinate completion of that flow in the original tab
  • the account-unlock flow only completes signin on the original tab, not the tab from the email link, and hence should work already.

If I can confirm that this is the case, I think it's better to special-case this to the sign-up flow for now, then deal with a more generalied version when we go to clean it up with e.g. tab-mutex or similar.

@@ -41,7 +41,22 @@ define([
// ignore the parse error.
}

this.set(values);
// Update new values, without overwriting items on the prototype.

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 7, 2015
Member

Maybe this logic should live in a new function "sync" - which could call "load", then perform your logic - that way the semantics of the original function are not changed and we don't break expectations inadvertently, but the intent is clear?

This comment has been minimized.

@rfk

rfk May 8, 2015
Author Member

On reflection, "sync" suggests a two-way synchronisation between the session and its backing store. How about "reload"?

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 8, 2015
Member

On reflection, "sync" suggests a two-way synchronisation between the session and its backing store. How about "reload"?

I thought that was the intent, no?

This comment has been minimized.

@rfk

rfk May 8, 2015
Author Member

no, this is more about just sucking in any changes that were made to the backing store

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

Ah, then reload makes sense. Carry on.

}
// The slight delay is to allow the functional tests time to bind
// event handlers before the flow completes.
return p().delay(100)

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 7, 2015
Member

This leaves open a 100ms window where the original tab could see that this.session.oauth exists, then finish the oauth flow, and the verification tab also finishes the oauth flow just afterwards.

I would wrap all of your new logic inside of:

  var self = this;
  return p().delay(100)
    .then(function () {
      self.session.load(); // or sync!
      if (self.session.oauth) {
      ....
         return self.finishOAuthFlow(account);
      }
  });

This comment has been minimized.

@rfk

rfk May 8, 2015
Author Member

nice catch

@shane-tomlinson shane-tomlinson self-assigned this May 7, 2015
@rfk rfk force-pushed the rfk/relier-keys-signup-flow branch 2 times, most recently from c0d1a43 to 6d9e883 May 8, 2015

/**
* Reload in-memory values based on what's currently in storage.
* @method sync

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 8, 2015
Member

you use sync in the docs and reload in the signature.

@rfk rfk force-pushed the rfk/relier-keys-signup-flow branch 2 times, most recently from 8837344 to 6e9478b May 8, 2015
@rfk
Copy link
Member Author

@rfk rfk commented May 8, 2015

OK, the latest push is moving a lot closer to something we might actually run with. Most importantly, it's got a bunch of tests! I get a clean run of the mocha tests and of npm run test-functional-oauth, apart from some force_auth tests that seem to be broken on master.

I coped the existing suite of oauth_webchannel tests into a new oauth_webchannel_keys suite and tweaked them to request and check key derivation. This was a bit fiddly because with the change, the original tab will often skip completing the oauth flow. So in e.g. the sign-up-and-verify-in-same-browser test, we have to check for the WebChannel events in the verification page rather than the main browser page.

This ickiness will go away if we move to a cross-tab mutex solution to ensure that the original tab is deterministically chosen as the one to complete the flow. But I've no idea how much work that will turn out to be, and it's nice to have some sort of fix ready in the meantime.

@rfk
Copy link
Member Author

@rfk rfk commented May 8, 2015

apart from some force_auth tests that seem to be broken on master.

Aha, I did not have mozilla/123done@65ac3b1 in my local dev setup; the whole oauth functional suite is now passing cleaning for me.

@shane-tomlinson
Copy link
Member

@shane-tomlinson shane-tomlinson commented May 9, 2015

apart from some force_auth tests that seem to be broken on master.
...
Aha, I did not have mozilla/123done@65ac3b1 in my local dev setup; the whole oauth functional suite is now passing cleaning for me.

We don't check in code if those tests fail. ;)

/**
* Reload in-memory values based on what's currently in storage.
* @method reload
* This method can be used to pull in any changes to localStorage

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

Super nit, can you move this note above the @method

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

Also, can you make a note that this operation is destructive, as in, it'll remove any values currently in Session that are not also in sessionStorage or localStorage?

this.set(values);
// Update new values, without overwriting items on the prototype.
_.each(values, function (value, key) {
if (! Session.prototype.hasOwnProperty(key)) {

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

I was thinking about this yesterday. If Session were an actual Backbone model, we wouldn't have to do this unnatural hasOwnProperty stuff because models store their data in its own field. It's a bit moot because I want to get rid of Session, and converting to a model is well out of the scope of this PR.

afterSignUpConfirmationPoll: function (account) {
// The original tab can finish the OAuth flow if it is still open,
// but not if the verification tab has already finished it.
this.session.reload();

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

Can this and the next line be combined (in all 4 places) into a descriptively named helper function that reduces the need for the comment, something like if (this.canFinishOAuthFlow()) { or if (! this.hasOAuthFlowFinished()) {

});
});

it('doesnt call sendOAuthResultToRelier if there is no session data', function () {

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

You can add a ' between the nt with doesn\'t. It looks funny in the JS, but renders well in HTML.

This comment has been minimized.

@rfk

rfk May 10, 2015
Author Member

Heh, yeah I deliberately didn't do that because it looked funny in the JS; will add it.

},

afterResetPasswordConfirmationPoll: function (account) {
// The original tab can finish the OAuth flow if it is still open,

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

Is the extra work needed for the reset password flow? In the reset password flow, the updated keys are generated once the user enters their new password in the verification page. The verification page can send the keys/web channel message to the browser w/o the original page being involved.

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

So, what I'm asking, if you haven't done so already, can you verify there is a problem w/ reset password before adding the extra logic to have the original tab send the message?

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

never mind, I found my own answer.

This comment has been minimized.

@rfk

rfk May 10, 2015
Author Member

For completeness: thanks to the existing session-token-juggling logic between the two tabs here, they were both already capable of finishing the flow with keys. The logic here is just to prevent them both from doing so, and hence triggering an error by trying to use the same keyFetchToken twice.

// but not if the verification tab has already finished it.
this.session.reload();
if (this.session.oauth) {
return this.finishOAuthFlow(account);

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 9, 2015
Member

Does this account already have the keyFetchToken and unwrapBKey?

This comment has been minimized.

@rfk

rfk May 10, 2015
Author Member

Yes, thanks to wantsKeys() being true and the user already typing their password into that tab. It's the same account that was passed into beforeSignUpConfirmationPoll above.

@rfk
Copy link
Member Author

@rfk rfk commented May 10, 2015

It's a shame we can't fetch keys until after the account is verified; if that were possible, we could do the key-derivation ahead of time and store just the relier keys in the session, rather than the master unwrapKBey...

@rfk rfk force-pushed the rfk/relier-keys-signup-flow branch from 6e9478b to e38d587 May 10, 2015
@rfk
Copy link
Member Author

@rfk rfk commented May 10, 2015

@vladikoff this PR adds some more functional tests for the oauth flow with keys, and shuffles the existing tests around a bit. Can you please give it a look over to sanity-check?

@rfk rfk force-pushed the rfk/relier-keys-signup-flow branch from e38d587 to 2f31784 May 11, 2015
@shane-tomlinson shane-tomlinson force-pushed the rfk/relier-keys-signup-flow branch 2 times, most recently from 07b098d to 585a1d0 May 11, 2015
@shane-tomlinson
Copy link
Member

@shane-tomlinson shane-tomlinson commented May 11, 2015

Thanks for working through this with me @rfk. I give this my r+.
@vladikoff, yer turn!

});
}

function openFxaFromRp(context, page, additionalQueryParams) {

This comment has been minimized.

@vladikoff

vladikoff May 11, 2015
Contributor

Nit: rename this to avoid confusion with the original openFxaFromRp?

@@ -17,7 +17,7 @@ define([
'lib/relier-keys',
'lib/url'
], function (_, Relier, ResumeToken, p, OAuthErrors, RelierKeys, Url) {
var RELIER_FIELDS_IN_RESUME_TOKEN = ['state'];
var RELIER_FIELDS_IN_RESUME_TOKEN = ['state', 'keys'];

This comment has been minimized.

@vladikoff

vladikoff May 11, 2015
Contributor

Adding keys here doesn't do anything, removing functional tests still pass. Currently we are not parsing the resume token anywhere AFAIK. I'm adding parsing in https://github.com/mozilla/fxa-content-server/pull/2366/files#diff-ca4d7096469c2a76694bb8214760cbaaR104

Not sure if we want to keep this here. Is this just for a the "different browser" flow to know that the relier wanted keys?

This comment has been minimized.

@rfk

rfk May 12, 2015
Author Member

After talking this over with @shane-tomlinson, I will just remove it from here.

// Hook up the new window to listen for WebChannel messages.
// XXX TODO: this is pretty gross to do universally like this...
// XXX TODO: it will go away if we can make the original tab
// reliably be the one to complete the oauth flow.

This comment has been minimized.

@vladikoff

vladikoff May 11, 2015
Contributor

@rfk @shane-tomlinson can we use https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API to reliably finish flow in the active tab?
Should that be part of this PR or leave it for later?

This comment has been minimized.

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 11, 2015
Member

@rfk @shane-tomlinson can we use https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API to reliably finish flow in the active tab?
Should that be part of this PR or leave it for later?

@vladikoff - at first blush this makes sense, but the preference is for the original tab to finish the flow, if it is still open. @rfk has already started on a PR to make it so.

This comment has been minimized.

@vladikoff

vladikoff May 11, 2015
Contributor

" but the preference is for the original tab" why is that?

This comment has been minimized.

@vladikoff

vladikoff May 11, 2015
Contributor

Why'd he have to tie that to jQuery?

Does not have to be, I saw the brutal example on MDN and looked for a contained solution.

This comment has been minimized.

@rfk

rfk May 12, 2015
Author Member

We prefer the original sign-up tab to finish the flow, since it's the one holding the encryption material. It doesn't come into play in this PR, but in a future iteration we want to avoid writing the encryption material to localStorage. In that case, the verification link would have to re-prompt for your password to complete the flow, while the originating tab could do it automatically. Hence the (future-looking) preference for completing in the original tab.

});
},

'signup, verify same browser with original tab open': function () {

This comment has been minimized.

@vladikoff

vladikoff May 11, 2015
Contributor

Nit: signup, verify same browser with original tab open' ->
signup, verify same browser, in a different tab, with original tab open' ?

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 11, 2015
Member

My fault. @rfk copied my test names from elsewhere.

.end();
},

'signup, verify same browser with original tab closed': function () {

This comment has been minimized.

.findByCssSelector('#fxa-confirm-header')
.end()

.then(FunctionalHelpers.openExternalSite(self))

This comment has been minimized.

@vladikoff

vladikoff May 11, 2015
Contributor

nit: this maybe needs documentation, original tab closed is actually -> "navigated away somewhere else" (to example.com in this case).


.findById('fxa-sign-up-complete-header')
.end();
},

This comment has been minimized.

@vladikoff

vladikoff May 11, 2015
Contributor

The 2 tests above: Is original tab closed vs replace original tab a bit redundant?
@shane-tomlinson thoughts?

This comment has been minimized.

@shane-tomlinson

shane-tomlinson May 11, 2015
Member

The 2 tests above: Is original tab closed vs replace original tab a bit redundant?

For this case, possibly, for other cases they are not. For example, with the redirect flow, if the user replaces the tab they signed up in with the verification tab, then the user should be redirected back to the relier automatically. If they verify in a 2nd tab and the original tab has closed, then no automatic redirection.

This comment has been minimized.

@rfk

rfk May 12, 2015
Author Member

I'd like to leave it in unless you object strongly, since it might highlight weirdness when we're trying to do "complete in the original tab" in the future.


.findByCssSelector('#fxa-reset-password-complete-header')
.end();
},
// It should not try to generate keys and complete the flow.
.findByCssSelector('#fxa-sign-up-complete-header')
.end();
},

This comment has been minimized.

This comment has been minimized.

@rfk

rfk May 12, 2015
Author Member

Yeah, I guess these "from new browser P.O.V." tests aren't really testing anything. I'm going to remove them here, they're equally well covered by their equivalents in the non-keys webchannel case.

@rfk rfk added this to the train 37 milestone May 12, 2015
@rfk rfk force-pushed the rfk/relier-keys-signup-flow branch from 585a1d0 to 4da4f5b May 12, 2015
@rfk
Copy link
Member Author

@rfk rfk commented May 13, 2015

OK, functional tests updated per @vladikoff feedback, and keys removed from the resume token. AFAICT this should be good to land for train-37, and we can continue iterating toward a better solution for train-38.

@rfk
Copy link
Member Author

@rfk rfk commented May 13, 2015

(But I won't be around to watch it so I'll leave the merge button for someone else...)

@shane-tomlinson shane-tomlinson force-pushed the rfk/relier-keys-signup-flow branch from 4da4f5b to fd2fc72 May 13, 2015
@coveralls
Copy link

@coveralls coveralls commented May 13, 2015

Coverage Status

Coverage increased (+0.01%) to 97.93% when pulling fd2fc72 on rfk/relier-keys-signup-flow into a52d6fb on master.

shane-tomlinson pushed a commit that referenced this pull request May 13, 2015
fix(client): Ensure WebChannel reliers can be sent derived relier keys during the signup flow.

@rfk - this is a great start, we can iterate towards a more optimal solution next train.

r=@shane-tomlinson
@shane-tomlinson shane-tomlinson merged commit 52c081e into master May 13, 2015
2 checks passed
2 checks passed
continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details
@shane-tomlinson shane-tomlinson deleted the rfk/relier-keys-signup-flow branch May 13, 2015
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

4 participants