fix(oauth): Ensure we can derive relier keys during the signup flow. #2385
Conversation
4c95163
to
b2ffb60
| // 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()) { |
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?
Maybe this should be done in persist so this same logic can be used for account unlock and password reset as well?
rfk
May 7, 2015
Author
Member
Makes sense, although IIRC the account is not current passed into persist().
Makes sense, although IIRC the account is not current passed into persist().
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.
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. | |||
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?
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?
rfk
May 8, 2015
Author
Member
On reflection, "sync" suggests a two-way synchronisation between the session and its backing store. How about "reload"?
On reflection, "sync" suggests a two-way synchronisation between the session and its backing store. How about "reload"?
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?
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?
rfk
May 8, 2015
Author
Member
no, this is more about just sucking in any changes that were made to the backing store
no, this is more about just sucking in any changes that were made to the backing store
shane-tomlinson
May 9, 2015
Member
Ah, then reload makes sense. Carry on.
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) |
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 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);
}
});
rfk
May 8, 2015
Author
Member
nice catch
nice catch
c0d1a43
to
6d9e883
|
|
||
| /** | ||
| * Reload in-memory values based on what's currently in storage. | ||
| * @method sync |
shane-tomlinson
May 8, 2015
Member
you use sync in the docs and reload in the signature.
you use sync in the docs and reload in the signature.
8837344
to
6e9478b
|
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 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. |
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 |
shane-tomlinson
May 9, 2015
Member
Super nit, can you move this note above the @method
Super nit, can you move this note above the @method
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?
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)) { |
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.
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(); |
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()) {
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 () { |
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.
You can add a ' between the nt with doesn\'t. It looks funny in the JS, but renders well in HTML.
rfk
May 10, 2015
Author
Member
Heh, yeah I deliberately didn't do that because it looked funny in the JS; will add it.
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, |
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.
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.
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?
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?
shane-tomlinson
May 9, 2015
Member
never mind, I found my own answer.
never mind, I found my own answer.
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.
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); |
shane-tomlinson
May 9, 2015
Member
Does this account already have the keyFetchToken and unwrapBKey?
Does this account already have the keyFetchToken and unwrapBKey?
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.
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.
|
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 |
|
@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? |
07b098d
to
585a1d0
|
Thanks for working through this with me @rfk. I give this my r+. |
| }); | ||
| } | ||
|
|
||
| function openFxaFromRp(context, page, additionalQueryParams) { |
vladikoff
May 11, 2015
Contributor
Nit: rename this to avoid confusion with the original openFxaFromRp?
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']; | |||
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?
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?
| // 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. |
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?
@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?
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.
@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.
vladikoff
May 11, 2015
Contributor
" but the preference is for the original tab" why is that?
" but the preference is for the original tab" why is that?
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.
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.
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.
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 () { |
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' ?
Nit: signup, verify same browser with original tab open' ->
signup, verify same browser, in a different tab, with original tab open' ?
| .end(); | ||
| }, | ||
|
|
||
| 'signup, verify same browser with original tab closed': function () { |
vladikoff
May 11, 2015
Contributor
nit: naming, with vs adding a , related to https://github.com/mozilla/fxa-content-server/pull/2385/files#r30045417
nit: naming, with vs adding a , related to https://github.com/mozilla/fxa-content-server/pull/2385/files#r30045417
| .findByCssSelector('#fxa-confirm-header') | ||
| .end() | ||
|
|
||
| .then(FunctionalHelpers.openExternalSite(self)) |
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).
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(); | ||
| }, |
vladikoff
May 11, 2015
Contributor
The 2 tests above: Is original tab closed vs replace original tab a bit redundant?
@shane-tomlinson thoughts?
The 2 tests above: Is original tab closed vs replace original tab a bit redundant?
@shane-tomlinson thoughts?
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.
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.
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.
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(); | ||
| }, |
vladikoff
May 11, 2015
Contributor
2 tests above, see https://github.com/mozilla/fxa-content-server/pull/2385/files#r30046801
2 tests above, see https://github.com/mozilla/fxa-content-server/pull/2385/files#r30046801
| // It should not try to generate keys and complete the flow. | ||
| .findByCssSelector('#fxa-sign-up-complete-header') | ||
| .end(); | ||
| }, |
vladikoff
May 11, 2015
Contributor
All different browser tests, see comment https://github.com/mozilla/fxa-content-server/pull/2385/files#r30043457
All different browser tests, see comment https://github.com/mozilla/fxa-content-server/pull/2385/files#r30043457
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.
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.
|
OK, functional tests updated per @vladikoff feedback, and |
|
(But I won't be around to watch it so I'll leave the merge button for someone else...) |
4da4f5b
to
fd2fc72
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
This is a rather brutal approach to ensuring that we can derive oauth relier keys during the signup flow. It persists the
keys=truestate in the oauth session data, and persistskeyFetchTokenandunwrapBKeyin 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.