Home
Support Forums

Weird behavior by netlify-identity-widget and role based access control / _redirects

Dear All,

I have been trying and reading other forum messages but not quite able to figure out what needs to be done to avoid the redirects even when I have a valid login

@sachinsancheti1, one thing I noticed is that you are using a 403 status code. Our documentation recommends that the fallback redirect use a 401 status code. Could you try changing to that and see if that works better?

Dear @Dennis ,

Thank you for the message. I tried with 401 but it still did not work. I checked in the console and it still responds with a 401 after completing the sign-in. The deployment is on here

Hi! If anyone has a clue, that would really help me and the future projects I undertake with netlify identity.

@sachinsancheti1 I have a similar setup and am having very similar problems on https://80northseries.com.

I personally can’t replicate the issue, but I’ve had hundreds of users email me with bug reports saying that they’re getting redirected to the payment page after they’ve already paid or are stuck in a redirect loop. Often someone will sign up, pay, watch 2 episodes, and then come back a few days later and get redirected to the payment page. In most cases, if they sign out and sign back in the issue is fixed.

Here are the relevant parts of my _redirects file:

# Show paid users the goods
/watch /watch 200! Role=paid
/watch/* /watch/:splat 200! Role=paid
/ /watch 302! Role=paid
/payment /watch 302! Role=paid

# Show unpaid users the payment screen
/watch /payment 302! Role=user
/watch/* /payment 302! Role=user
/ /payment 302! Role=user
/payment /payment 200! Role=user

# Show anonymous users the login/homepage
/watch /login 302!
/watch/* /login?to=/watch/:splat 302!
/ / 200!

I have two functions that manage roles.

  1. identity-signup.js marks all new users with the user role

    // Empty handler to allow signup without confirming email, this is handled in payment step
    exports.handler = async (event, context) => {
      return {
        statusCode: 200,
        body: JSON.stringify({"app_metadata": {"roles": ["user"]}})
      }
    }
    
  2. payment.js handles payment and replaces role with paid

    exports.handler = async (event, context) => {
      const { identity, user } = context.clientContext;
    
      // Handle payments
    
      // Mark user as paid
      const res = await fetch(`${identity.url}/admin/users/${user.sub}`, {
        method: "PUT",
        headers: { Authorization: `Bearer ${identity.token}` },
        body: JSON.stringify({ app_metadata: { roles: ['paid'] }})
      });
    
      if(res.ok) {
        return {
          statusCode: 200,
          body: JSON.stringify({"app_metadata": {"roles": ["user"]}})
        }
      } else {
        return {
          statusCode: 422,
          body: "Error"
        }
      }
    }
    

Initially I was using netlify-identity-widget to handle signup, but after a few weeks of being unable to identify the issue, I thought I could get better logging/error handling if I wrote my own. So I’m now using gotrue-js directly and implemented my own views to manage sign up, sign in, etc.

I attempt to refresh the jwt before it expires with client-side code that looks something like this:

const auth = new GoTrue({
  APIUrl: '/.netlify/identity',
  audience: '',
  setCookie: true,
});

const user = auth.currentUser();

async function refresh(force = false) {
  await user.jwt(force);
  const ttl = +new Date(user.token.expires_at) - new Date()
  setTimeout(() => refresh(true), ttl);
}

refresh();

I also attempt to refresh to the local user data to get the updated roles after calling either of the functions above:

await user.getUserData()

I’m at a loss for what else to do.

I’ve been a happy Netlify customer for years (I used to work with the the founder at GitHub), but I am about to move off Netlify because this experience has caused me so much embarrassment with my customers.

After 6 months of struggling with Netlify Identity, it feels like an alpha product at best. The docs are incomplete, it doesn’t work with netlify dev, and there are a ton of gotchas. But maybe I’m just doing something wrong. :man_shrugging:

Hey @bkeepers :wave:t2:

And welcome to The Forums :netliheart:

I’m not a Netlify Employee but I’ve had a lot of experience with Netlify Identity, wrote a pure-react-wrapper (and Gatsby) because I too was dissatisfied with gotrue-js and have read the GoTrue source itself many times over to wrap my brain around the process end-to-end. I’ve headed a number of discussions around Netlify Identity + Role-Based Access Control via _redirects, and, while my personal general disposition on the matter is to not use it (and instead use a front-end-based gating methodology using N-ID + Functions), hopefully I can shed some light on your particular situation.

I’m sorry to hear this. But I also know too well this feeling. Luckily I dug really, really deep into NID (and built my own library) before shipping it live for any of my clients, but I empathize with you on the… “it sounds super easy but once you get into it suddenly it doesn’t really work right” vibe.


Anyway, there’s a consistent / recurring issue I see folks fall into with RBAC that I imagine you’re feeling, and one more specific issue I’m seeing in your particular implementation that may be causing headaches too.

Expired JWT in cookie => redirector

The more pervasive issue is the simpler one — you wrote front-end code to automatically refresh JWTs (which is actually similar to what my react library does) but that doesn’t help anybody that doesn’t actively have a tab open. If someone logs in on your site then closes the tab and waits an hour (the default JWT TTL) then comes back to a gated page, they’ll be kicked out to (reading your RBAC rules…) /login (possibly with the ?to=...etc depending on where said user was attempting to load). That’s because the Netlify Redirector considers an expired JWT to be no JWT at all.

Once that happens and /login loads, do your gotrue-js scripts run to refresh the JWT?

gotrue-js handles the JWT/Refresh Token and User objects separately

So, even though the JWT contains the User object / data, gotrue-js handles and treats them as separate objects. That’s not a bad thing per se (my library does too) but the issue comes in this unfortunate reality: .refreshToken() doesn’t update the user object… even though the new/refreshed JWT contains the updated user data. It just refreshes the in-memory / Localstorage JWT string.

Which leads to this situation: if a user is logged in and you have a Function that updates that user’s Roles, then client-side you run a user.jwt(true), it won’t actually update the user object in the client-side Javascript context with the new roles. :neutral_face: You would need to call user.getUserData() to force gotrue-js to update its user data.

A Side Note

You mention

Which is good, but then why do the functions return body: JSON.stringify({"app_metadata": {"roles": ["user"]}})? My opinion here would be that the function shouldn’t return that data, the refresh to the user’s object data should provide that. So, client side you’d want to call the function .then(_ => user.getUserData()). This situation is what I’ve described in the docs for my react-library an “external alteration” — meaning that the data on a User’s object has been altered outside of actions that specific user took (e.g. executed via the client-side library). Your Function updating their role is exactly one of these “external alterations”. That is specifically why my library has .refreshUser():

This method is a utility to forcibly refresh the local user’s information and authorization. While not ostensibly the most useful functionality, it presents a particular use case for when you know a user’s data has been altered externally. This typically isn’t the case - a user’s own identity.user data tends to only be changed by that user but if the user kicks off a process that externally alters the user data, this method can be useful.

The demo site exhibits this use-case for clarity - when clicking the “Make me a member!” or “Make me an admin!” buttons, the Netlify Function that runs behind the scenes makes a change to the user data - it adds or removes role(s). Since we know that’s what’s happening, we can use .refreshUser() once the button execution has completed in order to refresh the user and pull down the new role(s).

(The ‘demo site’ it’s referencing is here — feel free to sign up. It’s built around the premise of Functions changing the user Roles then forcibly updating the user data client-side, so we’re talking apples to apples here :+1:t2:)

Not bringing that up for no reason, just supporting that what you’re doing isn’t an uncommon thing and should be handled more gracefully / better.

A Hunch

Again, I can’t actually see much of your site’s code (I imagine this is a private repo) but based on the snippets you shared I’m wondering if you’re using the response from the function that contains the new role somehow more directly to un-gate content, maybe in some kind of session storage or just memory(?) then when the user comes back a few hours later (after closing any tabs) perhaps your refresh code isn’t running?

Mulling through this quite a bit but it’s a little tough to postulate without making a user / playing with roles on the site etc. Clearly there’s an issue in the client-side JWT not being fresh (either by TTL or by Roles) by the time it gets sent to the Redirect engine that handles RBAC.

Anyway, other bits:

Totally true. RBAC doesn’t work with Netlify Dev, but FWIW, if you do end up taking a client-side-gating approach in future projects, that does work in local development, you just have to use your production Identity instance. That has its own pros and cons, but it does work and that makes for a much nicer development process for sure. (One could also spin up a second Netlify Site and enable Identity on the second site to get a “second instance”. Close enough :stuck_out_tongue: )


Sorry for the mouthful. I’ll leave it there for now. Let’s iterate on this, I would love to help you find a properly-working solution. Even if that’s not re-writing in React and using my library :stuck_out_tongue_winking_eye: I’m more than happy to look at specific site code and/or create (an) account(s) on the site to help debug things through if you’re up for it.


Jon

1 Like

Wow! @jonsully - that is a lot of information here. I shall definitely go through the same and see what all may help me.

@bkeepers Thank you for your insights. Glad to hear you setup 80northseries. I see the logic you have used and then finally used the gotrue-js directly.

I too wishes the ntl dev or netlify dev command would have replicated a property identity scenario locally. Creating a duplicate repo or a second instance as jonsully put it, is not an ideal way of working on a project.

I have created a public repo with the issue that I am facing. This is for all the be able to check out and help me with the problem I am facing. I have done my testing and face the same issues as I had indicated earlier.

Netlify App - https://nifty-payne-1700e9.netlify.app/
Github Repo - https://github.com/sachinsancheti1/netlify-identity-redirect-check/

@sachinsancheti1 I really appreciate you recreating your case as a public example / repository. Helps so much :sweat_smile: I started looking through it for just a moment. Here’s what I’m seeing

  1. You’ve got redirects in both your netlify.toml and a _redirects file — definitely want to put all redirects in one or the other.
  2. I am seeing some oddness where I log in and then /houses-for-sale/ still shows the “Access Forbidden” screen, even though I can manually get to /houses-for-sale/title-2 and requesting /houses-for-sale/ with my JWT via CLI brings back the proper /houses-for-sale/ markup. I’m wondering if this is some odd HTTP caching due to headers. For the sake of the experiment, could you remove your _headers file? Beyond this test scope, I don’t recommend folks override Netlify’s caching headers (Netlify sets them well), but if somehow that’s causing the content of the /houses-for-sale/ path to be browser-cached then that could be the issue.


Jon

Dear @jonsully

I have done the following:

  • removed the redirect from the netlify.toml file as suggested
  • removed the _headers file completely

I seem to be facing the same behaviour pattern as before. Have I made a mistake in the function/script or is there something I have missed. I am getting the feeling that I may be lost right now, though I have tried to follow all guides and a good number of github public repos that use netlify identity, the widget and the redirect documentation.

@jonsully: thanks for all the info.

@sachinsancheti1 thanks for creating the demo app. Hopefully someone will be able to duplicate the issue and show us what we’re doing wrong.

I also had a _redirects file that looked like this:

/watch*
  Cache-Control: max-age=0,no-cache,no-store,must-revalidate

I added that because I was having issues where users would visit a page they’ve been to before, appear they were logged in, and then get redirected away because they couldn’t be authenticated with GoTrue. By setting the header, at least they were redirected to sign in right away.

I just removed _headers and will try again.

Thanks @bkeepers . I hope this can be solved and benefit us and the rest of the community.

1 Like

Dear All,

I think I have been able to solve the problems (partly) I have been facing here. The fixes are 2 fold, partly in the redirect, and partly in the identity token management.

  • I added a script to check if the token has expired, and if so, then netlify is to refresh the token and give a suitable message in the console.
if (user) {
    const timeCheck =
      netlifyIdentity.currentUser().token.expires_at > new Date().getTime() ? false : true;
    if (timeCheck) {
      netlifyIdentity.refresh(); //.then((jwt)=>console.log(jwt))
      console.log('Welcome', user.user_metadata.full_name);
    }
}
  • If someone is not signed in on a page which needs authentication, a simple script can be added for the netlify widget to open up.
if (user) {
...
} else {
   netlifyIdentity.open();
}
  • Next I created 3 redirect rules
/houses-for-sale/* 200! Role=customer
/houses-for-sale/ /houses-for-sale/ 200! Role=customer
/houses-for-sale/* /no-access/ 401!

I thought rule 2 in the redirects was redundant but it just behaved better than without it.

In summary,

  • I have a clean headers file
  • A good routing in the _redirects file
  • a decent on-page script that refreshes tokens or pops up the widget in case you are not logged in.
  • a refresh button which you should press ideally 1-2 mins after a page loads. I know it is not intuitive, thus I marked the button to be refreshed only after 2 mins explicitly though one could add a js script to activate the button after 120 seconds.

The refresh behaviour is probably such because of the caching and probably the script below could do some more magic!

I have updated the demo repository for my own purposes when I make apps using identity for the future and I hope it is also helpful for anyone who may use it in the future.

Demo Repo: https://github.com/sachinsancheti1/netlify-identity-redirect-check
live demo: https://nifty-payne-1700e9.netlify.app/

1 Like

Thanks for chiming in and sharing this, @sachinsancheti1! We appreciate it.

I have the same issue. After setting the nf_jwt cookie, the gated page that caused a 401 redirect can only be accessed after 10 minutes have passed. Other gated pages can be accessed immediately. This must be a caching issue.

Is there a solution on Netlify’s end?

This is the same issue that I am having… it certainly seems to be an issue with Netlify’s widget.

We are considering using Netlify for a fairly big project; but gated content is an essential feature for us.

Hi all,

So i have been using the application with the above configuration I had suggested earlier. This is my observations.

  • If authorized user lands on a gated page before the token is refreshed, say because someone shared the gated link via email / chat etc. and the end use just opened it, THAT ONE PAGE will be blocked out for a few minutes while the other pages seem to work fine
  • If authorized navigated to another gate page correctly and then came back to the original link, the original link would not work at times.

While most desktop users may not find it difficult, hard refreshing and re-opening a page after navigating are not very intuitive procedures followed on a mobile app.

Hard refreshing doesn’t seem to help refresh the page that was used to login for me. I have to wait for a while and sometimes it doesn’t ever refresh whilst all other pages show as expected. In a similar way; when the session is logged out, other tabs continue to show authorized content for a while (with refreshes).

I have reported this as an issue here - There is an issue with gated content · Issue #479 · netlify/netlify-identity-widget · GitHub

If this strange caching issue cannot be resolved then this is a deal breaker for us with Netlify and we would potentially adopt Netlify in quite a big way.

Hey there!

We’ll continue the conversation with you, @kruncher, in your topic!

1 Like