-
Notifications
You must be signed in to change notification settings - Fork 6
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
Persist bearer token details across page reloads #75
Conversation
lib/oauth.js
Outdated
@@ -152,9 +153,14 @@ module.exports = new Model({ | |||
}, | |||
|
|||
_checkForPanoptesSession: function() { | |||
var sessionTokenDetails = sessionStorage.getItem(LOCAL_STORAGE_PREFIX + 'tokenDetails'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing JSON.parse here.
Use window session storage to store the current bearer token across page reloads, in case 3rd party cookies are blocked.
977666e
to
e3bdc3c
Compare
Reading up on sessionStorage, keys are stored for the page session, not browser session, so this code won't preserve the token across tabs or windows. |
I wonder if this might introduce changes that confuse/frustrate volunteers. What if they accidentally close their current window and lose work? |
I don't follow. At the moment they can't log in at all. |
I've verified on a local copy of Shakespeare's World that this fixes zooniverse/shakespeares_world#347. I can log in when 3rd party cookies are blocked. I'm not sure if login persists across tabs. |
I see that being able to login is critical, but how many people have 3rd parties cookies disabled? Session not persisting in new tabs would affects lots of users I think. Ideally we would like both, no? |
Having read the MDN description of
I think this misses the point. @camallen pointed out that the OAuth flow we use means that If I remember correctly, the original bug report was Safari on OSX, so that may have 3rd party cookies disabled by default. They'll also be blocked by anyone who uses an ad-blocker, or who blocks tracking cookies for privacy reasons. |
What alternatives do we have then? |
Well |
Another alternative would be to rewrite the API for the oauth module, so that the client app (Shakespeare's World/ASM/Intro2Astro) is responsible for tracking expired tokens and issuing refresh requests. I'm not sure about making changes that break live sites. |
This is meant to refresh the token before it actually expires https://github.com/zooniverse/panoptes-javascript-client/blob/master/lib/oauth.js#L242 |
Yes, but that uses an iframe to refresh the token, which is why this 3rd-party domain bug exists in the first place. |
Btw why do you think sessionStorage isn't suitable here @eatyourgreens ? |
@simoneduca The MDN docs imply that it uses page sessions, rather than browser sessions. See my original comment in this thread. |
Confirmed on a local copy of Shakespeare's World in Chrome with 3rd party cookies disabled, and this version of the client installed. If I login, then cmd-click 'Get started' to open in a new tab, I'm not logged in on the transcription tab. Auth tokens don't persist across tabs with this PR. If panoptes-javascript-client/lib/oauth.js Lines 231 to 240 in 85bdc16
|
I'd be happy to pair up to add the necessary code. But I'm not sure using |
Console warn if the iframe fails, rather than swallowing the original error and throwing a new one.
@@ -179,7 +185,7 @@ module.exports = new Model({ | |||
} | |||
} catch (error) { | |||
if (error.name === 'SecurityError') { | |||
error = new Error('No existing Panoptes session found'); | |||
console.warn('No existing Panoptes session found'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not logging the actual error
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That happens on the next line, when the promise rejects.
@simoneduca can you expand on why using local storage isn't right? |
2f9cbde
to
941247d
Compare
I seem to remember Cam having good reasons for not doing it. However, after a bit of research it looks like I am misremembering https://stackoverflow.com/a/25198454/2794702. |
Use String.split() to get key/value pairs from the string, rather than regex.
By storing the token we increase the attack surface for clickjacking and XSS attacks, as described in the issue you linked above @simoneduca. This is one consideration when implementing the login flows on our apps. Also the bearer token is a JWT (https://jwt.io/), it is an encoded & signed payload that includes the user details and the expiry of the token. Check the debugger at the link above and paste in your JWT from the panoptes server. It should include details like:
So we can use the token expiration time to modify the renewal behaviour instead of relying on the The implicit grant (oauth.js) differs from the way PFE handles authentication. Mostly that it authenticates on another site (signin/panoptes.zooniverse.org) and noting that the implicit flow doesn't return a refresh token (used to get a new JWT when the current one expires in PFE). Implicit flows used iframes to request a new token but rely on the session cookie with panoptes to verify they are allowed, blocking these cookies (3rd party) breaks the flow with iframes as their is no session cookie passed so panoptes thinks there is no session :( We allow PFE to use the credentials flow (submit user creds directly) to gain access to JWT token & refresh tokens, thus zooniverse.org doesn't suffer from the 3rd party cookie issue, though it all runs on the same domain so it would be ok if we did. We could change our implicit apps to use the credential flows as they get refresh tokens this way and thus avoid the use of iframes / consolidate on one flow / code base. I can't remember the reason why we moved to implicit for custom apps, perhaps to simplify / standardize the authentication code ? Alternatively we can build out he implicit flows with detection / fallback for 3rd party cookie blocking. As the first login flow uses full page redirects to return the JWT not an iframe, then if i have a JWT but i can't refresh it using iframe them i'm most likely blocking 3rd party cookies. Noting that Jim has debugged this and it appears that the sigin bug i described in the original issue is due to the JWT returned in the hash fragment is ignored and we request one again from an iframe :sadpanda:. Noting that the UX on blocked 3rd party cookies is poor, notify the user is being logged out, save work? and force them to login again (or whitelist the blocked domain?) through page redirects. Though i am not sure how best to avoid this for blocked sites, maybe just warn they will be logged out in x seconds due to blocking our login cookies...? I hope the above is useful, happy to clarify the oauth flows / options we have as it can be quite confusing. |
Any thoughts on the code in this PR? Before I do any more work on it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The authentication code flows seem a little confused but i think this will works fine to solve the borked can't login issue.
However, I don't this this will renew a token correctly once it has expired as it'll gets the data from local storage and returns that if it is set. happy to be wrong but will this break long sessions on existing apps?
this._handleBearerToken(tokenDetails); | ||
SESSION_STORAGE.setItem(LOCAL_STORAGE_PREFIX + 'tokenDetails', JSON.stringify(tokenDetails)); | ||
|
||
// And redirect to the desired page | ||
var url = ls.get(LOCAL_STORAGE_PREFIX + 'redirectUri'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the redirect straight after this local storage fetch for a saved page location that a user want's to access? E.g. /#!/transcribe
? I'm not sure why we redirect again after the authentication server round trip? Perhaps the redirectUri
name is confusing as it's also used in the oauth authentication flow..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd assume so, from the comments in the code. I don't know why it requests a new URL from the server, rather than using pushState
to update the history without reloading the page.
By the way, location.assign()
can't be passed a relative URL. It must be passed a fully qualified URL. This is what is causing the sign-in bug, because the browser will throw away the bearer token when the page refreshes, and initialise the JS app from scratch.
My best guess as to why the code is written like this would be that this library doesn't know how the client app is set up, so doesn't know how to call a new page short of making a new page request. For example, would a React app need different code here from an Angular app and, if so, why should this library suddenly have to know about javascript app frameworks and how they navigate between views? A better oauth API might do the token initialisation, then leave it up to the calling app to decide how to navigate back to the original view and re-render the page.
Tell me about it! The bug turns out to be really simple, but understanding the code had me going around in circles for a bit. I think the code could be cleaned up a lot, but that might be a long job and I wasn't sure how quickly a fix is needed for the inability to log in.
I'm using EDIT: I should add that my reservation about using |
A quick note on security:
Window |
It's been broken for a long time, it's not super urgent but something to fix and the current behaviour it's not great UX from us.
This code is so entangled it confusing to get the flow right in my head...here is my take on this PR (please correct me where it's wrong). Assuming a blanks slate (no data in session store):
If what i believe is happening in 2-3 is correct in this PR change, then our sites will effectively log the user out after 1hr 59 mins but will not update the UI so to users they will appear logged in. All subsequent API interactions will be not logged in, new subjects fetch, classification submission, etc. Talk will work fine as it's a redirect to zooniverse.org so users will report they worked on a subject but it won't be recorded for them. This may be already happening if there is a delay between the |
You're right. Somewhere in the current code, it should be calling either I don't think there's any way, with the API defined in this code, for the client app to be notified that you've been signed out. That would probably require a callback to be set when the app initialises. There might be an auth change event that Shakespeare's World could listen to, to detect when the user has been logged out. Oddly, I'm glad I'm not the only one who finds this code hard to follow. I was starting to think I was just missing something here when I couldn't quite understand what was going on. |
Should panoptes-javascript-client/lib/oauth.js Lines 242 to 250 in 85bdc16
|
Once again we're swallowing errors and printing an innocuous message instead 😠 panoptes-javascript-client/lib/oauth.js Lines 247 to 249 in 85bdc16
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good as it stands and fixes the original broken login issue when third party cookies are disabled.
Noting that users that block 3rd party cookies will appear logged in but will actual be logged out on cookie expiration as it stands.
@@ -240,8 +249,10 @@ module.exports = new Model({ | |||
}, | |||
|
|||
_refreshBearerToken: function() { | |||
this._deleteBearerToken(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we unset the _currentUserPromise
like
panoptes-javascript-client/lib/oauth.js
Line 122 in 85bdc16
_currentUserPromise: Promise.resolve(null), |
If we can get the the calling app react to this state we can update the UI. I don't think we can do this in SW / Annotate via auth factory without a code change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That promise resolves when checkCurrent()
resolves, as far as I can tell. Once a promise is resolved, further changes don't affect it. So, I think this line here cancels any calls to checkCurrent()
that are in progress from the client app, by immediately resolving the session check with a null value.
This is working with edu API front end with 3rd party cookies disabled in chrome, though, it may need some UI work similar to the other apps to signal to re-sign in. Fixing the google sign in will be more involved because I forgot that it's basically impossible to test on staging because of this unresolved issue: zooniverse/panoptes#2024 |
One more small change, to report that error that's currently being swallowed, then I'm going to merge this and publish a minor version bump for the client. |
Use window session storage to store the current bearer token across page reloads, in case 3rd party cookies are blocked.
I think this will fix #72, where sign-in does not work on sites like antislaverymanuscripts.org if 3rd party cookies are disabled.
It does not fix token refreshes, for long sessions, being blocked in that case.