diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e27ac8cd7e3b73bde3ab4ab48a43bb91f12f105d..2e1c39e79103c1ce69a10d0a99146fcba5d33e20 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -53,7 +53,7 @@ jobs:
 
     needs: smoke-tests
 
-    container: ghcr.io/pi-hole/ftl-build:v1.27-${{ matrix.arch }}
+    container: ghcr.io/pi-hole/ftl-build:v1.28-${{ matrix.arch }}
 
     strategy:
       fail-fast: false
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 32dff361a54cf32e929b3f212cd83de20c1091e1..a7e24f889a59d05ed5ba0dbeee7bd290a2c19025 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -282,9 +282,9 @@ set(THREADS_PREFER_PTHREAD_FLAG TRUE)
 find_package(Threads REQUIRED)
 
 # for DNSSEC we need the nettle (+ hogweed) crypto and the gmp math libraries
-find_library(LIBHOGWEED NAMES libhogweed${CMAKE_STATIC_LIBRARY_SUFFIX} hogweed)
+find_library(LIBHOGWEED NAMES libhogweed${CMAKE_STATIC_LIBRARY_SUFFIX} hogweed HINTS /usr/local/lib64)
 find_library(LIBGMP NAMES libgmp${CMAKE_STATIC_LIBRARY_SUFFIX} gmp)
-find_library(LIBNETTLE NAMES libnettle${CMAKE_STATIC_LIBRARY_SUFFIX} nettle)
+find_library(LIBNETTLE NAMES libnettle${CMAKE_STATIC_LIBRARY_SUFFIX} nettle HINTS /usr/local/lib64)
 find_library(LIBIDN NAMES libidn${CMAKE_STATIC_LIBRARY_SUFFIX} idn)
 
 target_link_libraries(pihole-FTL rt Threads::Threads ${LIBHOGWEED} ${LIBGMP} ${LIBNETTLE} ${LIBIDN})
diff --git a/src/FTL.h b/src/FTL.h
index f9e5a0ed62fed2976a5a0f92ca64b2aa7e11c64e..7c4151b98044fb01a41e6cf51d302053013c3794 100644
--- a/src/FTL.h
+++ b/src/FTL.h
@@ -110,12 +110,6 @@
 // How many authenticated API clients are allowed simultaneously? [.]
 #define API_MAX_CLIENTS 16
 
-// How many challenges are valid simultaneously? [.]
-#define API_MAX_CHALLENGES 8
-
-// How long are challenges considered valid? [seconds]
-#define API_CHALLENGE_TIMEOUT 10
-
 // After how many seconds do we check again if a client can be identified by other means?
 // (e.g., interface, MAC address, hostname)
 // Default: 60 (after one minutee)
diff --git a/src/api/auth.c b/src/api/auth.c
index 2316847f554de8a83fa183b0419bdf53a262a48d..305bc2e56cb2a2f963508613ed7ffbb58d9a68e5 100644
--- a/src/api/auth.c
+++ b/src/api/auth.c
@@ -70,13 +70,6 @@ static struct {
 	char sid[SID_SIZE];
 } auth_data[API_MAX_CLIENTS] = {{false, {false, false}, 0, 0, {0}, {0}, {0}}};
 
-#define CHALLENGE_SIZE (2*SHA256_DIGEST_SIZE)
-static struct {
-	char challenge[CHALLENGE_SIZE + 1];
-	char response[CHALLENGE_SIZE + 1];
-	time_t valid_until;
-} challenges[API_MAX_CHALLENGES] = {{{0}, {0}, 0}};
-
 // Can we validate this client?
 // Returns -1 if not authenticated or expired
 // Returns >= 0 for any valid authentication
@@ -88,8 +81,8 @@ int check_client_auth(struct ftl_conn *api)
 	                                              strcmp(api->request->remote_addr, LOCALHOSTv6) == 0))
 		return API_AUTH_LOCALHOST;
 
-	// Check if there is a password hash
-	if(strlen(config.webserver.api.pwhash.v.s) == 0u)
+	// When the pwhash is unset, authentication is disabled
+	if(config.webserver.api.pwhash.v.s[0] == '\0')
 		return API_AUTH_EMPTYPASS;
 
 	// Does the client provide a session cookie?
@@ -213,29 +206,6 @@ int check_client_auth(struct ftl_conn *api)
 	return user_id;
 }
 
-// Check received response
-static bool check_response(const char *response, const time_t now)
-{
-	// Loop over all responses and try to validate response
-	for(unsigned int i = 0; i < API_MAX_CHALLENGES; i++)
-	{
-		// Skip expired entries
-		if(challenges[i].valid_until < now)
-			continue;
-
-		if(strcasecmp(challenges[i].response, response) == 0)
-		{
-			// This challange-response has been used
-			// Invalidate to prevent replay attacks
-			challenges[i].valid_until = 0;
-			return true;
-		}
-	}
-
-	// If transmitted challenge wasn't found -> this is an invalid auth request
-	return false;
-}
-
 static int get_all_sessions(struct ftl_conn *api, cJSON *json)
 {
 	const time_t now = time(NULL);
@@ -328,7 +298,6 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
 		log_debug(DEBUG_API, "API Auth status: OK (localhost does not need auth)");
 
 		cJSON *json = JSON_NEW_OBJECT();
-		JSON_ADD_NULL_TO_OBJECT(json, "challenge");
 		get_session_object(api, json, user_id, now);
 		JSON_SEND_OBJECT(json);
 	}
@@ -338,7 +307,6 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
 		log_debug(DEBUG_API, "API Auth status: OK (empty password)");
 
 		cJSON *json = JSON_NEW_OBJECT();
-		JSON_ADD_NULL_TO_OBJECT(json, "challenge");
 		get_session_object(api, json, user_id, now);
 		JSON_SEND_OBJECT(json);
 	}
@@ -356,7 +324,6 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
 		}
 
 		cJSON *json = JSON_NEW_OBJECT();
-		JSON_ADD_NULL_TO_OBJECT(json, "challenge");
 		get_session_object(api, json, user_id, now);
 		JSON_SEND_OBJECT(json);
 	}
@@ -369,7 +336,6 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
 
 		strncpy(pi_hole_extra_headers, FTL_DELETE_COOKIE, sizeof(pi_hole_extra_headers));
 		cJSON *json = JSON_NEW_OBJECT();
-		JSON_ADD_NULL_TO_OBJECT(json, "challenge");
 		get_session_object(api, json, user_id, now);
 		JSON_SEND_OBJECT_CODE(json, 410); // 410 Gone
 	}
