Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide human-readable message for session status #1939

Merged
merged 2 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/api/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ int api_handler(struct mg_connection *conn, void *ignored)
http_method(conn),
NULL,
NULL,
NULL,
API_AUTH_UNAUTHORIZED,
double_time(),
{ false, NULL, NULL, NULL, 0u },
Expand Down
31 changes: 25 additions & 6 deletions src/api/auth.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
// This may be allowed without authentication depending on the configuration
if(!config.webserver.api.localAPIauth.v.b && is_local_api_user(api->request->remote_addr))
{
api->message = "no auth for local user";
add_request_info(api, NULL);
return API_AUTH_LOCALHOST;
}

// When the pwhash is unset, authentication is disabled
if(config.webserver.api.pwhash.v.s[0] == '\0')
{
api->message = "no password set";
add_request_info(api, NULL);
return API_AUTH_EMPTYPASS;
}
Expand Down Expand Up @@ -186,7 +188,8 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)

if(!sid_avail)
{
log_debug(DEBUG_API, "API Authentication: FAIL (no SID provided)");
api->message = "no SID provided";
log_debug(DEBUG_API, "API Authentication: FAIL (%s)", api->message);
return API_AUTH_UNAUTHORIZED;
}

Expand All @@ -212,21 +215,28 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
}
else
{
log_debug(DEBUG_API, "API Authentication: FAIL (Cookie authentication without CSRF token)");
api->message = "Cookie authentication without CSRF token";
log_debug(DEBUG_API, "API Authentication: FAIL (%s)", api->message);
return API_AUTH_UNAUTHORIZED;
}
}

bool expired = false;
for(unsigned int i = 0; i < max_sessions; i++)
{
if(auth_data[i].used &&
auth_data[i].valid_until >= now &&
strcmp(auth_data[i].sid, sid) == 0)
{
// Check if session is known but expired
if(auth_data[i].valid_until < now)
expired = true;

// Check CSRF if authentiating via cookie
if(need_csrf && strcmp(auth_data[i].csrf, csrf) != 0)
{
log_debug(DEBUG_API, "API Authentication: FAIL (CSRF token mismatch, received \"%s\", expected \"%s\")",
csrf, auth_data[i].csrf);
api->message = "CSRF token mismatch";
log_debug(DEBUG_API, "API Authentication: FAIL (%s, received \"%s\", expected \"%s\")",
api->message, csrf, auth_data[i].csrf);
return API_AUTH_UNAUTHORIZED;
}
user_id = i;
Expand Down Expand Up @@ -266,12 +276,14 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
}
else
{
log_debug(DEBUG_API, "API Authentication: FAIL (SID invalid/expired)");
api->message = expired ? "session expired" : "session unknown";
log_debug(DEBUG_API, "API Authentication: FAIL (%s)", api->message);
return API_AUTH_UNAUTHORIZED;
}

api->user_id = user_id;

api->message = "correct password";
return user_id;
}

Expand Down Expand Up @@ -314,6 +326,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
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_REF_STR_IN_OBJECT(session, "message", api->message);
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
return 0;
}
Expand All @@ -326,6 +339,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
JSON_REF_STR_IN_OBJECT(session, "sid", auth_data[user_id].sid);
JSON_REF_STR_IN_OBJECT(session, "csrf", auth_data[user_id].csrf);
JSON_ADD_NUMBER_TO_OBJECT(session, "validity", auth_data[user_id].valid_until - now);
JSON_REF_STR_IN_OBJECT(session, "message", api->message);
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
return 0;
}
Expand All @@ -335,6 +349,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
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_REF_STR_IN_OBJECT(session, "message", api->message);
JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
return 0;
}
Expand Down Expand Up @@ -632,6 +647,8 @@ int api_auth(struct ftl_conn *api)
"API seats exceeded",
"increase webserver.api.max_sessions");
}

