From fd67a5ce51e5d97cc8955252764ee811d5a18032 Mon Sep 17 00:00:00 2001 From: Bob Beaty Date: Wed, 25 Feb 2015 14:00:38 -0600 Subject: [PATCH 1/3] Added support for re-athentication on Google We needed to be able to re-authenticate on Google, and that meant that we needed to add several infrastructural changes to the endpoints as well as adding in a new 'defmethod' for the refreshing, and then we need to add in the top-level function for refreshing and update the docs. This is all tested out, and works like a charm. --- .gitignore | 5 ++- CHANGELOG.md | 6 +++ README.md | 60 +++++++++++++++++++++---- project.clj | 2 +- src/clj_oauth2/client.clj | 95 ++++++++++++++++++++++----------------- 5 files changed, 116 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index b8c1b21..ef475ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ +/target +/lib +/classes pom.xml *jar -/lib/ -/classes/ .lein-deps-sum 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..c2ee476 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,43 @@ (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)))) + +(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)))) From d0447bbddc95348586dcf73cb2836e98780cc84c Mon Sep 17 00:00:00 2001 From: Bob Beaty Date: Wed, 25 Feb 2015 14:03:59 -0600 Subject: [PATCH 2/3] Added a few more exclusions to the list We don't need to check in any of these files, and they just clutter the git status command. Just keeping things clean. --- .gitignore | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ef475ec..47ecbaf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,15 @@ /lib /classes pom.xml -*jar +*.jar +*.class +*.swp +*.swo +*.out .lein-deps-sum +.lein-failures +.lein-plugins +.lein-repl-history +.nrepl-port +.DS_Store +#README.md# From 5cb1f2c9d516e58a0ae4cd31c34240c6b9d6c536 Mon Sep 17 00:00:00 2001 From: Bob Beaty Date: Wed, 25 Feb 2015 14:17:26 -0600 Subject: [PATCH 3/3] Added the refresh-token BACK to token data When we are doing a refresh of the OAuth2 token, we need to add it back into the map to make sure that it's there and available for the next time we might want to call this function. This was something I was doing in the calling code, but it really belongs here as everyone is likely to need this. --- src/clj_oauth2/client.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clj_oauth2/client.clj b/src/clj_oauth2/client.clj index c2ee476..63a4c72 100644 --- a/src/clj_oauth2/client.clj +++ b/src/clj_oauth2/client.clj @@ -122,7 +122,8 @@ (nil? refresh-token) (throw (OAuth2Exception. (format "No :refresh-token in %s" token))) :else - (request-access-token (assoc endpoint :grant-type "refresh_token") token)))) + (-> (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]}]