Accept cookie authentication only when CSRF header is provided (and correct)
Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
parent
af4ce5cbba
commit
813509841b
|
@ -159,7 +159,7 @@ int api_handler(struct mg_connection *conn, void *ignored)
|
|||
}
|
||||
|
||||
// Verify requesting client is allowed to see this resource
|
||||
if(api_request[i].require_auth && check_client_auth(&api) == API_AUTH_UNAUTHORIZED)
|
||||
if(api_request[i].require_auth && check_client_auth(&api, true) == API_AUTH_UNAUTHORIZED)
|
||||
{
|
||||
unauthorized = true;
|
||||
break;
|
||||
|
|
|
@ -81,7 +81,7 @@ int api_list(struct ftl_conn *api);
|
|||
int api_group(struct ftl_conn *api);
|
||||
|
||||
// Auth method
|
||||
int check_client_auth(struct ftl_conn *api);
|
||||
int check_client_auth(struct ftl_conn *api, const bool is_api);
|
||||
int api_auth(struct ftl_conn *api);
|
||||
void delete_all_sessions(void);
|
||||
int api_auth_sessions(struct ftl_conn *api);
|
||||
|
|
|
@ -74,7 +74,7 @@ static struct {
|
|||
// Can we validate this client?
|
||||
// Returns -1 if not authenticated or expired
|
||||
// Returns >= 0 for any valid authentication
|
||||
int check_client_auth(struct ftl_conn *api)
|
||||
int check_client_auth(struct ftl_conn *api, const bool is_api)
|
||||
{
|
||||
// Is the user requesting from localhost?
|
||||
// This may be allowed without authentication depending on the configuration
|
||||
|
@ -158,6 +158,28 @@ int check_client_auth(struct ftl_conn *api)
|
|||
const time_t now = time(NULL);
|
||||
log_debug(DEBUG_API, "Read sid=\"%s\" from %s", sid, sid_source);
|
||||
|
||||
// If the SID has been sent through a cookie, we require a CSRF token in
|
||||
// the header to be sent along with the request for any API requests
|
||||
char csrf[SID_SIZE];
|
||||
const bool need_csrf = strcmp(sid_source, "cookie") == 0;
|
||||
if(need_csrf && is_api)
|
||||
{
|
||||
const char *csrf_header = NULL;
|
||||
// Try to extract CSRF token from header
|
||||
if((csrf_header = mg_get_header(api->conn, "X-CSRF-TOKEN")) != NULL)
|
||||
{
|
||||
// Copy CSRF string
|
||||
strncpy(csrf, csrf_header, SID_SIZE - 1u);
|
||||
// Zero terminate CSRF string
|
||||
csrf[SID_SIZE-1] = '\0';
|
||||
}
|
||||
else
|
||||
{
|
||||
log_debug(DEBUG_API, "API Authentication: FAIL (Cookie authentication without CSRF token)");
|
||||
return API_AUTH_UNAUTHORIZED;
|
||||
}
|
||||
}
|
||||
|
||||
for(unsigned int i = 0; i < API_MAX_CLIENTS; i++)
|
||||
{
|
||||
if(auth_data[i].used &&
|
||||
|
@ -165,6 +187,12 @@ int check_client_auth(struct ftl_conn *api)
|
|||
strcmp(auth_data[i].remote_addr, api->request->remote_addr) == 0 &&
|
||||
strcmp(auth_data[i].sid, sid) == 0)
|
||||
{
|
||||
if(need_csrf && strcmp(auth_data[i].csrf, csrf) != 0)
|
||||
{
|
||||
log_debug(DEBUG_API, "API Authentication: FAIL (CSRF token mismatch, recevied \"%s\", expected \"%s\")",
|
||||
csrf, auth_data[i].csrf);
|
||||
return API_AUTH_UNAUTHORIZED;
|
||||
}
|
||||
user_id = i;
|
||||
break;
|
||||
}
|
||||
|
@ -203,7 +231,10 @@ int check_client_auth(struct ftl_conn *api)
|
|||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log_debug(DEBUG_API, "API Authentication: FAIL (SID invalid/expired)");
|
||||
return API_AUTH_UNAUTHORIZED;
|
||||
}
|
||||
|
||||
api->user_id = user_id;
|
||||
|
||||
|
@ -260,6 +291,7 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
|
|||
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_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_ADD_ITEM_TO_OBJECT(json, "session", session);
|
||||
JSON_ADD_BOOL_TO_OBJECT(json, "dns", dns);
|
||||
|
@ -384,7 +416,7 @@ int api_auth(struct ftl_conn *api)
|
|||
}
|
||||
|
||||
// Did the client authenticate before and we can validate this?
|
||||
int user_id = check_client_auth(api);
|
||||
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)
|
||||
|
|
|
@ -214,6 +214,7 @@ components:
|
|||
required:
|
||||
- valid
|
||||
- sid
|
||||
- csrf
|
||||
- validity
|
||||
- totp
|
||||
properties:
|
||||
|
@ -227,6 +228,10 @@ components:
|
|||
type: string
|
||||
description: Session ID
|
||||
nullable: true
|
||||
csrf:
|
||||
type: string
|
||||
description: CSRF token
|
||||
nullable: true
|
||||
validity:
|
||||
type: integer
|
||||
description: Remaining lifetime of this session unless refreshed (seconds)
|
||||
|
@ -354,6 +359,7 @@ components:
|
|||
valid: true
|
||||
totp: false
|
||||
sid: null
|
||||
csrf: null
|
||||
validity: 300
|
||||
dns: true
|
||||
no_login_required:
|
||||
|
@ -363,6 +369,7 @@ components:
|
|||
valid: true
|
||||
totp: false
|
||||
sid: null
|
||||
csrf: null
|
||||
validity: -1
|
||||
dns: true
|
||||
login_required:
|
||||
|
@ -372,6 +379,7 @@ components:
|
|||
valid: false
|
||||
totp: false
|
||||
sid: null
|
||||
csrf: null
|
||||
validity: -1
|
||||
dns: true
|
||||
login_failed:
|
||||
|
@ -381,6 +389,7 @@ components:
|
|||
valid: false
|
||||
totp: false
|
||||
sid: null
|
||||
csrf: null
|
||||
validity: -1
|
||||
dns: true
|
||||
dns_failure:
|
||||
|
@ -390,6 +399,7 @@ components:
|
|||
valid: false
|
||||
totp: false
|
||||
sid: null
|
||||
csrf: null
|
||||
validity: -1
|
||||
dns: false
|
||||
errors:
|
||||
|
|
|
@ -114,11 +114,12 @@ int request_handler(struct mg_connection *conn, void *cbdata)
|
|||
}
|
||||
|
||||
// Every LUA page except admin/login requires authentication
|
||||
const int authorized = check_client_auth(&api, false) != API_AUTH_UNAUTHORIZED;
|
||||
if(!login)
|
||||
{
|
||||
// This is not the login page - check if the user is authenticated
|
||||
// Check if the user is authenticated
|
||||
if(check_client_auth(&api) == API_AUTH_UNAUTHORIZED)
|
||||
if(!authorized)
|
||||
{
|
||||
// Append query string to target
|
||||
char *target = NULL;
|
||||
|
@ -169,7 +170,7 @@ int request_handler(struct mg_connection *conn, void *cbdata)
|
|||
{
|
||||
// This is the login page - check if the user is already authenticated
|
||||
// Check if the user is authenticated
|
||||
if(check_client_auth(&api) != API_AUTH_UNAUTHORIZED)
|
||||
if(authorized)
|
||||
{
|
||||
// User is already authenticated, redirect to index page
|
||||
log_web("User is already authenticated, redirecting to %s", config.webserver.paths.webhome.v.s);
|
||||
|
|
|
@ -87,7 +87,9 @@ class FTLAPI():
|
|||
elif authenticate == AuthenticationMethods.BODY:
|
||||
json_data = {"sid": self.session['sid'] }
|
||||
elif authenticate == AuthenticationMethods.COOKIE:
|
||||
# Cookie authentication needs both the session ID and the CSRF header
|
||||
cookies = {"sid": self.session['sid'] }
|
||||
headers = { "X-CSRF-Token": self.session['csrf'] }
|
||||
|
||||
self.auth_method = authenticate.name
|
||||
|
||||
|
|
|
@ -268,7 +268,7 @@ class ResponseVerifyer():
|
|||
# Dive into the example to get to the property we want
|
||||
for p in props:
|
||||
if p not in example:
|
||||
self.errors.append(f"Example {flat_path} does not have an '{p}' item")
|
||||
self.errors.append(f"Example {t} does not have an '{p}' item")
|
||||
return False
|
||||
example = example[p]
|
||||
# Check if the type of the example matches the type we defined in the API specs
|
||||
|
|
Loading…
Reference in New Issue