diff --git a/.gitignore b/.gitignore index b8c1b21..47ecbaf 100644 --- a/.gitignore +++ b/.gitignore @@ -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# diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da16a2..b6b8294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index bbf5143..f071914 100644 --- a/README.md +++ b/README.md @@ -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]) @@ -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 @@ -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"} @@ -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)) diff --git a/project.clj b/project.clj index 1227c19..d623f96 100644 --- a/project.clj +++ b/project.clj @@ -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"] diff --git a/src/clj_oauth2/client.clj b/src/clj_oauth2/client.clj index 7149745..63a4c72 100644 --- a/src/clj_oauth2/client.clj +++ b/src/clj_oauth2/client.clj @@ -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]]) @@ -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 @@ -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})) @@ -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] @@ -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) @@ -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) @@ -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))))