solid / authentication-panel

GitHub repository for the Solid Authentication Panel
MIT License
11 stars 15 forks source link

Allow only main script to silently log back in after a page refresh #84

Open michielbdejong opened 3 years ago

michielbdejong commented 3 years ago

Consider an in-browser Solid app with no backend, where tokens are stored in a closure (in the way ISCAJ proposes, so that if other malicious scripts would be present on the page, they would not get access to those tokens.

After a page refresh, you want the app to be able to use the refresh token to silently get back into logged-in mode otherwise the UX would get very annoying.

With Refresh Token Rotation, after a page refresh, it would still be possible that a malicious script on the page gets to logged-in state instead of the app itself, but then at least you know that only one script on the page will be able to use the refresh token to silently get back into logged-in mode.

Then if the app itself is unable to, you can at least detect that the token was stolen. Smart! Would it be worth requiring support for this from Solid IDPs?

michielbdejong commented 3 years ago

Now that I'm thinking about this, suppose a malicious script manages to run before the page's main script runs. Then it can change the content of the page. It could maybe even remove the main script from the page altogether before it starts to run. Then to both the IDP and the user this malicious script would be indistinguishable from the app's main script. So even if you do prompt the user to log in again after a page refresh, if a malicious script manages to consistently come before the main script, it can just be the app you see.

So I think what we're trying to protect against here is malicious scripts that run after the main script.

michielbdejong commented 3 years ago

Suppose the main script runs, uses refresh token 1 to get to a logged-in state, obtains refresh token 2, and stores it in localStorage. A malicious script running after the main script could use refresh token 2, and the main app would then detect this as soon as it tries to use refresh token 2 itself (whether after some time, or after a page refresh).

But now suppose the malicious script uses refresh token 2 to silently get itself logged in, then obtains refresh token 3, and stores that in the place where the main script expects it. Then it would still go undetected by the main script. The IDP could still show the user "it looks like you refreshed the page at 13:52 CEST", and if the user didn't do that, then the user could detect that it must have been some malicious script that did it.

So maybe the main script should keep a copy of the contents of localStorage inside its closure, and check once every 10 seconds whether it still matches. If it doesn't, it knows some other script is trying to trick it.

I didn't find a discussion of these additional details in https://tools.ietf.org/html/draft-ietf-oauth-browser-based-apps-05 which may just mean that there is some other mechanism related to refresh tokens that I don't know about and that already covers this. :)

acoburn commented 3 years ago

and stores it in localStorage

I would sincerely hope that no one is storing any sort of token in localStorage. It is a little like writing your password on a sticky note and attaching it to your monitor in a shared workspace.

For browser-based applications, I'm honestly not convinced that "offline" scope (i.e. refresh tokens) is the best approach. The login flow of every identity provider I've ever encountered uses cookies and so after first passing through a login flow, any subsequent flow will be transparent to a user. Why wouldn't the browser code just direct the user through the authorization code flow in order to get a new token?

elf-pavlik commented 3 years ago

I'd like to consider relying on non-extractable WebCrypto instead of using refresh tokens. Together with Self-Issued Assertion and JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants we might not need refresh token grant all together. Client would use the same key it uses for DPoP.

michielbdejong commented 3 years ago

The login flow of every identity provider I've ever encountered uses cookies and so after first passing through a login flow, any subsequent flow will be transparent to a user. Why wouldn't the browser code just direct the user through the authorization code flow in order to get a new token?

Because if the app's main script can use the IDP cookie to get back in after a page refresh, then so can a malicious script. If a script has access to localStorage then it also has access to such cookies.

relying on non-extractable WebCrypto

Would that survive a page refresh?

I think our best bet would have to be something combining:

elf-pavlik commented 3 years ago

relying on non-extractable WebCrypto

Would that survive a page refresh?

Yes, non-extractable key can be stored in IndexedDB thanks to the structured clone algorithm

michielbdejong commented 3 years ago

Ah cool. It still wouldn't resolve the goal of keeping them available only to the main script on a page, but that definitely helps, because it will stop the malicious script from taking the keys to elsewhere, rather than using them in XHR from the page.

I'll rename this issue to describe the goal rather than a part of a proposed solution.

I don't intend to work on this myself, so unless/until anyone else does, maybe we should just "park" this topic as unsolved.

CC @ianconsolata

michielbdejong commented 3 years ago

Just to make this explicit, an on-server malicious script would still get full access to whatever the app itself is allowed to do on the pod.

But we consider it more likely that a malicious script would exist inside the in-browser part of a Solid app than that it would exist in the backend part (if one exists). Therefore, if there is a server-side backend, and it only has a higher-level API, with methods like "delete this todo item", then an in-browser malicious script would be restricted in the damage it can do to only the (higher level) actions that the main in-browser script is allowed to take.

If the main in-browser script manages to load first, before any malicious scripts, after a page refresh, then it can protect its API access using e.g. a xsrf token, which would be sort of equivalent to the partial protection that refresh token rotation provides (protecting the pod access token instead of the API access token, but to the same effect).

But if the malicious script comes first after a page load then the backend would have no way of knowing whether an incoming API interaction is malicious or not. So if the backend has generic powerful methods like 'runArbitraryHttpRequest', then the separation between in-browser and on-server would not provide any real security barrier. An in-browser malicious script would be able to use the backend's generic API calls do anything, e.g. to wipe the pod.

Therefore, we say the risk of malicious scripts inside a Solid app mainly affects JavaScript apps without a backend, even though in reality the boundaries are more subtle.

michielbdejong commented 3 years ago

Cookies on the pod server's API would also work, I guess, as @jaxoncreed proposed in https://gitter.im/solid/solidos?at=5f9c1995f2fd4f60fc42b95d

michielbdejong commented 3 years ago

cc @nicolasmondada

michielbdejong commented 3 years ago

@jaxoncreed regarding your cookie-on-RS proposal, I just realised that if app1.com only accesses pod.com, there will only be a cookie session established on pod.com, and not on e.g. friend.com. So if then the page is refreshed, and then the app wants to access data from friend.com after the refresh, it will still fail.

That's why I said the cookie should be on the IDP, not on the RS. If the IDP sets a HttpOnly cookie then the user will be silently logged back in to their identity for the whole web after a page refresh, not just to their own pod.

What do you think?