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 ?

3 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 https://betterprogramming.pub/how-to-securely-implement-authentication-in-single-page-applications-670534da746f. 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!

1 Like