Skip to content

Instantly share code, notes, and snippets.

@joelkuiper
Created March 26, 2021 11:05
Show Gist options
  • Save joelkuiper/8ebaf2a4ffebac071b3b7614ceae0249 to your computer and use it in GitHub Desktop.
Save joelkuiper/8ebaf2a4ffebac071b3b7614ceae0249 to your computer and use it in GitHub Desktop.
(ns app.keycloak
(:require
[app.config :as config]
[clj-http.client :as client]
[taoensso.timbre :refer [info debugf infof]]
[expiring-map.core :as em]
[buddy.auth.protocols :as proto]
[buddy.auth.http :as http]
[buddy.auth :refer [authenticated?]]
[buddy.core.codecs :as codecs]
[buddy.core.nonce :refer [random-nonce]]
[buddy.auth.middleware :as buddy-auth-middleware]
[ring.util.request :refer [request-url]]
[ring.util.http-response :as resp]
[keycloak.deployment :as kc-deploy])
(:import [org.keycloak.adapters KeycloakDeployment]
[org.keycloak.representations AccessToken]
[org.keycloak RSATokenVerifier]
[org.keycloak.common.util KeycloakUriBuilder]
[org.keycloak.constants ServiceUrlConstants]
[java.net URLEncoder]))
(def kc-token "X-Authorization-Token")
(defn token-from-cookie
[req]
(get-in req [:cookies kc-token :value]))
(defn token-from-headers
[req]
(get-in req [:headers kc-token]))
(defn request-token
[req]
(or (token-from-headers req)
(token-from-cookie req)))
(def kc-cfg
(get-in config/config [:auth :api]))
(def kc-deployment
(kc-deploy/deployment
(kc-deploy/client-conf kc-cfg)))
(defn verify
([token]
(verify kc-deployment token))
([^KeycloakDeployment deployment ^String token]
(let [kid (get-in config/config [:auth :kid])
public-key (.getPublicKey (.getPublicKeyLocator deployment) kid deployment)]
(RSATokenVerifier/verifyToken token public-key (.getRealmInfoUrl deployment)))))
(defn unexceptional-verify
[token]
(try
(verify token)
(catch Exception _ nil)))
(defn extract
"return a map with keys with values extracted from the Keycloak access token"
[^AccessToken access-token]
{:username (.getPreferredUsername access-token)
:id (.getId access-token)
:email (.getEmail access-token)
:roles (set (map keyword (.getRoles (.getRealmAccess access-token))))})
(defn kc-backend
[& [{:keys [unauthorized-handler authfn] :or {authfn identity}}]]
(reify
proto/IAuthentication
(-parse [_ request]
(request-token request))
(-authenticate [_ request data]
(authfn data))
proto/IAuthorization
(-handle-unauthorized [_ request metadata]
(if unauthorized-handler
(unauthorized-handler request metadata)
(if (authenticated? request)
(http/response "Permission denied" 403)
(http/response "Unauthorized" 401))))))
(defn ->obj-array
[val]
(into-array Object [val]))
(defn nonce
[]
(codecs/bytes->hex (random-nonce 32)))
(defn login-redirect-uri
[state redirect]
(let [base-auth-url (.getAuthServerBaseUrl ^KeycloakDeployment kc-deployment)
auth-url (-> (KeycloakUriBuilder/fromUri ^String base-auth-url)
(.path ServiceUrlConstants/AUTH_PATH)
(.build (->obj-array (.getRealm ^KeycloakDeployment kc-deployment)))
(.toString))
query-string (client/generate-query-string
{:client_id (:client-id kc-cfg)
:response_type "code"
:redirect_uri redirect
:state state
:nonce (nonce)})]
(str auth-url "?" query-string)))
(defn callback-url
[request]
(str (-> request :scheme name)
"://"
(get-in request [:headers "host"])
"/auth/callback"
"?origin=" (URLEncoder/encode (request-url request) "UTF-8")))
(def redirect-state (em/expiring-map 30))
(defn redirect-unauthorized
[handler]
(fn [request]
(let [redirect-to (callback-url request)
token (request-token request)
state (nonce)]
(em/assoc! redirect-state state redirect-to)
(if (and token (unexceptional-verify token))
(handler request)
(http/redirect (login-redirect-uri state redirect-to))))))
(defn get-token
[session_state code redirect-uri]
(let [params {:headers {"Content-Type" "application/x-www-form-urlencoded"}
:basic-auth [(:client-id kc-cfg) (:client-secret kc-cfg)]
:as :json
:form-params
{:grant_type "authorization_code"
:code code
:state session_state
:redirect_uri redirect-uri
:client_id (:client-id kc-cfg)}}
url (.getTokenUrl ^KeycloakDeployment kc-deployment)]
(client/post url params)))
(defn exchange-token
[request]
(let [{:strs [code state session_state origin]} (:query-params request)
redirect (get redirect-state state)
token (get-token session_state code redirect)]
(if-let [access-token (get-in token [:body :access_token])]
{:status 302
:body ""
:headers {"Location" origin}
:cookies {kc-token
{:path "/"
:max-age 3600
:value access-token}}}
(resp/unauthorized {:error "Not authorized"}))))
;; Middleware
(defn authentication
"Middleware used on routes requiring authentication."
[handler]
(buddy-auth-middleware/wrap-authentication
handler
(kc-backend {:authfn unexceptional-verify})))
(defn authorization
"Middleware used on routes requiring authorization.
Adds user info to the request"
[handler]
(fn [request]
(if (authenticated? request)
(try
(let [access-token (verify (request-token request))
user-info (extract access-token)]
(handler (-> request (assoc :user-info user-info))))
(catch Exception _ (resp/unauthorized {:error "Not authorized"})))
(resp/unauthorized {:error "Not authorized"}))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment