Last active
February 24, 2023 20:53
-
-
Save titogarcia/4f09bcc5fa38fbdc1076954b9a99a8fc to your computer and use it in GitHub Desktop.
Example on how to implement an OAuth token store in Clojure
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
;; 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