Add Buddy example

This commit is contained in:
Valtteri Harmainen 2020-07-11 13:35:43 +03:00
parent 56cbd67abd
commit d3c21e3c6c
4 changed files with 278 additions and 0 deletions

11
examples/buddy-auth/.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/

View file

@ -0,0 +1,16 @@
# Buddy auth example
A Sample project with Buddy Authentication.
## Usage
```clj
> lein repl
(start)
```
See annotated example in [server.clj](src/example/server.clj) file.
## License
Copyright © 2017-2018 Metosin Oy

View file

@ -0,0 +1,7 @@
(defproject ring-example "0.1.0-SNAPSHOT"
:description "Reitit Buddy Auth App"
:dependencies [[org.clojure/clojure "1.10.0"]
[ring/ring-jetty-adapter "1.7.1"]
[metosin/reitit "0.5.3"]
[buddy "2.0.0"]]
:repl-options {:init-ns example.server})

View file

@ -0,0 +1,244 @@
(ns example.server
"This example demonstrates how to use Buddy authentication with Reitit
to implement simple authentication and authorization flows.
HTTP Basic authentication is used to authenticate with username and
password. HTTP Basic authentication middleware checks credentials
against a 'database' and if credentials are OK, a signed jwt-token
is created and returned. The token can be used to call endpoints
that require token authentication. Token payload contains users
roles that can be used for authorization.
NOTE: This example is not production-ready."
(:require [buddy.auth :as buddy-auth]
[buddy.auth.backends :as buddy-auth-backends]
[buddy.auth.backends.httpbasic :as buddy-auth-backends-httpbasic]
[buddy.auth.middleware :as buddy-auth-middleware]
[buddy.hashers :as buddy-hashers]
[buddy.sign.jwt :as jwt]
[muuntaja.core :as m]
[reitit.ring :as ring]
[reitit.ring.coercion :as coercion]
[reitit.ring.middleware.muuntaja :as muuntaja]
[ring.adapter.jetty :as jetty]
[ring.middleware.params :as params]))
(def db
"We use a simple map as a db here but in real-world you would
interface with a real data storage in `basic-auth` function."
{"user1"
{:id 1
:password (buddy-hashers/encrypt "kissa13")
:roles ["admin" "user"]}
"user2"
{:id 2
:password (buddy-hashers/encrypt "koira12")
:roles ["user"]}})
(def private-key
"Used for signing and verifying JWT-tokens In real world you'd read
this from an environment variable or some other configuration that's
not included in the source code."
"kana15")
(defn create-token
"Creates a signed jwt-token with user data as payload.
`valid-seconds` sets the expiration span."
[user & {:keys [valid-seconds] :or {valid-seconds 7200}}] ;; 2 hours
(let [payload (-> user
(select-keys [:id :roles])
(assoc :exp (.plusSeconds
(java.time.Instant/now) valid-seconds)))]
(jwt/sign payload private-key {:alg :hs512})))
(def token-backend
"Backend for verifying JWT-tokens."
(buddy-auth-backends/jws {:secret private-key :options {:alg :hs512}}))
(defn basic-auth
"Authentication function called from basic-auth middleware for each
request. The result of this function will be added to the request
under key :identity.
NOTE: Use HTTP Basic authentication always with HTTPS in real setups."
[db request {:keys [username password]}]
(let [user (get db username)]
(if (and user (buddy-hashers/check password (:password user)))
(-> user
(dissoc :password)
(assoc :token (create-token user)))
false)))
(defn create-basic-auth-backend
"Creates basic-auth backend to be used by basic-auth-middleware."
[db]
(buddy-auth-backends-httpbasic/http-basic-backend
{:authfn (partial basic-auth db)}))
(defn create-basic-auth-middleware
"Creates a middleware that authenticates requests using http-basic
authentication."
[db]
(let [backend (create-basic-auth-backend db)]
(fn [handler]
(buddy-auth-middleware/wrap-authentication handler backend))))
(defn token-auth-middleware
"Middleware used on routes requiring token authentication."
[handler]
(buddy-auth-middleware/wrap-authentication handler token-backend))
(defn admin-middleware
"Middleware used on routes requiring :admin role."
[handler]
(fn [request]
(if (-> request :identity :roles set (contains? "admin"))
(handler request)
{:status 403 :body {:error "Admin role required"}})))
(defn auth-middleware
"Middleware used in routes that require authentication. If request is
not authenticated a 401 unauthorized response will be
returned. Buddy checks if request key :identity is set to truthy
value by any previous middleware."
[handler]
(fn [request]
(if (buddy-auth/authenticated? request)
(handler request)
{:status 401 :body {:error "Unauthorized"}})))
(def routes
[["/no-auth"
[""
{:get (fn [_] {:status 200 :body {:message "No auth succeeded!"}})}]]
["/basic-auth"
[""
{:middleware [(create-basic-auth-middleware db) auth-middleware]
:get
(fn [req]
{:status 200
:body
{:message "Basic auth succeeded!"
:user (-> req :identity)}})}]]
["/token-auth"
[""
{:middleware [token-auth-middleware auth-middleware]
:get (fn [_] {:status 200 :body {:message "Token auth succeeded!"}})}]]
["/token-auth-with-admin-role"
[""
{:middleware [token-auth-middleware
auth-middleware
admin-middleware]
:get (fn [_] {:status 200 :body {:message "Token auth with admin role succeeded!"}})}]]])
(def app
(ring/ring-handler
(ring/router
routes
{:data
{:muuntaja m/instance
:middleware ; applied to all routes
[params/wrap-params
muuntaja/format-middleware
coercion/coerce-exceptions-middleware
coercion/coerce-request-middleware
coercion/coerce-response-middleware]}})
(ring/create-default-handler)))
(defn start []
(jetty/run-jetty #'app {:port 3000, :join? false})
(println "server running in port 3000"))
(comment
;; Start server to try with real HTTP clients.
(start)
;; ...or just execute following sexps in the REPL. :)
(def headers {"accept" "application/edn"})
(def read-body (comp read-string slurp :body))
(-> {:headers headers :request-method :get :uri "/no-auth"}
app
read-body)
;; => {:message "No auth succeeded!"}
(-> {:headers headers :request-method :get :uri "/basic-auth"}
app
read-body)
;; => {:error "Unauthorized"}
(-> {:headers headers :request-method :get :uri "/token-auth"}
app
read-body)
;; => {:error "Unauthorized"}
(import java.util.Base64)
(defn ->base64
"Encodes a string as base64."
[s]
(.encodeToString (Base64/getEncoder) (.getBytes s)))
(defn basic-auth-headers [user pass]
(merge headers {:authorization (str "Basic " (->base64 (str user ":" pass)))}))
(def bad-creds (basic-auth-headers "juum" "joo"))
(-> {:headers bad-creds :request-method :get :uri "/basic-auth"}
app
read-body)
;; => {:error "Unauthorized"}
(def admin-creds (basic-auth-headers "user1" "kissa13"))
(-> {:headers admin-creds :request-method :get :uri "/basic-auth"}
app
read-body)
;; {:message "Basic auth succeeded!",
;; :user
;; {:id 1,
;; :roles [:admin :user],
;; :token
;; "eyJhbGciOiJIUzUxMiJ9.eyJp....."
(def admin-token
(-> {:headers admin-creds :request-method :get :uri "/basic-auth"}
app
read-body
:user
:token))
(def user-creds (basic-auth-headers "user2" "koira12"))
(def user-token
(-> {:headers user-creds :request-method :get :uri "/basic-auth"}
app
read-body
:user
:token))
(defn token-auth-headers [token]
(merge headers {:authorization (str "Token " token)}))
(def user-token-headers (token-auth-headers user-token))
(-> {:headers user-token-headers :request-method :get :uri "/token-auth"}
app
read-body)
;; => {:message "Token auth succeeded!"}
(-> {:headers user-token-headers :request-method :get :uri "/token-auth-with-admin-role"}
app
read-body)
;; => {:error "Admin role required"}
(def admin-token-headers (token-auth-headers admin-token))
(-> {:headers admin-token-headers :request-method :get :uri "/token-auth-with-admin-role"}
app
read-body)
;; => {:message "Token auth with admin role succeeded!"}
)