@@ -379,47 +345,11 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
 
 		strncpy(pi_hole_extra_headers, FTL_DELETE_COOKIE, sizeof(pi_hole_extra_headers));
 		cJSON *json = JSON_NEW_OBJECT();
-		JSON_ADD_NULL_TO_OBJECT(json, "challenge");
 		get_session_object(api, json, user_id, now);
 		JSON_SEND_OBJECT_CODE(json, 401); // 401 Unauthorized
 	}
 }
 
-static void generateChallenge(const unsigned int idx, const time_t now)
-{
-	uint8_t raw_challenge[SHA256_DIGEST_SIZE];
-	if(getrandom(raw_challenge, sizeof(raw_challenge), 0) < 0)
-	{
-		log_err("getrandom() failed in generateChallenge()");
-		return;
-	}
-	sha256_raw_to_hex(raw_challenge, challenges[idx].challenge);
-	challenges[idx].valid_until = now + API_CHALLENGE_TIMEOUT;
-}
-
-static void generateResponse(const unsigned int idx)
-{
-	uint8_t raw_response[SHA256_DIGEST_SIZE];
-	struct sha256_ctx ctx;
-	sha256_init(&ctx);
-
-	// Add challenge in hex representation
-	sha256_update(&ctx,
-	              sizeof(challenges[idx].challenge)-1,
-	              (uint8_t*)challenges[idx].challenge);
-
-	// Add separator
-	sha256_update(&ctx, 1, (uint8_t*)":");
-
-	// Get and add password hash from setupVars.conf
-	sha256_update(&ctx,
-	              strlen(config.webserver.api.pwhash.v.s),
-	              (uint8_t*)config.webserver.api.pwhash.v.s);
-
-	sha256_digest(&ctx, SHA256_DIGEST_SIZE, raw_response);
-	sha256_raw_to_hex(raw_response, challenges[idx].response);
-}
-
 static void generateSID(char *sid)
 {
 	uint8_t raw_sid[SID_SIZE];
@@ -433,17 +363,15 @@ static void generateSID(char *sid)
 }
 
 // api/auth
-//  GET: Check authentication and obtain a challenge
+//  GET: Check authentication
 //  POST: Login
 //  DELETE: Logout
 int api_auth(struct ftl_conn *api)
 {
 	// Check HTTP method
+	char *password = NULL;
 	const time_t now = time(NULL);
-
-	const bool empty_password = strlen(config.webserver.api.pwhash.v.s) == 0u;
-
-	int user_id = API_AUTH_UNAUTHORIZED;
+	const bool empty_password = config.webserver.api.pwhash.v.s[0] == '\0';
 
 	if(api->item != NULL && strlen(api->item) > 0)
 	{
@@ -451,17 +379,14 @@ int api_auth(struct ftl_conn *api)
 		return 0;
 	}
 
-	bool reponse_set = false;
-	char response[CHALLENGE_SIZE+1u] = { 0 };
-
 	// Did the client authenticate before and we can validate this?
-	user_id = check_client_auth(api);
+	int user_id = check_client_auth(api);
 
 	// If this is a valid session, we can exit early at this point
 	if(user_id != API_AUTH_UNAUTHORIZED)
 		return send_api_auth_status(api, user_id, now);
 
-	// Login attempt, extract response
+	// Login attempt, check password
 	if(api->method == HTTP_POST)
 	{
 		// Try to extract response from payload
@@ -479,11 +404,11 @@ int api_auth(struct ftl_conn *api)
 				                       api->payload.json_error);
 		}
 
-		// Check if response is available
-		cJSON *json_response;
-		if((json_response = cJSON_GetObjectItemCaseSensitive(api->payload.json, "response")) == NULL)
+		// Check if password is available
+		cJSON *json_password;
+		if((json_password = cJSON_GetObjectItemCaseSensitive(api->payload.json, "password")) == NULL)
 		{
-			const char *message = "No response found in JSON payload";
+			const char *message = "No password found in JSON payload";
 			log_debug(DEBUG_API, "API auth error: %s", message);
 			return send_json_error(api, 400,
 			                       "bad_request",
@@ -491,10 +416,10 @@ int api_auth(struct ftl_conn *api)
 			                       NULL);
 		}
 
-		// Check response length
-		if(strlen(json_response->valuestring) != CHALLENGE_SIZE)
+		// Check password type
+		if(!cJSON_IsString(json_password))
 		{
-			const char *message = "Invalid response length";
+			const char *message = "Field password has to be of type 'string'";
 			log_debug(DEBUG_API, "API auth error: %s", message);
 			return send_json_error(api, 400,
 			                       "bad_request",
@@ -502,10 +427,8 @@ int api_auth(struct ftl_conn *api)
 			                       NULL);
 		}
 
-		// Accept challenge
-		strncpy(response, json_response->valuestring, CHALLENGE_SIZE);
-		// response is already null-terminated
-		reponse_set = true;
+		// password is already null-terminated
+		password = json_password->valuestring;
 	}
 
 	// Logout attempt
@@ -515,151 +438,114 @@ int api_auth(struct ftl_conn *api)
 		return send_api_auth_status(api, user_id, now);
 	}
 
-	// Login attempt and/or auth check
-	if(reponse_set || empty_password)
+	// If this is not a login attempt, we can exit early at this point
+	if(password == NULL && !empty_password)
+		return send_api_auth_status(api, user_id, now);
+
+	// else: Login attempt
+	// - Client tries to authenticate using a password, or
+	// - There no password on this machine
+	if(empty_password ? true : verify_password(password, config.webserver.api.pwhash.v.s))
 	{
-		// - Client tries to authenticate using a challenge response, or
-		// - There no password on this machine
-		const bool response_correct = check_response(response, now);
-		if(response_correct || empty_password)
+		// Accepted
+
+		// Zero-out password in memory to avoid leaking it when it is
+		// freed at the end of the current API request
+		if(password != NULL)
+			memset(password, 0, strlen(password));
+
+		// Check possible 2FA token
+		if(strlen(config.webserver.api.totp_secret.v.s) > 0)
 		{
-			// Accepted
+			// Get 2FA token from payload
+			cJSON *json_totp;
+			if((json_totp = cJSON_GetObjectItemCaseSensitive(api->payload.json, "totp")) == NULL)
+			{
+				const char *message = "No 2FA token found in JSON payload";
+				log_debug(DEBUG_API, "API auth error: %s", message);
+				return send_json_error(api, 400,
+							"bad_request",
+							message,
+							NULL);
+			}
 
-			// Check possible 2FA token
-			if(strlen(config.webserver.api.totp_secret.v.s) > 0)
+			if(!verifyTOTP(json_totp->valueint))
 			{
-				// Get 2FA token from payload
-				cJSON *json_twofa;
-				if((json_twofa = cJSON_GetObjectItemCaseSensitive(api->payload.json, "totp")) == NULL)
-				{
-					const char *message = "No 2FA token found in JSON payload";
-					log_debug(DEBUG_API, "API auth error: %s", message);
-					return send_json_error(api, 400,
-					                       "bad_request",
-					                       message,
-					                       NULL);
-				}
+				// 2FA token is invalid
+				return send_json_error(api, 401,
+							"unauthorized",
+							"Invalid 2FA token",
+							NULL);
+			}
+		}
 
-				if(!verifyTOTP(json_twofa->valueint))
-				{
-					// 2FA token is invalid
-					return send_json_error(api, 401,
-					                       "unauthorized",
-					                       "Invalid 2FA token",
-					                       NULL);
-				}
+		// Find unused authentication slot
+		for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
+		{
+			// Expired slow, mark as unused
+			if(auth_data[i].used &&
+				auth_data[i].valid_until < now)
+			{
+				log_debug(DEBUG_API, "API: Session of client %u (%s) expired, freeing...",
+						i, auth_data[i].remote_addr);
+				delete_session(i);
 			}
 
-			// Find unused authentication slot
-			for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
+			// Found unused authentication slot (might have been freed before)
+			if(!auth_data[i].used)
 			{
-				// Expired slow, mark as unused
-				if(auth_data[i].used &&
-				   auth_data[i].valid_until < now)
+				// Mark as used
+				auth_data[i].used = true;
+				// Set validitiy to now + timeout
+				auth_data[i].login_at = now;
+				auth_data[i].valid_until = now + config.webserver.sessionTimeout.v.ui;
+				// Set remote address
+				strncpy(auth_data[i].remote_addr, api->request->remote_addr, sizeof(auth_data[i].remote_addr));
+				auth_data[i].remote_addr[sizeof(auth_data[i].remote_addr)-1] = '\0';
+				// Store user-agent (if available)
+				const char *user_agent = mg_get_header(api->conn, "user-agent");
+				if(user_agent != NULL)
 				{
-					log_debug(DEBUG_API, "API: Session of client %u (%s) expired, freeing...",
-					          i, auth_data[i].remote_addr);
-					delete_session(i);
+					strncpy(auth_data[i].user_agent, user_agent, sizeof(auth_data[i].user_agent));
+					auth_data[i].user_agent[sizeof(auth_data[i].user_agent)-1] = '\0';
 				}
-
-				// Found unused authentication slot (might have been freed before)
-				if(!auth_data[i].used)
+				else
 				{
-					// Mark as used
-					auth_data[i].used = true;
-					// Set validitiy to now + timeout
-					auth_data[i].login_at = now;
-					auth_data[i].valid_until = now + config.webserver.sessionTimeout.v.ui;
-					// Set remote address
-					strncpy(auth_data[i].remote_addr, api->request->remote_addr, sizeof(auth_data[i].remote_addr));
-					auth_data[i].remote_addr[sizeof(auth_data[i].remote_addr)-1] = '\0';
-					// Store user-agent (if available)
-					const char *user_agent = mg_get_header(api->conn, "user-agent");
-					if(user_agent != NULL)
-					{
-						strncpy(auth_data[i].user_agent, user_agent, sizeof(auth_data[i].user_agent));
-						auth_data[i].user_agent[sizeof(auth_data[i].user_agent)-1] = '\0';
-					}
-					else
-					{
-						auth_data[i].user_agent[0] = '\0';
-					}
-
-					auth_data[i].tls.login = api->request->is_ssl;
-					auth_data[i].tls.mixed = false;
-
-					// Generate new SID
-					generateSID(auth_data[i].sid);
-
-					user_id = i;
-					break;
+					auth_data[i].user_agent[0] = '\0';
 				}
-			}
 
-			// Debug logging
-			if(config.debug.api.v.b && user_id > API_AUTH_UNAUTHORIZED)
-			{
-				char timestr[128];
-				get_timestr(timestr, auth_data[user_id].valid_until, false, false);
-				log_debug(DEBUG_API, "API: Registered new user: user_id %i valid_until: %s remote_addr %s (accepted due to %s)",
-				          user_id, timestr, auth_data[user_id].remote_addr,
-				          response_correct ? "correct response" : "empty password");
-			}
-			if(user_id == API_AUTH_UNAUTHORIZED)
-			{
-				log_warn("No free API seats available, not authenticating client");
+				auth_data[i].tls.login = api->request->is_ssl;
+				auth_data[i].tls.mixed = false;
+
+				// Generate new SID
+				generateSID(auth_data[i].sid);
+
+				user_id = i;
+				break;
 			}
 		}
-		else
+
+		// Debug logging
+		if(config.debug.api.v.b && user_id > API_AUTH_UNAUTHORIZED)
 		{
-			log_debug(DEBUG_API, "API: Response incorrect. Response=%s, FTL=%s", response, config.webserver.api.pwhash.v.s);
+			char timestr[128];
+			get_timestr(timestr, auth_data[user_id].valid_until, false, false);
+			log_debug(DEBUG_API, "API: Registered new user: user_id %i valid_until: %s remote_addr %s (accepted due to %s)",
+					user_id, timestr, auth_data[user_id].remote_addr,
+					empty_password ? "empty password" : "correct response");
+		}
+		if(user_id == API_AUTH_UNAUTHORIZED)
+		{
+			log_warn("No free API seats available, not authenticating client");
 		}
-
-		// Free allocated memory
-		return send_api_auth_status(api, user_id, now);
 	}
 	else
 	{
-		// Client wants to get a challenge
-		// Generate a challenge
-		unsigned int i;
-
-		// Get an empty/expired slot
-		for(i = 0; i < API_MAX_CHALLENGES; i++)
-			if(challenges[i].valid_until < now)
-				break;
-
-		// If there are no empty/expired slots, then find the oldest challenge
-		// and replace it
-		if(i == API_MAX_CHALLENGES)
-		{
-			unsigned int minidx = 0;
-			time_t minval = now;
-			for(i = 0; i < API_MAX_CHALLENGES; i++)
-			{
-				if(challenges[i].valid_until < minval)
-				{
-					minval = challenges[i].valid_until;
-					minidx = i;
-				}
-			}
-			i = minidx;
-		}
-
-		// Generate and store new challenge
-		generateChallenge(i, now);
-
-		// Compute and store expected response for this challenge (SHA-256)
-		generateResponse(i);
-
-		log_debug(DEBUG_API, "API: Sending challenge=%s", challenges[i].challenge);
-
-		// Return to user
-		cJSON *json = JSON_NEW_OBJECT();
-		JSON_REF_STR_IN_OBJECT(json, "challenge", challenges[i].challenge);
-		get_session_object(api, json, -1, now);
-		JSON_SEND_OBJECT(json);
+		log_debug(DEBUG_API, "API: Password incorrect: '%s'", password);
 	}
