Subdomains and CSRF Tokens

Hey all!

I’m working to add some subdomains to an application in production, am struggling to identify a clear path forward to support them in our application and am hoping to gain some insight into best practices around application configuration and how to handle CSRF tokens for forms and LIveViews.

In the docs for the Endpoint module it states The endpoint can be customized to add additional plugs, to allow HTTP basic authentication, CORS, subdomain routing and more. however I haven’t been able to identify where that configuration goes.

My use case is very similar to the one found in Strategy for multiple subdomains --> scoped queries? in that we would like to maintain a single tenant implementation at the database layer yet scope requests based on the subdomin.

For example, let’s say our url host is example.com and we want to now add an irrelevant_subdomain so that we can add business logic in our application layer to scope all requests to irrelevant_subdomain.example.com to only include results that are related to irrelevant_subdomain? Where would I go about configuring the subdomains and how can I handle CSRF token generation for all domains given we have a single host.

In Modify Conn.host to include a subdomain Chis points out that we’ll have to add the CSRF token as an option in the form_for function but I haven’t had success with that and have also tried using https://hexdocs.pm/plug/Plug.CSRFProtection.html#get_csrf_token_for/1 and passing our configured application host without success. Additionally, I am seeing issues with the tokens in LiveView when visiting irrelevant_subdomain.example.com, [debug] LiveView session was misconfigured or the user token is outdated.

Any insight would be greatly appreciated!

2 Likes

Does this thread help?

Hey @dimitarvp! Thanks so much for taking the time, I really appreciate it!

Does this thread help?

Very topical, thanks for sharing! I’ve been able to successfully add subdomains for my app but am struggling to understand how CSRF token generation/validation works and what would cause different behavior between different environments. I have a staging environment that is working fine with a given domain and x number of subdomains, no issues with forms or LiveView. However, locally I am still getting CSRF token warnings/errors when attempting to submit forms and visit pages with LiveView.

Any ideas as to what could cause the different behavior between say, foo.localhost:5300 and foo.example.com?

I’ve confirmed it isn’t due to an old cookie and believe I have the config setup correctly. Maybe I have a gap in knowledge around something fundamental about running a localhost?

1 Like

Not sure if it’s your problem here, but consider that localhost is a special hostname, not a domain resolved by the DNS, so you cannot add subdomains to it the same way you do for real domains. You can map your local subdomains in your /etc/hosts file though, like this:

127.0.0.1    myapp.dev
127.0.0.1    subdomain.myapp.dev

This way, myapp.dev and subdomain.myapp.dev on your machine would both resolve to the localhost IP 127.0.0.1.

3 Likes

Thanks @lucaong this is almost certainly my issue! After setting the values as you describe I"m getting the following error after setting my development host to myapp.dev, any insight? A quick search through the phoenix docs didn’t turn anything up…

[info] Running MyApp.Endpoint with cowboy 1.1.2 at 0.0.0.0:5300 (http)
[info] Access MyAppEndpoint at http://mayapp.dev:5300

webpack is watching the files…

Browserslist: caniuse-lite is outdated. Please run next command `npm update`
[error] Cowboy returned 400 because it was unable to parse the request headers.

This may happen because there are no headers, or there are too many headers
or the header name or value are too large (such as a large cookie).

You can customize those values when configuring your http/https
server. The configuration option and default values are shown below:

    protocol_options: [
      max_header_name_length: 64,
      max_header_value_length: 4096,
      max_headers: 100,
      max_request_line_length: 8096
    ]

Is it possible that you have a very large cookie set in your browser for that domain? Can you try clearing the cookies (or using an incognito window)?

Thanks again for taking the time, using an incognito window produced the same error.

Is it possible that a header with a name longer than 64 characters or value longer than 4096 characters is sent? Or that more than 100 headers are sent? Or that a request line is longer than 8096 characters?

You could also try to make the request with curl, so you control the headers. Something like:

curl -v http://mayapp.dev:5300
1 Like

Yes, clearly something is either misconfigured or we are, unknowingly at this point, sending over massive headers. I’m still not fully groking the implications of modifying the /etc/hosts file and the config of a phoenix application and have setup a sample application: https://github.com/A-Legg/csrf_subdomain_example

If you would’t mind taking a look and hopefully shedding some light as to why various changes to the host file and config either work or don’t work it would hugely beneficial, thanks!

1 Like

One thing I noticed in your repo, most likely not related to your problem but worth telling, is that you removed the localhost entries in the /etc/hosts file. You should not remove them, and rather leave them unchanged and add the additional ones for the test domain and sub domain. In other words, the lines I posted above are meant to be added, not substituted.

Also, this line is wrong. If you will use the host myapp.dev for development instead of localhost, it should be url: [host: “myapp.dev”].

Finally, the host myapp.dev is an example, but it is better to use something that cannot actually be a real domain name. Something like myapp.internal. Actually, using .dev might be the source of your problem.

The problem with myapp.dev (that I overlooked before), is that the .dev top level domain forces https with a HSTS preload list. This likely prevents you to connect to it, because your browser will force https, but your app is setup for http. Using myapp.internal (or another made-up test domain of your choice) will fix that.

2 Likes