Redirects for 404s with Role Based Access Control

So I’m testing some jwt auth on a test site (goofy-pike-69d4a9.netlify.app) and i’m using functions to do the token creation. I have a function called auth-start that is basically my ‘login’ page. I’m trying to restrict a single path to authorized users only.
my entire site is at /developers/ so I have a few rules in my netlify.toml:

    #Generic Redirect incase anyone hits the root in previews
        [[redirects]]
          from = "/"
          to = "/developer"
          status = 302
    # 404s on anything that doesn't exist
        [[redirects]]
          from = "/*"
          to = "/developer/404.html"
          status = 404
    # require user role to access the workshops folder
        [[redirects]]
          from = "/developer/workshops/*"
          force = true
          status = 200
          conditions = {Role = ["user"]}
    # if the user condition is not met (not authed) redirect to login function
        [[redirects]]
          from = "/developer/workshops/*"
          to = "/.netlify/functions/auth-start?path=/developer/workshops/:splat"
          force = true
          status = 302

The two problems i’m seeing is:

  1. users with the user role in their token are hitting the 302 instead of the 200
  2. when users hit the login redirect, the path param that hits the function is actually blank. Not only is the splat missing but the leading path too.

Hey @tybritten :wave:t2:

Welcome to The Community :netliheart:

RE: issue #1, it sounds like you’re essentially doing RBAC with an external provider — granted, that external provider is your own Functions, but point being that you’re not using Netlify Identity. Given that constraint, can you walk me through what your JWT actually looks like? Just want to make sure it adheres to the particular format that the Netlify _redirects engine is looking for in order to work correctly. Additionally, did you set the secret/signing key that you’re using as the “JWT Secret” in the Site’s settings?

RE: issue #2, there’s a long-standing thread running based around redirects and rewrites to a function. I’d recommend giving it a thorough read, as it should inform you on how to refactor that particular redirect.

Hope that helps!


Jon

Thanks for the fast reply!

My tokens look like this:
Encoded:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidHlicml0dGVuIiwiZXhwIjoxNjExOTM0NDYwLCJpYXQiOjE2MTE5MzI2NjAsInN1YiI6InR5YnJpdHRlbiIsImlzcyI6Imh0dHBzOi8vZ29vZnktcGlrZS02OWQ0YTkubmV0bGlmeS5hcHAiLCJhcHBfbWV0YWRhdGEiOnsiYXV0aG9yaXphdGlvbiI6eyJyb2xlcyI6WyJ1c2VyIl19fX0.-fLER6izUaX1vDW5wSllTBnWHjQnWJXxaV7AP0wSaTw

Decoded:

{
  "name": "tybritten",
  "exp": 1611934460,
  "iat": 1611932660,
  "sub": "tybritten",
  "iss": "https://goofy-pike-69d4a9.netlify.app",
  "app_metadata": {
    "authorization": {
      "roles": [
        "user"
      ]
    }
  }
}

I have the signing key set, and the token is saved in nf_jwt
I’ll checkout that thread on the redirects

Nice! Well nothing seems particularly out of line there. I know the netlify docs on external providers do reference an “id” field being present in the JWT and yours does not have one, but the actual _redirects engine code that handles RBAC is closed source so I couldn’t verify whether or not that field’s presence is critical. And, while you may have already done this, I think it’s always worth digging in to really ensure the cookie header is actually being sent to the page correctly

Otherwise there was also this thread that came up recently which, while not on the same exact topic, included some tips for external JWT providers

May be worth a read :slight_smile:


Jon

1 Like

awesome, this is helpful. thanks for the tip

1 Like

ok so i’ve gotten the jwt token working properly and the path to function stuff.

I’ve gotten my main issue down to my 404 redirect breaking the others.
My config looks like this now:

[[redirects]]
  from = "/*"
  to = "/developer/404.html"
  status = 404

[[redirects]]
  from = "/developer/workshops/*"
  to = "/developer/workshops/:splat"
  force = true
  status = 200
  conditions = {Role = ["user"]}

[[redirects]]
  from = "/developer/workshops/*"
  to = "/.netlify/functions/auth-start/:splat"
  force = true
  status = 200

When it’s configured like this, for some reason the first 404 redirect (even though it’s not force) seems to interrupt the others from running (no token check or redirect). If I move it to the end, It works but I don’t get any 404s on /developer/workshops/ which is expected due to the force on those.

Hey @tybritten!

Generally speaking, the 404 redirect should always be last. Given the way that Netlify’s Shadowing premise operates, you’ll want it to be the last thing assessed.

But you do have two conflicting redirects between the 404 and the

[[redirects]]
  from = "/developer/workshops/*"
  to = "/.netlify/functions/auth-start/:splat"
  force = true
  status = 200

Under the premise that you put the 404 redirect last, if you request /develop/workshops/foo, you’re not going to get the 404 page — instead the above re-write is going to run and you’ll simply get whatever that function returns. Does that make sense? Both the 404 rule and the above rule are overlapping in which paths they cover.

