Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue that could lead to user being logged into normal Hypothesis account on websites using third-party accounts #572

Merged
merged 2 commits into from
Nov 21, 2017

Conversation

robertknight
Copy link
Member

This fixes an issue where the client could end up using OAuth tokens for a regular Hypothesis account from a previous session when visiting a website which configures the client to use third-party accounts. This happened if the user was anonymous on the third-party website and so the "grantToken" property in the service configuration was null.

Fixes #571

@robertknight robertknight requested a review from seanh November 9, 2017 17:29
@robertknight robertknight force-pushed the fix-anon-third-party-user-oauth branch from 5781fe3 to 5bbc6cd Compare November 9, 2017 18:07
@codecov
Copy link

codecov bot commented Nov 9, 2017

Codecov Report

Merging #572 into master will increase coverage by <.01%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #572      +/-   ##
==========================================
+ Coverage   90.92%   90.92%   +<.01%     
==========================================
  Files         132      132              
  Lines        5346     5347       +1     
  Branches      928      929       +1     
==========================================
+ Hits         4861     4862       +1     
  Misses        485      485
Impacted Files Coverage Δ
src/sidebar/oauth-auth.js 98.43% <100%> (+0.01%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ed148af...ea7f5e4. Read the comment docs.

@robertknight robertknight removed the WIP label Nov 9, 2017
@seanh seanh self-assigned this Nov 9, 2017
@seanh
Copy link
Contributor

seanh commented Nov 13, 2017

Works here

} else {
// User is anonymous on the publisher's website.
tokenInfoPromise = Promise.resolve(null);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here, if I understand it correctly, is that whenever there's a truthy serviceConfig() (which currently I believe is equivalent to whenever the embedding page contains a js-hypothesis-config or a window.hypothesisConfig() with a services object) then first-party accounts are disabled and third-party accounts are used.

But in the longer term I think what we want is for the default hypothes.is service to be a service like any other, one that appears in the services object alongside any other service configs, it's just that it's found in a services object in the app.html page instead of in the embedding page.

Do you agree? And if so, do we need another way of deciding when to enable third-party accounts, instead of checking for the presence of a service config?

I guess this also introduces another way to disable Hypothesis on a site, by embedding the client and including a services config (e.g. one with an authority that doesn't exist server-side and a grant token that doesn't work).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here, if I understand it correctly, is that whenever there's a truthy serviceConfig() (which currently I believe is equivalent to whenever the embedding page contains a js-hypothesis-config or a window.hypothesisConfig() with a services object) then first-party accounts are disabled and third-party accounts are used.

Here we aren't testing for first vs third party services but rather the use of automatic login via a grant token. That is what matters because this method is responsible for obtaining an access token from somewhere. In future if h becomes "just another annotation service" then it would, in theory, be eligible to use grant tokens for login as well.

But in the longer term I think what we want is for the default hypothes.is service to be a service like any other, one that appears in the services object alongside any other service configs, it's just that it's found in a services object in the app.html page instead of in the embedding page.

Yes, that is where I expect we'll eventually end up.

return auth.tokenGetter().then(() => {
assert.notCalled(fakeLocalStorage.setObject);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so this test was replaced by the parametrize-style one below

// User is anonymous on the publisher's website.
authority: 'publisher.org',
grantToken: null,
expectedToken: null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add more cases to the tokenGetter() tests above this? In describe('#tokenGetter' I see it('should request an access token if a grant token was provided', but that's all. Looking at the code I think there are a number of cases to test, seems a likely candidate for a parametrized test like this one:

  1. No service config at all
  2. Service config contains no grantToken
  3. Service config contains a grantToken but it's null. I believe other falsey values e.g. false may behave the same as null too.
  4. Service config contains a truthy grantToken

Case 2 is also tested currently, in it('should return null if no grant token was provided', but it's way further down in the file than it('should request an access token if a grant token was provided',, may be worth bringing them together in one parametrized test and adding additional cases.

return auth.tokenGetter().then(token => {
assert.equal(token, expectedToken);
assert.notCalled(fakeLocalStorage.getObject);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these tests be in describe('#tokenGetter'? They look like tests for tokenGetter() to me, although I think the existence of describe('persistence of tokens to storage' confuses things (all the other describes in the file are for describing functions: describe('#tokenGetter', describe('#login', describe('#logout',, and then this one is different).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Reading tokens from & writing tokens to storage is part of what is triggered by by tokenGetter().

var cfg = serviceConfig(settings);
if (cfg && typeof cfg.grantToken !== 'undefined') {
shouldPersist = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tokenGetter() is getting pretty long (over 70 lines). It probably needs to be split up very soon, if not overdue. It's doing a lot of different things

  1. Decides whether automatic login is in use
  2. Exchanges grant token for access token
  3. Shows error for expired access token
  4. Exchanges auth code for access token
  5. Attempts to load tokens from previous session
  6. Decides whether tokens should be persisted
  7. Attempts to refresh tokens
  8. ...

This is probably a sign that oauth-auth.js itself has gotten too long and needs to be broken up soon.

Copy link
Member Author

@robertknight robertknight Nov 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. There are three things going on here:

  1. Fetch a token from one of three sources (automatic login, auth code, browser storage) if we haven't got one yet.
  2. Await the fetched token
    3a. Return the fetched token if valid
    3b. Otherwise, refresh the token and await the result.

I could split out (1) into separate functions but I'd prefer to do that as a separate PR to avoid distracting from the bug being fixed here.

@robertknight
Copy link
Member Author

Depends on #573, will need rebasing after that is merged.

@robertknight robertknight force-pushed the fix-anon-third-party-user-oauth branch from 5bbc6cd to 20d79db Compare November 13, 2017 15:45
@robertknight
Copy link
Member Author

Rebased on #573

When automatic login to a third-party account is being used, via a grant
token, OAuth tokens persisted by other client sessions should not be
used.

Fix this in the case where the user is anonymous on the publisher's
website and so the publisher will have set the "grantToken" property in
the "services" array to `null`.

Also add a test for the case where the user is logged in and the grant
token is a JWT (although opaque to the client). This case was already
working.
If the initial access token was acquired via an automatic login using a
grant token provided by the publisher, neither the initial access token
nor refreshed tokens should be persisted to local storage.
@seanh seanh force-pushed the fix-anon-third-party-user-oauth branch from 20d79db to ea7f5e4 Compare November 13, 2017 17:54
@seanh
Copy link
Contributor

seanh commented Nov 13, 2017

Rebased

// Check if automatic login is being used, indicated by the presence of
// the 'grantToken' property in the service configuration.
if (cfg && typeof cfg.grantToken !== 'undefined') {
if (cfg.grantToken) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in Slack there's an unhandled case here when there is a service config but it doesn't contain any grantToken (not even null). This should be fixed elsewhere, in a separate PR: a service config without a grantToken should be considered invalid and rejected. The docs for grantToken need to be updated to say that it's required and does not default to null.

@@ -367,6 +388,20 @@ describe('sidebar.oauth-auth', function () {
});
});

function expireAndRefreshAccessToken() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a docstring here.

@seanh
Copy link
Contributor

seanh commented Nov 21, 2017

Following a Slack chat to explain some of the details we're gonna merge this now in order to not block other ongoing work, but @robertknight has some good ideas for refactoring this file in follow up pull requests that should make the file a lot easier to follow

@seanh seanh merged commit 868dabd into master Nov 21, 2017
@seanh seanh deleted the fix-anon-third-party-user-oauth branch November 21, 2017 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

User can end up logged into regular Hypothesis account on a page configured to use a third party account
2 participants