Problems with Redirect JWT role-gated pages on edge

@jen Thanks for your reply.

So External provider JWT is only available on Business Plan. Thank you for clarifying.

So to summarize, to truly protect routes via server-side redirects, either Identity or Business Plan is required. (Because front-end SPA routers are not actually secure).

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

Thank you

Hey @i_a :wave:t2:

That is true, but do understand that Netlify Identity is itself a secure Auth provider (like Okta, Auth0, etc.) and has deep integration with Netlify’s Redirects engine and Netlify Functions (which are super powerful when combined with Identity).

That’s exactly what Netlify Identity was built to do. I’m not sure why you would phrase your statement in that way: “Other than by using the thing made to accomplish the task, we can’t accomplish the task” …well, yes.


Jon

@jonsully Thanks for your question.

We’ll if you’re developing / experimenting with an Auth system using JWT, starting out / testing at $99/month is not going to cut it.

And if you use Identity, you’re not going to be able to use JWTs.

And if you just use Identity overall, after the first 1,000 you’re pretty much locked in at $99/month. Hopefully those first 1,000 users would be paying $10/month sooner than later.

So there’s lots of pressure to create an early price-module for userland, versus creating a cool service for userland that can just gain traction slowly over time.

I’m still confused here. Identity is built on JWTs… and free to use, including Netlify’s deeply-integrated visitor access control, for up to a thousand users. It’s a great bootstrapping value proposition.


Jon

Let me clarify:

It’s a great proposition, but it also could get you locked-in. Not sure what would happen if at 1,000 users or whenever, you would like to switch to a different JWT Auth system.

Well migrating from one auth provider to another is always a tricky task… that’s a fairly large piece of architecture to swap. That said, you can migrate off of Netlify identity if you’d like to or need to. A bit of an older thread and doesn’t give all the details per se (I can if you need)…

But the point is that Identity’s API absolutely offers endpoint access to your full instance data that you could use to migrate / hydrate a different provider’s user DB with. Your users would obviously need to reset their passwords since you can’t export password data (for good reason), but I don’t think there’s a “locked in” vibe here. Nothing at Netlify aims to lock folks in and take their money :slight_smile:


Jon

Also I think every service charges money at some point :stuck_out_tongue: Auth0 looks like you can get up to 7k users for free, but do remember that all of these companies are hosting, maintaining, and paying for running cloud hardware 24/7 to keep their services up. Nothing is free :grin:

Awesome, will look into the exporting. Thanks for that link.

When you’re using FaunaDB which already has user auth, it’s kind of redundant to use another auth system.

I can generate my own JWTs (in functions) based on the authorized FaunaDB user/role, and just pass that role information on to the host, but in this case, Netlify wouldn’t do the redirects based on the JWT unless using Business Plan.

Going the Identity route, I suppose I would dynamically create users/roles in FaunaDB based on the credentials generated by Identity.

Either way, I have to write code to keep the two identity systems in sync.

That’s correct, since Netlify would need to know the signing key used in your JWTs to truly verify the role object’s contents.

For what it’s worth, I’ve done this on production sites before - not with Fauna and Identity but actually with Identity and Contentful - where I keep a synced clone of each User so that the site admins can create content bound for each user specifically. Then when the end-user uses the website and gets to the ‘my-account’ sort of page, the front-end reaches out to a Netlify Function which verifies that the user is legit, then uses some ENV keys to reach out to Contentful and grab the content for that specific user.

There’s a few moving parts but it works surprisingly well. What helps is that Netlify has event-based Functions that can kick off from Identity events (a user logging in, a user signing up, etc.) and those Functions can be configured to run as ‘background functions’. Great for when you don’t want the sync process to prevent a user from signing in, but rather you want a background job to just make sure that user’s data is in sync with your second system :slight_smile:

Anyway, all that to say too, role-gated _redirects can be …touchy. Since they’re (httpOnly) cookie-based and can’t interface directly with javascript, the interop and workflow can be difficult. I encourage folks to go with the above route instead - using Identity JWTs to hit Functions (which get special powers when your site has Identity enabled) to serve private content. I think it’s more flexible and works better than creating static content that’s just role-gated. My 2c.


Jon

Ah so you’re serving SSR html layouts via Functions, based on Identity. That’s got to be slower than the host just serving static content.

To be able to redirect say /admin to /login at the server level, before any Functions run or html is served, is what I’m looking to achieve.

Also trying to avoid the synchronization routines that you’ve described above, if possible. However, good to know that that is working well for you.

I like the Event based Functions feature and background Functions features you mentioned, good work-arounds, but I would prefer just having one source of truth.

Yes, that’s the whole point, to have httpOnly secure cookie based tokens that can’t interact with javascript, that part is supposed to be difficult, strict. That is the most secure way to prevent XSS and CSRF, pass token information only in secure cookies, in the request headers between allowed domains. Letting the standard security protocols built into the browser do their thing.

Anyhow, rethinking strategies here.

Thanks for your helpful feedback. I’m sure it will be helpful to others as well.

1 Like

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!