Problems with Redirect JWT role-gated pages on edge

Ehhh the Function just proxies Contentful for security and returns JSON that React then hydrates into the DOM with various markup. Slower than a pure SSR just sending back already-marked-up HTML for that particular user? Yeah possibly, but I don’t ever have to worry about Netlify Identity scaling, Netlify Functions scaling, or Contentful scaling :stuck_out_tongue: They all also run really fast, so that entire interaction is rarely more than 300-400ms.

If the goal is to redirect before attempting to call an Authorized Function, it doesn’t matter if it happens at the server level or as part of client-side routing. The point is to push the user to authenticate before attempting to make an Authorized call. Doesn’t much matter if that comes in the form of a 302 from the server or client-side routing in a PWA just pushing the user back to the <Login> component if they’re not logged in when attempting to open a page that requires auth. (Heck, just don’t even render the button to open that page if they’re not logged in :stuck_out_tongue: )

I get that. I’ve definitely made the active choice to consider Identity the ‘source of truth’ and Contentful the clone-target in this case. Netlify just has so many connecting features when using Identity, Functions, Redirects, etc. that it doesn’t make sense to try to make my own piece-meal integration with Auth0 or Okta, etc. I’m okay with having Identity be the source of truth and it’s served me well thus far.

Ehh. Yes and no. In the JAMstack world, cookies aren’t the first-class citizen they used to be in a single-server user-to-server-back-and-forth model. Storing JWTs in localstorage exclusively also isn’t considered a security threat if you’re following some guidelines and enforcing secure connections / secure scripts. There are good discussions to be had there (security is worth it, always), but JWTs have been rocking the web world outside of httpOnly specific-domain cookies for a while :slight_smile:


Jon

Not really. Depends on webpack configuration and framework, but essentially, all affordances are in the app’s js files, and all routes to dynamically loaded components as well. Sure it may be an undertaking, but it’s possible to sift out information and see the unauthorized content in the bundles. Something to be careful with and something that blocking routes server-side can eliminate, vs just javascript routing to a different component in the PWA/SPA.

For instance, this example https://login-to-gated-site.netlify.app/ from this post: https://www.netlify.com/blog/2019/01/31/restrict-access-to-your-sites-with-role-based-redirects/

The supposedly protected text content “Congratulations, you have provided the appropriate JWT authentication” … well searching in the app.js file, you can find that text and also the path to the protected route without having to create the JWT.

Yes correct, the SSL/TLS connection is the principle measure.

Oh no, I fully agree with you. My point is that even if someone hacks my /admin/ component markup, I wrote it in such a way that it’s an innocuous shell- there’s nothing valuable in that statically-generated-at-build-time markup at all. Everything of importance has to come back from the fetch() that hydrates that shell. And as long as that fetch() is going to a Netlify Function, the Functions have automatic JWT decoding support and can validate that the user calling the function is legitimately the right person. So yes, someone could hack up and find out that my static component markup has a header that says “Important Numbers”, but since the actual important numbers are coming from a Function response, the best they’d get is the URL to the Function. They can’t force the Function to tell them the important numbers unless they have the right JWT.

This is fundamentally how the Netlify Admin UI works too. Netlify is built on Netlify. The admin ui (app.netlify.com) is all statically build, but most of it is just a big shell and fairly empty until the API calls come back, but the API calls are authenticated and authorized so there’s no way to go and hack Netlify :slight_smile:

I get where you’re coming from, and I’d agree if the case was that all the ‘private’ content was just in a statically rendered component. That would be a security problem. You’re correct. That’s not what most/all JAMstack sites are doing.

Good thing Netlify basically forces all sites to run on HTTPS only and automatically provisions Let’s Encrypt certs for all sites, domains, and subdomains that it can :slight_smile:

This seems like another 100 comment thread in the making :slight_smile:

I just wanted to pick up on one thing @i_a said

(Because front-end SPA routers are not actually secure).

Well the security for OAuth / OpenID is predicated on signed (not encypted) JWTs which expire. There are specific flows designed for relatively open browser clients (not just SPAs). So even if the JWT was accessed from Local Storage (which Auth0 use) or otherwise they have limited use. On the wire the tokens could be sniffed whatever the client unless they are encripted, as in HTTPS for web clients.

OK two things (no one expected …)

Other than using Netlify Identity or selecting the Business Plan, there is no gated user authorization / secure area for a Netlify hosted JamStack site.

I think John answered this but there is if you use Netlify Identity - possibly with the very limited (and developer focused) set of 3rd Party federated identiy providers

@jonsully said

Good thing Netlify basically forces all sites to run on HTTPS only and automatically provisions Let’s Encrypt certs for all sites, domains, and subdomains that it can

yes this is a very good thing and makes me happy.

@slim When I said SPA routers are not secure, I mean specifically in protecting bundle content. As @jonsully emphasized, with this model any information that truly needs to be secure must reside behind a server-side Function that checks authorization via JWT or cookie.

(Unless entire url routes are blocked and server-side redirects are implemented to protect the entire bundle. Hmm, this has got me thinking, but perhaps a little off topic, I’m wondering if it’s possible to deploy a site in Netlify with multiple bundle entry-points, thus /admin bundle could be gated behind its server-side redirect, and /auth bundle or /browse bundle could be their own lighter-weight and specifically purposed code bases. MicroFEs each residing at their own gated route … hmmm)

I did just go check out app.netlify.com as you kind of suggested @jonsully. And yes, I see that the same 713k app.js file that contains all routes, menus, front-end functions, and general text content is loaded for both authorized and non-authorized users.

I’m sure there must be some other more important bundles though (like billing perhaps) that are protected via gated routes and only loaded with proper credentials. All depends on use-case I suppose.

Thanks for the great discussion!

I looked at Wrangler but really don’t like that you are dependent on an internet connection to develop (if you unplug, their local dev server just shuts down lol). I like that netlify-lambda still offers that functionality.

For now, it looks like syncing FaunaDB auth to Netlify Identity is the most affordable, bleeding-edge, developer friendly, and JamStack optimized solution.

1 Like

with let’s encrypt’s proliferation, this is a standard feature that most services provide now days. I don’t even know if there still exists a use case for plain HTTP lol

@slim When I said SPA routers are not secure, I mean specifically in protecting bundle content. As @jonsully emphasized, with this model any information that truly needs to be secure must reside behind a server-side Function that checks authorization via JWT or cookie

Oh I see. Yes anything that need needs security has to be server side behind an api using an access token (though many examples seem to use an id token as an access token which seems risky to me)

Theoretically this could work but I don’t know of any SSGs utilizing this workflow at the moment — setting it up yourself may be a nasty journey through Webpack configs :rofl: Tbh the ‘hide important data behind authorized fetches’ is very much so the common and secure practice.

No, I don’t believe this is the case. Because ultimately everyone sees the same headers and boxes around the content, so it’s fine that it’s statically available inside the bundle. The actual billing details and data are protected through authorized fetches and API calls to Netlify’s core API.

This can definitely be the case. Again, I really like my sync-to-contentful workflow, but I know it’s not for everybody. My Contentful workflow syncs the user ID, full name, and possibly email address (forgot, to be honest) over every time a user logs in (to make sure the Contentful side is always up to date), but in your case maybe you only ever need to push over the user ID to Fauna since your other interactions on the Fauna side would only need to FK to the user id.

Alternatively, I don’t think I mentioned this but it’s definitely worth mentioning - you can store arbitrary data on the user objects in Netlify Identity. With the asterisk that, since the JWTs are signed, not encrypted, so the user can technically see the contents of the entire JWT, Netlify Identity exposes two JWT fields: app_metadata and user_metadata - the latter being updatable by the user themselves, and the former only being updatable by app admins (e.g. Netlify admin UI). For example, the demo site for my open source React-Netlify-Identity connector (react-netlify-identity-gotrue) uses the user_metadata block as a means for storing additional data about the user - things they can edit like their phone number, their address, full name, etc… whereas the app_metadata block could be used to store (hypothetical) foreign keys that reference that same user in a different system (like that user’s Stripe ID so you could pass them through to an authenticated Stripe Checkout session).

All that to say, if your application doesn’t need its own data model outside of auth and just needs auth and a way of securely storing keys on users, Identity alone could do it. Just wanted to make sure you knew that you can store data on users.

Things that don’t require auth or secure data :stuck_out_tongue: HTTPS is great but it does add a few extra round trips to the server. I’m not saying I actively encourage non-SSL web usage, bu there are use cases for it :sweat_smile:

Re: the cookie-based _redirects

@slim and I worked through the _redirects stuff intensely for a number of days in another huge thread, but I don’t encourage people to go down this track unless they really know what they’re doing and accept the downsides of this method. For starters, your site has to be using gotrue-js under the hood and has to be using it very carefully as to make sure to signal GoTrue (the core API behind Netlify Identity) to send back the set-cookie header to the client. That cookie header simply contains the JWT, but since it’s an httpOnly cookie, authorized exclusively for the responding domain, things get tricky.

gotrue-js does not automatically refresh JWTs that are stale. Meaning that even though the cookie gets set in the browser, it will only correctly resolve in role-gated _redirects for one hour. After that, the JWT needs to be refreshed from the server (again, sending the ‘use-cookie’ flag so the response updates the client cookie too)… but gotrue-js doesn’t really have any plumbing to do that. You would have to build some javascript in the application-level to do some expiration-checking and force the token refresh if it’s out of date.

