Skip to content
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
17 changes: 14 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
/target
/lib
/classes
pom.xml
*jar
/lib/
/classes/
*.jar
*.class
*.swp
*.swo
*.out
.lein-deps-sum
.lein-failures
.lein-plugins
.lein-repl-history
.nrepl-port
.DS_Store
#README.md#
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.4.0

* added `refresh-access-token` to get new token from Google
* added support for additional elements of the config for handling the
infrastructure for `refresh-access-token`

## 0.3.0

* support OAuth 2.0 draft 10 for Force.com
Expand Down
60 changes: 52 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ implementations such as Facebook to be practical.

clj-oauth2 wraps clj-http for accessing protected resources.

## Basic Usage
Check CHANGELOG.md for release notes.

## Basic Facebook Usage

```clojure
(:require [clj-oauth2.client :as oauth2])
Expand Down Expand Up @@ -40,6 +42,48 @@ clj-oauth2 wraps clj-http for accessing protected resources.
(oauth2/get "https://graph.facebook.com/me" {:oauth2 access-token})
```

## Basic Google Usage

```clojure
(:require [clj-oauth2.client :as oauth2])

(def google-oauth2
{:authorization-uri "https://accounts.google.com/o/oauth2/auth"
:access-token-uri "https://accounts.google.com/o/oauth2/token"
:redirect-uri "http://example.com/oauth2-callback"
:client-id "1234567890"
:client-secret "0987654321"
:access-query-param :access_token
:scope ["https://www.googleapis.com/auth/dfareporting"
"https://www.googleapis.com/auth/devstorage.read_only"
"https://www.googleapis.com/auth/dfatrafficking"]
:grant-type "authorization_code"
:approval-prompt "force"
:access-type "offline"})

;; redirect user to (:uri auth-req) afterwards
(def auth-req
(oauth2/make-auth-request google-oauth2))


;; auth-resp is a keyword map of the query parameters added to the
;; redirect-uri by the authorization server
;; e.g. {:code "abc123"}
(def access-token
(oauth2/get-access-token google-oauth2 auth-resp auth-req))

;; get list of user profiles for this Google (DFA) account
(oauth2/get
"https://www.googleapis.com/dfareporting/v2.0/userprofiles"
{:oauth2 access-token})

;; refresh an expired access-token - NOTE: it has to contain the
;; :refresh-token that Google provides in the `get-access-token`
;; function!
(oauth2/refresh-access-token google-oauth2 access-token)

```

## Ring Middleware

```clojure
Expand Down Expand Up @@ -71,10 +115,10 @@ clj-oauth2 wraps clj-http for accessing protected resources.
(defroutes handler
; Just do a 'describe' on the Account object and dump the resulting
; output
(GET "/"
{params :params session :session oauth :oauth}
(let [url (str
(:instance_url (:params oauth))
(GET "/"
{params :params session :session oauth :oauth}
(let [url (str
(:instance_url (:params oauth))
"/services/data/v24.0/sobjects/Account/describe/")
response (oauth2/get url {:oauth2 oauth})]
{:headers {"Content-type" "text/plain; charset=UTF-8"}
Expand All @@ -83,10 +127,10 @@ clj-oauth2 wraps clj-http for accessing protected resources.
(route/not-found "Page not found"))

; Set up the wrappers
(def app
(-> handler
(def app
(-> handler
(wrap-oauth2 force-com-oauth2)
wrap-session
wrap-session
wrap-keyword-params
wrap-params))

Expand Down
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(def dev-dependencies
'[[ring "0.3.11"]])

(defproject clj-oauth2 "0.3.0"
(defproject clj-oauth2 "0.4.0"
:description "clj-http and ring middlewares for OAuth 2.0"
:dependencies [[org.clojure/clojure "1.3.0"]
[org.clojure/data.json "0.1.1"]
Expand Down
96 changes: 55 additions & 41 deletions src/clj_oauth2/client.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
(ns clj-oauth2.client
"The basic client functions for OAuth2 authentication."
(:refer-clojure :exclude [get])
(:use [clj-http.client :only [wrap-request]]
[clojure.data.json :only [read-json]])
Expand All @@ -9,8 +10,7 @@
[org.apache.commons.codec.binary Base64]))

(defn make-auth-request
[{:keys [authorization-uri client-id client-secret redirect-uri scope]}
& [state]]
[{:keys [authorization-uri client-id client-secret redirect-uri scope approval-prompt access-type]} & [state]]
(let [uri (uri/uri->map (uri/make authorization-uri) true)
query (assoc (:query uri)
:client_id client-id
Expand All @@ -19,7 +19,9 @@
query (if state (assoc query :state state) query)
query (if scope
(assoc query :scope (str/join " " scope))
query)]
query)
query (if approval-prompt (assoc query :approval_prompt approval-prompt) query)
query (if access-type (assoc query :access_type access-type) query)]
{:uri (str (uri/make (assoc uri :query query)))
:scope scope
:state state}))
Expand All @@ -38,10 +40,13 @@
(defmethod prepare-access-token-request
"authorization_code" [request endpoint params]
(merge-with merge request
{:body {:code
(:code params)
:redirect_uri
(:redirect-uri endpoint)}}))
{:body {:code (:code params)
:redirect_uri (:redirect-uri endpoint)}}))

(defmethod prepare-access-token-request
"refresh_token" [request endpoint params]
(merge-with merge request
{:body {:refresh_token (:refresh-token params)}}))

(defmethod prepare-access-token-request
"password" [request endpoint params]
Expand All @@ -52,24 +57,17 @@
(defn- add-client-authentication [request endpoint]
(let [{:keys [client-id client-secret authorization-header?]} endpoint]
(if authorization-header?
(add-base64-auth-header
request
"Basic"
(str client-id ":" client-secret))
(merge-with
merge
request
{:body
{:client_id client-id
:client_secret client-secret}}))))
(add-base64-auth-header request "Basic" (str client-id ":" client-secret))
(merge-with merge request
{:body {:client_id client-id
:client_secret client-secret}}))))

(defn- request-access-token
[endpoint params]
(let [{:keys [access-token-uri access-query-param grant-type]} endpoint
request
{:content-type "application/x-www-form-urlencoded"
:throw-exceptions false
:body {:grant_type grant-type}}
request {:content-type "application/x-www-form-urlencoded"
:throw-exceptions false
:body {:grant_type grant-type}}
request (prepare-access-token-request request endpoint params)
request (add-client-authentication request endpoint)
request (update-in request [:body] uri/form-url-encode)
Expand All @@ -80,7 +78,8 @@
(.startsWith content-type "text/javascript"))) ; Facebookism
(read-json body)
(uri/form-url-decode body)) ; Facebookism
error (:error body)]
error (:error body)
refresh-token (:refresh_token body)]
(if (or error (not= status 200))
(throw (OAuth2Exception. (if error
(if (string? error)
Expand All @@ -90,29 +89,44 @@
(if error
(if (string? error)
error
(:type error)) ; Facebookism
(:type error)) ; Facebookism
"unknown")))
{:access-token (:access_token body)
:token-type (or (:token_type body) "draft-10") ; Force.com
:query-param access-query-param
:params (dissoc body :access_token :token_type)})))
(-> {:access-token (:access_token body)
:token-type (or (:token_type body) "draft-10") ; Force.com
:query-param access-query-param
:params (dissoc body :access_token :refresh_token :token_type)}
(merge (if refresh-token {:refresh-token refresh-token}))))))

(defn get-access-token
[endpoint
& [params {expected-state :state expected-scope :scope}]]
[endpoint & [params {expected-state :state expected-scope :scope}]]
(let [{:keys [state error]} params]
(cond (string? error)
(throw (OAuth2Exception. (:error_description params) error))
(and expected-state (not (= state expected-state)))
(throw (OAuth2StateMismatchException.
(format "Expected state %s but got %s"
state expected-state)
state
expected-state))
:else
(request-access-token endpoint params))))

(defn with-access-token [uri {:keys [access-token query-param]}]
(cond
(string? error)
(throw (OAuth2Exception. (:error_description params) error))
(and expected-state (not (= state expected-state)))
(throw (OAuth2StateMismatchException.
(format "Expected state %s but got %s"
state expected-state)
state
expected-state))
:else
(request-access-token endpoint params))))

(defn refresh-access-token
"Function to take the existing `refresh-token` and configuration data
and refresh this token to make sure that we have a valid token to work
with."
[endpoint token]
(let [{:keys [refresh-token]} token]
(cond
(nil? refresh-token)
(throw (OAuth2Exception. (format "No :refresh-token in %s" token)))
:else
(-> (request-access-token (assoc endpoint :grant-type "refresh_token") token)
(assoc :refresh-token refresh-token)))))

(defn with-access-token
[uri {:keys [access-token query-param]}]
(str (uri/make (assoc-in (uri/uri->map (uri/make uri) true)
[:query query-param]
access-token))))
Expand Down