Permalink
Browse files

* parameterize behaviour when responding to unauthenticated requests

  that require authentication (new :unauthenticated-handler option)
* don't automatically send http-basic denial when http-basic creds aren't provided
* fixes gh-38
  • Loading branch information...
cemerick committed Jan 13, 2013
1 parent d5c9b63 commit 81d96e71061b31983079bf793e5fd371425a0ca6
View
@@ -1,4 +1,4 @@
-(defproject com.cemerick/friend "0.1.2"
+(defproject com.cemerick/friend "0.1.3-SNAPSHOT"
:description "Authentication and authorization library for Ring Clojure web apps and services."
:url "http://github.com/cemerick/friend"
:license {:name "Eclipse Public License"
View
@@ -1,6 +1,6 @@
(ns cemerick.friend
- (:require [clojure.set :as set]
- [cemerick.friend.util :as util])
+ (:require [cemerick.friend.util :as util]
+ [clojure.set :as set])
(:use (ring.util [response :as response :only (redirect)])
[slingshot.slingshot :only (throw+ try+)]
[clojure.core.incubator :only (-?>)])
@@ -153,21 +153,26 @@ Equivalent to (complement current-authentication)."}
(update-in [:session] dissoc ::unauthorized-uri))
resp))))
-(defn- redirect-unauthorized
- [redirect-uri request]
- (-> (util/resolve-absolute-uri request redirect-uri)
+(defn default-unauthenticated-handler
+ [request]
+ (-> request
+ ::auth-config
+ :login-uri
+ (util/resolve-absolute-uri request)
ring.util.response/redirect
(assoc :session (:session request))
(assoc-in [:session ::unauthorized-uri] (:uri request))))
(defn- authenticate*
[handler config request]
- (let [{:keys [allow-anon? unauthorized-handler workflows login-uri] :as config}
+ (let [{:keys [allow-anon? unauthorized-handler unauthenticated-handler
+ workflows login-uri] :as config}
(merge {:allow-anon? true
:default-landing-uri "/"
:login-uri "/login"
:credential-fn (constantly nil)
:workflows []
+ :unauthenticated-handler #'default-unauthenticated-handler
:unauthorized-handler #'default-unauthorized-handler}
config)
request (assoc request ::auth-config config)
@@ -184,7 +189,7 @@ Equivalent to (complement current-authentication)."}
auth (identity request)]
(binding [*identity* auth]
(if (and (not auth) (not allow-anon?))
- (redirect-unauthorized login-uri request)
+ (unauthenticated-handler request)
(try+
(if-not new-auth?
(handler request)
@@ -193,11 +198,8 @@ Equivalent to (complement current-authentication)."}
(ensure-identity request)))
(catch ::type error-map
;; TODO log unauthorized access at trace level
- (if auth
- (unauthorized-handler (assoc request
- ::authorization-failure
- error-map))
- (redirect-unauthorized login-uri request))))))))))
+ ((if auth unauthorized-handler unauthenticated-handler)
+ (assoc request ::authorization-failure error-map))))))))))
(defn authenticate
[ring-handler auth-config]
@@ -21,7 +21,7 @@
(str \? query-string))))
(defn resolve-absolute-uri
- [request uri]
+ [uri request]
(-> (original-url request)
java.net.URI.
(.resolve uri)
@@ -34,9 +34,7 @@
(defn http-basic
[& {:keys [credential-fn realm] :as basic-config}]
(fn [{{:strs [authorization]} :headers :as request}]
- (if-not authorization
- (when-not (-> request ::friend/auth-config :allow-anon?)
- (http-basic-deny realm request))
+ (when authorization
(if-let [[[_ username password]] (try (-> (re-matches #"\s*Basic\s+(.+)" authorization)
second
(.getBytes "UTF-8")
@@ -64,9 +62,10 @@
(let [param (str "&login_failed=Y&username="
(java.net.URLEncoder/encode (:username params "")))
login-uri (-> request ::friend/auth-config :login-uri)]
- (util/resolve-absolute-uri request
+ (util/resolve-absolute-uri
(str (if (.contains login-uri "?") login-uri (str login-uri "?"))
- param)))))
+ param)
+ request))))
(defn interactive-form
[& {:keys [login-uri credential-fn login-failure-handler redirect-on-auth?] :as form-config
@@ -16,14 +16,20 @@
(catch [:status 401] {{:strs [www-authenticate]} :headers}
(is (= www-authenticate (str "Basic realm=\"" mock-app-realm \"))))))
-(deftest http-basic-missing
+(defn- anon-request
+ [path]
(try+
- (http/get (url "/auth-api"))
+ (http/get (url path))
(assert false) ; should never get here
(catch [:status 401] {{:strs [www-authenticate]} :headers}
(is (= www-authenticate (str "Basic realm=\"" mock-app-realm \"))))))
+(deftest http-basic-missing
+ (anon-request "/auth-api")
+ (anon-request "/requires-authentication"))
+
(deftest http-basic
(let [{:keys [body]} (http/get (url "/auth-api") {:basic-auth "api-key:api-pass"
:as :json})]
- (is (= {:data 42} body))))
+ (is (= {:data "authorized"} body))
+ (is (= {:data "anon"} (:body (http/get (url "/anon") {:as :json}))))))
@@ -2,15 +2,15 @@
(:require [cemerick.friend :as friend])
(:use clojure.test
ring.mock.request
- [cemerick.friend.workflows :only (http-basic)]))
+ [cemerick.friend.workflows :as workflows :only (http-basic)]))
(deftest basic-workflow
(let [req (request :get "/uri")
auth "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" ; shamelessly ripped from wikipedia :-P
got-creds (atom nil)]
(is (= {:status 401, :headers {"Content-Type" "text/plain"
"WWW-Authenticate" "Basic realm=\"friend-test\""}}
- ((http-basic :realm "friend-test") req)))
+ (workflows/http-basic-deny "friend-test" req)))
(println "Don't worry, an exception is expected here:")
(is (= 400 (:status ((http-basic) (header req "Authorization" "BadAuthHeader")))))
@@ -37,6 +37,7 @@
(header req "Authorization" "Basic Og=="))]
(is (= {:identity ""} auth))))
- (is (= {:status 401, :headers {"Content-Type" "text/plain", "WWW-Authenticate" "Basic realm=\"friend-test\""}}
- ((http-basic :realm "friend-test" :credential-fn (constantly nil))
- (header req "Authorization" auth))))))
+ (is (= {:status 401, :headers {"Content-Type" "text/plain"
+ "WWW-Authenticate" "Basic realm=\"friend-test\""}}
+ ((http-basic :realm "friend-test" :credential-fn (constantly nil))
+ (header req "Authorization" auth))))))
@@ -90,8 +90,11 @@
(defroutes api-routes
;;;;; API
- (GET "/auth-api" request (friend/authorize #{:api}
- (api-call 42))))
+ (GET "/auth-api" request
+ (friend/authorize #{:api} (api-call :authorized)))
+ (GET "/anon" request (api-call :anon))
+ (GET "/requires-authentication" request
+ (friend/authenticated (api-call :authenticated))))
(def users {"root" {:username "root"
:password (creds/hash-bcrypt "admin_password")
@@ -122,7 +125,8 @@
(handler/api
(friend/authenticate
api-routes
- {:allow-anon? false
+ {:allow-anon? true
+ :unauthenticated-handler #(workflows/http-basic-deny mock-app-realm %)
:workflows [(workflows/http-basic
:credential-fn (partial creds/bcrypt-credential-fn api-users)
:realm mock-app-realm)]})))

0 comments on commit 81d96e7

Please sign in to comment.