All of that breaks the nice UX though. If I log into a site, go to a role-gated _redirects page, then close it, go watch a movie for 2 hrs, then come back and attempt to open up that very same page, I’ll be kicked out and won’t be able to access that page until the JWT gets refreshed. Which is weird, because I’m still technically, and validly, logged in as far as JWT parlance goes. That’s not a pleasant UX but there’s no way around it since the JWT management happens in Javascript but the expiration happens in… time. So you will always get kicked out until your javascript can run (hopefully on the page you get kicked out to), refresh the token, then you can try again.

Adding to this, any site on Netlify gets provisioned one GoTrue instance. That means that local development, deploy-previews's (from PRs in GitHub), branch-deploy's (from long-running branches), and production all use the same Identity instance. That presents a particular issue when the set-cookie header is requiring HTTPS (local dev) and forcing the cookie to only the prod Identity instance domain. Essentially this means that getting role-gated _redirects working in non-production may be very hard. I played around with proxy-redirects and ngrok'ing a local dev server for a while but didn’t come out with strong results. This whole problem is a non-issue if you don’t use the cookie-based JWT and just use the javascript/JSON layer.

There’s a reason I didn’t include any cookie-based JWT support into react-netlify-identity-gotrue lol. It’s a massive PITA and the only benefit of role-gated _redirects is for purely-static sites that want to gate content. Since most sites using Identity (IMO) are using it with a PWA that can restrict content through a combo of client-side routing and authorized-fetching, I don’t see the cost/benefit ratio for role-gated _redirects. It just isn’t there. That’s why even Netlify CMS (which is a big PWA, essentially) uses Netlify Identity for all its needs but doesn’t prescribe that folks install role-gated _redirects to even get to the CMS login page.

Sorry for the essay. I don’t think I ever really got around to surmising my points on role-gated _redirects so probably good to get them written out somewhere.

Hope that provides some insight :slight_smile: :netliheart:


Jon

Now this☝️ is an epic reply. Essay? Sorry? No way, this is blog worthy stuff, I hope you can use it as a basis for a post, you know? I enjoyed reading every bit of it.

Earlier today I was looking at https://github.com/netlify/netlify-identity-widget and https://github.com/netlify/gotrue and was leaning towards the gotrue-js route.

Great that you pointed out that GoTrue does no refresh stale cookies, that they expire after 1 hour, better to know about this potential pitfall ahead of time.

And good mention about the UX of a user being redirected on a page refresh (could do an onbeforeunload but now we’re getting really hacky lol).

LOL I’ve been pretty far down that rabbit hole, I actually built my own SSG to deploy static sites from CMSs via GraphQL and deploy static content from the domain apex to CDN edges. Indeed it was a workout, but that isn’t always a bad thing though :wink: Learn by doing. The final trip-up that prevented launching it were hosting limitations I ran into, and not ready to move that e-commerce site to a new host just yet (considering Netlify for this site as well).

Much appreciated. I’ll be pointing others to this thread when these kinds of topics come up.

Haha. Thanks! Yeah I’ll probably write about it more formally at some point; for now I keep a a sort of ‘Netlify Community Hall of Fame’ on my website which is more for me so I can quickly find these in-depth conversations in the future :stuck_out_tongue_winking_eye: https://jonsully.net/project/netlify-community/

I’ll insert a quick plug here for React users - the library I built to interface Netlify Identity to React, react-netlify-identity-gotrue actually does automatic token refreshing and ensures that a user’s JWT is always refreshed and ready! It’s important because the JWT being expired breaks role-gated _redirects as I mentioned above, but at actually also makes the JWT not valid for Authorized Functions. So just best to always have it fresh.

That still doesn’t solve the issue for role-gated _redirects though - since a cold fetch from the browser will have an expired JWT and there is no way to refresh that before the browser’s request, the _redirects rule will kick the user out :confused: my library just fixes the issue for when using Authorized Functions - where my JS will ensure a fresh token before any Authorized Fetch could take place :slight_smile:

That’s a window event, so unfortunately still runs after the server has responded; _redirects control server behavior so there’s really no way around it.

Well, I did just think about it and I guess if you installed a service worker on the site whose sole existing purpose was to ensure a fresh token, then injected that refresh process before the main request… maybe that could do it… but… yikes. Sounds sketchy :rofl:

Nice! Well feel free to reach out if I / we can help in the future. The posts I love the most here in The Community are the hacky, hard to figure out ones :slight_smile:

Cheers!