+
+	// Free allocated memory
+	return send_api_auth_status(api, user_id, now);
 }
 
 int api_auth_sessions(struct ftl_conn *api)
diff --git a/src/api/config.c b/src/api/config.c
index 14965e7242951b06e2d711d7b3ac0537d32a06d1..3990320667d509355a7d4aab7d62190d95de65d1 100644
--- a/src/api/config.c
+++ b/src/api/config.c
@@ -286,6 +286,15 @@ static const char *getJSONvalue(struct conf_item *conf_item, cJSON *elem, struct
 			// Get password hash as allocated string (an empty string is hashed to an empty string)
 			char *pwhash = strlen(elem->valuestring) > 0 ? create_password(elem->valuestring) : strdup("");
 
+			// Verify that the password hash is valid
+			const bool verfied = verify_password(elem->valuestring, pwhash);
+
+			if(!verfied)
+			{
+				free(pwhash);
+				return "Failed to create password hash (verification failed), password remains unchanged";
+			}
+
 			// Get pointer to pwhash instead
 			conf_item--;
 
@@ -707,7 +716,8 @@ static int api_config_patch(struct ftl_conn *api)
 		struct conf_item *conf_item = get_conf_item(&config, i);
 
 		// Skip processing if value didn't change compared to current value
-		if(compare_config_item(conf_item->t, &new_item->v, &conf_item->v))
+		if(compare_config_item(conf_item->t, &new_item->v, &conf_item->v) &&
+		   conf_item->t != CONF_PASSWORD)
 		{
 			log_debug(DEBUG_CONFIG, "Config item %s: Unchanged", conf_item->k);
 			continue;
diff --git a/src/api/docs/content/specs/auth.yaml b/src/api/docs/content/specs/auth.yaml
index 20df66afa314ee86af26df4a1022152ac3c25465..61fb31fac26a11ab463b8453f1171af230fb4495 100644
--- a/src/api/docs/content/specs/auth.yaml
+++ b/src/api/docs/content/specs/auth.yaml
@@ -3,16 +3,12 @@ components:
   paths:
     auth:
       get:
-        summary: Request challenge for login
+        summary: Check if authentication is required
         tags:
           - Authentication
         operationId: "get_auth"
         description: |
-          Request challenge for digest-access challenge-response authentication.
-
           The API may chose to reply with a valid session if no authentation is needed for this server.
-
-          Note that challenges are only valid for a very short time, use them to immediately compute a response. There is no need to get a challenge before users have finished entering their passwords.
         responses:
           '200':
             description: OK
@@ -32,18 +28,18 @@ components:
                   dns_failure:
                     $ref: 'auth.yaml#/components/examples/dns_failure'
       post:
-        summary: Submit response for login
+        summary: Submit password for login
         tags:
           - Authentication
         operationId: "add_auth"
         description: |
-          Submit computed response
+          Login with a password. The password is not stored in the session, and neither when to generating the session token.
         requestBody:
           description: Callback payload
           content:
             'application/json':
               schema:
-                $ref: 'auth.yaml#/components/schemas/response'
+                $ref: 'auth.yaml#/components/schemas/password'
         responses:
           '200':
             description: OK
@@ -69,10 +65,10 @@ components:
                 examples:
                   no_payload:
                     $ref: 'auth.yaml#/components/examples/errors/no_payload'
-                  no_response:
-                    $ref: 'auth.yaml#/components/examples/errors/no_response'
-                  response_inval:
-                    $ref: 'auth.yaml#/components/examples/errors/response_inval'
+                  no_password:
+                    $ref: 'auth.yaml#/components/examples/errors/no_password'
+                  password_inval:
+                    $ref: 'auth.yaml#/components/examples/errors/password_inval'
           '401':
             description: Unauthorized
             content:
@@ -194,7 +190,7 @@ components:
                   missing_session_id:
                     $ref: 'auth.yaml#/components/examples/errors/missing_session_id'
                   session_id_oob:
-                    $ref: 'auth.yaml#/components/examples/errors/no_response'
+                    $ref: 'auth.yaml#/components/examples/errors/no_password'
                   session_not_in_use:
                     $ref: 'auth.yaml#/components/examples/errors/session_not_in_use'
           '401':
@@ -210,13 +206,8 @@ components:
     session:
       type: object
       required:
-        - challenge
         - session
       properties:
-        challenge:
-          type: string
-          description: Challenge to be used for computing response
-          nullable: true
         session:
           type: object
           description: Session object
@@ -243,13 +234,13 @@ components:
           type: boolean
           description: Whether the DNS server is up and running. False only in failed state
 
-    response:
+    password:
       type: object
-      description: Response to be sent to the API
+      description: Password to be sent to the API
       properties:
-        response:
+        password:
           type: string
-          description: Response to previous challenge
+          description: Password to be used for login
           example: abcdef
     sessions_list:
       type: object
@@ -359,7 +350,6 @@ components:
     login_okay:
       summary: Login successful
       value:
-        challenge: null
         session:
           valid: true
           totp: false
@@ -369,7 +359,6 @@ components:
     no_login_required:
       summary: No login required for this client
       value:
-        challenge: null
         session:
           valid: true
           totp: false
@@ -379,7 +368,6 @@ components:
     login_required:
       summary: Login required
       value:
-        challenge: "a2926b025bcc8618c632f81cd6cf7c37ee051c08aab74b565fd5126350fcd056"
         session:
           valid: false
           totp: false
@@ -389,7 +377,6 @@ components:
     login_failed:
       summary: Login failed
       value:
-        challenge: null
         session:
           valid: false
           totp: false
@@ -399,7 +386,6 @@ components:
     dns_failure:
       summary: DNS server failure
       value:
-        challenge: "a2926b025bcc8618c632f81cd6cf7c37ee051c08aab74b565fd5126350fcd056"
         session:
           valid: false
           totp: false
@@ -414,19 +400,19 @@ components:
             key: "bad_request"
             message: "No valid JSON payload found"
             hint: null
-      no_response:
-        summary: Bad request (no response)
+      no_password:
+        summary: Bad request (no password)
         value:
           error:
             key: "bad_request"
-            message: "No response found in JSON payload"
+            message: "No password found in JSON payload"
             hint: null
-      response_inval:
-        summary: Bad request (invalid response)
+      password_inval:
+        summary: Bad request (password is not a string)
         value:
           error:
             key: "bad_request"
-            message: "Invalid response length"
+            message: "Field password has to be of type 'string'"
             hint: null
       missing_session_id:
         summary: Bad request (missing session ID)
diff --git a/src/config/cli.c b/src/config/cli.c
index 471da6d13d149e2cca1d3d252667e4a289abd335..7f6f33528044c6c0402a16f88a4e55121b10e680 100644
--- a/src/config/cli.c
+++ b/src/config/cli.c
@@ -163,6 +163,16 @@ static bool readStringValue(struct conf_item *conf_item, const char *value, stru
 			// Get password hash as allocated string (an empty string is hashed to an empty string)
 			char *pwhash = strlen(value) > 0 ? create_password(value) : strdup("");
 
+			// Verify that the password hash is valid
+			const bool verfied = verify_password(value, pwhash);
+
+			if(!verfied)
+			{
+				log_err("Failed to create password hash (verification failed), password remains unchanged");
+				free(pwhash);
+				return false;
+			}
+
 			// Free old password hash if it was allocated
 			if(conf_item->t == CONF_STRING_ALLOCATED)
 					free(conf_item->v.s);
@@ -388,7 +398,7 @@ int set_config_from_CLI(const char *key, const char *value)
 	// Also check if this is the password config item change as this
 	// actually changed pwhash behind the scenes
 	if(!compare_config_item(conf_item->t, &new_item->v, &conf_item->v) ||
-	   new_item == &newconf.webserver.api.password)
+	   conf_item->t == CONF_PASSWORD)
 	{
 		// Config item changed
 
diff --git a/src/config/password.c b/src/config/password.c
index 1f71a75f816c1435957afa852da64028056b137b..de476c258f803132a6bd5db716a896b7b132d59a 100644
--- a/src/config/password.c
+++ b/src/config/password.c
@@ -9,12 +9,33 @@
 *  Please see LICENSE file for your rights under this license. */
 
 #include "FTL.h"
+#include "log.h"
+#include "config/config.h"
 #include "password.h"
+// genrandom()
+#include <sys/random.h>
+
+// Randomness generator
+#include "webserver/x509.h"
+
+// writeFTLtoml()
+#include "config/toml_writer.h"
 
 // crypto library
 #include <nettle/sha2.h>
 #include <nettle/base64.h>
 #include <nettle/version.h>
+#include <nettle/balloon.h>
+
+// Salt length for balloon hashing
+// The purpose of including salts is to modify the function used to hash each
+// user's password so that each stored password hash will have to be attacked
+// individually. The only security requirement is that they are unique per user,
+// there is no benefit in them being unpredictable or difficult to guess.
+//
+// As a prime example, Linux uses 64 bits in the shadow password system. In
+// 2023, using 128 bits should be sufficient for the foreseeable future.
+#define SALT_LEN 16 // 16 bytes = 128 bits
 
 // Convert RAW data into hex representation
 // Two hexadecimal digits are generated for each input byte.
@@ -54,7 +75,281 @@ static char * __attribute__((malloc)) double_sha256_password(const char *passwor
 	return strdup(response);
 }
 
+static char * __attribute__((malloc)) base64_encode(const uint8_t *data, const size_t length)
+{
+	// Base64 encoding requires 4 bytes for every 3 bytes of input, plus
+	// additional bytes for padding. The output buffer must be large enough
+	// to hold the encoded data.
+	char *encoded = calloc(BASE64_ENCODE_LENGTH(length) + BASE64_ENCODE_FINAL_LENGTH, sizeof(char));
+
+	// Encode the data
+	size_t out_len;
+	struct base64_encode_ctx ctx;
+	base64_encode_init(&ctx);
+	out_len = base64_encode_update(&ctx, encoded, length, data);
+	out_len += base64_encode_final(&ctx, encoded + out_len);
+
+	return encoded;
+}
+
+static uint8_t * __attribute__((malloc)) base64_decode(const char *data, size_t *length)
+{
+	// Base64 decoding requires 3 bytes for every 4 bytes of input, plus
+	// additional bytes for padding. The output buffer must be large enough
+	// to hold the decoded data.
+	uint8_t *decoded = calloc(BASE64_DECODE_LENGTH(strlen(data)), sizeof(uint8_t));
+
+	// Decode the data
+	struct base64_decode_ctx ctx;
+	base64_decode_init(&ctx);
+	base64_decode_update(&ctx, length, decoded, strlen(data), data);
+	base64_decode_final(&ctx);
+
+	return decoded;
+}
+
+// Balloon hashing is a key derivation function presenting proven memory-hard
+// password-hashing and modern design. It was created by Dan Boneh, Henry
+// Corrigan-Gibbs (both at Stanford University) and Stuart Schechter (Microsoft
+// Research) in 2016. It is a recommended function in NIST password
+// guidelines. (see https://pages.nist.gov/800-63-3/sp800-63b.html#memsecretver)
+// (introduction taken from https://en.wikipedia.org/wiki/Balloon_hashing)
+//
+// If phc_string is true, the output will be formatted as a PHC string
+// Otherwise, the output will be the raw password hash
+static char * __attribute__((malloc)) balloon_password(const char *password,
+                                                       const uint8_t salt[SALT_LEN],
+                                                       const bool phc_string)
+{
+	struct timespec start, end;
+	// Record starting time
+	if(config.debug.api.v.b)
+		clock_gettime(CLOCK_MONOTONIC, &start);
+
+	// The space parameter s_cost determines how many blocks of working
+	// space the algorithm will require during its computation.  It is
+	// common to set s_cost to a high value in order to increase the cost of
+	// hardware accelerators built by the adversary.
+	// The algorithm will need (s_cost + 1) * digest_size
+	//    -> 32KB for s_cost = 1024 and algo = SHA256
+	const size_t s_cost = 1024;
+
+	// The time parameter t_cost determines the number of rounds of
+	// computation that the algorithm will perform. This can be used to
+	// further increase the cost of computation without raising the memory
+	// requirement.
+	const size_t t_cost = 32;
+
+	// Scratch buffer scratch is a user allocated working space required by
+	// the algorithm.  To determine the required size of the scratch buffer
+	// use the utility function balloon_itch.  Output of BALLOON algorithm
+	// will be written into the output buffer dst that has to be at least
+	// digest_size bytes long.
+	uint8_t *scratch = calloc(balloon_itch(SHA256_DIGEST_SIZE, s_cost), sizeof(uint8_t));
+
+	// Compute hash of given password password salted with salt and write
+	// the result into the output buffer dst
+	balloon_sha256(s_cost, t_cost,
+	               strlen(password), (const uint8_t *)password,
+	               SALT_LEN, salt, scratch, scratch);
+
+	if(config.debug.api.v.b)
+	{
+		// Record ending time
+		clock_gettime(CLOCK_MONOTONIC, &end);
+
+		// Compute elapsed time
+		double elapsed = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1000000000.0;
+		log_debug(DEBUG_API, "Balloon hashing took %.1f milliseconds", 1e3*elapsed);
+	}
+
+	if(!phc_string)
+	{
+		// Return raw hash
+		return (char*)scratch;
+	}
+
+	char *salt_base64 = base64_encode(salt, SALT_LEN);
+	char *scratch_base64 = base64_encode(scratch, SHA256_DIGEST_SIZE);
+
+	// Build PHC string-like output (output string is 101 bytes long (measured))
+	char *output = calloc(128, sizeof(char));
+	int size = snprintf(output, 128, "$BALLOON-SHA256$v=1$s=%zu,t=%zu$%s$%s",
+	                    s_cost,
+	                    t_cost,
+	                    salt_base64,
+	                    scratch_base64);
+
+	if(size < 0)
+	{
+		// Error
+		log_err("Error while generating PHC string: %s", strerror(errno));
+		goto clean_and_exit;
+	}
+
+clean_and_exit:
+	free(scratch);
+	free(salt_base64);
+	free(scratch_base64);
+
+	return output;
+}
+
+// Parse a PHC string and return the parameters and hash
+// Returns true on success, false on error
+static bool parse_PHC_string(const char *phc, size_t *s_cost, size_t *t_cost, uint8_t **salt, uint8_t **hash)
+{
+	int version = 0;
+	char algorithm[64] = { 0 };
+	char salt_base64[64] = { 0 };
+	char hash_base64[64] = { 0 };
+
+	// Parse PHC string
+	int size = sscanf(phc, "$%63[^$]$v=%d$s=%zu,t=%zu$%63[^$]$%63[^$]$",
+	                  algorithm,
+	                  &version,
+	                  s_cost,
+	                  t_cost,
+	                  salt_base64,
+	                  hash_base64);
+
+	// Add null-terminators
+	algorithm[sizeof(algorithm) - 1] = '\0';
+	salt_base64[sizeof(salt_base64) - 1] = '\0';
+	hash_base64[sizeof(hash_base64) - 1] = '\0';
+
+	// Debug output
+	log_debug(DEBUG_API, "Parsed PHC string: '%s'", phc);
+	log_debug(DEBUG_API, "   -> Algorithm: '%s'", algorithm);
+	log_debug(DEBUG_API, "   -> Version: %d", version);
+	log_debug(DEBUG_API, "   -> s_cost: %zu", *s_cost);
+	log_debug(DEBUG_API, "   -> t_cost: %zu", *t_cost);
+	log_debug(DEBUG_API, "   -> Salt: '%s'", salt_base64);
+	log_debug(DEBUG_API, "   -> Hash: '%s'", hash_base64);
+
+	// Check parsing result
+	if(size != 6)
+	{
+		// Error
+		log_err("Error while parsing PHC string: Found %d instead of 6 elements in definition", size);
+		return false;
+	}
+
+	// Check PHC string version and algorithm
+	if(version != 1)
+	{
+		// Error
+		log_err("Unsupported PHC string version: %d", version);
+		return false;
+	}
+
+	if(strcmp(algorithm, "BALLOON-SHA256") != 0)
+	{
+		// Error
+		log_err("Unsupported PHC string algorithm: %s", algorithm);
+		return false;
+	}
+
+	// Decode salt and hash
+	size_t salt_len = 0;
+	*salt = base64_decode(salt_base64, &salt_len);
+	if(salt == NULL)
+	{
+		// Error
+		log_err("Error while decoding salt: %s", strerror(errno));
+		return false;
+	}
+	if(salt_len != SALT_LEN)
+	{
+		// Error
+		log_err("Invalid decoded salt length: %zu, should be %d",
+		        salt_len, SALT_LEN);
+		return false;
+	}
+
+	size_t hash_len = 0;
+	*hash = base64_decode(hash_base64, &hash_len);
+	if(hash == NULL)
+	{
+		// Error
+		log_err("Error while decoding hash: %s", strerror(errno));
+		return false;
+	}
+	if(hash_len != SHA256_DIGEST_SIZE)
+	{
+		// Error
+		log_err("Invalid decoded hash length: %zu, should be %d",
+		        hash_len, SHA256_DIGEST_SIZE);
+		return false;
+	}
+
+	return true;
+}
+
 char * __attribute__((malloc)) create_password(const char *password)
 {
-	return double_sha256_password(password);
+	// Generate a 128 bit random salt
+	// genrandom() returns cryptographically secure random data
+	uint8_t salt[SALT_LEN] = { 0 };
+	if(getrandom(salt, sizeof(salt), 0) < 0)
+	{
+		log_err("getrandom() failed in create_password()");
+		return NULL;
+	}
+
+	// Generate balloon PHC-encoded password hash
+	return balloon_password(password, salt, true);
+}
+
+bool verify_password(const char *password, const char* pwhash)
+{
+	// No password supplied
+	if(password == NULL || password[0] == '\0')
+		return false;
+
+	// No password set
+	if(pwhash == NULL || pwhash[0] == '\0')
+		return true;
+
+	if(pwhash[0] == '$')
+	{
+		// Parse PHC string
+		size_t s_cost = 0;
+		size_t t_cost = 0;
+		uint8_t *salt = NULL;
+		uint8_t *config_hash = NULL;
+		if(!parse_PHC_string(pwhash, &s_cost, &t_cost, &salt, &config_hash))
+			return false;
+		if(salt == NULL || config_hash == NULL)
+			return false;
+		char *supplied = balloon_password(password, salt, false);
+		const bool result = memcmp(config_hash, supplied, SHA256_DIGEST_SIZE) == 0;
+		free(supplied);
+		return result;
+	}
+	else
+	{
+		// Legacy password
+		char *supplied = double_sha256_password(password);
+		const bool result = strcmp(pwhash, supplied) == 0;
+		free(supplied);
+
+		// Upgrade double-hased password to BALLOON hash
+		if(result)
+		{
+			char *new_hash = balloon_password(password, NULL, true);
+			if(new_hash != NULL)
+			{
+				log_info("Upgrading password from SHA256^2 to BALLOON-SHA256");
+				if(config.webserver.api.pwhash.t == CONF_STRING_ALLOCATED)
+					free(config.webserver.api.pwhash.v.s);
+				config.webserver.api.pwhash.v.s = new_hash;
+				config.webserver.api.pwhash.t = CONF_STRING_ALLOCATED;
+				writeFTLtoml(true);
+				free(new_hash);
+			}
+		}
+
+		return result;
+	}
 }
\ No newline at end of file
diff --git a/src/config/password.h b/src/config/password.h
index 7828ea95ab4373f7c544e7790e918f9837615c53..6cacc7581bddf2ed7149f71a1eb6b5868ce726a7 100644
--- a/src/config/password.h
+++ b/src/config/password.h
@@ -16,5 +16,6 @@
 
 void sha256_raw_to_hex(uint8_t *data, char *buffer);
 char *create_password(const char *password) __attribute__((malloc));
+bool verify_password(const char *password, const char *pwhash);
 
 #endif //PASSWORD_H
diff --git a/src/lua/ftl_lua.c b/src/lua/ftl_lua.c
index f8c556b662cda88e27cebb10cd3dfda5d9a357b2..21e503e87984f302b6623e5f917b63202103bcea 100644
--- a/src/lua/ftl_lua.c
+++ b/src/lua/ftl_lua.c
@@ -212,7 +212,7 @@ static int pihole_needLogin(lua_State *L) {
 
 	// Check if password is set
 	const bool has_password = config.webserver.api.pwhash.v.s != NULL &&
-	                          strlen(config.webserver.api.pwhash.v.s) > 0;
+	                          config.webserver.api.pwhash.v.s[0] != '\0';
 
 	// Check if address is loopback
 	const bool is_loopback = strcmp(remote_addr, LOCALHOSTv4) == 0 ||
diff --git a/src/webserver/x509.c b/src/webserver/x509.c
index 114c0d2fd7cab4e80b49df4581aafb66fa00a47c..ef998569f21a61ed7bb2bb8f5ac1f692566cb2e5 100644
--- a/src/webserver/x509.c
+++ b/src/webserver/x509.c
@@ -3,13 +3,13 @@
 *  Network-wide ad blocking via your own hardware.
 *
 *  FTL Engine
-*  X.509 certificate routines
+*  X.509 certificate and randomness generator routines
 *
 *  This file is copyright under the latest version of the EUPL.
 *  Please see LICENSE file for your rights under this license. */
 
-#include "../FTL.h"
-#include "../log.h"
+#include "FTL.h"
+#include "log.h"
 #include "x509.h"
 #include <mbedtls/rsa.h>
 #include <mbedtls/x509.h>
@@ -105,6 +105,7 @@ bool generate_certificate(const char* certfile, bool rsa)
 		return false;
 	}
 
+	// Generate key
 	if(rsa)
 	{
 		// Generate RSA key
@@ -126,7 +127,6 @@ bool generate_certificate(const char* certfile, bool rsa)
 		}
 	}
 
-
 	// Create string with random digits for unique serial number
 	// RFC 2459: The serial number is an integer assigned by the CA to each
 	// certificate. It MUST be unique for each certificate issued by a given
diff --git a/src/webserver/x509.h b/src/webserver/x509.h
index 42b1cfc765b3cef45617ed96871fd34569c738da..3a7837487543f0da92aaea1553af12ca14b326a7 100644
--- a/src/webserver/x509.h
+++ b/src/webserver/x509.h
@@ -3,13 +3,16 @@
 *  Network-wide ad blocking via your own hardware.
 *
 *  FTL Engine
-*  X.509 certificate routines
+*  X.509 certificate and randomness generator prototypes
 *
 *  This file is copyright under the latest version of the EUPL.
 *  Please see LICENSE file for your rights under this license. */
 #ifndef X509_H
 #define X509_H
 
+#include <mbedtls/entropy.h>
+#include <mbedtls/ctr_drbg.h>
+
 bool generate_certificate(const char* certfile, bool rsa);
 
 #endif // X509_H
diff --git a/test/api/checkAPI.py b/test/api/checkAPI.py
index bc6286640fb9b5513239c96a0eaa40ee4b0a1c89..89d966a23710ca485339fd9a9a79b46996ba4c9b 100644
--- a/test/api/checkAPI.py
+++ b/test/api/checkAPI.py
@@ -20,7 +20,7 @@ if __name__ == "__main__":
 		exit(1)
 
 	# Get endpoints from FTL
-	ftl = FTLAPI("http://127.0.0.1:8080")
+	ftl = FTLAPI("http://127.0.0.1:8080", "ABC")
 	ftl.get_endpoints()
 
 	errs = [0, 0, 0]
diff --git a/test/api/json/add_password.json b/test/api/json/add_password.json
index 2a1557d9f913f727e9c326f670c6019466c642f8..075c70e7307c60f308e0ba29ab782a99cd2032e1 100644
--- a/test/api/json/add_password.json
+++ b/test/api/json/add_password.json
@@ -2,7 +2,7 @@
         "config": {
                 "webserver": {
                         "api": {
-                                "pwhash": "183c1b634da0078fcf5b0af84bdcbb3e817708c3f22b329be84165f4bad1ae48"
+                                "password": "ABC"
                         }
                 }
         }
diff --git a/test/api/libs/FTLAPI.py b/test/api/libs/FTLAPI.py
index 4a325a4acbc4b7b5c749c885578f68ad5d648eaf..e23dcd85fff3e3f5efc3ed81da5b9f486c384213 100644
--- a/test/api/libs/FTLAPI.py
+++ b/test/api/libs/FTLAPI.py
@@ -18,15 +18,6 @@ from hashlib import sha256
 
 url = "http://pi.hole/api/auth"
 
-"""
-challenge = requests.get(url).json()["challenge"].encode('ascii')
-response = sha256(challenge + b":" + pwhash).hexdigest().encode("ascii")
-session = requests.post(url, data = {"response": response}).json()
-
-valid = session["session"]["valid"] # True / False
-sid = session["session"]["sid"] # SID string if succesful, null otherwise
-"""
-
 class AuthenticationMethods(Enum):
 	RANDOM = 0
 	HEADER = 1
@@ -38,7 +29,7 @@ class FTLAPI():
 
 	auth_method = "?"
 
-	def __init__(self, api_url: str):
+	def __init__(self, api_url: str, password: str = None):
 		self.api_url = api_url
 		self.endpoints = {
 			"get": [],
@@ -52,60 +43,30 @@ class FTLAPI():
 		self.verbose = False
 
 		# Login to FTL API
-		self.login()
+		self.login(password)
 		if self.session is None or 'valid' not in self.session or not self.session['valid']:
 			raise Exception("Could not login to FTL API")
 
 	def login(self, password: str = None):
-		# Get challenge from FTL
+		# Check if we even need to login
 		response = self.GET("/api/auth")
 
 		# Check if we are already logged in or authentication is not
 		# required
 		if response is None:
 			raise Exception("No response from FTL API")
-		if 'session' in response and response['session']['valid']:
-			if 'session' not in response:
-				raise Exception("FTL returned invalid challenge item")
+		if 'session' not in response:
+			raise Exception("FTL returned invalid challenge item")
+		if 'session' in response and response['session']['valid'] == True:
 			self.session = response["session"]
+			print(response)
+			if password is not None:
+				raise Exception("Password provided but API does not require authentication")
 			return
 
-		pwhash = None
-		if password is None:
-			# Try to obtain the password hash from pihole.toml
-			try:
-				with open("/etc/pihole/pihole.toml", "r") as f:
-					# Iterate over all lines
-					for line in f:
-						# Find the line with the password hash
-						if line.startswith("    pwhash = "):
-							# Remove quotes and whitespace
-							line = line.split("=")[1].split("\"")
-							if len(line) > 2:
-								pwhash = line[1].strip()
-							break
-			except Exception as e:
-				# Could not read pihole.toml, throw an error
-				raise Exception("Could not read pihole.toml: " + str(e))
-			if pwhash is None:
-				# The password hash was not found in pihole.toml, throw an error
-				raise Exception("No password hash found in pihole.toml")
-
-		else:
-			# Generate password hash
-			pwhash = sha256(password.encode("ascii")).hexdigest()
-			pwhash = sha256(pwhash.encode("ascii")).hexdigest()
-		print("Using password hash: " + pwhash)
-
-		if len(pwhash) != 64:
-			raise Exception("Invalid length of password hash")
-
-		# Get the challenge from FTL
-		challenge = response["challenge"].encode("ascii")
-		response = sha256(challenge + b":" + pwhash.encode("ascii")).hexdigest()
-		response = self.POST("/api/auth", {"response": response})
+		response = self.POST("/api/auth", {"password": password})
 		if 'session' not in response:
-			raise Exception("FTL returned invalid challenge item")
+			raise Exception("FTL returned invalid response item")
 		self.session = response["session"]
 
 
diff --git a/test/test_suite.bats b/test/test_suite.bats
index 19aa207a6ce7c0de6965dd4c60f0c52bcf54976d..8a5f5982f1421905f61d9170567d04f746a875bc 100644
--- a/test/test_suite.bats
+++ b/test/test_suite.bats
@@ -1242,37 +1242,25 @@
 @test "API authorization (without password): No login required" {
   run bash -c 'curl -s 127.0.0.1:8080/api/auth'
   printf "%s\n" "${lines[@]}"
-  [[ ${lines[0]} == '{"challenge":null,"session":{"valid":true,"totp":false,"sid":null,"validity":-1},"dns":true,"took":'*'}' ]]
+  [[ ${lines[0]} == '{"session":{"valid":true,"totp":false,"sid":null,"validity":-1},"dns":true,"took":'*'}' ]]
 }
 
-@test "API authorization (with password): FTL challenges us" {
+@test "API authorization: Setting password" {
   # Password: ABC
-  run bash -c 'curl -X PATCH http://127.0.0.1:8080/api/config -d "@test/api/json/add_password.json"'
-  run bash -c 'curl -s 127.0.0.1:8080/api/auth | jq ".challenge | length"'
+  run bash -c 'curl -s -X PATCH http://127.0.0.1:8080/api/config/webserver/api/password -d "{\"config\":{\"webserver\":{\"api\":{\"password\":\"ABC\"}}}}"'
   printf "%s\n" "${lines[@]}"
-  [[ ${lines[0]} == "64" ]]
+  [[ ${lines[0]} == "{\"config\":{\"webserver\":{\"api\":{\"password\":\"********\"}}},\"took\":"*"}" ]]
 }
 
-@test "API authorization (with password): Incorrect response is rejected" {
-  run bash -c 'curl -s -X POST 127.0.0.1:8080/api/auth -d "{\"response\":\"0123456789012345678901234567890123456789012345678901234567890123\"}" | jq .session.valid'
+@test "API authorization (with password): Incorrect password is rejected if password auth is enabled" {
+  # Password: ABC
+  run bash -c 'curl -s -X POST 127.0.0.1:8080/api/auth -d "{\"password\":\"XXX\"}" | jq .session.valid'
   printf "%s\n" "${lines[@]}"
   [[ ${lines[0]} == "false" ]]
 }
 
 @test "API authorization (with password): Correct password is accepted" {
-  computeResponse() {
-      local pwhash challenge response
-      pwhash="${1}"
-      challenge="${2}"
-      response=$(echo -n "${challenge}:${pwhash}" | sha256sum | sed 's/\s.*$//')
-      echo "${response}"
-  }
-  pwhash="183c1b634da0078fcf5b0af84bdcbb3e817708c3f22b329be84165f4bad1ae48"
-  challenge="$(curl -s -X GET 127.0.0.1:8080/api/auth | jq --raw-output .challenge)"
-  printf "Challenge: %s\n" "${challenge}"
-  response="$(computeResponse "$pwhash" "$challenge")"
-  printf "Response: %s\n" "${response}"
-  session="$(curl -s -X POST 127.0.0.1:8080/api/auth -d "{\"response\":\"$response\"}")"
+  session="$(curl -s -X POST 127.0.0.1:8080/api/auth -d "{\"password\":\"ABC\"}")"
   printf "Session: %s\n" "${session}"
   run jq .session.valid <<< "${session}"
   printf "%s\n" "${lines[@]}"