-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
auth: Split auth code into API and utils
- Loading branch information
Showing
8 changed files
with
201 additions
and
127 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,13 @@ | ||
(ns apossiblespace.parts.api.auth | ||
(:require | ||
[buddy.sign.jwt :as jwt] | ||
[buddy.auth :refer [authenticated?]] | ||
[buddy.auth.backends :as backends] | ||
[buddy.auth.middleware :refer [wrap-authentication wrap-authorization]] | ||
[buddy.hashers :as hashers] | ||
[apossiblespace.parts.auth :as auth] | ||
[ring.util.response :as response] | ||
[apossiblespace.parts.db :as db] | ||
[apossiblespace.parts.config :as conf] | ||
[com.brunobonacci.mulog :as mulog]) | ||
(:import | ||
[java.time Instant])) | ||
|
||
(def secret | ||
(conf/jwt-secret (conf/config))) | ||
|
||
(def auth-backend | ||
(backends/jws | ||
{:secret secret | ||
:options {:alg :hs256} | ||
:on-error (fn [_request ex] | ||
(mulog/log ::auth-backend :error (.getMessage ex)) | ||
nil) | ||
:token-name "Bearer" | ||
:auth-fn (fn [claims] | ||
(mulog/log ::auth-backend-auth-fn :claims claims) | ||
claims)})) | ||
|
||
(defn create-token | ||
"Create a JWT token that will expire in 1 hour" | ||
[user-id] | ||
(let [now (Instant/now) | ||
exp (.plusSeconds now 3600) | ||
claims {:iss "http://localhost:3000/api" ;; TODO: Set this from configuration? | ||
:sub user-id | ||
:aud "http://localhost:3000" | ||
:iat (.getEpochSecond now) | ||
:exp (.getEpochSecond exp)}] | ||
(jwt/sign claims secret {:alg :hs256}))) | ||
|
||
(defn hash-password | ||
[password] | ||
(hashers/derive password)) | ||
|
||
(defn check-password | ||
[password hash] | ||
(:valid (hashers/verify password hash))) | ||
|
||
(defn authenticate | ||
"Checks if a user represented by EMAIL exists in db, checks their PASSWORD if so" | ||
[{:keys [email password]}] | ||
(when-let [user (db/query-one (db/sql-format {:select [:*] | ||
:from [:users] | ||
:where [:= :email email]}))] | ||
(when (check-password password (:password_hash user)) | ||
(create-token (:id user))))) | ||
[com.brunobonacci.mulog :as mulog])) | ||
|
||
(defn login | ||
[request] | ||
(let [{:keys [email password]} (:body request)] | ||
(if-let [token (authenticate {:email email :password password})] | ||
(if-let [token (auth/authenticate {:email email :password password})] | ||
(do | ||
(mulog/log ::login :email email :status :success) | ||
(-> (response/response {:token token}) | ||
|
@@ -78,39 +26,3 @@ | |
[_] | ||
(-> (response/response {:message "Logged out successfully"}) | ||
(response/status 200))) | ||
|
||
(defn wrap-jwt-authentication | ||
"Middleware adding JWT authentication to a route" | ||
[handler] | ||
(-> handler | ||
(wrap-authentication auth-backend) | ||
(wrap-authorization auth-backend))) | ||
|
||
(defn jwt-auth | ||
"Middleware ensuring a route is only accessible to authenticated users" | ||
[handler] | ||
(fn [request] | ||
(if (authenticated? request) | ||
(handler request) | ||
(-> (response/response {:error "Unauthorized"}) | ||
(response/status 401))))) | ||
|
||
(defn get-user-id-from-token | ||
[request] | ||
(get-in request [:identity :user-id])) | ||
|
||
(comment | ||
;; Example usage in REPL | ||
(def user {:email "[email protected]" | ||
:username "testuser" | ||
:display-name "Test User" | ||
:password "password123" | ||
:role "client"}) | ||
(register user) | ||
|
||
(def token (authenticate {:email "[email protected]" :password "password123"})) | ||
|
||
(def invalid-token (authenticate {:email "[email protected]" :password "wrongpassword"})) | ||
|
||
(when token (jwt/unsign token secret)) | ||
#_()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
(ns apossiblespace.parts.auth | ||
(:require | ||
[buddy.sign.jwt :as jwt] | ||
[buddy.auth.backends :as backends] | ||
[buddy.hashers :as hashers] | ||
[apossiblespace.parts.db :as db] | ||
[apossiblespace.parts.config :as conf] | ||
[com.brunobonacci.mulog :as mulog]) | ||
(:import | ||
[java.time Instant])) | ||
|
||
(def secret | ||
(conf/jwt-secret (conf/config))) | ||
|
||
(def backend | ||
(backends/jws | ||
{:secret secret | ||
:options {:alg :hs256} | ||
:on-error (fn [_request ex] | ||
(mulog/log ::auth-backend :error (.getMessage ex)) | ||
nil) | ||
:token-name "Bearer" | ||
:auth-fn (fn [claims] | ||
(mulog/log ::auth-backend-auth-fn :claims claims) | ||
claims)})) | ||
|
||
(defn create-token | ||
"Create a JWT token that will expire in 1 hour" | ||
[user-id] | ||
(let [now (Instant/now) | ||
exp (.plusSeconds now 3600) | ||
claims {:iss "http://localhost:3000/api" ;; TODO: Set this from configuration? | ||
:sub user-id | ||
:aud "http://localhost:3000" | ||
:iat (.getEpochSecond now) | ||
:exp (.getEpochSecond exp)}] | ||
(jwt/sign claims secret {:alg :hs256}))) | ||
|
||
(defn hash-password | ||
[password] | ||
(hashers/derive password)) | ||
|
||
(defn check-password | ||
[password hash] | ||
(:valid (hashers/verify password hash))) | ||
|
||
(defn authenticate | ||
"Checks if a user represented by EMAIL exists in db, checks their PASSWORD if so" | ||
[{:keys [email password]}] | ||
(when-let [user (db/query-one (db/sql-format {:select [:*] | ||
:from [:users] | ||
:where [:= :email email]}))] | ||
(when (check-password password (:password_hash user)) | ||
(create-token (:id user))))) | ||
|
||
(defn get-user-id-from-token | ||
[request] | ||
(get-in request [:identity :user-id])) | ||
|
||
(comment | ||
;; Example usage in REPL | ||
(def user {:email "[email protected]" | ||
:username "testuser" | ||
:display-name "Test User" | ||
:password "password123" | ||
:role "client"}) | ||
(register user) | ||
|
||
(def token (authenticate {:email "[email protected]" :password "password123"})) | ||
|
||
(def invalid-token (authenticate {:email "[email protected]" :password "wrongpassword"})) | ||
|
||
(when token (jwt/unsign token secret)) | ||
#_()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
(ns apossiblespace.parts.api.auth-test | ||
(:require [clojure.test :refer [deftest is testing use-fixtures]] | ||
[apossiblespace.parts.auth :as auth-utils] | ||
[apossiblespace.parts.api.auth :as auth] | ||
[apossiblespace.parts.db :as db] | ||
[apossiblespace.parts.entity.user :as user] | ||
[apossiblespace.parts.api.account :as account] | ||
[buddy.sign.jwt :as jwt] | ||
|
@@ -14,8 +14,8 @@ | |
(deftest test-create-token | ||
(testing "create-token generates a valid JWT" | ||
(let [user-id 1 | ||
token (auth/create-token user-id) | ||
secret auth/secret | ||
token (auth-utils/create-token user-id) | ||
secret auth-utils/secret | ||
decoded (jwt/unsign token secret) | ||
now-seconds (.getEpochSecond (Instant/now))] | ||
(is (= user-id (:sub decoded))) | ||
|
@@ -25,38 +25,38 @@ | |
(deftest test-hash-password | ||
(testing "hash-password creates a valid hash" | ||
(let [password "secret123" | ||
hash (auth/hash-password password)] | ||
hash (auth-utils/hash-password password)] | ||
(is (not= password hash)) | ||
(is (auth/check-password password hash))))) | ||
(is (auth-utils/check-password password hash))))) | ||
|
||
(deftest test-check-password | ||
(testing "check-password validates correct password" | ||
(let [password "correct-password" | ||
hash (auth/hash-password password)] | ||
(is (auth/check-password password hash)))) | ||
hash (auth-utils/hash-password password)] | ||
(is (auth-utils/check-password password hash)))) | ||
|
||
(testing "check-password rejects incorrect password" | ||
(let [password "correct-password" | ||
hash (auth/hash-password password)] | ||
(is (not (auth/check-password "wrong-password" hash)))))) | ||
hash (auth-utils/hash-password password)] | ||
(is (not (auth-utils/check-password "wrong-password" hash)))))) | ||
|
||
(deftest test-authenticate | ||
(testing "authenticate succeeds with correct credentials" | ||
(let [user-data (factory/create-test-user) | ||
{:keys [email password]} user-data] | ||
(user/create! user-data) | ||
(let [token (auth/authenticate {:email email :password password})] | ||
(let [token (auth-utils/authenticate {:email email :password password})] | ||
(is (string? token)) | ||
(is (jwt/unsign token auth/secret))))) | ||
(is (jwt/unsign token auth-utils/secret))))) | ||
|
||
(testing "authenticate fails with incorrect password" | ||
(let [user-data (factory/create-test-user) | ||
{:keys [email]} user-data] | ||
(user/create! user-data) | ||
(is (nil? (auth/authenticate {:email email :password "wrongpassword"}))))) | ||
(is (nil? (auth-utils/authenticate {:email email :password "wrongpassword"}))))) | ||
|
||
(testing "authenticate fails with non-existent user" | ||
(is (nil? (auth/authenticate {:email "[email protected]" :password "anypassword"}))))) | ||
(is (nil? (auth-utils/authenticate {:email "[email protected]" :password "anypassword"}))))) | ||
|
||
(deftest test-login | ||
(testing "login succeeds with correct credentials" | ||
|
@@ -67,7 +67,7 @@ | |
(let [response (auth/login {:body {:email email :password password}})] | ||
(is (= 200 (:status response))) | ||
(is (:token (:body response))) | ||
(is (jwt/unsign (:token (:body response)) auth/secret))))) | ||
(is (jwt/unsign (:token (:body response)) auth-utils/secret))))) | ||
|
||
(testing "login fails with incorrect password" | ||
(let [user-data (factory/create-test-user) | ||
|
@@ -89,28 +89,13 @@ | |
(is (= 200 (:status response))) | ||
(is (= {:message "Logged out successfully"} (:body response)))))) | ||
|
||
(deftest test-jwt-auth-middleware | ||
(testing "jwt-auth middleware allows authenticated requests" | ||
(let [handler (auth/jwt-auth (fn [_] {:status 200 :body "Success"})) | ||
request {:identity {:user-id 1}} | ||
response (handler request)] | ||
(is (= 200 (:status response))) | ||
(is (= "Success" (:body response))))) | ||
|
||
(testing "jwt-auth middleware blocks unauthenticated requests" | ||
(let [handler (auth/jwt-auth (fn [_] {:status 200 :body "Success"})) | ||
request {} | ||
response (handler request)] | ||
(is (= 401 (:status response))) | ||
(is (= {:error "Unauthorized"} (:body response)))))) | ||
|
||
(deftest test-get-user-id-from-token | ||
(testing "get-user-id-from-token extracts user-id from request" | ||
(let [request {:identity {:user-id 1}} | ||
user-id (auth/get-user-id-from-token request)] | ||
user-id (auth-utils/get-user-id-from-token request)] | ||
(is (= 1 user-id)))) | ||
|
||
(testing "get-user-id-from-token returns nil for unauthenticated request" | ||
(let [request {} | ||
user-id (auth/get-user-id-from-token request)] | ||
user-id (auth-utils/get-user-id-from-token request)] | ||
(is (nil? user-id))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.