api->message = result == APPPASSWORD_CORRECT ? "app-password correct" : "password correct";
}
else if(result == PASSWORD_RATE_LIMITED)
{
Expand All @@ -644,10 +661,12 @@ int api_auth(struct ftl_conn *api)
else if(result == NO_PASSWORD_SET)
{
// No password set
api->message = "password incorrect";
log_debug(DEBUG_API, "API: Trying to auth with password but none set: '%s'", password);
}
else
{
api->message = "password incorrect";
log_debug(DEBUG_API, "API: Password incorrect: '%s'", password);
}

Expand Down
13 changes: 12 additions & 1 deletion src/api/docs/content/specs/auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ components:
- sid
- csrf
- validity
- message
- totp
properties:
valid:
Expand All @@ -299,6 +300,10 @@ components:
validity:
type: integer
description: Remaining lifetime of this session unless refreshed (seconds)
message:
type: string
description: Human-readable message describing the session status
nullable: true

password:
type: object
Expand Down Expand Up @@ -431,14 +436,15 @@ components:

examples:
auth_okay:
summary: Authentication valid
summary: Session valid
value:
session:
valid: true
totp: false
sid: null
csrf: null
validity: 300
message: null
login_okay:
summary: Login successful
value:
Expand All @@ -448,6 +454,7 @@ components:
sid: "vFA+EP4MQ5JJvJg+3Q2Jnw="
csrf: "Ux87YTIiMOf/GKCefVIOMw="
validity: 300
message: correct password
no_login_required:
summary: No login required for this client
value:
Expand All @@ -457,6 +464,7 @@ components:
sid: null
csrf: null
validity: -1
message: no auth for local user
login_required:
summary: Login required, 2FA disabled
value:
Expand All @@ -466,6 +474,7 @@ components:
sid: null
csrf: null
validity: -1
message: password incorrect
login_required_2fa:
summary: Login required, 2FA enabled
value:
Expand All @@ -475,6 +484,7 @@ components:
sid: null
csrf: null
validity: -1
message: password incorrect
login_failed:
summary: Login failed
value:
Expand All @@ -484,6 +494,7 @@ components:
sid: null
csrf: null
validity: -1
message: no SID provided
errors:
no_payload:
summary: Bad request (no valid JSON payload)
Expand Down
1 change: 1 addition & 0 deletions src/webserver/http-common.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct ftl_conn {
const enum http_method method;
char *action_path;
const char *item;
const char *message;
int user_id;
double now;
struct {
Expand Down
6 changes: 4 additions & 2 deletions src/webserver/lua_web.c
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,10 @@ int request_handler(struct mg_connection *conn, void *cbdata)
free(target);

// User is not authenticated, redirect to login page
log_web("Authentication required, redirecting to %slogin?target=%s", config.webserver.paths.webhome.v.s, encoded_target);
mg_printf(conn, "HTTP/1.1 302 Found\r\nLocation: %slogin?target=%s\r\n\r\n", config.webserver.paths.webhome.v.s, encoded_target);
log_web("Authentication required, redirecting to %slogin?target=%s",
config.webserver.paths.webhome.v.s, encoded_target);
mg_printf(conn, "HTTP/1.1 302 Found\r\nLocation: %slogin?target=%s\r\n\r\n",
config.webserver.paths.webhome.v.s, encoded_target);
free(encoded_target);
return 302;
}
Expand Down
13 changes: 6 additions & 7 deletions test/test_suite.bats
Original file line number Diff line number Diff line change
Expand Up @@ -1470,7 +1470,7 @@
@test "API authorization (without password): No login required" {
run bash -c 'curl -s 127.0.0.1/api/auth'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == '{"session":{"valid":true,"totp":false,"sid":null,"validity":-1},"took":'*'}' ]]
[[ ${lines[0]} == '{"session":{"valid":true,"totp":false,"sid":null,"validity":-1,"message":"no password set"},"took":'*'}' ]]
}

@test "Config validation working on the CLI (type-based checking)" {
Expand Down Expand Up @@ -1592,17 +1592,16 @@

@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/api/auth -d "{\"password\":\"XXX\"}" | jq .session.valid'
run bash -c 'curl -s -X POST 127.0.0.1/api/auth -d "{\"password\":\"XXX\"}"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "false" ]]
[[ ${lines[0]} == "{\"session\":{\"valid\":false,\"totp\":false,\"sid\":null,\"validity\":-1,\"message\":\"password incorrect\"},\"took\":"*"}" ]]
}

@test "API authorization (with password): Correct password is accepted" {
session="$(curl -s -X POST 127.0.0.1/api/auth -d "{\"password\":\"ABC\"}")"
printf "Session: %s\n" "${session}"
run jq .session.valid <<< "${session}"
# Password: ABC
run bash -c 'curl -s -X POST 127.0.0.1/api/auth -d "{\"password\":\"ABC\"}"'
printf "%s\n" "${lines[@]}"
[[ ${lines[0]} == "true" ]]
[[ ${lines[0]} == "{\"session\":{\"valid\":true,\"totp\":false,\"sid\":\""*"\",\"csrf\":\""*"\",\"validity\":300,\"message\":\"password correct\"},\"took\":"*"}" ]]
}

@test "Test TLS/SSL server using self-signed certificate" {
Expand Down
Loading