Skip to content

Instantly share code, notes, and snippets.

@arohner
Created November 5, 2016 21:53
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save arohner/8d94ee5704b1c0c1b206186525d9f7a7 to your computer and use it in GitHub Desktop.
Save arohner/8d94ee5704b1c0c1b206186525d9f7a7 to your computer and use it in GitHub Desktop.
Clojure example of accessing google APIs directly
(ns example.api.google
(:require [cemerick.url :as url]
[cheshire.core :as json]
[clj-jwt.core :as jwt]
[clj-jwt.key :as key]
[clj-time.core :as time]
[clj-http.client :as http]
[clojure.string :as str])
(:import java.io.StringReader))
;;; Example code for calling Google apis using a service account.
(defn load-creds
"Takes a path to a service account .json credentials file"
[secrets-json-path]
(-> secrets-json-path slurp (json/parse-string keyword)))
;; list of API scopes requested, e.g. https://developers.google.com/admin-sdk/directory/v1/guides/authorizing
(def scopes ["https://www.googleapis.com/auth/admin.directory.user"
"https://www.googleapis.com/auth/admin.directory.group"])
(defn create-claim [creds & [{:keys [sub] :as opts}]]
(let [claim (merge {:iss (:client_email creds)
:scope (str/join " " scopes)
:aud "https://www.googleapis.com/oauth2/v4/token"
:exp (-> 1 time/hours time/from-now)
:iat (time/now)}
(when sub
;; when using the Admin API, delegating access, :sub may be needed
{:sub sub}))]
(-> claim jwt/jwt (jwt/sign :RS256 (-> creds :private_key (#(StringReader. %)) (#(key/pem->private-key % nil)))) (jwt/to-str))))
(defn request-token [creds & [{:keys [sub] :as opts}]]
(let [claim (create-claim creds opts)
resp (http/post "https://www.googleapis.com/oauth2/v4/token" {:form-params {:grant_type "urn:ietf:params:oauth:grant-type:jwt-bearer"
:assertion claim}
:as :json})]
(when (= 200 (-> resp :status))
(-> resp :body :access_token))))
;; Call request-token to make an API call to google to create a new access token, using creds. Access-tokens are valid for 1 hour after creating. Pass the received token to API calls.
(defn api-req [{:keys [] :as request} token]
(http/request (-> request
(assoc-in [:headers "Authorization"] (str "Bearer " token))
(assoc :as :json))))
@enaeher
Copy link

enaeher commented Jul 5, 2017

Thank you for posting this-it saved me a lot of time.

@benjamin-asdf
Copy link

(ns
    example.google.auth
    (:require
     [cheshire.core :as json]
     [babashka.curl :as curl]
     [buddy.core.keys :as keys]
     [buddy.sign.jwt :as jwt]
     [clojure.string :as str])
    (:import java.io.StringReader))

(defn load-creds
  "Takes a path to a service account .json credentials file"
  [secrets-json-path]
  (-> secrets-json-path slurp (json/parse-string keyword)))

(defn
  create-claim
  [creds & [{:keys [sub scopes] :as opts}]]
  (let [now (java.time.Instant/now)
        claim (merge
               {:iss (:client_email creds)
                :scope (str/join " " scopes)
                :aud "https://oauth2.googleapis.com/token"
                :exp (+ (.getEpochSecond now) (* 60 30))
                :iat (.getEpochSecond now)}
               (when
                   sub
                 ;; when using the Admin API, delegating access, :sub may be needed
                   {:sub sub}))]
    (jwt/sign
     claim
     (keys/str->private-key
      (:private_key creds))
     {:alg :rs256})))

(defn request-token [creds & [{:keys [sub] :as opts}]]
  (let [claim (create-claim creds opts)
        resp (curl/post
              "https://www.googleapis.com/oauth2/v4/token"
              {:form-params
               {:grant_type "urn:ietf:params:oauth:grant-type:jwt-bearer"
                :assertion claim}
               :as :json})]
    (if (= 200 (-> resp :status))
      (-> resp :body (json/decode keyword) :access_token)
      resp)))

(defn
  auth
  [request token]
  (->
   request
   (assoc-in [:headers "Authorization"] (str "Bearer " token))
   (assoc :as :json)))

(comment
  (request-token
   (load-creds credential-path)
   {:scopes ["https://www.googleapis.com/auth/spreadsheets.readonly"]}))

I needed to adapt a bit also not using outdated clj-time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment