mirror of
https://github.com/metosin/reitit.git
synced 2025-12-16 16:01:11 +00:00
Add Buddy example
This commit is contained in:
parent
56cbd67abd
commit
d3c21e3c6c
4 changed files with 278 additions and 0 deletions
11
examples/buddy-auth/.gitignore
vendored
Normal file
11
examples/buddy-auth/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/target
|
||||
/classes
|
||||
/checkouts
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.jar
|
||||
*.class
|
||||
/.lein-*
|
||||
/.nrepl-port
|
||||
.hgignore
|
||||
.hg/
|
||||
16
examples/buddy-auth/README.md
Normal file
16
examples/buddy-auth/README.md
Normal 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
|
||||
7
examples/buddy-auth/project.clj
Normal file
7
examples/buddy-auth/project.clj
Normal 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})
|
||||
244
examples/buddy-auth/src/example/server.clj
Normal file
244
examples/buddy-auth/src/example/server.clj
Normal 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!"}
|
||||
)
|
||||
Loading…
Reference in a new issue