Last active
August 29, 2015 14:16
-
-
Save drbobbeaty/44f60ab64a270ebf71ae to your computer and use it in GitHub Desktop.
OAuth2 Login to Google
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns bartender.dcm | |
"The DCM-specific functions for loading data. The Google OAuth2 data for the | |
application is obtained from the Developer Console." | |
(:require [bartender.util :refer [parse-json update lcase]] | |
[cheshire.core :as json] | |
[clj-http.client :as http] | |
[clj-oauth2.client :as oauth2] | |
[clj-time.coerce :refer [to-long from-long]] | |
[clj-time.core :refer [now]] | |
[clojure.set :refer [rename-keys]] | |
[clojure.tools.logging :refer [infof warn warnf error errorf]] | |
[clojure.walk :refer [keywordize-keys]] | |
[compojure | |
[core :refer [defroutes GET POST DELETE PUT]] | |
[handler :as handler]] | |
[ring.middleware.jsonp :refer [wrap-json-with-padding]] | |
[ring.middleware.params :refer [wrap-params]] | |
[ring.util.io :refer [piped-input-stream]]) | |
(:import [java.io BufferedWriter OutputStreamWriter IOException])) | |
(declare user-profiles) | |
;; | |
;; Missing functions from clj-oauth2 | |
;; | |
(defn refresh-access-token | |
"Function to take the existing `access-token` and configuration data | |
and refresh this token with Google to make sure that we have a valid | |
token to work with." | |
[refresh-token {:keys [client-id client-secret access-token-uri]}] | |
(let [req (http/post access-token-uri {:form-params {:client_id client-id | |
:client_secret client-secret | |
:refresh_token refresh-token | |
:grant_type "refresh_token"} | |
:as :json | |
:throw-entire-message? true})] | |
(when (= (:status req) 200) | |
(parse-json (:body req))))) | |
;; | |
;; Constants from the Google Developer Console as to how to log in using | |
;; OAuth 2.0 to the site. | |
;; | |
(def BASE_URI "https://accounts.google.com") | |
(def AUTH_URI (str BASE_URI "/o/oauth2/auth")) | |
(def TOKEN_URI (str BASE_URI "/o/oauth2/token")) | |
(def CLIENT_ID "<my_client_id>") | |
(def CLIENT_SECRET "<my_client_secret>") | |
(def SCOPE | |
"For Google's OAuth2, we need to provide to them the 'scope' of the login - | |
meaning those things that we want to be able to control and call using the | |
`access-token` that Google will return to us. These are all the things we | |
need from DCM." | |
["https://www.googleapis.com/auth/dfareporting" | |
"https://www.googleapis.com/auth/devstorage.read_only" | |
"https://www.googleapis.com/auth/dfatrafficking"]) | |
(def TRAFFIC_URI | |
"This is the base URI for all the trafficking-related endpoints at Google. | |
This will be appended to in the individual functions make the calling URL | |
but it's nice to have it here at the top to make a global change easier." | |
"https://www.googleapis.com/dfareporting/v2.0") | |
(def REDIRECT_URI | |
"This is the one endpoint that **we** have to implement which will be the | |
target of Google's OAuth2 callback which will contain all that we need to | |
get the access-token and the supporting values." | |
"http://localhost:3000/api/integration/google_auth_callback") | |
(def gconfig | |
"Map of basic config data for successfully hitting Gogle using OAuth2 for | |
the `access-token` to hit the DCM endpoints for all the trafficking data and | |
reporting data." | |
{ :authorization-uri AUTH_URI | |
:access-token-uri TOKEN_URI | |
:redirect-uri REDIRECT_URI | |
:client-id CLIENT_ID | |
:client-secret CLIENT_SECRET | |
:access-query-param :access_token | |
:scope SCOPE | |
:grant-type "authorization_code" }) | |
(def auth-req | |
"The clj-oauth2 library needs to use the config to make an 'authorization | |
request' map that will be used in getting the tokens, etc." | |
(oauth2/make-auth-request gconfig)) | |
(def gtoken | |
"This will be the data returned from Google that includes the `access-token`, | |
what we need to refresh it, and all the goodies. This will look something like: | |
{ :access-token \"ya29.JAG00FF1ZLK-UuRdK6e7zf5uFBuNdw_X46546536I9dbmKMbGVBSfAl7\" | |
:token-type \"bearer\" | |
:query-param :access_token | |
:expires-in 3600 } | |
" | |
(atom {})) | |
(def gprofile | |
"This will be the user profile for the authenticated user that generated the | |
`access-token` in use. This is obtained by calling the /userprofiles endpoint | |
in the Google API, and then extracting out the first item that will look | |
something like this: | |
{ :kind \"dfareporting#userProfile\" | |
:etag \"\"3_UO98K-Piv7TXZrLaOcjWlCRrs/XCU7oULm-5DPoOwijph80\"\" | |
:profileId \"123456890\" | |
:userName \"yo-yo-ma\" | |
:accountId \"1234\" | |
:accountName \"The Account for All\" } | |
" | |
(atom {})) | |
(defn auth-google | |
"Function to do the initial authentication request to Google for the above | |
configuration. This will be called from the `REDIRECT_URI` endpoint, where | |
the argument is the `:query-params` of the compojure route. This will log | |
the response from Google and then save it in the `gtoken` atom for later." | |
[params] | |
(let [pull (fn [m] (assoc m :expires-in (:expires_in (:params m)))) | |
tok (-> (oauth2/get-access-token gconfig (keywordize-keys params) auth-req) | |
(update :token-type lcase) | |
(pull) | |
(dissoc :params))] | |
(infof "token response: %s" tok) | |
(reset! gtoken tok) | |
(infof "loading user profile...") | |
(reset! gprofile (first (user-profiles tok))) | |
tok)) | |
(defn re-auth-google | |
"Function to update the `access-token` for the current session of the | |
authenticated user - which may, in fact, be the exact same token, but | |
Google will let us know either way." | |
[& [token]] | |
(let [tok (-> (refresh-access-token (:access-token (or token @gtoken)) gconfig) | |
(rename-keys {:token_type :token-type | |
:expires_in :expires-in | |
:access_token :access-token}) | |
(update :token-type lcase))] | |
(infof "re-auth token response: %s" tok) | |
(reset! gtoken tok) | |
tok)) | |
;; | |
;; Administrative Functions | |
;; | |
(defn user-profiles | |
"Function to get the complete list of allowed user profiles for the | |
authenticated user in `gtoken`." | |
[& [token]] | |
(let [url (str TRAFFIC_URI "/userprofiles")] | |
(:items (parse-json (:body (oauth2/get url {:oauth2 (or token @gtoken)})))))) | |
;; | |
;; Trafficking Functions | |
;; | |
(defn get-cities | |
"Function to list all the cities for the provided token and profile. The | |
default values are the current profile and token, but these can be passed | |
in if you want to make a special call." | |
[& [token profile]] | |
(let [pid (:profileId (or profile @gprofile)) | |
url (str TRAFFIC_URI "/userprofiles/" pid "/cities")] | |
(parse-json (:body (oauth2/get url {:oauth2 (or token @gtoken)}))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defproject bartender "1.0.0" | |
:description "The bartender makes sure that the system is filled with data." | |
:url "https://github.com/drbobbeaty/bartender" | |
:license {:name "Eclipse Public License" | |
:url "http://www.eclipse.org/legal/epl-v10.html"} | |
:plugins [[lein-marginalia "0.7.1"]] | |
:min-lein-version "2.3.4" | |
:source-paths ["src"] | |
:dependencies [[org.clojure/clojure "1.6.0"] | |
;; nice utilities | |
[clj-time "0.6.0"] | |
[org.clojure/math.numeric-tower "0.0.4"] | |
[org.clojure/core.memoize "0.5.7"] | |
;; command line option processing | |
[org.clojure/tools.cli "0.2.2"] | |
;; logging with log4j | |
[log4j/log4j "1.2.16"] | |
[org.clojure/tools.logging "0.2.6"] | |
[robert/hooke "1.3.0"] | |
;; JSON parsing library | |
[cheshire "5.3.1"] | |
;; nice HTTP client library | |
[clj-http "1.0.1"] | |
[clj-oauth2-ls "0.3.0"] | |
;; web server | |
[compojure "1.1.3"] | |
[lib-noir "0.9.5"] | |
[ring/ring-jetty-adapter "1.1.6"] | |
[ring.middleware.jsonp "0.1.1"]] | |
:aot [bartender.main] | |
:uberjar-merge-with {#"\.sql\.Driver$" [#(str (clojure.string/trim (slurp %)) "\n") str spit]} | |
:test-selectors {:default (complement :exercise) | |
:integration :integration | |
:all (constantly true)} | |
:jvm-opts ["-Xmx64m"] | |
:main bartender.main) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns bartender.server | |
"The routes for the web server." | |
(:require [bartender.dcm :as dcm] | |
[bartender.util :as util] | |
[cheshire.core :as json] | |
[clj-time.coerce :refer [to-long from-long]] | |
[clj-time.core :refer [now]] | |
[clojure.tools.logging :refer [infof warn warnf error errorf]] | |
[compojure | |
[core :refer [defroutes GET POST DELETE PUT]] | |
[handler :as handler]] | |
[noir.response :refer [redirect]] | |
[ring.middleware.jsonp :refer [wrap-json-with-padding]] | |
[ring.middleware.params :refer [wrap-params]] | |
[ring.util.io :refer [piped-input-stream]]) | |
(:import [java.io BufferedWriter OutputStreamWriter IOException])) | |
(extend-protocol cheshire.generate/JSONable | |
org.joda.time.DateTime | |
(to-json [t jg] | |
(cheshire.generate/write-string jg (str t)))) | |
(defn return-code | |
"Creates a ring response for returning the given return code." | |
[code] | |
{:status code | |
:headers {"Content-Type" "application/json; charset=UTF-8"}}) | |
(defn return-json | |
"Creates a ring response for returning the given object as JSON." | |
([ob] (return-json ob (now) 200)) | |
([ob lastm] (return-json ob lastm 200)) | |
([ob lastm code] | |
{:status code | |
:headers {"Content-Type" "application/json; charset=UTF-8" | |
"Access-Control-Allow-Origin" "*" | |
"Last-Modified" (str (or lastm (now)))} | |
:body (piped-input-stream | |
(bound-fn [out] | |
(with-open [osw (OutputStreamWriter. out) | |
bw (BufferedWriter. osw)] | |
(let [error-streaming | |
(fn [e] | |
;; Since the HTTP headers have already been sent, | |
;; at this point it is too late to report the error | |
;; as a 500. The best we can do is abruptly print | |
;; an error and quit. | |
(.write bw "\n\n---BARTENDER SERVER ERROR WHILE STREAMING JSON---\n") | |
(.write bw (str e "\n\n")) | |
(warnf "Streaming exception for JSONP: %s" (.getMessage e)))] | |
(try | |
(json/generate-stream ob bw) | |
;; Handle "pipe closed" errors | |
(catch IOException e | |
(if (re-find #"Pipe closed" (.getMessage e)) | |
(infof "Pipe Closed exception: %s" (.getMessage e)) | |
(error-streaming e))) | |
(catch Throwable t | |
(error-streaming t)))))))})) | |
(defroutes app-routes | |
"Primary routes for the webserver." | |
(GET "/" [] | |
(return-json {:app "Bartender", | |
:hello? "World!", | |
:code (or (util/git-commit) "unknown commit")})) | |
(GET "/heartbeat" [] | |
(return-code 200)) | |
;; | |
;; Authentication Endpoints for DCM (Google) - these are necessary due to | |
;; Google's OAuth2 implementation. But as soon as we have an access-token, | |
;; we can use Custos Server to hold this and use it over and over. | |
;; | |
(GET "/api/integration/google_auth_callback" {params :query-params} | |
(dcm/auth-google params) | |
(return-code 200)) | |
(GET "/google" [] | |
(redirect (:uri dcm/auth-req))) | |
(GET "/token" [] | |
(return-json @dcm/gtoken)) | |
(GET "/re-auth" [] | |
(dcm/re-auth-google) | |
(return-json @dcm/gtoken)) | |
;; | |
;; | |
;; | |
(GET "/cities" [] | |
(return-json (dcm/get-cities))) | |
) | |
(defn wrap-logging | |
"Ring middleware to log requests and exceptions." | |
[handler] | |
(fn [req] | |
(infof "Handling request: %s" (:uri req)) | |
(try (handler req) | |
(catch Throwable t | |
(error t "Server error!") | |
(throw t))))) | |
(def app | |
"The actual ring handler that is run -- this is the routes above | |
wrapped in various middlewares." | |
(-> app-routes | |
wrap-json-with-padding | |
handler/site | |
wrap-params | |
wrap-logging)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment