metosin / reitit

A fast data-driven routing library for Clojure/Script
https://cljdoc.org/d/metosin/reitit/
Eclipse Public License 1.0
1.43k stars 256 forks source link

Bundled CORS-support #236

Open ikitommi opened 5 years ago

ikitommi commented 5 years ago

Currently, a third party mw/interceptor is needed for CORS. There should be a fast default mw & interceptor for this. Those could be configured either via route data or via mw/interceptor options. Example data format from https://github.com/metosin/reitit/issues/143#issuecomment-422079662:

{:access-control
 {:allow-origin "http://foo.example"
  :allow-methods #{:get :post :put}
  :allow-credentials true
  :allow-headers #{"X-PINGOTHER" "Content-Type"}
  :expose-headers #{"X-My-Custom-Header" "X-Another-Custom-Header"}
  :max-age 86400}}

Some prior work:

Related issues:

lilactown commented 5 years ago

Just putting in my 2 cents: it would be useful to have :allow-origin (at the very least) be a RegEx or function. Depending on the case, users might want to modify the other headers as well depending on parts of the request (e.g. origin).

kennyjwilli commented 5 years ago

I needed CORS to get my local dev setup with a SPA and backend API working. Ended up writing this interceptor that uses ring-cors.

(s/def ::allow-origin string?)
(s/def ::allow-methods (s/coll-of keyword? :kind set?))
(s/def ::allow-credentials boolean?)
(s/def ::allow-headers (s/coll-of string? :kind set?))
(s/def ::expose-headers (s/coll-of string? :kind set?))
(s/def ::max-age nat-int?)
(s/def ::access-control
  (s/keys :opt-un [::allow-origin
                   ::allow-methods
                   ::allow-credentials
                   ::allow-headers
                   ::expose-headers
                   ::max-age]))

(s/def ::cors-interceptor
  (s/keys :opt-un [::access-control]))

(def cors-interceptor
  {:name    ::cors
   :spec    ::access-control
   :compile (fn [{:keys [access-control]} _]
              (when access-control
                (let [access-control (cors/normalize-config (mapcat identity access-control))]
                  {:enter (fn cors-interceptor-enter
                            [ctx]
                            (let [request (:request ctx)]
                              (if (or (and (cors/preflight? request)
                                           (cors/allow-request? request access-control)))
                                (let [resp (cors/add-access-control
                                             request
                                             access-control
                                             cors/preflight-complete-response)]
                                  (assoc ctx
                                    :response resp
                                    :queue nil))
                                ctx)))
                   :leave (fn cors-interceptor-leave
                            [ctx]
                            (let [request (:request ctx)]
                              (if (and (cors/origin request)
                                       (cors/allow-request? request access-control))
                                (if-let [response (:response ctx)]
                                  (assoc ctx
                                    :response
                                    (cors/add-access-control
                                      request
                                      access-control
                                      response)))
                                ctx)))})))})

Happy to submit a PR if interested.

thenonameguy commented 4 years ago

@kennyjwilli, cleaned up your example a little bit:

(def cors-interceptor
  {:name    ::cors
   :spec    ::access-control
   :compile (fn [{:keys [access-control]} _]
              (when access-control
                (let [access-control (cors/normalize-config (mapcat identity access-control))]
                  {:enter (fn cors-interceptor-enter
                            [{:keys [request] :as ctx}]
                            (if (and (cors/preflight? request)
                                     (cors/allow-request? request access-control))
                              (let [resp (cors/add-access-control
                                          request
                                          access-control
                                          cors/preflight-complete-response)]
                                (assoc ctx
                                       :response resp
                                       :queue nil))
                              ctx))
                   :leave (fn cors-interceptor-leave
                            [{:keys [request response] :as ctx}]
                            (cond-> ctx
                              (and (cors/origin request)
                                   (cors/allow-request? request access-control)
                                   response)
                              (assoc :response
                                     (cors/add-access-control
                                      request
                                      access-control
                                      response))))})))})
wpcarro commented 4 years ago

What's the status of this? I like the idea of reitit supporting CORS configuration. I've been searching for an idiomatic way to whitelist origins for my ring server and stumbled upon this thread.

onetom commented 3 years ago

So, will this support be included into reitit? I just tried to use ring-cors today, but couldn't make it work.

It seems like the casing of the content-type header makes it fail to allow a method on this line: https://github.com/r0man/ring-cors/blob/e762decdd4778f70da0dec2b840c5632da559271/src/ring/middleware/cors.cljc#L72 where the access-control-request-method header is looked up simply with get-in, instead of the get-header function within the same file (copied from ring-core's ring.util.reponse.

Since such (presumed) bugs are still present in ring-cors, it might make sense delaying its inclusion into reitit, but it feels like such a common use-case, it would be great to include it...

brancusi commented 3 years ago

Would really like this to be a default option.

fuadsaud commented 1 year ago

What are the current limitations with the :reitit.ring/default-options-endpoint solution? I understand it allows for arbitrary logic to be injected. Is the limitation the fact that it needs to be applied to the entire router instead of individual routes (e.g. via route middleware)?

Edit: I guess the missing piece would be adding the headers to the non-preflight requests.

WorldsEndless commented 8 months ago

I have no answer, but this I am an interested onlooker! Right now our CSRF is managed with Ring handlers, but it would be cool if it were part of Reitit, as per the code you shared.