Permalink
Browse files

Refactored tests primarily. Uses dummy app now rather than calling wo…

…rkflow function directly. Full tests authentication flow from start to finish.
  • Loading branch information...
1 parent 340138c commit 15771f280c88d7e300b70ea31ded4fc08b9a0d6d @ddellacosta committed Apr 7, 2013
View
17 project.clj
@@ -3,17 +3,20 @@
:url "https://github.com/ddellacosta/friend-oauth2"
:license {:name "MIT License"
:url "http://dd.mit-license.org"}
+
:dependencies [[org.clojure/clojure "1.4.0"]
- [com.cemerick/friend "0.1.5" :exclusions [ring/ring-core]]
+ [com.cemerick/friend "0.1.5" :exclusions [ring/ring-core slingshot]]
[ring "1.2.0-beta2"]
[ring/ring-codec "1.0.0"]
[clj-http "0.6.5" :exclusions [org.apache.httpcomponents/httpclient slingshot]]
[cheshire "5.0.2"]
[crypto-random "1.1.0"]]
- :plugins [[lein-ring "0.8.3"]
- [lein-midje "3.0.0"]
+
+ :plugins [[lein-midje "3.0.0"]
[codox "0.6.4"]]
- :profiles
- {:dev {:dependencies [[ring-mock "0.1.3"]
- [midje "1.5.0" :exclusions [org.clojure/core.incubator joda-time]]
- [com.cemerick/url "0.0.7" :exclusions [org.clojure/core.incubator]]]}})
+
+ :profiles {:dev
+ {:dependencies [[ring-mock "0.1.3"]
+ [midje "1.5.0" :exclusions [org.clojure/core.incubator joda-time]]
+ [com.cemerick/url "0.0.7" :exclusions [org.clojure/core.incubator]]
+ [compojure "1.1.5"]]}})
View
11 src/friend_oauth2/workflow.clj
@@ -25,7 +25,7 @@
(defn replace-authorization-code
"Formats the token uri with the authorization code"
[uri-config code]
- (assoc-in (uri-config :query) [:code] code))
+ (assoc-in (:query uri-config) [:code] code))
;; http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-5.1
(defn extract-access-token
@@ -90,7 +90,7 @@
;; Step 4:
;; access_token response. Custom function for handling
- ;; response body is pass in via the :access-token-parsefn
+ ;; response body is passed in via the :access-token-parsefn
access-token ((or (:access-token-parsefn config)
extract-access-token)
@@ -102,9 +102,10 @@
(:config-auth config))))
;; Step 1: redirect to OAuth2 provider. Code will be in response.
- (let [anti-forgery-token (generate-anti-forgery-token)]
+ (let [anti-forgery-token (generate-anti-forgery-token)
+ session-with-af-token (assoc (:session request)
+ (keyword anti-forgery-token) "state")]
(assoc
(ring.util.response/redirect
(format-authentication-uri (:uri-config config) anti-forgery-token))
- :session (assoc (:session request) (keyword anti-forgery-token) "state"))
- ))))))
+ :session session-with-af-token)))))))
View
29 test/friend_oauth2/fixtures.clj
@@ -0,0 +1,29 @@
+(ns friend-oauth2.fixtures
+ (:require
+ [friend-oauth2.workflow :as friend-oauth2]))
+
+(def client-config-fixture
+ {:client-id "my-client-id"
+ :client-secret "my-client-secret"
+ :callback {:domain "http://127.0.0.1" :path "/redirect"}})
+
+(def uri-config-fixture
+ {:authentication-uri {:url "http://example.com/authenticate"
+ :query {:client_id (:client-id client-config-fixture)
+ :redirect_uri (friend-oauth2/format-config-uri client-config-fixture)}}
+
+ :access-token-uri {:url "http://example.com/get-access-token"
+ :query {:client_id (client-config-fixture :client-id)
+ :client_secret (client-config-fixture :client-secret)
+ :redirect_uri (friend-oauth2/format-config-uri client-config-fixture)
+ :code ""}}})
+
+;; http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-5.1
+(def access-token-response-fixture
+ (ring.util.response/content-type
+ (ring.util.response/response
+ "{\"access_token\": \"my-access-token\"}")
+ "application/json"))
+
+(def identity-fixture
+ {:identity "my-access-token", :access_token "my-access-token"})
View
44 test/friend_oauth2/helper_facts.clj
@@ -0,0 +1,44 @@
+(ns friend-oauth2.helper-facts
+ (:use
+ midje.sweet
+ friend-oauth2.fixtures)
+ (:require
+ [friend-oauth2.workflow :as friend-oauth2]
+ [cemerick.friend :as friend]
+ [ring.mock.request :as ring-mock]))
+
+(fact
+ "Extracts the access token from a JSON access token response"
+ (friend-oauth2/extract-access-token access-token-response-fixture)
+ => "my-access-token")
+
+(fact
+ "Returns nil if there is no code in the request"
+ ;; No longer necessary since ring params/keyword-params handles this for us.
+ ;; Not sure if this test is necessary anymore either, but leaving in for now.
+ ;; (friend-oauth2/extract-code (ring-mock/request :get default-redirect))
+ (-> (ring-mock/request :get "/redirect") :params :code)
+ => nil)
+
+(fact
+ "Formats URI from domain and path pairs in a map"
+ (friend-oauth2/format-config-uri client-config-fixture)
+ => "http://127.0.0.1/redirect")
+
+(fact
+ "Formats the client authentication uri"
+ (friend-oauth2/format-authentication-uri uri-config-fixture "anti-forgery-token")
+ => "http://example.com/authenticate?redirect_uri=http%3A%2F%2F127.0.0.1%2Fredirect&client_id=my-client-id&state=anti-forgery-token")
+
+(fact
+ "Replaces the authorization code"
+ ((friend-oauth2/replace-authorization-code (uri-config-fixture :access-token-uri) "my-code") :code)
+ => "my-code")
+
+(fact
+ "Creates the auth-map for Friend with proper meta-data"
+ (meta (friend-oauth2/make-auth identity-fixture))
+ =>
+ {:type ::friend/auth
+ ::friend/workflow :email-login
+ ::friend/redirect-on-auth? true})
View
90 test/friend_oauth2/test_helpers.clj
@@ -0,0 +1,90 @@
+(ns friend-oauth2.test-helpers
+ (:use
+ compojure.core
+ friend-oauth2.fixtures)
+ (:require
+ [friend-oauth2.workflow :as friend-oauth2]
+ [cemerick.friend :as friend]
+ [cemerick.url :as url]
+ [compojure.handler :as handler]
+ [ring.mock.request :as ring-mock]))
+
+(defn extract-header
+ "Extracts header value from headers in response."
+ [header response]
+ (-> response :headers (get header)))
+
+(defn extract-cookie
+ "Extracts cookie from headers in response."
+ [response]
+ (first
+ (extract-header "Set-Cookie" response)))
+
+(defn extract-ring-session-val
+ "Returns ring-session value from Set-Cookie
+ header in a ring response."
+ [response]
+ (let [cookie (extract-cookie response)
+ cookie-vars (mapcat
+ #(clojure.string/split % #"=")
+ (clojure.string/split cookie #";"))]
+ ;;(println cookie " | " cookie2 "\n")
+ (get (apply hash-map cookie-vars) "ring-session")))
+
+(defn make-cookie-request
+ "Wraps ring-request with hash-map formatted
+ properly to pass a ring-session cookie."
+ [request cookie-val]
+ (merge
+ request
+ {:cookies {"ring-session" {:value cookie-val}}}))
+
+(defn extract-location
+ "Extracts location from headers from redirect response."
+ [response]
+ (extract-header "Location" response))
+
+(defn extract-state-from-redirect-url
+ "Parses the response's Location redirect url's query string
+ to get the 'state' value passed to the OAuth2 endpoint server
+ on the authentication request. (Whew.)"
+ [response]
+ (-> response
+ extract-location
+ url/url
+ :query
+ (get "state")))
+
+;; The following provides testing for ring requests/responses
+;; via a "real" friend-authorized/authenticated app.
+
+(declare test-app)
+
+(defn make-ring-session-get-request
+ [path params ring-session-val]
+ (test-app
+ (make-cookie-request
+ (ring-mock/request :get path params)
+ ring-session-val)))
+
+(defroutes test-app-routes
+ (GET "/authlink" request
+ (friend/authorize #{::user} "Authorized page.")))
+
+(def test-app
+ (handler/site
+ (friend/authenticate
+ test-app-routes
+ {:allow-anon? true
+ :workflows [(friend-oauth2/workflow
+ {:client-config client-config-fixture
+ :uri-config uri-config-fixture
+ :config-auth {:roles #{::user}}})]})))
+
+(defn setup-valid-state
+ "Initiates login to provide valid state for later requests."
+ []
+ (let [response (test-app (ring-mock/request :get "/login"))
+ state (extract-state-from-redirect-url response)
+ ring-session-val (extract-ring-session-val response)]
+ {:state state :ring-session-val ring-session-val}))
View
231 test/friend_oauth2/workflow_facts.clj
@@ -1,208 +1,69 @@
(ns friend-oauth2.workflow-facts
- (:use midje.sweet)
- (:require [friend-oauth2.workflow :as friend-oauth2]
- [cemerick.friend :as friend]
- [cemerick.url :as url]
- [clj-http.client :as client]
- [ring.middleware.params :as ring-params]
- [ring.middleware.keyword-params :as ring-keyword-params]
- [ring.mock.request :as ring-mock]
- [ring.util.codec :as codec]
- [cheshire.core :as j]))
-
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;
-;; Configuration fixtures
-;;
-
-(def client-config-fixture
- {:client-id "my-client-id"
- :client-secret "my-client-secret"
- :callback {:domain "http://127.0.0.1" :path "/redirect"}})
-
-(def uri-config-fixture
- {:authentication-uri {:url "http://example.com/authenticate"
- :query {:client_id (:client-id client-config-fixture)
- :redirect_uri (friend-oauth2/format-config-uri client-config-fixture)}}
-
- :access-token-uri {:url "http://example.com/get-access-token"
- :query {:client_id (client-config-fixture :client-id)
- :client_secret (client-config-fixture :client-secret)
- :redirect_uri (friend-oauth2/format-config-uri client-config-fixture)
- :code ""}}})
-
-;; Default workflow function with above config
-(defn default-workflow-function [request-or-response]
- ((friend-oauth2/workflow {:client-config client-config-fixture
- :uri-config uri-config-fixture
- :login-uri "/login"}) ;; Friend provides this normally.
- request-or-response))
-
-(def identity-fixture
- {:identity "my-access-token", :access_token "my-access-token"})
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;
-;; Dummy responses/requests for various OAuth2 endpoints
-;;
-
-;; http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-4.1.2
-(defn redirect-request-fixture
- [redirect-uri]
- (ring-keyword-params/keyword-params-request
- (ring-params/params-request
- (ring-mock/content-type
- (ring-mock/request :get redirect-uri
- {:code "my-code" :state "some-state"})
- "application/x-www-form-urlencoded"))))
-
-(def default-redirect "/redirect")
-
-(defn redirect-with-default-redirect-uri []
- (redirect-request-fixture default-redirect))
-
-(defn query-string-to-params [request]
- (assoc-in
- request
- [:query-params]
- (codec/form-decode (request :query-string))))
-
-;; http://tools.ietf.org/html/draft-ietf-oauth-v2-31#section-5.1
-(def access-token-response-fixture
- (ring.util.response/content-type
- (ring.util.response/response
- "{\"access_token\": \"my-access-token\"}")
- "application/json"))
-
-;; Initial redirect to login
-(defn login-request
- [login-path]
- (ring-mock/request :get login-path))
-
-(def default-login-path "/login")
-
-(defn default-login-request []
- (login-request default-login-path))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;
-;; Stubs
-;;
-
-(background
- (client/post
- "http://example.com/authenticate"
- {:form-params
- {:client_id "my-client-id"
- :response_type "code"
- :redirect_uri "http://127.0.0.1/redirect"
- :scope anything
- :state anything}})
- => access-token-response-fixture
-
- (client/post
- "http://example.com/get-access-token"
- {:form-params
- {:client_id "my-client-id"
- :client_secret "my-client-secret"
- :grant_type "authorization_code"
- :redirect_uri "http://127.0.0.1/redirect"
- :code "my-code"}})
- => access-token-response-fixture)
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;
-;; Facts (helper functions)
-;;
+ (:use
+ midje.sweet
+ friend-oauth2.test-helpers
+ friend-oauth2.fixtures)
+ (:require
+ [cemerick.url :as url]
+ [ring.mock.request :as ring-mock]))
(fact
- "Extracts the access token from a JSON access token response"
- (friend-oauth2/extract-access-token access-token-response-fixture)
- => "my-access-token")
+ "A user can authenticate via OAuth2
+ (tests 'happy path' for the whole process)."
-(fact
- "Returns nil if there is no code in the request"
- ;; No longer necessary since ring params/keyword-params handles this for us.
- ;; Not sure if this test is necessary anymore either, but leaving in for now.
- ;; (friend-oauth2/extract-code (ring-mock/request :get default-redirect))
- (-> (ring-mock/request :get default-redirect) :params :code)
- => nil)
+ (let [authlink-response (test-app (ring-mock/request :get "/authlink"))
+ ring-session-val (extract-ring-session-val authlink-response)
-(fact
- "Formats URI from domain and path pairs in a map"
- (friend-oauth2/format-config-uri client-config-fixture)
- => "http://127.0.0.1/redirect")
+ login-response (make-ring-session-get-request
+ "/login" {} ring-session-val)
+ state (extract-state-from-redirect-url login-response)
-(fact
- "Formats the client authentication uri"
- (friend-oauth2/format-authentication-uri uri-config-fixture "anti-forgery-token")
- => "http://example.com/authenticate?redirect_uri=http%3A%2F%2F127.0.0.1%2Fredirect&client_id=my-client-id&state=anti-forgery-token")
+ ;; refactor to use cemerick.url?
+ authentication-url (str
+ "http://example.com/authenticate?redirect_uri=http%3A%2F%2F127.0.0.1%2Fredirect&client_id=my-client-id&state="
+ (clojure.string/replace state #"\+" "%2B"))
-(fact
- "Replaces the authorization code"
- ((friend-oauth2/replace-authorization-code (uri-config-fixture :access-token-uri) "my-code") :code)
- => "my-code")
+ authlink-redirect (make-ring-session-get-request
+ "/redirect"
+ {:code "my-code" :state state}
+ ring-session-val)
+ authlink (extract-location authlink-redirect)
-(fact
- "Creates the auth-map for Friend with proper meta-data"
- (meta (friend-oauth2/make-auth identity-fixture))
- =>
- {:type ::friend/auth
- ::friend/workflow :email-login
- ::friend/redirect-on-auth? true})
+ authed-response (make-ring-session-get-request
+ authlink {} ring-session-val)]
+
+ (:status authlink-response) => 302
+ (extract-location authlink-response) => "http://localhost/login"
+ (:status login-response) => 302
+ (extract-location login-response) => authentication-url
+ (:status authlink-redirect) => 303
+ (extract-location authlink-redirect) => "/authlink"
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;;
-;; Facts (core workflow)
-;;
+ (:status authed-response) => 200
+ (:body authed-response) => "Authorized page."))
(fact
"A login request redirects to the authorization uri"
- (let [auth-redirect (default-workflow-function (default-login-request))
- status (:status auth-redirect)
- ;; Isn't there something in Ring that will do all this for me?
- location (get (:headers auth-redirect) "Location")
+ (let [auth-redirect (test-app (ring-mock/request :get "/login"))
+ location (extract-location auth-redirect)
redirect-query (-> location url/url :query clojure.walk/keywordize-keys)]
- status => 302
+ (:status auth-redirect) => 302
(:redirect_uri redirect-query) => "http://127.0.0.1/redirect"
(:client_id redirect-query) => "my-client-id"
(not (nil? (:state redirect-query))) => true))
-(fact
- "extract-access-token is used for access-token-parsefn if none is passed in."
- (default-workflow-function
- (redirect-with-default-redirect-uri))
- => {:identity "my-access-token" :access_token "my-access-token"}
- (provided
- (friend-oauth2/extract-access-token access-token-response-fixture)
- => "my-access-token" :times 1))
-
-(fact
- "If there is a code in the request it posts to the token-uri"
- (default-workflow-function
- (redirect-with-default-redirect-uri))
- => {:identity "my-access-token" :access_token "my-access-token"}
- (provided
- (client/post "http://example.com/get-access-token"
- {:form-params
- (merge {:grant_type "authorization_code"}
- (friend-oauth2/replace-authorization-code
- (uri-config-fixture :access-token-uri) "my-code"))})
- => access-token-response-fixture :times 1))
-
-(fact
- "A CSRF token is passed by default"
- (let [auth-redirect (default-workflow-function (default-login-request))
- location (get (:headers auth-redirect) "Location")
- redirect-query (-> location url/url :query clojure.walk/keywordize-keys)]
+(future-fact
+ "access-token-parsefn is used for the token if provided."
- (not (nil? (:state redirect-query))) => true))
+ (let [{state :state
+ ring-session-val :ring-session-val} (setup-valid-state)
+ authlink-redirect (make-ring-session-get-request
+ "/redirect"
+ {:code "my-code" :state state}
+ ring-session-val)]))
(future-fact
- "If the session state is not the same as the auth-response state, it does not proceed"
- {(keyword (generate-anti-forgery-token)) "state"
- :some-other-var "something"})
+ "If the session state is not the same as the auth-response state, it does not proceed")

0 comments on commit 15771f2

Please sign in to comment.