Learn how to separate identity proof from access decisions in Clojure, and how to design sessions, tokens, and policy checks without collapsing them into one security layer.
Authentication answers “who is this caller?” Authorization answers “what may this caller do?” Good Clojure systems keep those concerns adjacent but distinct. A user may be authenticated and still not be allowed to read a record, invoke an admin action, or mutate another tenant’s data.
In Clojure web systems, the cleanest design is usually:
That is more robust than burying everything inside a single “security middleware” abstraction that nobody can reason about under incident pressure.
Authentication is about establishing a principal:
That proof may come from:
The output of authentication should be a small identity shape that the rest of the application can trust:
1(defn request-identity [user]
2 {:user/id (:user/id user)
3 :tenant/id (:tenant/id user)
4 :roles (:roles user)
5 :permissions (:permissions user)})
Notice what is not here:
An identity should be compact, reviewable, and safe to attach to the request.
Authorization is the decision layer. It answers questions such as:
That means authorization usually depends on more than a role name. Good checks often combine:
OWASP’s current authorization guidance emphasizes deny-by-default behavior, validating permissions on every request, and preferring richer models than flat role checks when the business rules demand it.
The request pipeline often has one middleware that resolves identity and attaches it to the request map:
1(defn wrap-authentication [handler lookup-session]
2 (fn [request]
3 (let [session-token (get-in request [:cookies "session" :value])
4 identity (some-> session-token lookup-session :identity)]
5 (handler (cond-> request
6 identity (assoc :identity identity))))))
Then the protected route or domain service performs an explicit decision:
1(defn can-view-project? [identity project]
2 (or (contains? (:roles identity) :admin)
3 (= (:tenant/id identity) (:tenant/id project))
4 (contains? (:project/member-ids project) (:user/id identity))))
5
6(defn show-project [request project]
7 (if (can-view-project? (:identity request) project)
8 {:status 200
9 :body project}
10 {:status 403
11 :body {:error :forbidden}}))
That structure scales better than hiding every permission rule inside session middleware.
Do not collapse these options into one generic “login” concept:
That distinction matters. A common mistake is saying “we use OAuth for login” when the actual identity layer is OIDC on top of OAuth flows.
If your system handles passwords directly:
OWASP’s current authentication guidance treats MFA as one of the strongest defenses against password attacks, but it still belongs inside a broader design that includes session rotation, token invalidation, and sensible audit logging.
The most common authorization bug is not “role handling failed” in the abstract. It is something more concrete:
That is why authorization must run on every relevant request and at the backend boundary that actually controls the resource.
Being logged in is not the same as being allowed.
Sessions should represent identity and current session state, not every business decision the application might ever need.
Role names are often too broad once systems become tenant-aware, resource-aware, or workflow-aware.
Changing password, rotating MFA, or updating payout details should not rely on the existence of any old active session alone.
Establish identity once, represent it cleanly on the request, and keep authorization explicit near the protected action. Prefer simple server-managed sessions for ordinary browser apps, use external identity providers when federation or central identity management is the real requirement, and treat MFA and reauthentication as controls for higher-risk actions rather than as afterthoughts. In Clojure, the goal is not a giant security framework. It is a small set of clear boundaries that remain understandable during audits and incidents.