Authenticating Offline-First Solid Apps

I recently started thinking about following an offline-first approach for an app I’m building, and there is an existing limitation with Inrupt’s authentication library that brings some UX challenges.

In particular, the limitation is that authenticating will always trigger a redirect. Most of the time, this redirect works without user interaction so it’s just a minor annoyance. But in the offline-first use-case, this is worse. If the application redirects upon booting up, an offline user would be greeted with a 404 page stuck on the identity provider’s domain. A solution to this problem is to postpone redirecting until interaction with the POD is necessary (or when the user comes back online), but that isn’t so easy to determine because of Lie-Fi. In order to combat Lie-Fi, I was thinking on syncing everything in the background, impervious to user interaction (potentially even using Background Sync that can happen with the browser closed).

I don’t fully understand why the authentication token can’t be stored in my app without triggering redirects, so I asked @zwifi (one of the maintainers of the library). He suggested moving the discussion here so that others can benefit from the conversation (and participate!).

So @zwifi, take it away! Sorry if you have to repeat what you already told me in private :).

5 Likes

Thanks for getting this started @NoelDeMartin ! This issue is one of the biggest blockers to real “production readiness” for Understory Garden (GitHub - understory-garden/understory.garden: Understory is a digital garden, a micro-publishing space for you to plant the seeds of your ideas and grow them into bi-directionally linked web portals. It's a social zettelkasten that lets users use Web Monetization to get paid when people spend time with their content.) so I’m very interested to hear more from @zwifi and to help design elegant UX for this.

3 Likes

Thanks @NoelDeMartin for opening this conversation, and investigating the interesting topic of offline-first :).

First, to set a framework for the discussion and make sure we talk about the same thing, let me recap what the OAuth2.0/OIDC login interaction looks like. This is a rough overview, but the things relevant to this discussion are present. It’s worthy to note that all of this comes from the OAuth2.0/OIDC specification, and that the Solid spec only adds a little on top of this, but does not change the general flow.

The first thing to note is that one redirect is absolutely necessary to the login process: the user provides their login/password not to the client application, but to the Identity Provider. At some point, in order to log in, the user must be redirected to the Identity Provider, which in turn will redirect them back to the Client Application. That’s the “front channel” part of the diagram. Only after this redirection back to the Client Application is the login process complete, with the Client Application obtaining an Access Token which enables accessing resources on behalf of the User on Solid Servers. At this point, the Client Application communicates with the Identity Provider through HTTP requests, that’s the “back channel” interaction.

When you mention the redirect working without user interaction, that’s indeed a common scenario, that happens when the user is already logged in the Identity Provider. When the user initiates login and gets redirected to the Identity Provider, if it has a cookie set on the browser, the user is immediately redirected back.

Now, that’s for how OAuth2.0/OIDC work. On top of this, there’s how our libraries work, in particular @inrupt/solid-client-authn-browser (sca-b for short). The library handles how/when redirects are triggered, and manages the access token.

How is the Access Token managed by the library ? The Access Token making it possible to access private resources, we want to limit as much as possible the risks of extraction. If we wanted the Access Token to remain available between refreshes, the only “permanent” storage available (i.e. that survives a page reload) would be the session/local storage, and that storage isn’t secure: any script loaded under an origin can read it. This is why it’s considered a vulnerability to store any sensitive information there. Therefore, the access token can only be kept in memory, and will be lost on reload. That’s why a second redirect is involved after reload: we can redirect the user to and from the identity provider “silently”, leveraging the fact that there is a cookie set, and that no user interaction is required.

So to summarize, you can only get an Access Token after an online interaction (even if it does not involve the user), and the Access Token is rather short-lived (typically not more than a 1-2 hours, often less than that). In any case, as you point out in your message reading/writing to a Pod is also an online interaction, so you’ll only need an Access Token when you are effectively online.

One important thing to note, though, is that sca-b won’t redirect the user away from the client application unpredictably. Redirects only happen in two cases:

  • when calling session.login({...}) (session being a Session object as defined by sca-b)
  • when calling session.handleIncomingRedirect({restorePreviousSesion: true}) after a page reload. If the user is being redirected from the Identity Provider, no additional redirect happens. Also, something noteworthy is that at any point, you’ll know if an Access Token is available or not: session.info.isLoggedIn is a boolean that indicates whether authenticated requests are possible or not.

Considering all of this, what do you think of the following design: initially, the user is logged out. They edit content offline, without being synced to the Pod. A “sync” icon may be greyed out, or stricken through somewhere on the screen. The user may force sync by clicking the icon. At this point, they are informed that syncing requires to login, which is only possible if you have some connectivity. This way, the user themselves work around the question of Lie-Fi, and assess whether a redirection will be successful or not. After being redirected back, sync can happen in the background without further user interaction, and you can try to figure out if the app has connectivity without disrupting the user experience.

This is a lot of information/suggestions, so I’m happy to discuss any specific point that’s unclear :). Overall, does this help to answer your question ?

4 Likes

Is the concern really about unidentified content injection vulnerabilities in the code?
If Content Security Policy is setup on all pages on an origin, the scripts would presumably be trusted, but might still allow an access token to somehow be exfiltrated?

Thanks for that introduction @zwifi, that clarifies how everything works :).

I have some follow up questions/comments:

In this scenario, wouldn’t it be possible to execute a fetch request instead of redirecting? If the user already has authenticated, the fetch request should be able to send the credentials using credentials: 'include' in the request.

If the user wasn’t authenticated, this should respond a “requires user interaction” response, and the application would be aware that a redirect is necessary.

I’ve been searching for others’ opinions on this, and I found this question in stackoverflow. The only risk they mention is XSS, which is the same thing you’re saying about scripts loaded under the origin.

For what I understand in that stackoverflow response, it is possible to mitigate XSS attacks, so shouldn’t it be the application’s responsibility to accept that risk? I’d understand it if it were opt-in, instead of opt-out, so that developers using it are aware of the risk and you could even link to some good-practices for mitigation in the documentation about this option. But is it necessary to disable it all-together? Considering all the UX trade-offs.

This kind of workflow is the kind of thing I want to avoid. I mean, if I really have to do it, that looks good (thanks for the suggestions!). But it’s still not good enough if I compare it with any other non-Solid app. I understand the issues and tradeoffs, but if this is the only option I may even consider implementing a server-side to my app (even though I really don’t want to do it).

So, since other developers may end up here, what are the options assuming the restrictions you mention? Here’s the ones I can think about, let me know if there are more:

  • Implementing a server-side. That way, the token can be safely stored in the server, and using a different OAuth flow the token doesn’t have to be short-lived.

    This would add a centralized dependency though, my server.

  • Wrap the webapp on a native app. That way, I could use a native plugin to store the token on a safe place. In this scenario, how would I access the token though, does the library expose it in any way? I understand that if that is available, the application could still store it in localStorage or whatever by themselves though.

    This solution is not ideal either, because you mention that tokens only last about 2 hours. But it’s an improvement, because I could at least implement Background Sync for those 2 hours.

  • Anything else?

3 Likes

What is the relation of this discussion with the OAuth2.0 capability to allow internet of things devices that lack a browser to login? See RFC 8628 - OAuth 2.0 Device Authorization Grant

Do I understand correctly that the Solid spec will not support these types of authentication methods?

1 Like

For out-of-browser experiences, you can use Solid-Node-Client which supports completely browserless login to NSS servers and a browserless login to ESS after initially obtaining a token via the browser.

1 Like

Where does solid-node-client store the token though? Is it intended to be run on a server, and stores the token in the hard drive?

It feels like my use-case is in the limbo, because my application will run on the browser but there are some things like Background Sync that run outside of the browser.

My post was more an answer to @hochstenbach’s question than a suggestion that you use it for your project. The way it works is the user runs a script to obtain an OIDC token, which directs them to a webpage where they sign in and get a token printed on their console. Then it is up to the user to store it (I store mine in an encrypted local vault of my passwords) then when the user runs an app, they get the token from their local vault and login with it, after that Solid-Node-Client attaches it to every fetch.

2 Likes

What is the relation of this discussion with the OAuth2.0 capability to allow internet of things devices that lack a browser to login? See RFC 8628 - OAuth 2.0 Device Authorization Grant
Do I understand correctly that the Solid spec will not support these types of authentication methods?

Note that the device flow does include a browser for the user to log in to the Identity Provider at some point, just not directly at the device level. The relation of the Solid specification to the OIDC one is defined in Solid-OIDC, but long story short both specs are mostly orthogonal: Solid-OIDC simply specifies some additional constraints on OIDC tokens, and adds a client authentication methods that makes dynamic client registration unnecessary. Otherwise, all of OIDC is supported, which includes the Device Authorization Grant. Implementing it in a decentralized ecosystem, where the Identity Provider of a given user is unknown in advance, may be challenging, but that’s a discussion for another day ;).

In this scenario, wouldn’t it be possible to execute a fetch request instead of redirecting? If the user already has authenticated, the fetch request should be able to send the credentials using credentials: 'include' in the request.
If the user wasn’t authenticated, this should respond a “requires user interaction” response, and the application would be aware that a redirect is necessary.

Unfortunately no, the OIDC protocol expects a front-channel interaction at the authorization endpoint, since the result is a redirection. The fetch API doesn’t deal well with these, as they are mostly intended to the browser, and not to scripts. However, note that the redirect may happen in an iframe (to some extent, security policies may restrict this use case), preventing a full-page redirect. We’re working on this for @inrupt/solid-client-authn-browser at the moment.

I’ve been searching for others’ opinions on this, and I found this question in stackoverflow. The only risk they mention is XSS, which is the same thing you’re saying about scripts loaded under the origin.
For what I understand in that stackoverflow response, it is possible to mitigate XSS attacks, so shouldn’t it be the application’s responsibility to accept that risk? I’d understand it if it were opt-in, instead of opt-out, so that developers using it are aware of the risk and you could even link to some good-practices for mitigation in the documentation about this option. But is it necessary to disable it all-together?

I think logistic chain attacks, where a dependency publishes a malicious update, would also be something to consider in the case of an Access Token stored in the localStorage. In any case, it really isn’t considered good practice to store the Access Token in the localStorage, and making it possible to disable such a feature is something we’re reluctant to do at this time. We hope that doing the redirect in an iframe will improve the UX, by hiding all reloading to the user. Moreover, the lifetime of the access token should be quite short, so even if the token was stored in the local storage, renewing it would be an issue, which is also why we are looking at silent token renewal through an iframe. Note that this isn’t really a Solid-specific issue, but rather an OIDC issue in general, which is only front and center here because Solid is decentralized, which adds complexity to the pre-existing OIDC issue.

what are the options assuming the restrictions you mention?

  • Indeed, having a server-side component is one easy way to work around the issue, but it’s not something that will work for all apps, as you mention in your case you’d really rather implement things client-side
  • I’m not very familiar with native apps, but I think both major mobile OS implement some app-specific storage vault, in which case securely storing a refresh token becomes possible, which mostly resolves the issue. However, in the case of a PWA (which I’m not very familiar with either, so I may be saying something stupid), where you’re still relying on browser APIs, the same issue still applies.
  • As I mentioned elsewhere in this post, what we are looking at right now is to implement “invisible refresh”, basically silently doing the redirect in an iframe as described in How To Securely Implement Authentication in Single Page Applications | by Dennis Stötzel | Better Programming. Iframes and cookies are a bit tricky to handle, but with an appropriate configuration of the Identity Providers it seems like this can be a satisfactory approach. It still requires one initial full-page redirect to authenticate the user, as you’d expect, but then as long as the Identity Provider cookie is valid it enables getting new access tokens without disrupting the user experience. I’ll keep you updated when the feature is released :).

I hope this helps, do you think that answers your questions ?

2 Likes

Thanks, that solves my doubts for the most part.

As I understand it, this is a limitation of the OIDC protocol, right? But it would be prefectly secure to communicate with the IDP in this way.

That’s a shame, but I know it wouldn’t be easy to extend or replace the protocol for Solid, so we’ll have to accept this limitation for the time being.

Yes a PWA is basically the same as a website, I was thinking about wrapping my app in something like Cordova or Capacitor in order to use the native storage. However, as you mention the lifetime of the token would be very short anyways, so I’m not sure if it’s even worth it.

It may be a good solution for Background Sync though, so I’ll keep this in mind.

Well it seems like the iframe workaround is our best hope. I’ll look into it, thanks for the info and your work!

2 Likes

Hey @zwifi I just noticed there is a secureStorage option for creating sessions in the library, but looking through the comments in the github issue, it seems like this has existed for a long time and doesn’t solve the issue with refreshing.

I was wondering, if this is supposed to be a secure storage, are there any reasons for not storing the token there? For example, if I’m developing a hybrid app, I could use a native plugin to provide a persistent secure storage.

By the way, I still haven’t given up on trying the iframe workaround, I just hadn’t been focusing on this for a while. I’ll pick it up eventually :).

Hi @NoelDeMartin ! The notion of “secure” storage was introduced very early in the development of the library, and it was indeed initially intended to store the token there. However, subsequent discussions and security considerations lead to the current design, where the token is only kept in the fetch closure.