Can you give me a better insight into what you want to have happen, exactly? Only redirect the users to the login page if they’re unauth’d and the page actually exists?


Jon

Basically I’d want:
authenticated users to get a 404 for any nonexistent page
unauthenticated users to get a login for anything under /developer/workshops exist or not
unauthenticated users to get a 404 for any nonexistent page on the rest of the site.

Gotcha. So the discrepancy is that you don’t want users to be redirected to the login page for a hidden page that doesn’t actually exist :thinking: … which makes sense if your login page is redirecting users back to the path they came from when they log in. E.g. don’t send them to a login page only to be redirected back to the non-existent page if they initially try to hit /developer/workshops/foo, right?

No actually the opposite. i’m fine with logging them in then redirecting them to a nonexistent page which gives them a 404.

Ah. I’m sorry I’m not following super well! But then where is the issue in just using:

[[redirects]]
  from = "/developer/workshops/*"
  to = "/developer/workshops/:splat"
  force = true
  status = 200
  conditions = {Role = ["user"]}

[[redirects]]
  from = "/developer/workshops/*"
  to = "/.netlify/functions/auth-start/:splat"
  force = true
  status = 200

[[redirects]]
  from = "/*"
  to = "/developer/404.html"
  status = 404

Is it that when an already-authorized user attempts to hit a page that doesn’t exist (/developer/workshops/foo), they get sent back to the login page instead?

If that’s the case, you may try this. I haven’t tested it and I don’t really know if it’ll work… but it’s essentially just applying the same shadowing + role-gating concept to the normal 404. Worth a shot! (note the force = false)

# RBAC Pt.1 - Pass through content for auth'd users
[[redirects]]
  from = "/developer/workshops/*"
  to = "/developer/workshops/:splat"
  force = true
  status = 200
  conditions = {Role = ["user"]}

# RBAC Pt.2 - Apply 404 rules for already-auth'd users
[[redirects]]
  from = "/developer/workshops/*"
  to = "/developer/404.html"
  force = false
  status = 404
  conditions = {Role = ["user"]}

# RBAC Pt.3 - Push un-auth'd users to login
[[redirects]]
  from = "/developer/workshops/*"
  to = "/.netlify/functions/auth-start/:splat"
  force = true
  status = 200

# Global 404 handler
[[redirects]]
  from = "/*"
  to = "/developer/404.html"
  status = 404

I guess maybe thats the problem I’m having is that I’m not understanding the shadowing properly. My understanding was the rules executed top to bottom, it would bypass rules that don’t apply until it hit one that did.

So basically I should switch those first two (RBAC Pt 1/2). since the 404 is a force = false it would go first and only apply to logged in users with files that don’t exist. Then the regular 200 rule to allow auth’ed users to access things that do exist. Does that sound right?

That’s true! “Shadowing” essentially means that if you have a rule like

from = "/developer/*
to = "/foo"

without force = true, the presence of an actual file on the path requested with supersede the redirect. So even with that rule in place, if I do legitimately have a file at /developer/first and try to hit that path in my browser, I will get /developer/first. That means the redirect rule acts more like a fallback for that path when an actual file isn’t present. This is called ‘shadowing’.

If you use force = true, the redirect will run regardless of the presence of the underlying file.


Conditions add more nuance to the redirect rules because now that rule only runs if the condition is met.

Looking at it again though, I think RBAC Pt.2 may never run, honestly. Because RBAC Pt.1 will work for any logged in user and go ahead and show them the content at that path. If there’s no content at that path, Netlify will follow the default 404 method… which is to just show the Netlify base 404 page or if your site has a ./404.html, render that.

But point being that I think RBAC Pt.1 will capture all the requests that would’ve gone to RBAC Pt.2 anyway :confused:

That actually worked- switching RBAC 1 and 2 and now it works as expected.


[[redirects]]
  from = "/developer/workshops/*"
  to = "/developer/404.html"
  force = false
  status = 404
  conditions = {Role = ["user"]}

[[redirects]]
  from = "/developer/workshops/*"
  to = "/developer/workshops/:splat"
  force = true
  status = 200
  conditions = {Role = ["user"]}

[[redirects]]
  from = "/developer/workshops/*"
  to = "/.netlify/functions/auth-start/:splat"
  force = true
  status = 200

# Global 404 handler
[[redirects]]
  from = "/*"
  to = "/developer/404.html"
  status = 404
1 Like

Ah! The shadow first… yeah I think that makes sense! Nice :slight_smile: Glad that works for you :+1:t2:

1 Like

yup, thanks for all the help. I just wanted to close the loop to avoid this: xkcd: Wisdom of the Ancients haha

Haha. That’s appreciated! :grinning: Cheers!

Does anyone know whether it should be authorization.roles or roles directly in the app_metadata? I am trying to acomplish same redirects with multiple sites using Netlify Identity and I can not go through the RBAC validation.

Netlify Identity JWT contains app_metadata.roles - without authorization field.