Design authentication, authorization, secret handling, and audit boundaries deliberately so Clojure integrations stay secure without turning middleware into policy chaos.
Security in a Clojure system is not one middleware decision. It is a set of boundaries: identity verification, authorization, secret handling, transport protection, and auditability. The implementation can involve libraries such as Buddy, Ring middleware, OAuth/OIDC providers, and JVM crypto tools, but the deeper design question is always the same: where does trust enter the system, and how is it constrained after that point?
Teams often mix these two ideas together.
If those boundaries blur, handlers become full of ad hoc if checks and security rules leak into unrelated business code.
flowchart LR
REQ["Incoming request"] --> AUTHN["Authentication"]
AUTHN --> ID["Identity on request"]
ID --> AUTHZ["Authorization policy"]
AUTHZ --> HANDLER["Business handler"]
HANDLER --> AUDIT["Audit / security events"]
The main design goal is to make identity explicit on the request map and to keep policy checks close to the route or use case that needs them.
For username/password flows, store password hashes, not encrypted passwords.
1(ns myapp.auth.passwords
2 (:require [buddy.hashers :as hashers]))
3
4(defn hash-password [raw-password]
5 (hashers/derive raw-password))
6
7(defn valid-password? [raw-password stored-hash]
8 (hashers/check raw-password stored-hash))
That gives you slow password hashing designed for credential storage. Application encryption and password hashing solve different problems and should not be treated as interchangeable.
Clojure applications often sit behind Ring-style middleware, which makes it natural to attach authenticated identity to the request. Whether you use session cookies, signed tokens, or delegated identity from an external provider, the same rule applies: validate before trusting.
1(ns myapp.auth.tokens
2 (:require [buddy.sign.jwt :as jwt]))
3
4(def signing-secret "replace-with-secret-from-env")
5
6(defn issue-token [identity]
7 (jwt/sign {:sub (:user-id identity)
8 :roles (:roles identity)}
9 signing-secret))
10
11(defn authenticate-token [token]
12 (try
13 (jwt/unsign token signing-secret)
14 (catch Exception _
15 nil)))
Important checks usually include:
Authorization is safer when the rule is readable and easy to test. For example, route-level access control can be expressed as small policy predicates.
1(ns myapp.authz)
2
3(defn has-role? [request role]
4 (contains? (set (get-in request [:identity :roles])) role))
5
6(defn require-role [handler role]
7 (fn [request]
8 (if (has-role? request role)
9 (handler request)
10 {:status 403
11 :body "Forbidden"})))
This is intentionally simple. The point is not to build a clever authorization DSL before the team even understands its policy surface.
Most real breaches do not come from a missing if. They come from leaked secrets, weak operational boundaries, or insecure defaults.
Minimum expectations:
Clojure runs on the JVM, so the crypto and TLS story is usually operationally mature. The danger is not missing primitives. The danger is careless integration.
Authentication without audit trails is hard to investigate. Authorization without audit trails is hard to defend.
Useful audit events include:
The logs should capture what happened without leaking credential material or secrets.
Security is a system property. Libraries help, but they do not choose the trust boundaries for you.