Identity + Role Based Access Control across subdomains

Hello,

I am trying to get Netlify Identity + Role Based Access Controls (RBAC) to work for a use-case where a single login can grant access to multiple sites and I am wondering if it is possible.

I have read a lot of information in this forum and in the Docs, but It feels like swimming upstream to get this working. The closest example I can find is this tutorial which allows you to generate a JWT on one subdomain (https://login-to-gated-site.netlify.app/) and use it to grant access to another (https://gated-site.netlify.app/)

Would really appreciate the help! Even is someone can say definitively that this is not possible and I should use X instead.

I am on the Business plan, so I believe I have access to a full Identity and RBAC functionality in my Netlify account.

Context:

I run a company and we host documentation for a number of internal projects on Netlify. Each project is hosted on a subdomain of our company’s main domain. For example:

internal-project-1.company.com
internal-project-2.company.com
internal-project-3.company.com

At the moment, we use Netlify’s simple password protection to restrict access to these sites. However, this is tedious to keep seeing the same password-entry form each time you visit a different site. It is also not very secure as the password is shared between all our company’s team members.

I would like to implement Netlify Identity + Role-Based Access Controls so that my team can login using their Netlify Identity credentials and then receive a JWT Cookie that grants access to each of the subdomains.

My aim is to have a login page at login.company.com which is connected to an Identity instance at login.company.com/.netlify/identity.

Once a user logs in they receive a JWT that allows them to visit any of the protected subdomains. Each of these protected sites would have identical _redirects file that looks like this:

/* 200! Role=team
/* https://login.company.com/

The secret used to sign the JWT at login.company.com/.netlify/identity would also be used as the JWT secret for all the protected sub-domains.

I was kinda expecting this to “Just Work”. Isn’t this the benefit of JWTs and fancy Edge Nodes?

However, the JWT Cookie stored when the user logs into login.company.com does not get sent with requests to internal-project-1.company.com and therefore Netlify denies access.

You can see Chrome excluding the Cookie in the request here:

Hovering on the i icon for the reason the cookie is filtered states that:

This cookie was blocked because neither did the request's URL domain exactly match the cookie's domain, nor was the request URL's domain a subdomain of the Cookie's Domain attribute value

I have set up a proof-of-concept to test out the implementation:

Login site: https://fervent-lamport-479b16.netlify.app/ served from login.company.com
Protected site: https://dreamy-boyd-287bfe.netlify.app served from gated-test.company.com

Is there a way to get the nf_jwt Cookie stored in a way that makes it portable between subdomains?

I think the missing step is to have the Domain attribute of the cookie set to company.com rather than login.company.com. Then the JWT should get sent with requests to other-subdomain.company.com according to my understanding of cookie security.

Because this is an HttpOnly Cookie, setting the cookie needs to happen on the server. Maybe this would need to be a custom Netlify Function or Self-hosted GoTrue instance as I don’t think Netlify Identity can be configured for this?

Greetings @james-king :wave:t2:

Welcome to The Community :netliheart:

And welcome to Identity. It’s not always simple :sweat_smile: Aside from that tutorial being a bit aged, I think the ‘second site’ that it redirects to for login probably just calls out to the Netlify Identity instance of the primary site to log the user in there. That doesn’t help you much — you’re wanting the ability to log in one place and access everywhere.

That said, your use case totally makes sense. Sounds pretty straightforward and like something that would be useful. Unfortunately there’s no easy way of making that work with Netlify Identity out of the box, precisely for two reasons you eluded to — GoTrue forces the cookie to match the exact domain it’s hosted on, and we don’t know the signing key GoTrue uses. That means a) the browser will never send that cookie to other subdo’s, and b) we couldn’t get RBAC working on other sites anyway without knowing the signing key from our primary auth instance. Netlify doesn’t give out that signing key for security purposes.

Netlify Identity is a great service, but it’s very much architected toward servicing and auth’ing for a single domain.

Accomplishing what you’re looking to do with RBAC and Netlify _redirects may be tricky. You can definitely use a third-party JWT / auth provider like SupaBase (which actually also uses GoTrue under the hood) or Auth0 or Okta to grant users a single JWT that would work across multiple Netlify Sites, but that’s not exactly the end of the story. You’d want to add the signing key for the JWT’s to each of your site’s Access Control settings (or if most of your sites will use the JWTs, you can set a team-wide (all sites) JWT signing key) (read more here), but for RBAC _redirects, I believe the cookie has to be sent under the nf_jwt cookie key. I’m not sure if those services a) set a cookie at all, or b) if they do, allow you to configure the cookie key. If they don’t do either, you could set the cookie from Javascript once your user is authenticated, but again, this is just postulation.

So, to wrap that up, if you get an external provider and can paste the JWT signing key in for each site (or team-wide), then can make sure that the JWT will be sent with a nf_jwt key in the cookies header, then have a _redirects for each site specifying role-based redirects, you could get it working.

I haven’t personally attempted this concept yet but would be happy to provide further help along the way if you’re going to try.

Hope that helps!

–
Jon

1 Like

Hi @jonsully thanks for the thoughtful and helpful reply!

Ah - I was assuming that GoTrue would sign the JWTs using the JWT Secret configured for the site in the Netlify settings. Thanks for quashing that assumption! I would have hit that brick wall eventually. But also - this leads me to another side question - does Netlify Identity work with Netlify RBAC at all? If you don’t know the secret used by GoTrue to sign the JWTs, how do you know what to add as the JWT secret in the Netlify Access control settings?

Do you know of any tutorials on this, or anyone who has got an external JWT provider working with Netlify’s RBAC? I tried to use Auth0 a couple of years ago for exactly this use-case, but the JWT it produced did not seem to have the right formatting if I remember correctly. I believe I couldn’t configure the JWT to have a Roles payload that was compatible with RBAC. Also Auth0 was super enterprise-y and not very user friendly. At the time, there were no tutorials or guides I could follow. Eventually I gave up. Thanks for the SupaBase tip - that is a new one for me.

Yes - the Cookie has to be set as nf_jwt and my understanding of Cookies and Cookie security is as follows:

  • You should not store sensitive JWTs in Cookies that are accessible by Javascript as there is too great a risk of a rogue script stealing them from the client. So the cookie has to be HttpOnly.
  • An HttpOnly Cookie cannot be set by client-side javascript
  • Therefore the cookie must be set on the server and sent to the client in the HTTP response
  • Browsers only accept Cookies if the Domain attribute matches the “real” domain of the HTTP response that initially set it (although I believe the “real” domain can be a subdomain of the Domain attribute).
  • Browsers only send cookies in the HTTP request to domains that match the Domain attribute of the cookie

This leads me to believe that the nf_jwt Cookie must be set by a service hosted at a domain or subdomain of the sites I am hoping to protect. If this is the case - I don’t really understand how Auth0 or Okta could help. Is there a part of the picture I am missing?

My current idea is that I could use Netlify Identity service as-is on my login.company.com site and pair it with a Netlify function that triggers on the login event and then sets a new nf_jwt cookie in the user’s browser. This JWT would be:

  • Signed with a secret that I choose and that I can add as the JWT secret for the sites I want to protect
  • Be sent to the client in a cookie that has the Domain attribute configured as company.com so that browsers will forward it in all requests to subdomains of company.com.

This makes me a little nervous because I am not a security engineer, but I’ll report back if I get it working, even if I’m not confident enough to put it in production.

As an update on progress… first challenge is to understand how to access user data from a Neltify function. Ran into an issue that I posted about here

Oooh - just found this: GitHub - netlify-labs/netlify-gated-sites: How to create Single Sign On flows with role based access controls & functions

Looks like a recipe for what I want to achieve.

Found it in this catalogue of Netlify Functions example Functions examples - Netlify Functions

You can’t actually have Identity enabled on a site and use the third-party JWT signing key access control. One or the other, and I think this is because the _redirects engine can only know about one signing key at a time. If you have Identity enabled, it’ll be that key. If you don’t, you can tell it your own (for external auth providers).

Yes, RBAC does work with Netlify Identity. It can be a little finicky (it really only works on your prod site, not in local dev or branches etc., and it’s not going to help with PWAs) but it definitely does work.

I don’t personally know off hand, no. I’ve done a lot of work in the Netlify Identity space, but it’s all been around Netlify Identity. I’m just now getting into Supabase so I’ll have to go down this road too since they’re considered an external provider, but until then I can’t offer too much help. As noted in the docs and as you’re already aware, the roles format has to be followed…

… although now looking at the docs, my spidey-sense is going off… The docs note the following structure requirement for external JWTs:

{
 "id": "some id",
 "exp": 1602522810,
 "app_metadata": {
   "authorization": {
    "roles": ["admin","editor"]
   }
 }
}

but Netlify Identity’s own JWTs don’t nest roles inside of authorization, they look like:

{
  "exp": 1610466759,
  "sub": "e2ddddd95-ab9b-4dd7-981a-5dddddddf756",
  "email": "foo@jonsully.net",
  "app_metadata": {
    "provider": "email",
    "roles": [
      "admin",
      "member"
    ]
  },
  "user_metadata": {
    "address": {
      "city": "Columbus",
      "state": "OH",
      "street": "11 Random Pl",
      "zip": "50005"
    },
    "full_name": "Tester",
    "phone_number": "123-123-1222"
  }
}

noting particularly that roles is not nested inside of authorization. Something to be aware of, I guess :thinking:

Yeah, I mean in a perfect world yes but (and I haven’t dug into this in a while so I may be out of date) I don’t think the security risk between the JS JWT and cookie JWT being accessible is massive. If you need to add the cookie from JS to get RBAC working, such is life. I don’t know of any external service that would allow you to set the cookie key yourself.

Interesting. I was thinking before about how to use a Function or two to get the job done, but that could potentially work — as long as Netlify injects the new user into the parameters of the login event function. Definitely getting into tricky waters here from a security standpoint, especially considering how you’d clear that cookie :confused:

If you search here on the forum, there’s a lot of really great content for this that I personally walked through with folks. Alternatively, I’ve got a few listed in my Community Hall of Fame (Netlify Pilot) that you can look at. And as far as examples, I’ve got a few that I’ve written. This repo is the demo for a react-based Identity package, but shows a few Authorized Functions and even show how to both get the user calling the function but then make admin-level Identity changes (add/remove a role from that user):


FWIW, I was thinking about this more and I think you may just want to spin up your own instance of GoTrue on Heroku or otherwise. That would allow you to control all the things you’re wanting to control, including the cookie key and keeping it httpOnly etc. — might just be more flexible.

1 Like

and also FWIW, I think most folks are just not using RBAC and instead using client-side routing for guarding content on a role-by-role basis. Not having to adhere to the cookie headers for RBAC / _redirects and just working purely off the javascript-level / local-storage JWT makes things a lot simpler across the board IMO.

1 Like

So I think this tutorial and example code from @DavidWells is exactly what I was looking for!

It puts all the pieces together and shows how to use Netlify functions to make it all work.

My main observation is that it is actually a lot more complicated than I had imagined. I don’t think I would have been able to put this together without this tutorial.

Some surprises for me in David’s implementation:

  • Each of the gated sites set their own nf_jwt cookie to grant access. I had assumed that it would be possible to have a single cookie that is portable to each subdomain.
  • Each gated site needs a couple of Functions (1 to set the cookie and another to delete it). I had assumed that all the Functions would be run on the login site.
  • There are a few more redirects required to bounce the user around and get the cookies set. I can now see why this is necessary.

@jonsully Thanks again for the really detailed reply. Really appreciate it.

In that case I think it would be very helpful if this was indicated in the Netlify settings UI. For my login site, with Identity enabled, I am able to set a JWT secret in the access control settings and there is no indication that this will be superseded by a secret determined by the Identity GoTrue instance. Maybe someone from Netlify will see this?

Yes - I had also noticed some inconsistencies there in the docs. Could definitely be a gotcha and something to be aware of - thanks for highlighting it.

I am no security expert, but there are some strong opinions here on how to handle JWTs from a security perspective.

Yes - I get the sense that not many people are not using RBAC and are finding other ways on the client side to solve this sort of problem.

@jonsully
Hi there, Just found this thread. I am having some problems with the nf_jwt cookie. We built our own auth provider, which correctly creates and sends a nf_jwt cookie to the browser for our netlify domain. The jwt has the following payload according to the docs:

{
"id":  "4711",
"exp": 1606522810,
"app_metadata": {
                    "authorization": {
                        "roles": ["viewer"]
                    }
                }
}

My _redirects file looks like this:

/secure/*	200!	Role=viewer

I correctly set the jwt secret inside my netlify site.

But I get a 404 when I try to access the url /secure
I would expect a 200, at least a 401.

What am I missing here?
Are the docs correct? Is the jwt payload ok or should it be without the “authorization” key.

Thanks
Christian

Answering my own problem here :slight_smile: My own login provider seems to be working now.

It looks like a had a small problem in generating my jwt (from a Rails API app). You may want to make that clear in the docs, that you have to explicitly set the “typ” header. This took me a couple of hours.

BTW: Suggestion for improvement: It would be really helpful if we could have some kind of analytics or detailed logs in the netlify backend when using jwt. Maybe the log could have said something like: “could not decode token, missing typ”. This would have helped me a lot in debugging.

Posted here for reference:
Before (not working):

    SECRET_KEY = Rails.application.secret_key_base.to_s
    ALGORITHM = 'HS256'

    def self.encode(payload, exp = 24.hours.from_now)
        payload[:exp] = exp.to_i
        JWT.encode(payload, SECRET_KEY, ALGORITHM)
    end

After (works):

    JWT.encode(payload, SECRET_KEY, ALGORITHM, { typ: 'JWT' })

Christian

Hey @james-king :wave:t2:

Apologies for taking a bit of time to get back to you here. I wanted to thoroughly walk through David Wells’ approach on the single-site sign on and make sure I understood some factors. I think my prior notes above may be overcomplicated and incorrect! The most important bit being where I noted that GoTrue’s cookie response (with the embedded JWT) won’t be sent to other subdomains. That’s not true. GoTrue’s set-cookie directive does not have SameSite set, so that JWT should be sent to other subdomains as long the original response from GoTrue was from a subdomain (www. or login. etc.)

If you’re not seeing that cross-subdomain-cookie-transfer happening, I’d wonder if you’re testing on xyz.netlify.app test sites (not your own domain). .netlify.app is on the public suffix list so even though two Netlify sites are indeed subdomains of .netlify.app, they won’t transfer cookies. I did confirm that on a custom domain where the login is served from www.mydomain.com, the nf_jwt cookie that GoTrue set is indeed sent in a following request to foo.mydomain.com.

This actually means that GoTrue is already setup to be the perfect identity source for single-site sign on functionality if you can just get the JWT signing key from your Identity instance (so that you can add it as the JWT secret key under “Access Control” in your gated site) — no need for extra functions or steps, the nf_jwt cookie would already be set :slight_smile: Now, as I’ve mentioned, I don’t think Netlify gives out that key, but I’m frankly not sure if that’s official policy. I’ll go ahead and tag in @Dennis because I’d like to know too :stuck_out_tongue:

I’d like to personally run some PoC’s on this once hearing back from Dennis. Getting it done with Netlify Identity seems simpler than a third party + an extra function or two.


To your thoughts on the David Wells video -

To be honest, I think most of that was actually only necessary because he was using the (public suffix) .netlify.app subdomain. If he was using a custom domain, he’d only need one function — the one that verifies the Okta session is valid then sets the nf_jwt cookie on that site. The cookie would then be passed around to subsequent subdomains. The process is a lot simpler in that way, eh? I hope that all makes sense. Again, I’m going to get a test set of sites going myself with Netlify Identity and a custom domain; this should be a bit more straightforward :stuck_out_tongue:


Hey @chriso0710! :wave:t2:
Welcome to The Community :netliheart:

Savage. I love it :heart_eyes: and in Rails? After my own heart :slight_smile: Let me know if it’s open source, I’d love to see it!

I agree with you, for sure. I don’t work for Netlify directly but this thread will be perused by at least one official support engineer who can open the appropriate internal ticket :slight_smile:

Sheesh. This is a great catch, and something you’d only ever find if you built your own auth provider :rofl: that said, I’m surprised the jwt gem doesn’t add that header by default :thinking: any-who, the typ header isn’t necessarily a Netlify-specific thing, it’s actually part of the JWT RFC spec… and apparently a critical thing for parsing any JWT. But I did double check and indeed GoTrue’s JWTs definitely contain both the typ and alg!

{
  "alg": "HS256",
  "typ": "JWT"
}

Hope that helps, friends!
Looking forward to hearing back from @Dennis on whether or not it’s possible to ascertain the JWT signing key used in any particular site’s Identity instance!

1 Like

Hi @jonsully

thank you very much for your detailed answer!

Regarding the documantation: There seems to be some uncertainty about the payload and whether app_metadata needs the authorization key or not. This is a little confusing. But in total I think the netlify docs are ok. The missing typ header clearly is the problem of the jwt ruby gem.

Apparently the default header was removed in the jwt gem starting version 2: Remove 'typ' optional parameter by ogonki-vetochki ¡ Pull Request #174 ¡ jwt/ruby-jwt ¡ GitHub
I just wrote a comment on the issue on github. We are creating jwt tokens here, so IMHO it makes no sense to omit the typ, although it is optional: There is no "typ" header in version 2.0.0 ¡ Issue #233 ¡ jwt/ruby-jwt ¡ GitHub

The project is not finished yet, but we are already thinking about open sourcing our rails-made jwt auth provider API. Thanks for the suggestion.
In the meantime if anyone here needs help building his or her own auth provider with Rails, feel free to contact me. Quite a few tricky things solved in the process… :slight_smile:

Best regards
Christian

1 Like

Hey @jonsully

Thanks for going down this particular rabbit hole with me!

Thanks! I was actually testing on my own domain. I didn’t want to post the real domain to this forum (for obvious reasons) but I did my testing with the equivalent of login.company.com and gated-site.company.com. Despite this, and as far as I can tell, the nf_jwt cookie was still not being forwarded in requests to other subdomains.

My understanding is that if SameSite is not set, then the Browser defaults to Lax which indeed means that the Cookie should get sent to other subdomains. However, when I tried this with a vanilla Netlify Identity instance at the start of this thread and found:

I might have been doing something wrong and would definitely appreciate it if you manage to demonstrate these nf_jwt cookies are sent in requests to other subdomains. It would make this a whole lot better.

However, my hunch is that the issue is not the SameSite attribute of the cookie, but the Domain attribute of the cookie. In the screenshot above, you can see that my vanilla Netlify Identity instance is setting the Domain attribute to login.company.com. [Note: I could be wrong and it could be the browser setting this attribute based on the response’s URL. Not an expert on cookies :grimacing:]

My guess/hope is that if the nf_jwt cookie Domain attribute could be set by GoTrue to company.com rather than login.company.com then the cookie would be portable to other Subdomains.

I have not managed to test this myself because Netlify Identity does not offer the possibility to customise this cookie header. Maybe GoTrue does?


^ This hypothesis could be completely wrong and perhaps the Cookie was being sent to gated-site.company.com. Chrome DevTools seems to show otherwise, but even if it was working I didn’t realise that Netlify RBAC would still deny access because the Netlify Identity JWT signing secret wasn’t the same as the one I used to protect gated-site.company.com.

Yes this would be much simpler. I don’t know if @DavidWells could comment on whether this approach is possible if you are using a domain that is not in the public suffix list. Having a single portable nf_jwt cookie that works across subdomains would be a lot simpler than needing Netlify Functions on each protected site to set and delete the cookies.

Maybe @chriso0710 could comment on whether the Rails auth service you have built can set an nf_jwt cookie that works across subdomains?


I have now successfully run through @DavidWell’s tutorial posted above and got everything working. Generally I think it is a workable solution but here are some issues:

1. Okta is not great to be honest.

For a company specialising in providing simple and secure auth solutions, this is a sign to me that they are asleep at the wheel.

2. Setting a new nf_jwt cookie on each subdomain means you cannot remove them when the user logs out

This means that:

  • if a user that logs into login.company.com
  • and then browses to gated-site.company.com
  • and then logs out of their Okta session
  • they might be surprised when they still have access to gated-site.company.com.

Setting a short expiry on the nf_jwt tokens could mitigate this, but then might get annoying if there is a little “redirect dance” every 15/60/90 minutes to get the token renewed. Will try this and see.

If we could have a single portable nf_jwt cookie provided by Netlify Identity that was signed with a key that we could then use as the JWT Secret for our gated sites, then I think it would be a much smoother ride to get this working.

Eager to hear what @Dennis thinks might be possible on that.

1 Like

@jonsully @james-king

It does, yes. That was mandatory.

We have a subdomain called login.company.de which runs a pure Rails 6.1 API with our auth provider (on heroku).
The subdomain.company.de (on netlify) can use this jwt cookie. The user login form on subdomain.company.de makes an ajax post request to the login subdomain. The auth provider sends back jwt token and cookie to the browser. The cookie is a session cookie, the jwt is valid 24 hours. The browser sends this cookie to the subdomain.company.de for every subsequent request.

Took some days to figure it all out (cors, jwt, set-cookie, ajax params, netlify token payload and role based _redirects), but it works great so far. Some minor issues/features are still on our todo-list. But we have a working PoC, if you would like to check it out :slight_smile:

Christian

1 Like

@james-king sure thing :slight_smile: I like rabbit holes :stuck_out_tongue:

RE: your tests, GoTrue’s Set-Cookie directive doesn’t actually contain a domain parameter. Here’s an example of the raw Set-Cookie response from one of my sites’s Identity instance:

Set-Cookie: nf_jwt=tokenstringhere; Path=/; Expires=Sat, 16 Jan 2021 22:29:30 GMT; Max-Age=86400; HttpOnly; Secure

And I checked again on one of my sites with Chrome - Chrome does show a ‘domain’ in the cookies viewer but I think that may just represent the domain the cookie came from. When I got an nf_jwt then quickly ran a fetch("other-subdo.site.com") the cookie was sent in the request headers to that new subdo. Were you inspecting the actual request to the gated-site.company.com for the outbound request headers to see if the nf_jwt was there?

Yeah… sort of the bigger issue anyway :confused:

Sure would @chriso0710!

1 Like

Hi @jonsully, @james-king,

here is a working prototype/PoC. Feel free to check it out.
(Content is in German, sorry for that)

A protected dir with role based access is located in
https://yourevent.goes-virtual.de/secure

The site has a login form (“Anmeldung”), which on submitting makes a request to our auth provider at login.goes-virtual.de. A valid demo code is “4711.multi”. This should set the jwt cookie and redirect to the protected subdir.

I would greatly appreciate any kind of feedback from you guys. Thanks!

Christian

1 Like

@chriso0710 Thank you! Works well for me! Looks really great!

However… I am not sure it is exactly the same use case I am looking for…

Imagining that I am a visitor to your site and I have access to more than 1 event: https://yourevent.goes-virtual.de & https://anotherevent.goes-virtual.de, would the JWT returned from your login.goes-virtual.de provide access to the /secure area on both subdomains?

@jonsully Thanks again!

Thanks for clarifying!

I believe I was checking this. But I would not be surprised if my lack of skills meant I had it wrong. Here is what I did:

  • After logging into Netlify Identity using a widget on login.company-site.com
  • I confirmed that Identity set an nf_jwt cookie with the appropriate roles
  • I then opened up Chrome’s dev tools, hit the :red_circle: button and then entered gated-site.company.com in the address bar and hit Enter.
  • This resulted in an initial request and then a redirect caused by Netlify’s RBAC back to the login.company.com site.
  • Then I opened up the Chrome Devs tools, deleted the initial outbound request and took the the screenshot I posted above. This shows the Cookies sent in the initial request to gated-test.company.com and the Cookie seems to being filtered out according to Chrome. Chrome’s reason was:

This cookie was blocked because neither did the request's URL domain exactly match the cookie's domain, nor was the request URL's domain a subdomain of the Cookie's Domain attribute value

Is running a fetch the same as visiting the site normally in a browser?

@james-king It works for both usecases. The jwt sets the authorization role.

A. If the user role for both domains is the same as the role set in the _redirects files, the user would be able to see protected pages on both sites.
Example:
user1 has role “visitor” and logs on. _redirects on site1 has “visitor” role and _redirects on site2 also has “visitor” role. user1 can see protected pages on both sites.

B. If the roles in the _redirects files for the domains are different, then the user would NOT be able to see protected pages on both sites. Only on the site he has a role for.
Example:
user1 has role “editor” and logs on. _redirects on site1 has “visitor” role and _redirects on site2 has “editor” role. user1 can only see protected pages on site2 as he has not the correct role for site1.

Hope that helps.

Christian

1 Like

Thanks for clarifying @chriso0710