Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add :strategy option and protocol to middleware #16

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ To install, add the following to your project `:dependencies`:
## Usage

The middleware function to use is `ring.middleware.oauth2/wrap-oauth2`.
This takes a Ring handler, and a map of profiles as arguments. Each
profile has a key to identify it, and a map of options that define how
to authorize against a third-party service.
This takes a Ring handler, and a map of profiles as arguments and an
optional map of options. Each profile has a key to identify it, and
a map of properties that define how to authorize against a third-party
service.

Here's an example that provides authentication with GitHub:

Expand Down Expand Up @@ -104,6 +105,18 @@ in order to make the callback request handling work correctly, eg:

[the specification]: https://tools.ietf.org/html/rfc6749#section-2.3.1

## Options

Using the optional options paramter, you can configure the behaviour
of wrap-oauth2.

Using the `:strategy` option, you are able to configure a `state`
management strategy. The `state` is a CSRF-protection mechanism. The
default strategy relies on a server-side session to store the token
used for `state` in order to compare `state` received to the
session-token. To use another strategy, implement the
`ring.middleware.oauth2.strategy/Strategy`-protocol.

## Workflow diagram

The following image is a workflow diagram that describes the OAuth2
Expand Down
1 change: 1 addition & 0 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
:license {:name "The MIT License"
:url "http://opensource.org/licenses/MIT"}
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/tools.logging "0.4.0"]
[cheshire "5.8.0"]
[clj-http "3.7.0"]
[clj-time "0.14.0"]
Expand Down
140 changes: 85 additions & 55 deletions src/ring/middleware/oauth2.clj
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
(ns ring.middleware.oauth2
(:require [clj-http.client :as http]
(:require [ring.middleware.oauth2.strategy :as csrf]
[ring.middleware.oauth2.strategy.session :as session]
[ring.middleware.oauth2.default-handlers :as default-handlers]
[clj-http.client :as http]
[clj-time.core :as time]
[clojure.string :as str]
[crypto.random :as random]
[ring.util.codec :as codec]
[ring.util.request :as req]
[ring.util.response :as resp]))
[ring.util.response :as resp]
[clojure.tools.logging :as log]))


(defn- redirect-uri [profile request]
(-> (req/request-url request)
Expand All @@ -25,78 +30,103 @@
:scope (scopes profile)
:state state})))

(defn- random-state []
(-> (random/base64 9) (str/replace "+" "-") (str/replace "/" "_")))

(defn- make-launch-handler [profile]
(fn [{:keys [session] :or {session {}} :as request}]
(let [state (random-state)]
(-> (resp/redirect (authorize-uri profile request state))
(assoc :session (assoc session ::state state))))))
(defn- make-launch-handler [strategy profile]
(fn [request]
(let [token (csrf/get-token strategy request)
response (resp/redirect (authorize-uri profile request token))]
(csrf/write-token strategy profile request response token))))

(defn- state-matches? [request]
(= (get-in request [:session ::state])
(get-in request [:query-params "state"])))

(defn- format-access-token
[{{:keys [access_token expires_in refresh_token id_token]} :body :as r}]
(-> {:token access_token}
(cond-> expires_in (assoc :expires (-> expires_in time/seconds time/from-now))
refresh_token (assoc :refresh-token refresh_token)
id_token (assoc :id-token id_token))))
[{{:keys [access_token expires_in refresh_token id_token error error_description error_uri]} :body :as r}]
(when error
(log/warn (str error ": " error_description ", " error_uri) {:request r}))
(cond-> {:token access_token}
expires_in (assoc :expires (-> expires_in time/seconds time/from-now))
refresh_token (assoc :refresh-token refresh_token)
id_token (assoc :id-token id_token)))

(defn- request-params [profile request]
{:grant_type "authorization_code"
:code (get-in request [:query-params "code"])
:redirect_uri (redirect-uri profile request)})
{:grant_type "authorization_code"
:code (get-in request [:query-params "code"])
:redirect_uri (redirect-uri profile request)})

(defn- add-header-credentials [opts id secret]
(assoc opts :basic-auth [id secret]))

(defn- add-form-credentials [opts id secret]
(assoc opts :form-params (-> (:form-params opts)
(merge {:client_id id
:client_secret secret}))))
(update-in
opts
[:form-params]
merge
{:client_id id, :client_secret secret}))

(defn- get-access-token
[{:keys [access-token-uri client-id client-secret basic-auth?]
:or {basic-auth? false} :as profile} request]
:or {basic-auth? false} :as profile} request]
(format-access-token
(http/post access-token-uri
(cond-> {:accept :json, :as :json,
:form-params (request-params profile request)}
basic-auth? (add-header-credentials client-id client-secret)
(not basic-auth?) (add-form-credentials client-id client-secret)))))

(defn state-mismatch-handler [_]
{:status 400, :headers {}, :body "State mismatch"})

(defn- make-redirect-handler [{:keys [id landing-uri] :as profile}]
(let [error-handler (:state-mismatch-handler profile state-mismatch-handler)]
(fn [{:keys [session] :or {session {}} :as request}]
(if (state-matches? request)
(let [access-token (get-access-token profile request)]
(-> (resp/redirect landing-uri)
(assoc :session (-> session
(assoc-in [::access-tokens id] access-token)
(dissoc ::state)))))
(error-handler request)))))

(defn- assoc-access-tokens [request]
(if-let [tokens (-> request :session ::access-tokens)]
(assoc request :oauth2/access-tokens tokens)
request))
(http/post access-token-uri
(cond-> {:accept :json, :as :json,
:form-params (request-params profile request)}
basic-auth? (add-header-credentials client-id client-secret)
(not basic-auth?) (add-form-credentials client-id client-secret)))))

(defn- parse-redirect-url [{:keys [redirect-uri]}]
(.getPath (java.net.URI. redirect-uri)))

(defn wrap-oauth2 [handler profiles]
(let [profiles (for [[k v] profiles] (assoc v :id k))
launches (into {} (map (juxt :launch-uri identity)) profiles)
(defn wrap-access-tokens [handler]
(fn [request]
(handler
(if-let [tokens (-> request :session ::access-tokens)]
(assoc request :ring.middleware.oauth2/access-tokens tokens)
request))))


(defn wrap-access-token-response
"if access-token-to-session? is true adds the access-token to the session for
response"
[{:keys [id landing-uri] :as profile}
{:keys [session] :or {session {}} :as request}
response
access-token
access-token-to-session?]
(if access-token-to-session?
(let [session (assoc-in session [:ring.middleware.oauth2/access-tokens id] access-token)]
(assoc response :session session))
response))

(defn- read-token [request]
(get-in request [:query-params "state"]))

(defn- make-redirect-handler [strategy {:keys [id landing-uri] :as profile} access-token-to-session?]
(let [error-handler (:state-mismatch-handler profile default-handlers/default-state-mismatch-handler)
success-handler (:success-handler profile default-handlers/default-success-handler)]
(fn [request]
(if (csrf/valid-token? strategy profile request (read-token request))
(let [access-token (get-access-token profile request)
response (wrap-access-token-response profile
request
(success-handler profile access-token request)
access-token
access-token-to-session?)]
(csrf/remove-token strategy profile response))
(error-handler profile request)))))

(defn wrap-oauth2-flow [handler profiles & {:keys [strategy access-token-to-session?]
:or {strategy (session/session-strategy)
access-token-to-session? true}}]
(let [profiles (for [[k v] profiles] (assoc v :id k))
launches (into {} (map (juxt :launch-uri identity)) profiles)
redirects (into {} (map (juxt parse-redirect-url identity)) profiles)]
(fn [{:keys [uri] :as request}]
(if-let [profile (launches uri)]
((make-launch-handler profile) request)
((make-launch-handler strategy profile) request)
(if-let [profile (redirects uri)]
((make-redirect-handler profile) request)
(handler (assoc-access-tokens request)))))))
((make-redirect-handler strategy profile access-token-to-session?) request)
(handler request))))))


(defn wrap-oauth2 [handler profiles & options]
(->
(apply wrap-oauth2-flow handler profiles options)
(wrap-access-tokens)))
11 changes: 11 additions & 0 deletions src/ring/middleware/oauth2/default_handlers.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
(ns ring.middleware.oauth2.default-handlers
(:require [ring.util.response :as resp]))

(defn default-success-handler
[{:keys [id landing-uri] :as profile} access-token request]
(resp/redirect landing-uri))



(defn default-state-mismatch-handler [_ _]
{:status 400, :headers {}, :body "State mismatch"})
41 changes: 41 additions & 0 deletions src/ring/middleware/oauth2/strategy.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
(ns ring.middleware.oauth2.strategy
"Helper functions to implement strategies."
(:require [clj-http.client :as http]
[clj-time.core :as time]
[clojure.string :as str]
[ring.util.request :as req]
[ring.util.codec :as codec]
[clojure.tools.logging :as log]))



(defprotocol Strategy
"CSRF protection is based on the fact, that some state is embedded
in the client webpage (e.g. as hidden form field)
and the server is able to validate that state.

OWASP documents a number of patterns how to create and validate that state
in the form of a 'token', each with its own advantages and disadvantages.

Strategy is the protocol to abstract the process
of token creation and validation."
(get-token [strategy request]
"Returns a token to be used. Users of ring.middleware.anti-forgery should
use the appropriate utility functions from `ring.util.anti-forgery`
namespace.")

(valid-token? [strategy profile request token]
"Given the `request` and the `token` from that request, `valid-token?`
returns true if the token is valid. Returns false otherwise.")

(write-token [strategy profile request response token]
"Some state management strategies do need to remember state (e.g., by
storing it to some storage accessible in different requests). `write-token`
is the method to handle state persistence, if necessary.")


(remove-token [strategy profile response]
)

)

48 changes: 48 additions & 0 deletions src/ring/middleware/oauth2/strategy/session.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
(ns ring.middleware.oauth2.strategy.session
(:require [ring.middleware.oauth2.strategy :as strategy]
[ring.util.response :as resp]
[clojure.string :as str]
[crypto.random :as random]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Managing state using sessions
;;
;; This requires a session (or a shared session, can easily be distributed horizontally








#_(defn access-token-to-session
[{:keys [id landing-uri] :as profile}
access-token
{:keys [session] :or {session {}} :as request}]
(-> (default-success-handler profile access-token request)
(assoc :session (-> session
(assoc-in [:ring.middleware.oauth2/access-tokens id] access-token)))))




(deftype SessionStrategy []
strategy/Strategy

(get-token [_ _]
(-> (random/base64 9) (str/replace "+" "-") (str/replace "/" "_")))

(write-token [strategy profile {:keys [session] :or {session {}} :as request} response token]
(assoc response :session (assoc session :ring.middleware.oauth2/state token)))

(remove-token [strategy profile response]
(update-in response [:session] dissoc :ring.middleware.oauth2/state))

(valid-token? [strategy profile request token]
(= (get-in request [:session :ring.middleware.oauth2/state])
token)))


(defn session-strategy []
(->SessionStrategy))
Loading