Skip to content

Instantly share code, notes, and snippets.

@titogarcia
Last active February 24, 2023 20:53
Show Gist options
  • Save titogarcia/4f09bcc5fa38fbdc1076954b9a99a8fc to your computer and use it in GitHub Desktop.
Save titogarcia/4f09bcc5fa38fbdc1076954b9a99a8fc to your computer and use it in GitHub Desktop.
Example on how to implement an OAuth token store in Clojure
;; https://gist.github.com/titogarcia/4f09bcc5fa38fbdc1076954b9a99a8fc
(ns token-refresh-example
(:require [clojure.string :refer [ends-with?]]
[clj-http.client :as http])
(:import [java.time Instant]
[java.time.temporal ChronoUnit]))
;;;; Logging implementation for exercising these tests
(defn log
"Print avoiding interleaving of concurrent prints"
[& more]
(print (str (apply str more) \newline))
(flush))
;;;; Emulation of HTTP requests
; This is to test the usual behavior of clj-http.client
(comment
(-> (http/get "https://jsonplaceholder.typicode.com/todos/1" {:as :json})
:body))
(defn generate-token-data []
(let [token (->> (rand (bit-shift-left 1 32))
long
(format "%x"))]
{:access_token (str "tok" token)
:refresh_token (str "ref" token)
:expires_in (-> (Instant/now)
(.plus 5 ChronoUnit/SECONDS))}))
(defn emulate-http-request [req]
; avoids interleaving of concurrent prints
(log "Sending request... " req)
(Thread/sleep 1000)
(let [resp (if (ends-with? (:url req) "/token")
{:status 200
:body (generate-token-data)}
(if (:oauth-token req)
{:status 200}
{:status 401}))]
(log "Received response. " resp)
resp))
(comment
(with-redefs [http/request emulate-http-request]
(http/request {:method :get :url "http://example.com/" :oauth-token "x"})))
;;;; Example
(defn request-oauth-token [authcode]
(:body (http/request {:method :post
:url "https://example.com/oauth2/token"
:form-params {:grant_type "authorization_code"
:code authcode
:etc :etc}})))
(defn token-needs-refresh? [token-data ^Instant now]
(pos? (compare
(.plus now 3 ChronoUnit/SECONDS)
(:expires_in token-data))))
(defn refresh-oauth-token [refresh-token]
(:body
(http/request {:method :post
:url "https://example.com/oauth2/token"
:form-params {:grant_type "refresh_token"
:refresh_token refresh-token
:etc :etc}})))
(defn revise-oauth-token [token-store]
(-> (swap! token-store
(fn [token-promise]
(if (token-needs-refresh? @token-promise (Instant/now))
(delay (refresh-oauth-token (:refresh_token @token-promise)))
token-promise)))
force
:access_token))
;; Alternative implementation.
(comment
(defn revise-oauth-token [token-store]
(:access_token
(let [token-promise @token-store
token-data @token-promise]
(if (token-needs-refresh? token-data (Instant/now))
(let [new-token-promise (promise)]
(if (compare-and-set! token-store token-promise new-token-promise)
@(new-token-promise (refresh-oauth-token (:refresh_token token-data)))
@@token-store))
token-data)))))
(defn example [authcode]
(let [token-store (atom (delay (request-oauth-token authcode)))]
(http/request
{:method :get
:url "https://example.com/"
:oauth-token (revise-oauth-token token-store)})
(println "Sleeping for OAuth token to expire...")
(Thread/sleep 10000)
(let [f1 (future (http/request
{:method :get
:url "https://example.com/1"
:oauth-token (revise-oauth-token token-store)}))
f2 (future (http/request
{:method :get
:url "https://example.com/2"
:oauth-token (revise-oauth-token token-store)}))]
@f1
@f2
nil)))
(comment
(with-redefs [http/request emulate-http-request]
(example "myauthcode")))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment