Skip to content

Commit d2ec66b

Browse files
authored
[Fix #4] Stop enforcing absolute URLs for redirects. (#5)
For long, the HTTP spec has allowed relative URLs in the Location header, see https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2. Absolute URLs have been introduced here: cemerick@5b04323 It was trying to fix the issue reported in 2013: cemerick#42 where they claimed that friend doesn't follow the HTTP spec. However, this lead to all sorts of problems with the clojure app running behind an SSL/TLS proxy, e.g. cemerick#84. To sum up: Original friend implementation got it right by using relative URLs for redirects but it wasn't, at the time, strictly following the HTTP spec. However, the HTTP spec has since been updated and there's no more reason to use absolute URLs - they are brittle and break apps. Use relative URL in default-unauthenticated-handler. This is to avoid redirecting to HTTP when the user request was in fact to an HTTPS endpoint. Such a situation happens when you run an SSL proxy in front of your plain HTTP app server. In that case, `ring.util.request/request-url` returns _almost_ a proper endpoint, but uses "http" not "https".
1 parent 924dc8d commit d2ec66b

File tree

7 files changed

+69
-45
lines changed

7 files changed

+69
-45
lines changed

CHANGES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
## [Friend](http://github.com/cemerick/friend) changelog
22

3+
### `0.3.x`
4+
5+
* HTTP redirect responses now don't enforce absolute URLs - this is a revert of the change made in 0.1.2.
6+
Note that if you are using ring-defaults, you need to
7+
[set `:absolute-redirects` to `false`](https://github.com/ring-clojure/ring-defaults#customizing).
8+
(a new [issue submitted](https://github.com/ring-clojure/ring-defaults/issues/39) for ring-defaults)
9+
See https://github.com/clj-commons/friend/issues/4.
10+
11+
312
### `0.3.0`
413

514
* The "classic" OpenId (i.e. OpenId 1.1/2.0) workflow provided by `cemerick.friend.openid` has been

project.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
[compojure "1.1.5"]
2222
[ring "1.2.0"]
2323
[robert/hooke "1.3.0"]
24-
[clj-http "0.3.6"]]}
24+
[clj-http "0.3.6"]
25+
;; test/.../functional.clj uses the jetty adapter
26+
[ring/ring-jetty-adapter "1.9.5"]]}
2527
:sanity-check {:aot :all
2628
:warn-on-reflection true
2729
:compile-path "target/sanity-check-aot"}

src/cemerick/friend.clj

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
(ns cemerick.friend
22
(:require [cemerick.friend.util :as util]
3-
[clojure.set :as set])
4-
(:use (ring.util [response :as response :only (redirect)])
5-
[slingshot.slingshot :only (throw+ try+)])
3+
[ring.util.response :as response]
4+
[slingshot.slingshot :refer [throw+ try+]])
65
(:refer-clojure :exclude (identity)))
76

87
(defn merge-authentication
@@ -105,10 +104,9 @@ Equivalent to (complement current-authentication)."}
105104
::auth-config
106105
:login-uri
107106
(#(str (:context request) %))
108-
(util/resolve-absolute-uri request)
109-
ring.util.response/redirect
107+
response/redirect
110108
(assoc :session (:session request))
111-
(assoc-in [:session ::unauthorized-uri] (util/original-url request))))
109+
(assoc-in [:session ::unauthorized-uri] (util/relative-url request))))
112110

113111
(defn authenticate-response
114112
"Adds to the response's :session for responses with a :friend/ensure-identity-request key."

src/cemerick/friend/util.clj

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,26 @@
1212
; this was never _really_ part of the API, was implemented (badly) before req/request-url was available
1313
(def ^:deprecated original-url req/request-url)
1414

15-
(defn resolve-absolute-uri
15+
(defn ^:deprecated resolve-absolute-uri
16+
"DEPRECATED. Use `relative-url`.
17+
18+
This function is only kept for backward compatibility.
19+
It was meant to conform to an old HTTP spec that required absolute URLs for redirects
20+
but that's no longer needed."
1621
[^String uri request]
1722
(-> (original-url request)
18-
java.net.URI.
19-
(.resolve uri)
20-
str))
23+
java.net.URI.
24+
(.resolve uri)
25+
str))
26+
27+
28+
(defn relative-url
29+
"Similar to `util/original-url` (and thus `ring.util.request/request-url`)
30+
except that it only uses `:uri` and `:query-string` parts of the request
31+
to build the URL and ignores `:scheme` and the 'host' header.
32+
This is to avoid issues with redirects using absolute URLs when the app is running behind an SSL proxy."
33+
[request]
34+
(str
35+
(:uri request)
36+
(when-let [query (:query-string request)]
37+
(str "?" query))))

src/cemerick/friend/workflows.clj

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
(ns cemerick.friend.workflows
22
(:require [cemerick.friend :as friend]
3-
[cemerick.friend.util :as util]
4-
[ring.util.request :as req])
5-
(:use [clojure.string :only (trim)]
6-
[cemerick.friend.util :only (gets)])
7-
(:import org.apache.commons.codec.binary.Base64))
3+
[cemerick.friend.util :as util :refer [gets]]
4+
[ring.util.request :as req]
5+
[ring.util.response :as response])
6+
(:import java.net.URLEncoder
7+
org.apache.commons.codec.binary.Base64))
88

99
(defn http-basic-deny
1010
[realm request]
@@ -68,14 +68,12 @@
6868

6969
(defn interactive-login-redirect
7070
[{:keys [form-params params] :as request}]
71-
(ring.util.response/redirect
71+
(response/redirect
7272
(let [param (str "&login_failed=Y&username="
73-
(java.net.URLEncoder/encode (username form-params params)))
73+
(URLEncoder/encode (username form-params params)))
7474
^String login-uri (-> request ::friend/auth-config :login-uri (#(str (:context request) %)))]
75-
(util/resolve-absolute-uri
76-
(str (if (.contains login-uri "?") login-uri (str login-uri "?"))
77-
param)
78-
request))))
75+
(str (if (.contains login-uri "?") login-uri (str login-uri "?"))
76+
param))))
7977

8078
(defn interactive-form
8179
[& {:keys [login-uri credential-fn login-failure-handler redirect-on-auth?] :as form-config

test/test_friend/functional.clj

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
:let [resp (http/get url)]]
3535
(is (http/success? resp))
3636
(is (= (page-bodies uri) (:body resp))))
37-
37+
3838
(let [api-resp (http/get (url "/free-api") {:as :json})]
3939
(is (http/success? api-resp))
4040
(is (= {:data 99} (:body api-resp)))))
@@ -69,17 +69,18 @@
6969
; clj-http *should* redirect us, but isn't yet; working on it:
7070
; https://github.com/dakrone/clj-http/issues/57
7171
(is (http/redirect? resp))
72-
(is (= (url "/user/account?query-string=test") (-> resp :headers (get "location")))))
72+
(is (= "/user/account?query-string=test"
73+
(-> resp :headers (get "location")))))
7374
(check-user-role-access)
7475
(is (= {:roles ["test-friend.mock-app/user"]} (:body (http/get (url "/echo-roles") {:as :json}))))
75-
76+
7677
; deny on admin role
7778
(try+
7879
(http/get (url "/admin"))
7980
(assert false) ; should never get here
8081
(catch [:status 403] _
8182
(is true)))
82-
83+
8384
(testing "logout blocks access to privileged routes"
8485
(is (= (page-bodies "/") (:body (http/get (url "/logout")))))
8586
(is (= (page-bodies "/login") (:body (http/get (url "/user/account"))))))))
@@ -100,13 +101,12 @@
100101
(is (= "auth-data" (post-session-data "auth-data")))
101102
(is (= "auth-data" (get-session-data)))
102103
(check-user-role-access)
103-
104+
104105
(http/get (url "/logout"))
105106
(let [should-be-login-redirect (http/get (url "/user/account")
106107
{:follow-redirects false})]
107108
(is (= 302 (:status should-be-login-redirect)))
108-
(is (re-matches #"http://localhost:\d+/login"
109-
(-> should-be-login-redirect :headers (get "location")))))
109+
(is (= "/login" (-> should-be-login-redirect :headers (get "location")))))
110110
; TODO should logout blow away the session completely?
111111
(is (= "auth-data" (get-session-data)))))))
112112

@@ -118,7 +118,7 @@
118118
(assert false) ; should never get here
119119
(catch [:status 403] resp
120120
(is (= "Sorry, you do not have access to this resource." (:body resp)))))
121-
121+
122122
(http/post (url "/login") {:form-params {:username "root" :password "admin_password"}})
123123
(is (= (page-bodies "/hook-admin") (:body (http/get (url "/hook-admin")))))))
124124

@@ -135,7 +135,7 @@
135135
(deftest admin-login
136136
(binding [clj-http.core/*cookie-store* (clj-http.cookies/cookie-store)]
137137
(is (= (page-bodies "/login") (:body (http/get (url "/admin")))))
138-
138+
139139
(http/post (url "/login") {:form-params {:username "root" :password "admin_password"}})
140140
(is (= (page-bodies "/admin") (:body (http/get (url "/admin")))))
141141
(check-user-role-access)
@@ -151,13 +151,13 @@
151151
(binding [clj-http.core/*cookie-store* (clj-http.cookies/cookie-store)]
152152
(is (= (page-bodies "/login") (:body (http/get (url "/admin")))))
153153
(http/post (url "/login") {:form-params {:username "root" :password "admin_password"}})
154-
154+
155155
(try+
156156
(http/get (url "/wat"))
157157
(assert false)
158158
(catch [:status 404] e))
159159
(is (= (page-bodies "/admin") (:body (http/get (url "/admin")))))
160-
160+
161161
(is (= (page-bodies "/") (:body (http/get (url "/logout")))))
162162
(is (= (page-bodies "/login") (:body (http/get (url "/admin")))))))
163163

test/test_friend/interactive_form.clj

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
(ns test-friend.interactive-form
2-
(:require [cemerick.friend :as friend])
3-
(:use clojure.test
4-
ring.mock.request
5-
[cemerick.friend.workflows :only (interactive-form)]))
2+
(:require [cemerick.friend :as friend]
3+
[clojure.test :refer :all]
4+
[ring.mock.request :as mock]
5+
[cemerick.friend.workflows :refer (interactive-form)]))
66

77
(deftest form-workflow
88
(let [got-creds (atom false)
@@ -14,28 +14,28 @@
1414
(when (and (= "open sesame" password)
1515
(= "Aladdin" username))
1616
{:identity username})))]
17-
(is (nil? (form-handler (request :get login-uri))))
17+
(is (nil? (form-handler (mock/request :get login-uri))))
1818

1919
(is (= {:status 302
20-
:headers {"Location" "http://localhost/my_login?&login_failed=Y&username="}
20+
:headers {"Location" "/my_login?&login_failed=Y&username="}
2121
:body ""}
22-
(form-handler (request :post login-uri))))
22+
(form-handler (mock/request :post login-uri))))
2323

2424
(is (= {:status 302
25-
:headers {"Location" "http://localhost/my_login?&login_failed=Y&username=foo"}
25+
:headers {"Location" "/my_login?&login_failed=Y&username=foo"}
2626
:body ""}
27-
(form-handler (assoc (request :post login-uri)
27+
(form-handler (assoc (mock/request :post login-uri)
2828
:params {:username "foo"}
2929
:form-params {"username" "foo"}))))
3030

3131
(is (= {:status 302
32-
:headers {"Location" "http://localhost/my_login?&login_failed=Y&username=foobar"}
32+
:headers {"Location" "/my_login?&login_failed=Y&username=foobar"}
3333
:body ""}
34-
(form-handler (assoc (request :post login-uri)
34+
(form-handler (assoc (mock/request :post login-uri)
3535
:params {:username "foo"}
3636
:form-params {"username" "foobar"}))))
3737

38-
(let [auth (form-handler (assoc (request :post login-uri)
38+
(let [auth (form-handler (assoc (mock/request :post login-uri)
3939
:params {:username "foo"
4040
:password "open sesame"}
4141
:form-params {"username" "Aladdin"

0 commit comments

Comments
 (0)