Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Experimenting with auth0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ns practicalli.authorisation-tools
(:require [clj-http.client :as http]
[muuntaja.core :as transformer]))
(defn auth0-m2m-token
"Obtain a token from auth0 using the developer account credentials and test application client id"
[{:keys [auth0-client-id auth-client-secret]}]
(->> {:content-type :json
:cookie-policy :standard
:body
(transformer/encode
"application/json"
{:audience "https://api.practicalli.com"
:client_id auth0-client-id
:client_secret auth-client-secret
:grant_type "client_credentials"})}
(http/post "https://statsbomb.eu.auth0.com/oauth/token")
(transformer/decode-response-body)
:access_token))
(comment
;; Set Environment variables first (restart REPL to pick up new values)
;; TODO: consider add client credentials to Integrant configuration
;; Client Credentials flow Environment Variables
(System/getenv "SERVICE_AUTH0_CLIENT_ID")
(System/getenv "SERVICE_AUTH0_CLIENT_SECRET")
;; Generate Token
(auth0-m2m-token {:auth0-client-id (System/getenv "SERVICE_AUTH0_CLIENT_ID")
:auth-client-secret (System/getenv "SERVICE_AUTH0_CLIENT_SECRET")})
;; Payload from token - expected structure
;; {"https://practical.li/accountID": "test-account", ;; M2M Client metadata to represent Practicalli customer
;; "iss": "https://practicalli.eu.auth0.com/", ;; Issuer of the token - Practicalli Auth0 tenent
;; "sub": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@clients", ;; Subject - who the token refers to - Auth0 M2M Client_ID
;; "aud": "https://api.practicalli.com", ;; Audience - Practicalli generic API reference
;; "iat": 1627568884, ;; Issued at: seconds since Unix epoch
;; "exp": 1627655284, ;; Expiration time: seconds since Unix epoch
;; "azp": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", ;; Authorised Party: party to which token was issued - Auth0 M2M Client
;; "gty": "client-credentials"} ;; Grant type: M2M Clients use client_credentials
#_())
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Decoding the access token
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(comment
(require '[ring.middleware.jwt :as json-web-token])
;; ring.midleware.jwt requires a handler, so lets create a dummy one
(def ^:private dummy-handler (constantly identity))
(defn decode-token
[token]
(json-web-token/wrap-jwt
dummy-handler {:alg :RS256
:jwk-endpoint "https://practicalli.eu.auth0.com/.well-known/jwks.json"}))
(decode-token (auth0-m2m-token))
#_())
{:paths
["src" "resources"]
:deps
{org.clojure/clojure {:mvn/version "1.10.3"}
;; Web Application
ring/ring-core {:mvn/version "1.9.3"}
ring/ring-jetty-adapter {:mvn/version "1.9.3"}
ring/ring-mock {:mvn/version "0.4.0"}
metosin/reitit {:mvn/version "0.5.13"}
;; Authorisation
clj-http/clj-http {:mvn/version "3.10.0"}
ovotech/ring-jwt {:mvn/version "1.3.0"}
;; Logging
com.brunobonacci/mulog {:mvn/version "0.8.0"}
;; suppress default logger warning from Jetty
org.slf4j/slf4j-nop {:mvn/version "1.7.32"}
;; System
aero/aero {:mvn/version "1.1.6"}
integrant/integrant {:mvn/version "0.8.0"}
integrant/repl {:mvn/version "0.3.2"}
;; Persistence
com.taoensso/faraday {:mvn/version "1.11.1"}
;; Entitlement Ids
nano-id/nano-id {:mvn/version "1.0.0"}
;; Convert Clojure entitlement data to JSON
org.clojure/data.json {:mvn/version "2.4.0"}
;; Prometheus Metrics endpoint
clj-commons/iapetos {:mvn/version "0.1.11"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.10.0"}}
}
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Handler functions and helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ns practicalli.handlers
(:require
[practicalli.persistence :as persistence]
[com.brunobonacci.mulog :as mulog]))
(defn account-id-from-claims [claims]
;; The claims object has string keys. As this accountID key is a URL, it's not
;; so nice to keywordify it...
(get claims "https://statsbomb.com/accountID"))
(defn response-401
"Build a 401 UNAUTHORIZED response with an error message."
[message]
{:status 401
:body {:errors [{:message message}]}})
(defn entitlement-response
"Generate the service response, based on entitlemet in the database
'N0tF0und1d' is used to prevent a nil value in the entitlements query"
[persistence]
(fn [{:keys [claims] :as request}]
(let [account-id (or (account-id-from-claims claims) "N0tF0und1d") ;; check value added to auth0 customer account in auth0
_ (mulog/log ::entitlement-response :account-id account-id :request request)]
(cond
;; 401 when the JWT can't be resolved to a valid claims object (expired or broken)
(empty? claims) (response-401 bad-token-message)
;; Return Customer access
:else (response-customer-authorised persistence account-id)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Middleware for the Entitlements Service
;;
;; All the wrap middleware functions for the service
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(ns practicalli.middleware
(:require
[ring.middleware.jwt :as json-web-token]
[com.brunobonacci.mulog :as mulog]))
;; All requests to the entitlement service should contain a JSON Web Token
;; obtained by customer secret from auth0
;; wrap-auth0
;; https://github.com/ovotech/ring-jwt
;; Search for a JWT token on each incoming request.
;; By default, parse the bearer token from Authorization HTTP header (overridden using the find-token-fn setting)
;; Associates claims found in the token with a :claims key on the incoming request (adds empty :claims map to request if no token found)
;; Responds with 401 if
;; - JWS signature in token cannot be verified.
;; - token has expired (i.e. the exp claim indicates a time in the past) - leeway-seconds setting defines time past the claim expiry
;; - token will only be active in the future (i.e. the nbf claim indicates a time in the future) - leeway-seconds setting can be used with this check
(def wrap-auth0
{:name ::auth0
:description "Middleware for auth0 authentication and authorization"
:wrap (fn [handler]
(json-web-token/wrap-jwt
handler
{:alg :RS256
:jwk-endpoint "https://auth.statsbomb.com/.well-known/jwks.json"}))})
;;;; Logging middleware
;; https://github.com/BrunoBonacci/mulog/blob/master/doc/ring-tracking.md
(defn wrap-trace-events
"Log event trace for each api event with mulog/log."
[handler id]
(fn [request]
;; Add context of each request to all trace events generated for the specific request
;; TODO: consider capturing the accountID value
(mulog/with-context
{:uri (get request :uri)
:request-method (get request :request-method)}
;; track the request duration and outcome
(mulog/trace :io.redefine.datawarp/http-request
;; add key/value pairs for tracking event only
{:pairs [:content-type (get-in request [:headers "content-type"])
:content-encoding (get-in request [:headers "content-encoding"])
:middleware id]
;; capture http status code from the response
:capture (fn [{:keys [status]}] {:http-status status})}
;; call the request handler
(handler request)))))
(ns practicalli.routes
(:require [practicalli.handlers :as handlers]
[practicalli.middleware :as middleware]
[com.brunobonacci.mulog :refer [log]]))
(defn routes
[environment]
;; DynamoDB connection from Integrant state
(let [persistence (-> environment :persistence :connection)
_ (log ::route-config)]
["/auth0-webhook"
["/v1"
{:swagger {:tags ["Use auth0 tokens"]
:description "Return service access based on auth0 token"}
:middleware [[middleware/wrap-auth0]]}
["/live-data" {:get {:summary "Customer access to API"
:description ""
:handler (handlers/entitlement-response persistence)}}]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Testing endpoints
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
["/testing"
["/live-data-mock"
{:get {:summary "Customer Entitlements to Live Data API"
:description ""
:handler handlers/mock-entitlement-response}}]
["/echo-request"
{:get {:summary "Echo the request which should include the auth0 claim"
:description "Debug issues with authentication by returning the request after it has been updated by the wrap-auth0 middleware"
:handler handlers/echo-request}}]
["/echo-request-claims"
{:get {:summary "Echo claims part of the request which should include the auth0 claim"
:description "Debug issues with authentication by returning the request after it has been updated by the wrap-auth0 middleware"
:handler handlers/echo-request-claims}}]
["/echo-account-id"
{:get {:summary "Echo claims part of the request which should include the auth0 claim"
:description "Debug issues with authentication by returning the request after it has been updated by the wrap-auth0 middleware"
:handler handlers/echo-request-account-id}}]]]]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment