Add TOTP 2FA to web interface and API

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER
2023-03-06 21:49:11 +01:00
parent 4ea0bf7dea
commit 9bf6176a64
16 changed files with 489 additions and 8 deletions
+339
View File
@@ -0,0 +1,339 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2023 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* FTL Engine
* API Implementation 2FA methods
*
* 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 <sys/random.h>
// TOTP+HMAC
#include <nettle/hmac.h>
#include <nettle/sha1.h>
static uint32_t hotp(const uint8_t *key, size_t key_len, const uint64_t counter, const uint8_t digits)
{
// Initialize HMAC-SHA1 (RFC 2104)
// TOTP uses HMAC-SHA1 (RFC 6238, section 5.1)
struct hmac_sha1_ctx ctx;
hmac_sha1_set_key(&ctx, key_len, key);
// Convert counter to big endian
const uint64_t counter_be = htobe64(counter);
// Compute HMAC-SHA1
hmac_sha1_update(&ctx, sizeof(counter_be), (uint8_t*)&counter_be);
uint8_t out[SHA1_DIGEST_SIZE];
hmac_sha1_digest(&ctx, SHA1_DIGEST_SIZE, out);
// Truncate HMAC-SHA1 for ease of use
// RFC 6238 (section 5.3): offset = last nibble of hash
const uint8_t offset = out[SHA1_DIGEST_SIZE-1] & 0x0F;
// RFC 6238 (section 5.3): binary = (hash[offset] & 0x7F) << 24 |
// (hash[offset+1] & 0xFF) << 16 |
// (hash[offset+2] & 0xFF) << 8 |
// (hash[offset+3] & 0xFF)
const uint32_t binary = (out[offset] & 0x7F) << 24 |
(out[offset+1] & 0xFF) << 16 |
(out[offset+2] & 0xFF) << 8 |
(out[offset+3] & 0xFF);
// RFC 6238 (section 5.3): HOTP = binary mod 10^digits
uint32_t mask = 10;
for(unsigned int i = 1; i < digits; i++)
mask *= 10;
return binary % mask;
}
// RFC 6238 (section 4.1): T0 is the Unix time to start counting time steps
// (default value is 0, i.e., the Unix epoch) and is also a system parameter.
#define RFC6238_T0 0
// RFC 6238 (section 5.2): We RECOMMEND a default time-step size of 30 seconds.
// This default value of 30 seconds is selected as a balance between security
// and usability.
#define RFC6238_X 30
// RFC 6238 (section 4, R6): The algorithm MUST use a strong shared secret. The
// length of the shared secret MUST be at least 128 bits (16 Byte). This
// document RECOMMENDs a shared secret length of 160 bits (20 Byte).
#define RFC6238_SECRET_LEN 160/8
// The number of digits to truncate to is not specified in RFC 6238. RFC 4226
// (section 5.3) specifies that the default is 6 (up to 8) digits, however, the
// example given in RFC 6238 uses 8 digits.
#define RFC6238_DIGITS 6
static uint32_t totp(const uint8_t *key, const size_t key_len, const time_t now)
{
// Get time
// RFC 6238 (section 4.2): T = (Current Unix time - T0) / X
// T is an integer and represents the number of time steps between the
// initial time T0 and the current time. T needs to be big endian
const uint64_t T = (now - RFC6238_T0) / RFC6238_X;
// RFC 6238 (section 4.2): TOTP(K, T) = HOTP(K,C) with C = T
return hotp(key, key_len, T, RFC6238_DIGITS);
}
static bool decode_base32_to_uint8_array(const char *base32, uint8_t *out, const size_t out_len)
{
// Base32 alphabet
const char *b32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
// Check input for validity
if(out_len == 0 || out_len*8/5 < strlen(base32) || out_len*8%5 != 0)
{
log_err("Decoding base32 2FA secret failed, invalid length (%zu)", out_len);
return false;
}
// Initialize output array
memset(out, 0, out_len);
// Iterate over input string
size_t out_pos = 0u;
for(size_t i = 0; i < strlen(base32); i++)
{
// Get current character
const char c = base32[i];
// Get position of current character in base32 alphabet
const char *c_pos = strchr(b32, toupper(c));
if(c_pos == NULL)
{
log_err("Decoding base32 2FA secret failed, invalid character '%c'", c);
return false;
}
// Get value of current character
const uint8_t c_val = (uint8_t)(c_pos-b32);
// Iterate over 5 bits of the current character
for(unsigned int j = 0; j < 5; j++)
{
// Current bit position
const unsigned int bit = 4-j;
// Get current bit in the current character
const uint8_t c_bit = (c_val >> bit) & 1;
// Get current byte position in the output array
out_pos = (i*5+j)/8;
// If we are out of bounds, return false
if(out_pos >= out_len)
{
log_err("Decoding base32 2FA secret failed, out of bounds (%zu >= %zu)", out_pos, out_len);
return false;
}
// Set current bit in the output array
out[out_pos] |= c_bit << (7-((i*5+j)%8));
}
}
return true;
}
static bool encode_uint8_t_array_to_base32(const uint8_t *in, const size_t in_len, char *base32, size_t base32_len)
{
// Base32 alphabet
const char *b32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
// Check input for validity
if(in_len == 0 || in_len > base32_len*5/8 || in_len%5 != 0)
{
log_err("Encoding base32 2FA secret failed, invalid input length");
return false;
}
// Initialize base32 output array
memset(base32, 0, base32_len);
// Iterate over input string
size_t base32_pos = 0u;
for(size_t i = 0; i < in_len; i++)
{
// Get current byte
const uint8_t b = in[i];
// Iterate over 8 bits of the current byte
for(unsigned int j = 0; j < 8; j++)
{
// Current bit position
const unsigned int bit = 7-j;
// Get current bit in the current byte
const uint8_t b_bit = (b >> bit) & 1;
// Get current byte position in the base32 output array
base32_pos = (i*8+j)/5;
// If we are out of bounds, return false
if(base32_pos >= base32_len)
{
log_err("Decoding base32 2FA secret failed, base32 output array is too small");
return false;
}
// Set current bit in the base32 output array
base32[base32_pos] |= b_bit << (4-((i*8+j)%5));
}
}
// Iterate over base32 output array and replace each byte with its
// corresponding character in the base32 alphabet
for(size_t i = 0; i <= base32_pos; i++)
base32[i] = b32[(uint8_t)base32[i]];
return true;
}
static uint32_t last_code = 0;
bool verifyTOTP(const uint32_t incode)
{
// Decode base32 secret
uint8_t decoded_secret[RFC6238_SECRET_LEN];
if(!decode_base32_to_uint8_array(config.webserver.api.totp_secret.v.s, decoded_secret, sizeof(decoded_secret)))
return false;
// Get current time
const time_t now = time(NULL);
// Verify code for the previous, the current and the next time step
for(int i = -1; i <= 1; i++)
{
const uint32_t gencode = totp(decoded_secret, sizeof(decoded_secret), now + i*RFC6238_X);
// Verify code
// RFC 6238 (section 4.2): If the calculated value matches the value
// provided by the user, then the user is authenticated
// RFC 6238 (section 4.3): The server MUST NOT accept a TOTP value
// generated more than 30 seconds in the future
// RFC 6238 (section 4.3): The server MUST NOT accept a TOTP value
// generated more than 30 seconds in the past
// RFC 6238 (section 4.3): The server MUST NOT accept a TOTP value
// it accepted previously
if(gencode == incode)
{
if(gencode == last_code)
{
log_warn("2FA code has already been used (%i, %u), please wait %ld seconds",
i, gencode, RFC6238_X - (now % RFC6238_X));
return false;
}
log_info("2FA code verified successfully at %i", i);
last_code = gencode;
return true;
}
}
return false;
}
// Print TOTP code to stdout (for CLI use)
int printTOTP(void)
{
// Decode base32 secret
uint8_t decoded_secret[RFC6238_SECRET_LEN];
if(!decode_base32_to_uint8_array(config.webserver.api.totp_secret.v.s, decoded_secret, sizeof(decoded_secret)))
return EXIT_FAILURE;
// Get current time
const time_t now = time(NULL);
const uint32_t code = totp(decoded_secret, sizeof(decoded_secret), now);
printf("%u\n", code);
return EXIT_SUCCESS;
}
// A QR code may be generated from the data using
// otpauth://totp/<label>?secret=<secret>&issuer=<issuer>&algorithm=<algorithm>&digits=<digits>&period=<period>
int generateTOTP(struct ftl_conn *api)
{
// Generate random secret using the system's random number generator
uint8_t random_secret[RFC6238_SECRET_LEN];
if(getrandom(random_secret, sizeof(random_secret), 0) < (ssize_t)sizeof(random_secret))
{
return send_json_error(api, 500, "internal_error", "Failed to generate random secret", strerror(errno));
}
// Encode base32 secret
const size_t base32_len = sizeof(random_secret)*8/5+1;
char *base32 = calloc(base32_len, sizeof(char));
if(!encode_uint8_t_array_to_base32(random_secret, sizeof(random_secret), base32, base32_len))
return send_json_error(api, 500, "internal_error", "Failed to encode secret", "Check FTL.log for details");
// Create JSON object
cJSON *tjson = cJSON_CreateObject();
JSON_REF_STR_IN_OBJECT(tjson, "type", "totp");
JSON_REF_STR_IN_OBJECT(tjson, "label", "Pi-hole%20API%3Api.hole");
JSON_REF_STR_IN_OBJECT(tjson, "issuer", "Pi-hole%20API");
JSON_REF_STR_IN_OBJECT(tjson, "algorithm", "SHA1");
JSON_ADD_NUMBER_TO_OBJECT(tjson, "digits", RFC6238_DIGITS);
JSON_ADD_NUMBER_TO_OBJECT(tjson, "period", RFC6238_X);
JSON_ADD_NUMBER_TO_OBJECT(tjson, "offset", RFC6238_T0);
JSON_COPY_STR_TO_OBJECT(tjson, "secret", base32);
free(base32);
base32 = NULL;
// Generate a few codes to show the user how to use the secret
cJSON *codes = cJSON_CreateArray();
for(int i = 0; i < 5; i++)
{
const time_t now = time(NULL) + (i-1)*RFC6238_X;
const uint32_t code = totp(random_secret, sizeof(random_secret), now);
JSON_ADD_NUMBER_TO_ARRAY(codes, code);
}
JSON_ADD_ITEM_TO_OBJECT(tjson, "codes", codes);
// Send JSON response
cJSON *json = cJSON_CreateObject();
JSON_ADD_ITEM_TO_OBJECT(json, "totp", tjson);
JSON_SEND_OBJECT(json);
}
#if 0
#define RFC6238_TESTKEY "12345678901234567890"
#define RFC6238_TESTTIME 59
#define RFC6238_TESTTOTP 94287082
int test_totp(struct ftl_conn *api)
{
// Generate base32 secret
uint8_t secret[sizeof(RFC6238_TESTKEY)-1];
for(size_t i = 0; i < sizeof(secret); i++)
secret[i] = RFC6238_TESTKEY[i];
// Encode base32 secret
char base32_secret[sizeof(secret)*8/5+1];
if(!encode_uint8_t_array_to_base32(secret, sizeof(secret), base32_secret, sizeof(base32_secret)))
return false;
// Decode base32 secret
uint8_t decoded_secret[sizeof(RFC6238_TESTKEY)-1];
if(!decode_base32_to_uint8_array(base32_secret, decoded_secret, sizeof(decoded_secret)))
return false;
// Get test time
const time_t now = RFC6238_TESTTIME;
// Verify code for the current time and the previous and next time step
for(int i = -1; i <= 1; i++)
{
// Verify code
const time_t t = now + i*RFC6238_X;
if(totp(decoded_secret, sizeof(decoded_secret), t) == RFC6238_TESTTOTP)
log_info("Code is valid for time %ld", t);
}
return 200;
}
#endif
+1
View File
@@ -9,6 +9,7 @@
# Please see LICENSE file for your rights under this license.
set(sources
2fa.c
action.c
api_helper.h
api.h
+1
View File
@@ -36,6 +36,7 @@ static struct {
// Note: The order of appearance matters here, more specific URIs have to
// 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/totp", "", generateTOTP, { false, true, 0 }, false, 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", "/{client}", api_list, { false, true, 0 }, true, HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_DELETE },
+5
View File
@@ -80,6 +80,11 @@ int api_auth(struct ftl_conn *api);
void delete_all_sessions(void);
int api_auth_sessions(struct ftl_conn *api);
// 2FA methods
bool verifyTOTP(const uint32_t code);
int generateTOTP(struct ftl_conn *api);
int printTOTP(void);
// Documentation methods
int api_docs(struct ftl_conn *api);
+30
View File
@@ -268,6 +268,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
{
cJSON *session = JSON_NEW_OBJECT();
JSON_ADD_BOOL_TO_OBJECT(session, "valid", true);
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
JSON_ADD_NULL_TO_OBJECT(session, "sid");
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", -1);
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
@@ -279,6 +280,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
{
cJSON *session = JSON_NEW_OBJECT();
JSON_ADD_BOOL_TO_OBJECT(session, "valid", true);
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
JSON_REF_STR_IN_OBJECT(session, "sid", auth_data[user_id].sid);
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", auth_data[user_id].valid_until - now);
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
@@ -288,6 +290,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
// No valid session
cJSON *session = JSON_NEW_OBJECT();
JSON_ADD_BOOL_TO_OBJECT(session, "valid", false);
JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
JSON_ADD_NULL_TO_OBJECT(session, "sid");
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", -1);
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
@@ -522,6 +525,33 @@ int api_auth(struct ftl_conn *api)
if(response_correct || empty_password)
{
// Accepted
// Check possible 2FA token
if(strlen(config.webserver.api.totp_secret.v.s) > 0)
{
// 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);
}
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
+6 -1
View File
@@ -511,7 +511,12 @@ static int api_config_get(struct ftl_conn *api)
conf_item->k, conf_item->t);
continue;
}
JSON_ADD_ITEM_TO_OBJECT(leaf, "value", val);
// Special case: write-only values
if(conf_item->f & FLAG_WRITE_ONLY)
JSON_ADD_ITEM_TO_OBJECT(leaf, "value", val);
else
JSON_REF_STR_IN_OBJECT(leaf, "value", "<write-only property>");
// Add default value
cJSON *dval = addJSONvalue(conf_item->t, &conf_item->d);
+55
View File
@@ -125,6 +125,26 @@ components:
application/json:
schema:
$ref: 'common.yaml#/components/errors/unauthorized'
totp:
get:
summary: Suggest new TOTP credentials
tags:
- Authentication
operationId: "get_auth_totp"
description: Suggest new TOTP credentials for two-factor authentication (2FA)
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: 'auth.yaml#/components/schemas/totp'
'401':
description: Unauthorized
content:
application/json:
schema:
$ref: 'common.yaml#/components/errors/unauthorized'
schemas:
session:
@@ -144,10 +164,14 @@ components:
- valid
- sid
- validity
- totp
properties:
valid:
type: boolean
description: Valid session indicator (client is authenticated)
totp:
type: boolean
description: Whether 2FA (TOTP) is enabled on this Pi-hole
sid:
type: string
description: Session ID
@@ -205,6 +229,33 @@ components:
valid_until: 1580000300
remote_addr: "192.168.0.34"
user_agent: "Mozilla/5.0 (X11; Linux x86_64; rv:107.0) Gecko/20100101 Firefox/107.0"
totp:
type: object
description: TOTP secret suggestion
properties:
totp:
type: object
properties:
type:
type: string
label:
type: string
issuer:
type: string
algorithm:
type: string
digits:
type: integer
period:
type: integer
offset:
type: integer
secret:
type: string
codes:
type: array
items:
type: integer
errors:
bad_request:
@@ -232,6 +283,7 @@ components:
challenge: null
session:
valid: true
totp: false
sid: null
validity: 300
no_login_required:
@@ -240,6 +292,7 @@ components:
challenge: null
session:
valid: true
totp: false
sid: null
validity: -1
login_required:
@@ -248,6 +301,7 @@ components:
challenge: "a2926b025bcc8618c632f81cd6cf7c37ee051c08aab74b565fd5126350fcd056"
session:
valid: false
totp: false
sid: null
validity: -1
login_failed:
@@ -256,6 +310,7 @@ components:
challenge: null
session:
valid: false
totp: false
sid: null
validity: -1
errors:
+3
View File
@@ -362,6 +362,8 @@ components:
type: string
pwhash:
type: string
totp_secret:
type: string
excludeClients:
type: array
items:
@@ -616,6 +618,7 @@ components:
prettyJSON: false
password: "********"
pwhash: ''
totp_secret: ''
excludeClients: [ '1.2.3.4', 'localhost', 'fe80::345' ]
excludeDomains: [ 'google.de', 'pi-hole.net' ]
temp:
+3
View File
@@ -60,6 +60,9 @@ paths:
/auth/sessions:
$ref: 'auth.yaml#/components/paths/session_list'
/auth/totp:
$ref: 'auth.yaml#/components/paths/totp'
/stats/summary:
$ref: 'stats.yaml#/components/paths/summary'
+15
View File
@@ -46,6 +46,8 @@
#include "zip/gzip.h"
// teleporter functions
#include "zip/teleporter.h"
// printTOTP()
#include "api/api.h"
// defined in dnsmasq.c
extern void print_dnsmasq_version(const char *yellow, const char *green, const char *bold, const char *normal);
@@ -252,6 +254,19 @@ void parse_args(int argc, char* argv[])
}
}
// Set config option through CLI
if(argc == 2 && strcmp(argv[1], "--totp") == 0)
{
cli_mode = true;
log_ctrl(false, false);
readFTLconf(&config, false);
log_ctrl(false, true);
clear_debug_flags(); // No debug printing wanted
exit(printTOTP());
}
// Create teleporter archive through CLI
if(argc == 2 && strcmp(argv[1], "--teleporter") == 0)
{
+8 -1
View File
@@ -431,6 +431,10 @@ int get_config_from_CLI(const char *key, const bool quiet)
if(strcmp(item->k, key) != 0)
continue;
// Skip write-only options
if(item->f & FLAG_WRITE_ONLY)
continue;
// This is the config option we are looking for
conf_item = item;
break;
@@ -449,7 +453,10 @@ int get_config_from_CLI(const char *key, const bool quiet)
return conf_item->v.b ? EXIT_SUCCESS : EXIT_FAILURE;
// Print value
writeTOMLvalue(stdout, -1, conf_item->t, &conf_item->v);
if(conf_item-> f & FLAG_WRITE_ONLY)
puts("<write-only property>");
else
writeTOMLvalue(stdout, -1, conf_item->t, &conf_item->v);
putchar('\n');
return EXIT_SUCCESS;
+9 -2
View File
@@ -888,12 +888,19 @@ void initConfig(struct config *conf)
conf->webserver.api.pwhash.d.s = (char*)"";
conf->webserver.api.password.k = "webserver.api.password";
conf->webserver.api.password.h = "Pi-hole web interface and API password. When set to something different than \""PASSWORD_VALUE"\", this porperty will compute the corresponding password hash to set webserver.api.pwhash";
conf->webserver.api.password.h = "Pi-hole web interface and API password. When set to something different than \""PASSWORD_VALUE"\", this property will compute the corresponding password hash to set webserver.api.pwhash";
conf->webserver.api.password.a = cJSON_CreateStringReference("<valid Pi-hole password>");
conf->webserver.api.password.t = CONF_PASSWORD;
conf->webserver.api.password.f = FLAG_WRITE_ONLY;
conf->webserver.api.password.f = FLAG_PSEUDO_ITEM;
conf->webserver.api.password.d.s = (char*)"";
conf->webserver.api.totp_secret.k = "webserver.api.totp_secret";
conf->webserver.api.totp_secret.h = "Pi-hole 2FA TOTP secret. When set to something different than \"""\", 2FA authentication will be enforced for the API and the web interface. This setting is write-only, you can not read the secret back.";
conf->webserver.api.totp_secret.a = cJSON_CreateStringReference("<valid TOTP secret (20 Bytes in Base32 encoding)>");
conf->webserver.api.totp_secret.t = CONF_STRING;
conf->webserver.api.totp_secret.f = FLAG_WRITE_ONLY | FLAG_INVALIDATE_SESSIONS;
conf->webserver.api.totp_secret.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");
+4 -2
View File
@@ -81,8 +81,9 @@ enum conf_type {
#define FLAG_RESTART_DNSMASQ (1 << 0)
#define FLAG_ADVANCED_SETTING (1 << 1)
#define FLAG_WRITE_ONLY (1 << 2)
#define FLAG_PSEUDO_ITEM (1 << 2)
#define FLAG_INVALIDATE_SESSIONS (1 << 3)
#define FLAG_WRITE_ONLY (1 << 4)
struct conf_item {
const char *k; // item Key
@@ -214,7 +215,8 @@ struct config {
struct conf_item localAPIauth;
struct conf_item prettyJSON;
struct conf_item pwhash;
struct conf_item password; // This is a write-only item
struct conf_item password; // This is a pseudo-item
struct conf_item totp_secret; // This is a write-only item
struct conf_item excludeClients;
struct conf_item excludeDomains;
struct {
+1 -1
View File
@@ -60,7 +60,7 @@ bool writeFTLtoml(const bool verbose)
struct conf_item *conf_item = get_conf_item(&config, i);
// Skip write-only items
if(conf_item->f & FLAG_WRITE_ONLY)
if(conf_item->f & FLAG_PSEUDO_ITEM)
continue;
// Get path depth
+8
View File
@@ -555,6 +555,14 @@
# <valid Pi-hole password hash>
pwhash = ""
# Pi-hole 2FA TOTP secret. When set to something different than "", 2FA authentication
# will be enforced for the API and the web interface. This setting is write-only, you
# can not read the secret back.
#
# Possible values are:
# <valid TOTP secret (20 Bytes in Base32 encoding)>
totp_secret = ""
# Array of clients to be excluded from certain API responses
# Example: [ "192.168.2.56", "fe80::341", "localhost" ]
#
+1 -1
View File
@@ -1231,7 +1231,7 @@
@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,"sid":null,"validity":-1}}' ]]
[[ ${lines[0]} == '{"challenge":null,"session":{"valid":true,"totp":false,"sid":null,"validity":-1}}' ]]
}
@test "API authorization (with password): FTL challenges us" {