However, in the node version of the library, the refresh token is stored in this secure storage, so it is something that may be of interest for hybrid apps.

Regarding the iframe workaround, don’t waste your time, it is going away ^^. Security policies by the browsers don’t allow to share cookies between a page and an iframe, which prevents doing what we intended. But the good news is, we are now using the refresh token in the browser code too. We initially weren’t sure it would be possible based on the spec, but basically every implementation we looked at had a loose interpretation of something we were more strict about. Which means that now, when you log in, a timeout is set to trigger a callback and refresh your access token before it expires. It is currently entierly transparent, and quite minimal, but we could look at adding more controls to this so that the refreshing behaviour is more customizable.

Thanks for clarifying!

Can you point to these discussions if they are public, or do a summary otherwise? I don’t understand why not store the token if there is a “secure storage” available.

How do you think this could be used? When I say “hybrid app” I refer to something like a webapp wrapped with something like Capacitor or Cordova, but the runtime is still the same as in a browser. The only difference is that it is possible to call native APIs using plugins. But as far as I understand the node version wouldn’t work, given that it only works with a node runtime (I’m not sure if there is a way to run node in a native app, or if it even makes sense).

That’s a bummer :(.

If I understand correctly, this fixes the issue with an app that is running for a long time. But we still have the same problem for offline-first, which is that upon a page refresh the token as been lost (both the refresh token and the actual authentication token). Am I right?

Can you point to these discussions if they are public, or do a summary otherwise? I don’t understand why not store the token if there is a “secure storage” available.

I don’t think most of these discussions happened in public, but basically this “secure” storage cannot be really secure in the browser for the reasons that have been listed in this thread (scripts sharing storage for a given origin, absence of memory isolation or of secret vault…). Which means that it gave an illusion of security, but did not provide actual security, which is why we migrated away from this to the current model.

When I say “hybrid app” I refer to something like a webapp wrapped with something like Capacitor or Cordova, but the runtime is still the same as in a browser.

Ah right, yeah the node library does expect a node runtime, and the browser library doesn’t store the refresh token in the provided storage for the same reasons as the access token. I think the issue remains the same then.

If I understand correctly, this fixes the issue with an app that is running for a long time. But we still have the same problem for offline-first, which is that upon a page refresh the token as been lost (both the refresh token and the actual authentication token). Am I right?

Absolutely, upon refresh you will be logged out. However, and maybe this has been discussed before but I skimmed through the thread and missed it if it has been, but if you are offline, what difference does it make to be logged in or logged out ? Being logged in, i.e. having an access token, is only useful to make requests to a remote Pod anyway. I know you mentioned in the past the difficulty of knowing whether you actually had connectivity or not, but do you think this makes sense:

  • Everything your app does happens locally. It means that if you refresh the page, you don’t lose any app state, only the access token if you were logged in in the first place.
  • When the page is refreshed, you can try to log back in if you have connectivity (assuming you can detect so). If successful, you’ll have an access token and then you can try to use it to sync data in the background, and if not you’re offline, which means there’s no syncing possible anyway.
  • If the user wishes to enforce sync and you’re logged out, then you could also refresh the page (since you don’t lose app data) and the UI may be still acceptable, since the refresh comes from the user action.

Does this make sense, or is the issue to detect if you have connectivity blocking enough that this isn’t acceptable ?

Well, it can be secure in the example I mentioned of having hybrid apps. I think this approach is very common even outside of Solid, and given the current status of native libraries for Solid authentication this is probably the only sane way to go about it for most developers.

In any case, if you are not catering for that use-case at the moment I understand, I was just mislead by the name of the setting. It seems like it is just a remnant of previous design decisions, so maybe you could deprecate it or add some clarifying comments in the documentation.

Yeah, the main problem is Lie-fy and slow internet connections. If I’m 100% sure that the user is offline, then it wouldn’t be a problem. But if I think the user is online, and my greeting to opening the application is redirecting them to another site, that may result in long wait times or even showing a connection timeout. Maybe the user only wanted to read data they already have locally, so this is completetly wasteful. And it goes against an offline-first design philosophy. If I wait until they actually need to write or read new data for this redirect, it’s also not great because it can interrupt their flow (for example, I wouldn’t be able to auto-save changes in forms).

In any case, none of these problems are really “blocking”, they just degrade the user experience. I’ll continue thinking if I can come up with a decent solution.