Coast does not have authentication built in. It's up to you to determine how you want to authenticate people.
Typically for clojure database-backed web applications, there are a few options:
Let's get into it. In the clojure world, there are two popular web application authentication libraries
Coast lends itself more to buddy, so that's what this guide covers.
Buddy is composed of several different libraries:
buddy-core
buddy-hashers
buddy-sign
buddy-auth
Setting up authentication middleware with Coast only really needs buddy-hashers
, so that's the library we'll choose
Here's how to set up buddy for use with a Coast application
Install the buddy-hashers
dependency in your deps.edn
file
; deps.edn
{; other keys not shown
:deps
{org.clojure/clojure {:mvn/version "1.9.0"}
coast-framework/coast.theta {:mvn/version "1.4.0"}
org.xerial/sqlite-jdbc {:mvn/version "3.25.2"}
buddy/buddy-hashers {:mvn/version "1.3.0"}}}
You can see the full documentation of buddy-hashers here, this guide summarizes basic usage:
(ns some-ns
(:require [buddy.hashers :as hashers]))
(hashers/derive "secretpassword")
;; => "bcrypt+sha512$4i9sd34m..."
(hashers/check "secretpassword" "bcrypt+sha512$4i9sd34m...")
;; => true
Buddy uses the bcrypt + sha512 algorithm by default.
The first step into integrating the buddy-hashers
library into a Coast application is to create six handlers:
Let's start with the auth middleware, that checks that a session exists before continuing:
(ns middleware
(:require [coast]))
(defn auth [handler]
(fn [request]
(if (some? (get-in request [:session :member/email))
(handler request)
(coast/unauthorized "HAL9000 says: I'm sorry Dave, I can't let you do that"))))
...and the routes:
; src/routes.clj
(ns routes
(:require [coast]
[middleware]))
(def routes
(coast/site
[:get "/sign-up" :member/build]
[:post "/members" :member/create]
[:get "/sign-in" :session/build]
[:post "/sessions" :session/create]
(coast/with middleware/auth
[:get "/dashboard" :member/dashboard]
[:delete "/sessions" :sessions/delete])))
Now create three new handler functions build
, create
and dashboard
in the src/member.clj
file:
; src/member.clj
(ns member
(:require [coast]
[buddy.hashers :as hashers]))
(defn build [request])
(defn create [request])
(defn dashboard [request])
Create a simple, unstyled form in the build
function so people can enter an email and a password:
; src/member.clj
(defn build [request]
(coast/form-for ::create
[:input {:type "text" :name "member/email"}]
[:input {:type "password" :name "member/password"}]
[:input {:type "submit" :value "Submit"}]))
And fill in the create
function to handle the submission of that form:
; src/member.clj
(defn create [request]
(let [[_ errors] (-> (:params request)
(select-keys [:member/email :member/password])
(coast/validate [[:email [:member/email]
[:required [:member/email :member/password]]]])
(update :member/password hashers/derive)
(coast/rescue))]
(if (some? errors)
(build (merge errors request))
(-> (coast/redirect-to ::dashboard)
(assoc :session (select-keys (:params request) [:member/email]))))))
NOTE: Two colons ::
in front of a keyword means use the namespace as the current namespace of the file, in this case ::member
really means :member/index
.
:member/email
and ::email
in the member namespace are equivalent.
Now fill in the dashboard
function with a simple message and a sign out link (which is an actual form):
(defn dashboard [request]
[:div
[:h1 "You're signed in! Welcome!"]
(coast/form-for :session/delete
[:input {:type "submit" :value "Sign out"}])
At this point we've handled a very simple whole sign up flow, minus the database migrations.
Next let's get sign in and sign out working:
Create a new file in src
named session.clj
; src/session.clj
(ns session
(:require [coast]))
[buddy.hashers :as hashers]
(defn build [request])
(defn create [request])
(defn delete [request])
Now let's fill in the handlers to show the sign in form:
(defn build [request]
[:div
(when (some? (:error/message request))
[:div (:error/message request)])
(coast/form-for ::build
[:input {:type "text" :name "member/email"}]
[:input {:type "password" :name "member/password"}]
[:input {:type "submit" :value "Submit"}])])
...and the form submission
(defn create [request]
(let [email (get-in request [:params :member/email])
member (coast/find-by :member {:email email})
[valid? errors] (-> (:params request)
(select-keys [:member/email :member/password])
(coast/validate [[:email [:member/email]
[:required [:member/email :member/password]]]]) ; these three lines could be middleware
(get :member/password) ; this returns the plaintext password from the params map
(hashers/check (:member/password member)) ; hashers/check is here
(coast/rescue))]
(if (or (some? errors)
(false? valid?))
(build (merge errors request {:error/message "Invalid email or password"}))
(-> (coast/redirect-to ::dashboard)
(assoc :session (select-keys (:params request) [:member/email]))))))
Notice the use of hashers/check
to check the plaintext password from the form against the
password from the existing hashed password in the database.
...and finally the sign out handler
(defn delete [request]
(-> (coast/redirect-to ::build)
(assoc :session nil)))
This is not the only way to implement authentication in your Coast app, but it is a complete example of one way of doing authentication.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close