Problem/Motivation
In order to support farmOS Field Kit the farmOS server must have CORS configured to accept requests from the farmOS.app domain.
Drupal 8.2+ allows this to be configured in the sites default.services.yml
file: https://www.drupal.org/node/2715637 This is sufficient, but it requires that the CORS origin(s) be configured in code by the site host.
A better solution would be to make CORS configurable in the farmOS UI and by the modules that are providing API integrations. We had this ability in 1.x: #3083205: Make Access-Control-Allow-Origin configurable We also supported multiple CORS origins: https://github.com/farmOS/farmOS/issues/271
Steps to reproduce
N/A
Proposed resolution
Since all of our Simple OAuth clients are represented by Consumers, lets allow each consumer to provide its own CORS configuration. We can do this by adding an additional text field to the consumer entity (we already add a couple additional fields for our OAuth customization).
@pcambra noted that we could use the (deprecated) CORS module's EventSubscriber
approach to add configured CORS origin headers to responses: https://git.drupalcode.org/project/cors/-/blob/8.x-1.x/src/EventSubscrib...
Remaining tasks
- Implement configurable CORS origins.
- Add tests.
User interface changes
None.
API changes
- CORS requests will be allowed if the request's origin is properly configured.
Data model changes
- Add an allowed_origins
text field to consumers.
Issue fork farm-3207529
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
paul121 CreditAttribution: paul121 commentedAdded link to the CORS module EventSubscriber.
Comment #3
paul121 CreditAttribution: paul121 commentedThinking more about this.. it seems like adding an EventSubscriber on the Kernel Response seems a bit heavy handed, especially if we need to load consumer entities and check their CORS configuration on every request. It seems like we did the same basic thing in 1.x though: the config was loaded from a single variable
farm_access_allow_origin
and headers were added to all requests viahook_init()
..... ah well of course! Because
hook_init
was removed from D8, an EventSubscriber is the new approach: https://www.drupal.org/node/2013014Also just reviewing the issued that added this support to 8.2 because I was curious how it was done...: this commit from #1869548: Opt-in CORS support
Turns out they used a 3rd party middleware: https://github.com/asm89/stack-cors The middleware is always added to the container, but removed in a compiler pass if
cors.config.enabled !== true
(nicely explained in this comment, also has a link to some Drupal middleware docs).A couple interesting things:
@catch commented:
and commented:
So both approaches have the potential to run on every request and slow things down... doesn't seem like we have a great way around this.
Instead of this, maybe we update a separate simple config value (or use a simple cache key/value...?) whenever consumer entities are saved and aggregate all the CORS config at that time. Then we just load the cached config value on each request.
Comment #4
paul121 CreditAttribution: paul121 commented@pcambra also mentioned that with this proposed approach we should consider what happens if the site's
default.services.yml
cors.config
is enabled... shouldfarm_api
override that value? Or try and merge all of the headers?It seems that the CORS module approach overrides the Access-Control headers.
Comment #5
m.stentaAre consumers being loaded on every request already by Simple OAuth?
I would imagine they only need to be loaded if a request that contains an OAuth token is being processed?
Comment #6
paul121 CreditAttribution: paul121 commentedGood point.. this is true.
That logic would probably cover most of our use cases. A CORS request could be coming from another tab in an authenticated browsing session too though.
But the general concept of *when* we add the CORS headers is something we could consider more.. the CORS logic in 1.x is pretty simple: it just checks if the request's origin is set, and if it is an "allowed origin", then we always allow the same headers and allow *all* methods. I have this same logic working in 2.x, just need to add some tests.
Wanted to point out too that Drupal core allows each origin to have specific Headers and Methods configured for it. The contrib
cors
module also allowed origins to only be configured for certain path(s), although that doesn't seem to be possible anymore. I can see why these would be useful... at the least, configuring the allowed Methods might be a nice feature for us to support.If we do get into supporting these advanced CORS features I think we should consider using the
asm89/stack-cors
library to handle the logic & setting the correct headers on responses. Main reason being that "it attempts to implement the W3C Recommendation for cross-origin resource sharing" so we don't need to re-invent the wheel ;-)This is the same library Drupal core is using - they use it as stack middleware, but it can also be used as a library: https://github.com/asm89/stack-cors#example-using-the-library
Comment #7
paul121 CreditAttribution: paul121 commentedThat said - the simplest approach should be fine for now :-)
I have it working here with tests and a simple documentation update: https://github.com/paul121/farmOS/tree/2.x-consumer_allowed_origins
NOTE: this change will conflict with #3207716: Use BundleFieldDefinition for additional Consumer fields. It might be easier to merge the other bundle fields change first, then rebase this branch on top since it adds another bundle field.
Comment #8
m.stentaI've merged #3207716: Use BundleFieldDefinition for additional Consumer fields.
Comment #10
m.stentaMerged @paul121's
2.x-consumer_allowed_origins
branch.