Add app password support

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER
2023-10-29 11:16:03 +01:00
parent 692787ef93
commit e741ba4c94
19 changed files with 282 additions and 32 deletions
+36 -6
View File
@@ -8,13 +8,15 @@
* 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 "api.h"
#include "../webserver/json_macros.h"
#include "../log.h"
#include "../config/config.h"
#include "FTL.h"
#include "api/api.h"
#include "webserver/json_macros.h"
#include "log.h"
#include "config/config.h"
// getrandom()
#include "../daemon.h"
#include "daemon.h"
// generate_app_password()
#include "config/password.h"
// TOTP+HMAC
#include <nettle/hmac.h>
@@ -306,6 +308,34 @@ int generateTOTP(struct ftl_conn *api)
JSON_SEND_OBJECT(json);
}
int generateAppPw(struct ftl_conn *api)
{
// Generate and set app password
char *password = NULL, *pwhash = NULL;
if(!generate_app_password(&password, &pwhash))
{
return send_json_error(api,
500,
"internal_error",
"Failed to generate app password",
"Check FTL.log for details");
}
// Create JSON object
cJSON *tjson = cJSON_CreateObject();
JSON_COPY_STR_TO_OBJECT(tjson, "password", password);
JSON_COPY_STR_TO_OBJECT(tjson, "hash", pwhash);
free(password);
password = NULL;
free(pwhash);
pwhash = NULL;
// Send JSON response
cJSON *json = cJSON_CreateObject();
JSON_ADD_ITEM_TO_OBJECT(json, "app", tjson);
JSON_SEND_OBJECT(json);
}
#if 0
#define RFC6238_TESTKEY "12345678901234567890"
#define RFC6238_TESTTIME 59
+2 -1
View File
@@ -36,7 +36,8 @@ static struct {
// appear *before* less specific URIs: 1. "/a/b/c", 2. "/a/b", 3. "/a"
{ "/api/auth/sessions", "", api_auth_sessions, { false, true, 0 }, true, HTTP_GET },
{ "/api/auth/session", "/{id}", api_auth_session_delete, { false, true, 0 }, true, HTTP_DELETE },
{ "/api/auth/totp", "", generateTOTP, { false, true, 0 }, false, HTTP_GET },
{ "/api/auth/app", "", generateAppPw, { false, true, 0 }, true, HTTP_GET },
{ "/api/auth/totp", "", generateTOTP, { false, true, 0 }, true, HTTP_GET },
{ "/api/auth", "", api_auth, { false, true, 0 }, false, HTTP_GET | HTTP_POST | HTTP_DELETE },
{ "/api/dns/blocking", "", api_dns_blocking, { false, true, 0 }, true, HTTP_GET | HTTP_POST },
{ "/api/clients/_suggestions", "", api_client_suggestions, { false, true, 0 }, true, HTTP_GET },
+1
View File
@@ -95,6 +95,7 @@ bool is_local_api_user(const char *remote_addr) __attribute__((pure));
bool verifyTOTP(const uint32_t code);
int generateTOTP(struct ftl_conn *api);
int printTOTP(void);
int generateAppPw(struct ftl_conn *api);
// Documentation methods
int api_docs(struct ftl_conn *api);
+22 -12
View File
@@ -26,8 +26,7 @@
// database session functions
#include "database/session-table.h"
static struct session auth_data[API_MAX_CLIENTS] = {{false, {false, false}, 0, 0, {0}, {0}, {0}, {0}}};
static struct session auth_data[API_MAX_CLIENTS] = {{false, false, {false, false}, 0, 0, {0}, {0}, {0}, {0}}};
static void add_request_info(struct ftl_conn *api, const char *csrf)
{
@@ -271,6 +270,7 @@ static int get_all_sessions(struct ftl_conn *api, cJSON *json)
JSON_ADD_NUMBER_TO_OBJECT(session, "valid_until", auth_data[i].valid_until);
JSON_REF_STR_IN_OBJECT(session, "remote_addr", auth_data[i].remote_addr);
JSON_REF_STR_IN_OBJECT(session, "user_agent", auth_data[i].user_agent);
JSON_ADD_BOOL_TO_OBJECT(session, "app", auth_data[i].app);
JSON_ADD_ITEM_TO_ARRAY(sessions, session);
}
JSON_ADD_ITEM_TO_OBJECT(json, "sessions", sessions);
@@ -417,13 +417,6 @@ int api_auth(struct ftl_conn *api)
return 0;
}
// Did the client authenticate before and we can validate this?
int user_id = check_client_auth(api, false);
// 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, check password
if(api->method == HTTP_POST)
{
@@ -469,6 +462,13 @@ int api_auth(struct ftl_conn *api)
password = json_password->valuestring;
}
// Did the client authenticate before and we can validate this?
int user_id = check_client_auth(api, false);
// If this is a valid session, we can exit early at this point if no password is supplied
if(user_id != API_AUTH_UNAUTHORIZED && (password == NULL || strlen(password) == 0))
return send_api_auth_status(api, user_id, now);
// Logout attempt
if(api->method == HTTP_DELETE)
{
@@ -483,8 +483,16 @@ int api_auth(struct ftl_conn *api)
// else: Login attempt
// - Client tries to authenticate using a password, or
// - There no password on this machine
const enum password_result result = empty_password ? true : verify_password(password, config.webserver.api.pwhash.v.s, true);
if(result == PASSWORD_CORRECT)
enum password_result result = PASSWORD_INCORRECT;
// If there is no password (or empty), check if there is any password at all
log_info("Password: \"%s\"", password);
if(empty_password && (password == NULL || strlen(password) == 0))
result = PASSWORD_CORRECT;
else
result = verify_login(password);
if(result == PASSWORD_CORRECT || result == APPPASSWORD_CORRECT)
{
// Accepted
@@ -494,7 +502,8 @@ int api_auth(struct ftl_conn *api)
memset(password, 0, strlen(password));
// Check possible 2FA token
if(strlen(config.webserver.api.totp_secret.v.s) > 0)
// Successful login with empty password does not require 2FA
if(strlen(config.webserver.api.totp_secret.v.s) > 0 && result != APPPASSWORD_CORRECT)
{
// Get 2FA token from payload
cJSON *json_totp;
@@ -555,6 +564,7 @@ int api_auth(struct ftl_conn *api)
auth_data[i].tls.login = api->request->is_ssl;
auth_data[i].tls.mixed = false;
auth_data[i].app = result == APPPASSWORD_CORRECT;
// Generate new SID and CSRF token
generateSID(auth_data[i].sid);
+1
View File
@@ -50,6 +50,7 @@
struct session {
bool used;
bool app;
struct {
bool login;
bool mixed;
+39 -1
View File
@@ -166,7 +166,6 @@ components:
tags:
- Authentication
operationId: "get_auth_totp"
security: []
description: Suggest new TOTP credentials for two-factor authentication (2FA)
responses:
'200':
@@ -221,6 +220,31 @@ components:
allOf:
- $ref: 'common.yaml#/components/errors/unauthorized'
- $ref: 'common.yaml#/components/schemas/took'
app:
get:
summary: Create new application password
tags:
- Authentication
operationId: "add_app"
description: |
Create a new application password. The generated password is shown only once and cannot be retrieved later so make sure to store it in a safe place. The application password can be used to authenticate against the API instead of the regular password. It does not require 2FA verification. Generating a new application password will invalidate all currently active sessions.
responses:
'200':
description: OK
content:
application/json:
schema:
allOf:
- $ref: 'auth.yaml#/components/schemas/app'
- $ref: 'common.yaml#/components/schemas/took'
'401':
description: Unauthorized
content:
application/json:
schema:
allOf:
- $ref: 'common.yaml#/components/errors/unauthorized'
- $ref: 'common.yaml#/components/schemas/took'
schemas:
session:
@@ -348,6 +372,20 @@ components:
type: array
items:
type: integer
app:
type: object
description: Application password
properties:
app:
type: object
properties:
password:
type: string
hash:
type: string
example:
password: "7ua0rHPZixX6B3bTa5o4Dv08iyEIm/2q9qgLrF9MNVw="
hash: "$BALLOON-SHA256$v=1$s=1024,t=32$4ERwJD4XucRP4PcDHNLiAg==$kSy0Eou8RUVtK2UTbc5MCpItV8YC3VRuoGhoENxSQ2I="
errors:
bad_request:
+3
View File
@@ -396,6 +396,8 @@ components:
type: string
totp_secret:
type: string
app_pwhash:
type: string
excludeClients:
type: array
items:
@@ -668,6 +670,7 @@ components:
password: "********"
pwhash: ''
totp_secret: ''
app_pwhash: ''
excludeClients: [ '1.2.3.4', 'localhost', 'fe80::345' ]
excludeDomains: [ 'google.de', 'pi-hole.net' ]
maxHistory: 86400
+3
View File
@@ -79,6 +79,9 @@ paths:
/auth/totp:
$ref: 'auth.yaml#/components/paths/totp'
/auth/app:
$ref: 'auth.yaml#/components/paths/app'
/stats/summary:
$ref: 'stats.yaml#/components/paths/summary'
-3
View File
@@ -14,13 +14,10 @@
#include "config/toml_helper.h"
#include "config/toml_writer.h"
#include "config/dnsmasq_config.h"
#include "log.h"
#include "datastructure.h"
// toml_table_t
#include "tomlc99/toml.h"
// hash_password()
#include "config/password.h"
+7
View File
@@ -930,6 +930,13 @@ void initConfig(struct config *conf)
conf->webserver.api.totp_secret.f = FLAG_WRITE_ONLY | FLAG_INVALIDATE_SESSIONS;
conf->webserver.api.totp_secret.d.s = (char*)"";
conf->webserver.api.app_pwhash.k = "webserver.api.app_pwhash";
conf->webserver.api.app_pwhash.h = "Pi-hole application password.\n After you turn on two-factor (2FA) verification and set up an Authenticator app, you may run into issues if you use apps or other services that don't support two-step verification. In this case, you can create and use an app password to sign in. An app password is a long, randomly generated password that can be used instead of your regular password + TOTP token when signing in to the API. The app password can be generated through the API and will be shown only once. You can revoke the app password at any time. If you revoke the app password, be sure to generate a new one and update your app with the new password.";
conf->webserver.api.app_pwhash.a = cJSON_CreateStringReference("<valid Pi-hole password hash>");
conf->webserver.api.app_pwhash.t = CONF_STRING;
conf->webserver.api.app_pwhash.f = FLAG_INVALIDATE_SESSIONS;
conf->webserver.api.app_pwhash.d.s = (char*)"";
conf->webserver.api.excludeClients.k = "webserver.api.excludeClients";
conf->webserver.api.excludeClients.h = "Array of clients to be excluded from certain API responses\n Example: [ \"192.168.2.56\", \"fe80::341\", \"localhost\" ]";
conf->webserver.api.excludeClients.a = cJSON_CreateStringReference("array of IP addresses and/or hostnames");
+1
View File
@@ -230,6 +230,7 @@ struct config {
struct conf_item pwhash;
struct conf_item password; // This is a pseudo-item
struct conf_item totp_secret; // This is a write-only item
struct conf_item app_pwhash;
struct conf_item excludeClients;
struct conf_item excludeDomains;
struct conf_item maxHistory;
+66 -1
View File
@@ -307,7 +307,27 @@ char * __attribute__((malloc)) create_password(const char *password)
return balloon_password(password, salt, true);
}
char verify_password(const char *password, const char* pwhash, const bool rate_limiting)
enum password_result verify_login(const char *password)
{
enum password_result pw = verify_password(password, config.webserver.api.pwhash.v.s, true);
if(pw == PASSWORD_CORRECT)
log_debug(DEBUG_API, "Password correct");
else
log_debug(DEBUG_API, "Password incorrect");
// Check if an application password is set and if it matches
if(pw == PASSWORD_INCORRECT &&
strlen(config.webserver.api.app_pwhash.v.s) > 0 &&
verify_password(password, config.webserver.api.app_pwhash.v.s, true) == PASSWORD_CORRECT)
{
log_debug(DEBUG_API, "App password correct");
return APPPASSWORD_CORRECT;
}
// Return result
return pw;
}
enum password_result verify_password(const char *password, const char *pwhash, const bool rate_limiting)
{
// No password set
if(pwhash == NULL || pwhash[0] == '\0')
@@ -361,6 +381,10 @@ char verify_password(const char *password, const char* pwhash, const bool rate_l
if(config_hash != NULL)
free(config_hash);
// Successful logins do not count against rate-limiting
if(result)
num_password_attempts--;
return result ? PASSWORD_CORRECT : PASSWORD_INCORRECT;
}
else
@@ -386,6 +410,10 @@ char verify_password(const char *password, const char* pwhash, const bool rate_l
}
}
// Successful logins do not count against rate-limiting
if(result)
num_password_attempts--;
return result ? PASSWORD_CORRECT : PASSWORD_INCORRECT;
}
}
@@ -597,3 +625,40 @@ bool set_and_check_password(struct conf_item *conf_item, const char *password)
return true;
}
bool generate_app_password(char **password, char **pwhash)
{
// 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 generate_app_password()");
return false;
}
// Generate a 256 bit random password
uint8_t password_raw[256/8] = { 0 };
if(getrandom(password_raw, sizeof(password_raw), 0) < 0)
{
log_err("getrandom() failed in generate_app_password()");
return false;
}
// Encode password as base64
*password = base64_encode(password_raw, sizeof(password_raw));
// Generate balloon PHC-encoded password hash
*pwhash = balloon_password(*password, salt, true);
// Verify that the password hash is valid
if(verify_password(*password, *pwhash, false) != PASSWORD_CORRECT)
{
free(password);
free(pwhash);
log_warn("Failed to create password hash (verification failed), app password not available");
return false;
}
return true;
}
+4 -1
View File
@@ -16,13 +16,16 @@
void sha256_raw_to_hex(uint8_t *data, char *buffer);
char *create_password(const char *password) __attribute__((malloc));
char verify_password(const char *password, const char *pwhash, const bool rate_limiting);
enum password_result verify_login(const char *password);
enum password_result verify_password(const char *password, const char *pwhash, const bool rate_limiting);
int run_performance_test(void);
bool set_and_check_password(struct conf_item *conf_item, const char *password);
bool generate_app_password(char **password, char **pwhash);
enum password_result {
PASSWORD_INCORRECT = 0,
PASSWORD_CORRECT = 1,
APPPASSWORD_CORRECT = 2,
PASSWORD_RATE_LIMITED = -1
} __attribute__((packed));
+9 -3
View File
@@ -18,10 +18,11 @@
#include "args.h"
// INT_MAX
#include <limits.h>
#include "../datastructure.h"
#include "datastructure.h"
// openFTLtoml()
#include "toml_helper.h"
#include "config/toml_helper.h"
// delete_all_sessions()
#include "api/api.h"
// Private prototypes
static toml_table_t *parseTOML(void);
@@ -114,6 +115,11 @@ bool readFTLtoml(struct config *oldconf, struct config *newconf,
log_debug(DEBUG_CONFIG, "%s CHANGED", new_conf_item->k);
if(new_conf_item->f & FLAG_RESTART_FTL && restart != NULL)
*restart = true;
// Check if this item changed the password, if so, we need to
// invalidate all currently active sessions
if(new_conf_item->f & FLAG_INVALIDATE_SESSIONS)
delete_all_sessions();
}
}
+15
View File
@@ -528,6 +528,21 @@ void db_init(void)
dbversion = db_get_int(db, DB_VERSION);
}
// Update to version 16 if lower
if(dbversion < 16)
{
// Update to version 16: Add app column to session table
log_info("Updating long-term database to version 16");
if(!add_session_app_column(db))
{
log_info("Session table cannot be updated, database not available");
dbclose(&db);
return;
}
// Get updated version
dbversion = db_get_int(db, DB_VERSION);
}
lock_shm();
import_aliasclients(db);
unlock_shm();
+33 -2
View File
@@ -42,6 +42,27 @@ bool create_session_table(sqlite3 *db)
return true;
}
bool add_session_app_column(sqlite3 *db)
{
// Start transaction of database update
SQL_bool(db, "BEGIN TRANSACTION;");
// Create session table
SQL_bool(db, "ALTER TABLE session ADD COLUMN app BOOL;");
// Update database version to 16
if(!db_set_FTL_property(db, DB_VERSION, 16))
{
log_err("add_session_app_column(): Failed to update database version!");
return false;
}
// Finish transaction
SQL_bool(db, "COMMIT");
return true;
}
// Store all session in database
bool backup_db_sessions(struct session *sessions)
{
@@ -60,7 +81,7 @@ bool backup_db_sessions(struct session *sessions)
// Insert session into database
sqlite3_stmt *stmt = NULL;
if(sqlite3_prepare_v2(db, "INSERT INTO session (login_at, valid_until, remote_addr, user_agent, sid, csrf, tls_login, tls_mixed) VALUES (?, ?, ?, ?, ?, ?, ?, ?);", -1, &stmt, 0) != SQLITE_OK)
if(sqlite3_prepare_v2(db, "INSERT INTO session (login_at, valid_until, remote_addr, user_agent, sid, csrf, tls_login, tls_mixed, app) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);", -1, &stmt, 0) != SQLITE_OK)
{
log_err("SQL error in backup_db_sessions(): %s (%d)",
sqlite3_errmsg(db), sqlite3_errcode(db));
@@ -134,6 +155,13 @@ bool backup_db_sessions(struct session *sessions)
sess->tls.mixed ? 1 : 0, sqlite3_errmsg(db), sqlite3_errcode(db));
return false;
}
// 9: app
if(sqlite3_bind_int(stmt, 8, sess->app ? 1: 0) != SQLITE_OK)
{
log_err("Cannot bind app = %d in backup_db_sessions(): %s (%d)",
sess->app ? 1 : 0, sqlite3_errmsg(db), sqlite3_errcode(db));
return false;
}
// Execute statement
if(sqlite3_step(stmt) != SQLITE_DONE)
@@ -200,7 +228,7 @@ bool restore_db_sessions(struct session *sessions)
// Get all sessions from database
sqlite3_stmt *stmt = NULL;
if(sqlite3_prepare_v2(db, "SELECT login_at, valid_until, remote_addr, user_agent, sid, csrf, tls_login, tls_mixed FROM session;", -1, &stmt, 0) != SQLITE_OK)
if(sqlite3_prepare_v2(db, "SELECT login_at, valid_until, remote_addr, user_agent, sid, csrf, tls_login, tls_mixed, app FROM session;", -1, &stmt, 0) != SQLITE_OK)
{
log_err("SQL error in restore_db_sessions(): %s (%d)",
sqlite3_errmsg(db), sqlite3_errcode(db));
@@ -259,6 +287,9 @@ bool restore_db_sessions(struct session *sessions)
// 8: tls_mixed
sess->tls.mixed = sqlite3_column_int(stmt, 7) == 1 ? true : false;
// 8: app
sess->app = sqlite3_column_int(stmt, 8) == 1 ? true : false;
// Mark session as used
sess->used = true;
}
+1
View File
@@ -15,6 +15,7 @@
#include "api/auth.h"
bool create_session_table(sqlite3 *db);
bool add_session_app_column(sqlite3 *db);
bool backup_db_sessions(struct session *sessions);
bool restore_db_sessions(struct session *sessions);
+14
View File
@@ -621,6 +621,20 @@
# <valid TOTP secret (20 Bytes in Base32 encoding)>
totp_secret = ""
# Pi-hole application password.
# After you turn on two-factor (2FA) verification and set up an Authenticator app, you
# may run into issues if you use apps or other services that don't support two-step
# verification. In this case, you can create and use an app password to sign in. An
# app password is a long, randomly generated password that can be used instead of your
# regular password + TOTP token when signing in to the API. The app password can be
# generated through the API and will be shown only once. You can revoke the app
# password at any time. If you revoke the app password, be sure to generate a new one
# and update your app with the new password.
#
# Possible values are:
# <valid Pi-hole password hash>
app_pwhash = ""
# Array of clients to be excluded from certain API responses
# Example: [ "192.168.2.56", "fe80::341", "localhost" ]
#
+25 -2
View File
@@ -453,7 +453,7 @@
[[ "${lines[@]}" == *"CREATE TABLE IF NOT EXISTS \"network\" (id INTEGER PRIMARY KEY NOT NULL, hwaddr TEXT UNIQUE NOT NULL, interface TEXT NOT NULL, firstSeen INTEGER NOT NULL, lastQuery INTEGER NOT NULL, numQueries INTEGER NOT NULL, macVendor TEXT, aliasclient_id INTEGER);"* ]]
[[ "${lines[@]}" == *"CREATE TABLE IF NOT EXISTS \"network_addresses\" (network_id INTEGER NOT NULL, ip TEXT UNIQUE NOT NULL, lastSeen INTEGER NOT NULL DEFAULT (cast(strftime('%s', 'now') as int)), name TEXT, nameUpdated INTEGER, FOREIGN KEY(network_id) REFERENCES network(id));"* ]]
[[ "${lines[@]}" == *"CREATE TABLE aliasclient (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, comment TEXT);"* ]]
[[ "${lines[@]}" == *"INSERT INTO ftl VALUES(0,15,'Database version');"* ]] # Expecting FTL database version 14
[[ "${lines[@]}" == *"INSERT INTO ftl VALUES(0,16,'Database version');"* ]]
# vvv This has been added in version 10 vvv
[[ "${lines[@]}" == *"CREATE VIEW queries AS SELECT id, timestamp, type, status, CASE typeof(domain) WHEN 'integer' THEN (SELECT domain FROM domain_by_id d WHERE d.id = q.domain) ELSE domain END domain,CASE typeof(client) WHEN 'integer' THEN (SELECT ip FROM client_by_id c WHERE c.id = q.client) ELSE client END client,CASE typeof(forward) WHEN 'integer' THEN (SELECT forward FROM forward_by_id f WHERE f.id = q.forward) ELSE forward END forward,CASE typeof(additional_info) WHEN 'integer' THEN (SELECT content FROM addinfo_by_id a WHERE a.id = q.additional_info) ELSE additional_info END additional_info, reply_type, reply_time, dnssec, regex_id FROM query_storage q;"* ]]
[[ "${lines[@]}" == *"CREATE TABLE domain_by_id (id INTEGER PRIMARY KEY, domain TEXT NOT NULL);"* ]]
@@ -465,7 +465,7 @@
[[ "${lines[@]}" == *"CREATE TABLE addinfo_by_id (id INTEGER PRIMARY KEY, type INTEGER NOT NULL, content NOT NULL);"* ]]
[[ "${lines[@]}" == *"CREATE UNIQUE INDEX addinfo_by_id_idx ON addinfo_by_id(type,content);"* ]]
# vvv This has been added in version 15 vvv
[[ "${lines[@]}" == *"CREATE TABLE session (id INTEGER PRIMARY KEY, login_at TIMESTAMP NOT NULL, valid_until TIMESTAMP NOT NULL, remote_addr TEXT NOT NULL, user_agent TEXT, sid TEXT NOT NULL, csrf TEXT NOT NULL, tls_login BOOL, tls_mixed BOOL);"* ]]
[[ "${lines[@]}" == *"CREATE TABLE session (id INTEGER PRIMARY KEY, login_at TIMESTAMP NOT NULL, valid_until TIMESTAMP NOT NULL, remote_addr TEXT NOT NULL, user_agent TEXT, sid TEXT NOT NULL, csrf TEXT NOT NULL, tls_login BOOL, tls_mixed BOOL, app BOOL);"* ]]
}
@test "Ownership, permissions and type of pihole-FTL.db correct" {
@@ -1292,6 +1292,29 @@
[[ ${lines[0]} == '{"session":{"valid":true,"totp":false,"sid":null,"validity":-1},"took":'*'}' ]]
}
@test "Create, set, and use application password" {
run bash -c 'curl -s 127.0.0.1/api/auth/app'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == '{"app":{"password":"'*'","hash":"'*'"},"took":'*'}' ]]
# Extract password and hash from response
password="$(echo ${lines[0]} | jq .app.password)"
pwhash="$(echo ${lines[0]} | jq .app.hash)"
printf "password: %s\n" "${password}"
printf "pwhash: %s\n" "${pwhash}"
# Set app password hash
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config/webserver/api/app_pwhash -d "{\"config\":{\"webserver\":{\"api\":{\"app_pwhash\":${0}}}}}"' "${pwhash}"
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "{\"config\":{\"webserver\":{\"api\":{\"app_pwhash\":${pwhash}}}},\"took\":"*"}" ]]
# Login using app password is successful
run bash -c 'curl -s -X POST 127.0.0.1/api/auth -d "{\"password\":${0}}" | jq .session.valid' "${password}"
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "true" ]]
}
@test "API authorization: Setting password" {
# Password: ABC
run bash -c 'curl -s -X PATCH http://127.0.0.1/api/config/webserver/api/password -d "{\"config\":{\"webserver\":{\"api\":{\"password\":\"ABC\"}}}}"'