Phil Cooper / May 18 2023
Buddy
Authentication and authorization in Clojure with Buddy.
{:deps {org.clojure/clojure {:mvn/version "1.10.3"} ;; complient is used for autocompletion compliment/compliment {:mvn/version "0.3.9"} buddy/buddy-auth {:mvn/version "3.0.323"} buddy/buddy-hashers {:mvn/version "1.8.158"} io.pedestal/pedestal.service {:mvn/version "0.5.9"} clojure.java-time/clojure.java-time {:mvn/version "1.2.0"}}}Extensible Data Notation
(ns my-buddy.auth (:require [buddy.hashers :as hashers] [buddy.auth :as auth] [buddy.sign.jwt :as jwt] [buddy.auth.backends :as backends]) (:import [java.security MessageDigest] [java.math BigInteger]))0.0s
(def jws-secret "some_randomish-string>5358596772")(def user-database-not-this-hard-coded-thing {"admin" {:username "admin" :hashed-password (hashers/encrypt "abc") :roles {:user :admin}} "joe" {:username "joe" :email "joe.cool@example.com" :hashed-password (hashers/encrypt "so-cool") :iex-public-key "some-fancy-thing-that-enables-iex-access" :roles {:user :admin}} "user" {:username "user" :hashed-password (hashers/encrypt "ok-pwd") :roles {:user}}})(defn check-user-password "return results from database (without hashed-password)" [username password & args] (if-let [user (get user-database-not-this-hard-coded-thing username)] (if (some->> (:hashed-password user) (hashers/verify password) :valid) (dissoc user :hashed-password))))(defn md5 "get md5 checksum of a string i.e. for hash of email for gravitar" [s] (let [algorithm (MessageDigest/getInstance "MD5") raw (.digest algorithm (.getBytes s))] (format "%032x" (BigInteger. 1 raw))))(defn login-handler [request] (let [{username :username password :password} (:json-params request)] (if-let [user-info (check-user-password username password)] (let [token (jwt/sign user-info jws-secret)] {:status 200 :body (assoc user-info :token token) ; not sure this header does anything here: :headers {"Authorization" (str "Token " token)}}) {:body "user/password combination not found " :status 401})))1.4s
(login-handler {:json-params {:username "joe" :password "so-cool"}})0.8s
Expiring tokens
(require [java-time.api :as jt])0.0s
(defn login-handler-expiring [request] (let [{username :username password :password} (:json-params request)] (if-let [user-info (some-> (check-user-password username password) )] (let [token (-> user-info (merge {:iat (jt/instant) :exp (jt/plus (jt/instant) (jt/minutes 15))}) (jwt/sign jws-secret))] {:status 200 :body (assoc user-info :token token)})))) ; not sure this header does anything here:\n :headers {0.0s
(login-handler-expiring {:json-params {:username "joe" :password "so-cool"}})0.5s
(def expiring-token (some-> (check-user-password "joe" "so-cool") (merge {:iat (jt/instant) :exp (jt/plus (jt/instant) (jt/seconds 10))}) (jwt/sign jws-secret)))(jwt/unsign expiring-token jws-secret)0.5s
(try (Thread/sleep 12000) (jwt/unsign expiring-token jws-secret) (catch Exception e (-> e Throwable->map (select-keys [:cause :data]))))12.0s
Pedestal Interceptor
(require [io.pedestal.interceptor :refer [interceptor]] [buddy.hashers :as hashers] [buddy.auth :refer [authenticated?]] [buddy.auth.backends.token :as token] [buddy.sign.jws :as jws] [buddy.auth.middleware :refer [authentication-request]] )0.0s
(def token-backend "created at startup time" (token/jws-backend {:secret jws-secret}));; from interceptor code(defn token-authentication "use the provided (:authfn backend) in authentication-request middleware e.g. call check-user-token" [backend] (interceptor {:name ::token-authentication :enter (fn [ctx] (-> ctx ;; just append it for future reference. ;; TODO does this already happen in in the backend: see buddy.auth.middleware/authenticate-request ;; (assoc :auth/backend backend) (update :request authentication-request backend)))}))(def token-authorization "on enter check for :identity and call unauthorized-handler if nil" (interceptor ;; should this be using the IAuthorization -handle-unauthenticated method? {:name ::token-authorization :enter (fn [{:keys [request] :as ctx}] (if (authenticated? request) ctx (-> ctx ;(interceptor.chain/terminate) ;; is this necessary shouldn't response cause terminate? is this just a testing thing? (assoc :response {:status 401 :headers {} :body "Unauthorized"})) ))}))0.0s
So this is what the signed token will look like that we pass back to an authenticated user
(def freds-token (-> {:username "fred" :email "fred.flintstone@example.com" :roles {"user" "toon"}} (jwt/sign jws-secret)))freds-token0.0s
(-> (login-handler {:json-params {:username "joe" :password "so-cool"}}) :body)0.6s
;; same as above(defn login-handler [request] (let [{username :username password :password} (:json-params request)] (if-let [user-info (check-user-password username password)] (let [token (jwt/sign user-info jws-secret)] {:status 200 :body (assoc user-info :token token) ; not sure this header does anything here: ; :headers {"Authorization" (str "Token " token)} }) {:body "user/password combination not found " :status 401})))0.0s
(def response (login-handler {:json-params {:username "joe" :password "so-cool"}}))0.7s
(-> response (get-in [:body :token]) (jwt/unsign jws-secret))0.0s
(require [io.pedestal.interceptor.chain :as chain])0.0s
;; empty request 401 unauthorized(-> {:request {}} (chain/execute [token-authorization]) :response print)0.3s
;; bad token(-> {:request {:headers {:authorization "Bearer of-ill-will"}}} (chain/execute [token-authorization]) :response print)0.4s
;; good token (from above) adds identity(-> {:request {:headers {:authorization (str "Token " freds-token)}}} (chain/execute [(token-authentication token-backend)]) print)0.3s
;; same good token with the token-authorization intercepor (which passes)(-> {:request {:headers {:authorization "Token eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImZyZWQiLCJlbWFpbCI6ImpvZS5jb29sQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidXNlciIsInRvb24iXX0.Y7w1rAtB6WDWpnsSX9yYJubxcC52M01u42Y-wCkykLU"}}} (chain/execute [(token-authentication token-backend) token-authorization]) print)0.4s
`token-authorization` just checks with `authenticated?` with only checks for for `:identity` . Let's add an interceptor appropriate for all points that would require access to iex (or quandl or Bloomberg or any other protected data).
(def iex-authorization "echeck whether " (interceptor ;; should this be using the IAuthorization -handle-unauthenticated method? {:name ::token-authorization :enter (fn [{:keys [request] :as ctx}] (if (get-in request [:identity :iex-public-key]) ctx (if (authenticated? request) (assoc ctx :response {:status 403 :headers {} :body "Unauthorized for IEX"}) (assoc ctx :response {:status 401 :headers {} :body "Unauthenticated"}))))}))0.0s
(-> {:request {:headers {}}} (chain/execute [(token-authentication token-backend) iex-authorization]) print)0.4s
(-> {:request {:headers {:authorization "Token eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImZyZWQiLCJlbWFpbCI6ImpvZS5jb29sQGV4YW1wbGUuY29tIiwicm9sZXMiOlsidXNlciIsInRvb24iXX0.Y7w1rAtB6WDWpnsSX9yYJubxcC52M01u42Y-wCkykLU"}}} (chain/execute [(token-authentication token-backend) iex-authorization]) print)0.4s
(-> {:request {:headers {:authorization "Token eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImpvZSIsImVtYWlsIjoiam9lLmNvb2xAZXhhbXBsZS5jb20iLCJpZXgtcHVibGljLWtleSI6InNvbWUtZmFuY3ktdGhpbmctdGhhdC1lbmFibGVzLWlleC1hY2Nlc3MiLCJyb2xlcyI6WyJhZG1pbiIsInVzZXIiXX0.ojkjMCRZaGWJcMARdWo_gToVKg00FoNMgQX93wQIwAU"}}} (chain/execute [(token-authentication token-backend) iex-authorization]) print)0.4s
0.0s