Created
March 26, 2021 11:05
-
-
Save joelkuiper/8ebaf2a4ffebac071b3b7614ceae0249 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
(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