Added WebSocket authentication options --ws-auth and --ws-auth-expire.
Some checks are pending
C build and Test / build (, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--enable-debug, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--enable-debug, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--enable-debug, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --enable-debug --with-getline, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --enable-debug --with-getline, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --enable-debug --with-getline, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --with-getline --enable-asan, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --with-getline --enable-asan, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--enable-utf8 --with-getline --enable-asan, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
C build and Test / build (--with-getline --enable-asan, macos-14, brew update && brew install ncurses gettext autoconf automake openssl@3 libmaxminddb jq) (push) Waiting to run
C build and Test / build (--with-getline --enable-asan, macos-latest, brew install ncurses gettext autoconf automake libmaxminddb openssl@3 jq) (push) Waiting to run
C build and Test / build (--with-getline --enable-asan, ubuntu-latest, sudo apt-get update && sudo apt-get install -y build-essential autoconf gettext autopoint libncursesw5-dev libssl-dev git libmaxminddb-dev jq) (push) Waiting to run
Docker / test (push) Waiting to run
Docker / push (push) Blocked by required conditions

This commit introduces two new command-line options to enhance WebSocket
security in GoAccess:

--ws-auth=<jwt[:secret]>: Enables JWT-based authentication for WebSocket
  connections. Supports an optional secret (as a string or file path) for token
  verification. Without a secret, it falls back to the GOACCESS_WSAUTH_SECRET
  environment variable or generates an HS256-compatible secret. When enabled, the
  HTML report delays bootstrapping initial data until authentication succeeds.

--ws-auth-expire=<secs>: Sets the JWT expiration time, defaulting to 3600
  seconds (1 hour). Supports flexible formats like "24h", "10m", or "10d" for
  user convenience.

These options strengthen real-time HTML output security by ensuring only
authenticated clients access the WebSocket feed.

Closes #2794, #1133, #2411
This commit is contained in:
Gerardo O 2025-03-27 19:44:12 -05:00
parent 8056805314
commit 95ca855183
19 changed files with 930 additions and 46 deletions

View File

@ -207,6 +207,12 @@ goaccess_SOURCES = \
src/xmalloc.c \
src/xmalloc.h
if WITH_SSL
goaccess_SOURCES += \
src/wsauth.c \
src/wsauth.h
endif
if USE_SHA1
goaccess_SOURCES += \
src/sha1.c \

View File

@ -74,6 +74,7 @@ if test "$openssl" = 'yes'; then
AC_CHECK_LIB([crypto], [CRYPTO_free],,[AC_MSG_ERROR([crypto library missing])])
AC_CHECK_LIB([ssl], [SSL_CIPHER_standard_name], [AC_DEFINE([HAVE_CIPHER_STD_NAME], 1, [HAVE_CIPHER_STD_NAME])])
fi
AM_CONDITIONAL([WITH_SSL], [test "x$with_openssl" = "xyes"])
# GeoIP
AC_ARG_ENABLE([geoip],[AS_HELP_STRING([--enable-geoip],[Enable GeoIP country lookup. Supported types: mmdb, legacy. Default is disabled])],[geoip="$enableval"],[geoip=no])

View File

@ -1,4 +1,4 @@
.TH goaccess 1 "MAY 2024" GNU+Linux "User Manuals"
.TH goaccess 1 "MAR 2025" GNU+Linux "User Manuals"
.SH NAME
goaccess \- fast web log analyzer and interactive viewer.
.SH SYNOPSIS
@ -460,6 +460,37 @@ Enable real-time HTML output.
GoAccess uses its own WebSocket server to push the data from the server to the
client. See http://gwsocket.io for more details how the WebSocket server works.
.TP
\fB\-\-ws-auth=<jwt[:secret]>
Enable WebSocket authentication using a JSON Web Token (JWT). Optionally, a secret key can be provided for verification.
.IP
When this option is used, the HTML report will not bootstrap the initial parsed data. Instead, it will only display the report if authentication succeeds.
.IP
The system processes this option as follows: if the argument starts with "jwt:", the part after the colon is treated as either a file path (if it exists, the secret is read from the file) or a direct secret string. If only "jwt" is provided, the secret is sourced from the environment variable \fBGOACCESS_WSAUTH_SECRET\fR, or a default HS256-compatible secret is generated if the variable is unset. See http://gwsocket.io for more details on the underlying WebSocket server.
.TP
\fB\-\-ws-auth-expire=<secs>
Set the time after which the JWT expires. Defaults to 8 hours (28800 seconds) if not specified.
.IP
Users can specify the expiration time in various formats. The value is converted to seconds for JWT expiration validation. Supported formats:
.RS
.IP \(bu 4
"3600" -> 3600 seconds
.IP \(bu 4
"24h" -> 24 hours = 86,400 seconds
.IP \(bu 4
"10m" -> 10 minutes = 600 seconds
.IP \(bu 4
"10d" -> 10 days = 864,000 seconds
.RE
.IP
The expiration time controls how long the JWT remains valid after issuance, ensuring secure WebSocket connections.
.TP
\fB\-\-ws-url=<[scheme://]url[:port]>
URL to which the WebSocket server responds. This is the URL supplied to the
WebSocket constructor on the client side.

View File

@ -30,11 +30,26 @@ h3 {
.expandable>td {
cursor: pointer;
}
.loading-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.spinner {
color: #999;
left: 50%;
position: absolute;
top: 50%;
margin-bottom: 10px; /* Space between spinner and text */
}
.app-loading-status {
color: #999;
text-align: center;
text-shadow: 1px 1px 0 #FFF;
text-transform: uppercase;
}
.powered {
bottom: 190px;

View File

@ -66,9 +66,20 @@ window.GoAccess = window.GoAccess || {
var ls = JSON.parse(localStorage.getItem('AppPrefs'));
this.AppPrefs = GoAccess.Util.merge(this.AppPrefs, ls);
}
if (Object.keys(this.AppWSConn).length)
this.setWebSocket(this.AppWSConn);
// Track if the app has been initialized
this.isAppInitialized = false;
// If window.goaccessJWT is set and WebSocket is configured, set it up
if (window.goaccessJWT && Object.keys(this.AppWSConn).length) {
this.setWebSocket(this.AppWSConn);
} else {
// No JWT or no WebSocket: initialize immediately
GoAccess.App.initialize();
if (Object.keys(this.AppWSConn).length) {
this.setWebSocket(this.AppWSConn);
}
}
},
getPanelUI: function (panel) {
@ -110,31 +121,69 @@ window.GoAccess = window.GoAccess || {
setWebSocket: function (wsConn) {
var host = null, pingId = null, uri = null, defURI = null, str = null;
const messages = [
'Validating WebSocket tokens... Please wait.',
'Authenticating WebSocket connection... Please wait.',
'Verifying WebSocket credentials... Please wait.',
'Authorizing WebSocket session... Please wait.'
];
let currentMessageIndex = 0;
let messageInterval;
function displayNextMessage() {
if (currentMessageIndex < messages.length) {
document.querySelector('.app-loading-status > small').innerHTML = messages[currentMessageIndex];
currentMessageIndex++;
}
}
// Start message rotation
messageInterval = setInterval(displayNextMessage, 100);
defURI = window.location.hostname ? window.location.hostname + ':' + wsConn.port : "localhost" + ':' + wsConn.port;
uri = wsConn.url && /^(wss?:\/\/)?[^\/]+:[0-9]{1,5}/.test(wsConn.url) ? wsConn.url : this.buildWSURI(wsConn);
str = uri || defURI;
str = !/^wss?:\/\//i.test(str) ? (window.location.protocol === "https:" ? 'wss://' : 'ws://') + str : str;
if (window.goaccessJWT) {
const separator = str.includes('?') ? '&' : '?';
str = str + separator + 'token=' + encodeURIComponent(window.goaccessJWT);
}
var socket = new WebSocket(str);
socket.onopen = function (event) {
clearInterval(messageInterval);
document.querySelector('.app-loading-status > small').innerHTML = 'Authentication successful.';
this.currDelay = this.wsDelay;
this.retries = 0;
// attempt to keep connection alive (e.g., ping/pong)
if (wsConn.ping_interval)
if (wsConn.ping_interval) {
pingId = setInterval(() => { socket.send('ping'); }, wsConn.ping_interval * 1E3);
}
GoAccess.Nav.WSOpen(str);
}.bind(this);
socket.onmessage = function (event) {
this.AppState['updated'] = true;
this.AppData = JSON.parse(event.data);
if (window.goaccessJWT && !this.isAppInitialized) {
GoAccess.App.initialize();
GoAccess.Nav.WSOpen(str);
this.isAppInitialized = true;
}
this.App.renderData();
}.bind(this);
socket.onclose = function (event) {
clearInterval(messageInterval);
document.querySelector('.app-loading-status > small').innerHTML = 'Unable to authenticate WebSocket.';
document.querySelector('.loading-container > .spinner').style.display = 'none';
GoAccess.Nav.WSClose();
window.clearInterval(pingId);
socket = null;
@ -395,7 +444,7 @@ GoAccess.OverallStats = {
'from': data.start_date,
'to': data.end_date,
}));
$('#overall').setAttribute('aria-labelledby', 'overall-heading');
$('#overall').setAttribute('aria-labelledby', 'overall-heading');
// Iterate over general data object
for (var x in data) {
@ -703,11 +752,12 @@ GoAccess.Nav = {
},
WSOpen: function (str) {
const baseUrl = str.split('?')[0].split('#')[0];
$$('.nav-ws-status', function (item) {
item.classList.remove('fa-stop');
item.classList.add('fa-circle');
item.setAttribute('aria-label', `${GoAccess.i18n.websocket_connected} (${str})`);
item.setAttribute('title', `${GoAccess.i18n.websocket_connected} (${str})`);
item.setAttribute('aria-label', `${GoAccess.i18n.websocket_connected} (${baseUrl})`);
item.setAttribute('title', `${GoAccess.i18n.websocket_connected} (${baseUrl})`);
});
},
@ -1926,16 +1976,20 @@ GoAccess.App = {
GoAccess.Tables.reloadTables();
},
initialize: function () {
this.setInitSort();
this.setTpls();
renderPanels: function () {
GoAccess.Nav.initialize();
this.initDom();
GoAccess.OverallStats.initialize();
GoAccess.Panels.initialize();
GoAccess.Charts.initialize();
GoAccess.Tables.initialize();
},
initialize: function () {
this.setInitSort();
this.setTpls();
this.initDom();
this.renderPanels();
},
};
// Adds the visibilitychange EventListener
@ -1961,6 +2015,5 @@ window.onload = function () {
'wsConnection': window.connection || null,
'prefs': window.html_prefs || {},
});
GoAccess.App.initialize();
};
}());

View File

@ -29,6 +29,7 @@
*/
#include <string.h>
#include <inttypes.h>
#include "base64.h"
#include "xmalloc.h"
@ -76,3 +77,87 @@ base64_encode (const void *buf, size_t size) {
return str;
}
/*
* base64_decode
*
* Given a Base64 encoded string in 'data', this function decodes it into
* a newly allocated binary buffer. The length of the decoded data is stored
* in *out_len. The caller is responsible for freeing the returned buffer.
*
* Returns NULL on error (for example, if the data's length is not a multiple
* of 4).
*/
char *
base64_decode (const char *data, size_t *out_len) {
size_t decoded_len = 0, i = 0, j = 0, pad = 0, len = 0;
char *out = NULL;
uint32_t triple = 0;
/* Create a lookup table for decoding.
* For valid Base64 characters 'A'-'Z', 'a'-'z', '0'-'9', '+', '/',
* we place their value; for '=' we simply return 0.
* All other characters are marked as 0x80 (an invalid marker).
*/
static const unsigned char dtable[256] = {
['A'] = 0,['B'] = 1,['C'] = 2,['D'] = 3,
['E'] = 4,['F'] = 5,['G'] = 6,['H'] = 7,
['I'] = 8,['J'] = 9,['K'] = 10,['L'] = 11,
['M'] = 12,['N'] = 13,['O'] = 14,['P'] = 15,
['Q'] = 16,['R'] = 17,['S'] = 18,['T'] = 19,
['U'] = 20,['V'] = 21,['W'] = 22,['X'] = 23,
['Y'] = 24,['Z'] = 25,
['a'] = 26,['b'] = 27,['c'] = 28,['d'] = 29,
['e'] = 30,['f'] = 31,['g'] = 32,['h'] = 33,
['i'] = 34,['j'] = 35,['k'] = 36,['l'] = 37,
['m'] = 38,['n'] = 39,['o'] = 40,['p'] = 41,
['q'] = 42,['r'] = 43,['s'] = 44,['t'] = 45,
['u'] = 46,['v'] = 47,['w'] = 48,['x'] = 49,
['y'] = 50,['z'] = 51,
['0'] = 52,['1'] = 53,['2'] = 54,['3'] = 55,
['4'] = 56,['5'] = 57,['6'] = 58,['7'] = 59,
['8'] = 60,['9'] = 61,
['+'] = 62,['/'] = 63,
['='] = 0,
/* All other values are implicitly 0 (or you can mark them as invalid
by setting them to 0x80 if you prefer stricter checking). */
};
len = strlen (data);
/* Validate length: Base64 encoded data must be a multiple of 4 */
if (len % 4 != 0)
return NULL;
/* Count padding characters at the end */
if (len) {
if (data[len - 1] == '=')
pad++;
if (len > 1 && data[len - 2] == '=')
pad++;
}
/* Calculate the length of the decoded data */
decoded_len = (len / 4) * 3 - pad;
out = (char *) xmalloc (decoded_len + 1); /* +1 for a null terminator if needed */
for (i = 0, j = 0; i < len;) {
unsigned int sextet_a = data[i] == '=' ? 0 : dtable[(unsigned char) data[i]];
unsigned int sextet_b = data[i + 1] == '=' ? 0 : dtable[(unsigned char) data[i + 1]];
unsigned int sextet_c = data[i + 2] == '=' ? 0 : dtable[(unsigned char) data[i + 2]];
unsigned int sextet_d = data[i + 3] == '=' ? 0 : dtable[(unsigned char) data[i + 3]];
i += 4;
triple = (sextet_a << 18) | (sextet_b << 12) | (sextet_c << 6) | sextet_d;
if (j < decoded_len)
out[j++] = (triple >> 16) & 0xFF;
if (j < decoded_len)
out[j++] = (triple >> 8) & 0xFF;
if (j < decoded_len)
out[j++] = triple & 0xFF;
}
out[decoded_len] = '\0'; /* Null-terminate the output buffer */
if (out_len)
*out_len = decoded_len;
return out;
}

View File

@ -33,5 +33,6 @@
#include <stddef.h>
char *base64_encode (const void *buf, size_t size);
char *base64_decode (const char *data, size_t *out_len);
#endif // for #ifndef BASE64_H

View File

@ -43,6 +43,7 @@
#include "json.h"
#include "settings.h"
#include "websocket.h"
#include "wsauth.h"
#include "xmalloc.h"
/* Allocate memory for a new GWSReader instance.
@ -356,6 +357,12 @@ set_ws_opts (void) {
ws_set_config_sslcert (conf.sslcert);
if (conf.sslkey)
ws_set_config_sslkey (conf.sslkey);
#ifdef HAVE_LIBSSL
if (conf.ws_auth_secret) {
ws_set_config_auth_secret (conf.ws_auth_secret);
ws_set_config_auth_cb (verify_jwt_token);
}
#endif
}
/* Setup and start the WebSocket threads. */

View File

@ -33,7 +33,9 @@
#endif
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <getopt.h>
#include <errno.h>
@ -47,6 +49,7 @@
#include "error.h"
#include "labels.h"
#include "util.h"
#include "wsauth.h"
#include "xmalloc.h"
@ -153,6 +156,10 @@ static const struct option long_opts[] = {
#endif
{"time-format" , required_argument , 0 , 0 } ,
{"ws-url" , required_argument , 0 , 0 } ,
#ifdef HAVE_LIBSSL
{"ws-auth" , required_argument , 0 , 0 } ,
{"ws-auth-expire" , required_argument , 0 , 0 } ,
#endif
{"ping-interval" , required_argument , 0 , 0 } ,
#ifdef HAVE_GEOLOCATION
{"geoip-database" , required_argument , 0 , 0 } ,
@ -234,6 +241,12 @@ cmd_help (void)
" --ssl-key=<priv.key> - Path to TLS/SSL private key.\n"
" --user-name=<username> - Run as the specified user.\n"
" --ws-url=<url> - URL to which the WebSocket server responds.\n"
#ifdef HAVE_LIBSSL
" --ws-auth=<jwt[:secret]> - Enables WebSocket authentication using a\n"
" JSON Web Token (JWT). Optionally, a secret key\n"
" can be provided for verification.\n"
" --ws-auth-expire=<secs> - Time after which the JWT expires.\n"
#endif
" --ping-interval=<secs> - Enable WebSocket ping with specified\n"
" interval in seconds.\n"
"\n"
@ -365,6 +378,113 @@ set_array_opt (const char *oarg, const char *arr[], int *size, int max) {
arr[(*size)++] = oarg;
}
#ifdef HAVE_LIBSSL
/*
* parse_ws_auth_expire_option:
* Parses a time duration string and converts it to seconds.
*
* Supported formats:
* "3600" -> 3600 seconds
* "24h" -> 24 hours = 24 * 3600 seconds
* "10m" -> 10 minutes = 10 * 60 seconds
* "10d" -> 10 days = 10 * 86400 seconds
*
* Returns 0 on success, nonzero on failure.
*/
static int
parse_ws_auth_expire_option (const char *input) {
char *endptr = NULL;
double value = 0, multiplier = 0, total_seconds = 0;
if (!input || *input == '\0')
return -1; // Empty or NULL string
value = strtod (input, &endptr);
if (endptr == input) {
// No conversion could be performed
return -1;
}
multiplier = 1.0;
/* Skip any whitespace after the number */
while (isspace ((unsigned char) *endptr))
endptr++;
if (*endptr != '\0') {
/* Expect a single unit character; any extra characters are not allowed */
char unit = *endptr;
endptr++;
while (isspace ((unsigned char) *endptr))
endptr++;
if (*endptr != '\0') {
// Extra unexpected characters
return -1;
}
switch (unit) {
case 's':
case 'S':
multiplier = 1.0;
break;
case 'm':
case 'M':
multiplier = 60.0;
break;
case 'h':
case 'H':
multiplier = 3600.0;
break;
case 'd':
case 'D':
multiplier = 86400.0;
break;
default:
// Unknown unit
return -1;
}
}
total_seconds = value * multiplier;
if (total_seconds < 0)
return -1; // Negative durations are not allowed
/* Store the result in your global configuration */
conf.ws_auth_expire = (long) total_seconds;
return 0;
}
static int
parse_ws_auth_option (const char *optarg) {
/* Check if the option starts with "jwt:" */
if (strncmp (optarg, "jwt:", 4) == 0) {
const char *secret_part = optarg + 4;
/* Check if the secret is a file path */
if (access (secret_part, F_OK) == 0) {
/* File exists: read the file content */
conf.ws_auth_secret = read_secret_from_file (secret_part);
} else {
/* Not a file: use the secret directly */
conf.ws_auth_secret = strdup (secret_part);
}
return 0;
}
/* Check for plain "jwt" option */
if (strcmp (optarg, "jwt") == 0) {
/* Try to get secret from environment variable */
const char *env_secret = getenv ("GOACCESS_WSAUTH_SECRET");
if (env_secret != NULL)
conf.ws_auth_secret = strdup (env_secret);
else
conf.ws_auth_secret = generate_ws_auth_secret ();
return 0;
}
return 1;
}
#endif
/* Parse command line long options. */
static void
parse_long_opt (const char *name, const char *oarg) {
@ -540,6 +660,17 @@ parse_long_opt (const char *name, const char *oarg) {
if (!strcmp ("ws-url", name))
conf.ws_url = oarg;
#ifdef HAVE_LIBSSL
/* WebSocket auth */
if (!strcmp ("ws-auth", name) && parse_ws_auth_option (oarg) != 0)
FATAL ("Invalid --ws-auth option.");
/* WebSocket auth JWT expires */
if (!strcmp ("ws-auth-expire", name) && parse_ws_auth_expire_option (oarg) != 0)
FATAL ("Invalid --ws-auth-expire option.");
#endif
/* WebSocket ping interval in seconds */
if (!strcmp ("ping-interval", name))
conf.ping_interval = oarg;

View File

@ -46,6 +46,7 @@
#include "settings.h"
#include "ui.h"
#include "util.h"
#include "wsauth.h"
#include "xmalloc.h"
#include "tpls.h"
@ -289,7 +290,10 @@ print_html_body (FILE * fp, const char *now)
fprintf (fp,
"<nav class='hidden-xs hidden-sm hide' aria-label='Main navigation'>"
"</nav>"
"<div class='loading-container'>"
"<i class='spinner fa fa-circle-o-notch fa-spin fa-3x fa-fw' aria-hidden='true' aria-label='Loading'></i>"
"<div class='app-loading-status'><small class='muted'></small></div>"
"</div>"
"<div class='container hide' role='document'>"
"<header class='page-header'>"
"<h1 class='h-dashboard'>"
@ -501,14 +505,14 @@ hits_bw_req_plot (FILE *fp, GHTMLPlot plot, int sp) {
/* Output JSON data definitions. */
static void
print_json_data (FILE *fp, GHolder *holder) {
print_json_data (FILE *fp, GHolder *holder, const char *jwt) {
char *json = NULL;
if ((json = get_json (holder, 1)) == NULL)
return;
fprintf (fp, external_assets ? "" : "<script type='text/javascript'>");
fprintf (fp, "var json_data=%s", json);
fprintf (fp, "var json_data=%s", (conf.ws_auth_secret && jwt ? "{}" : json));
fprintf (fp, external_assets ? "\n" : "</script>");
free (json);
@ -516,7 +520,7 @@ print_json_data (FILE *fp, GHolder *holder) {
/* Output WebSocket connection definition. */
static void
print_conn_def (FILE *fp) {
print_conn_def (FILE *fp, const char *jwt) {
int sp = 0;
/* use tabs to prettify output */
if (conf.json_pretty_print)
@ -527,6 +531,9 @@ print_conn_def (FILE *fp) {
fprintf (fp, external_assets ? "" : "<script type='text/javascript'>");
if (conf.ws_auth_secret && jwt)
fprintf (fp, "window.goaccessJWT=\"%s\";", jwt);
fprintf (fp, "var connection = ");
fpopen_obj (fp, sp);
fpskeysval (fp, "url", (conf.ws_url ? conf.ws_url : ""), sp, 0);
@ -1279,6 +1286,7 @@ void
output_html (GHolder *holder, const char *filename) {
FILE *fp, *fjs = NULL, *fcs = NULL;
char now[DATE_TIME] = { 0 };
char *jwt = NULL;
if (filename != NULL)
fp = fopen (filename, "w");
@ -1302,15 +1310,21 @@ output_html (GHolder *holder, const char *filename) {
generate_time ();
strftime (now, DATE_TIME, "%Y-%m-%d %H:%M:%S %z", &now_tm);
#ifdef HAVE_LIBSSL
jwt = create_jwt_token ();
#endif
print_html_header (fp, fcs);
print_html_body (fp, now);
print_json_defs ((fjs ? fjs : fp));
print_json_data ((fjs ? fjs : fp), holder);
print_conn_def ((fjs ? fjs : fp));
print_json_data ((fjs ? fjs : fp), holder, jwt);
print_conn_def ((fjs ? fjs : fp), jwt);
print_html_footer (fp, fjs);
free (jwt);
if (fjs)
fclose (fjs);
if (fcs)

View File

@ -358,26 +358,6 @@ free_glog (GLogItem *logitem) {
free (logitem);
}
/* Decodes the given URL-encoded string.
*
* On success, the decoded string is assigned to the output buffer. */
#define B16210(x) (((x) >= '0' && (x) <= '9') ? ((x) - '0') : (toupper((unsigned char) (x)) - 'A' + 10))
static void
decode_hex (char *url, char *out) {
char *ptr;
const char *c;
for (c = url, ptr = out; *c; c++) {
if (*c != '%' || !isxdigit ((unsigned char) c[1]) || !isxdigit ((unsigned char) c[2])) {
*ptr++ = *c;
} else {
*ptr++ = (char) ((B16210 (c[1]) * 16) + (B16210 (c[2])));
c += 2;
}
}
*ptr = 0;
}
/* Entry point to decode the given URL-encoded string.
*
* On success, the decoded trimmed string is assigned to the output
@ -390,10 +370,10 @@ decode_url (char *url) {
return NULL;
out = decoded = xstrdup (url);
decode_hex (url, out);
decode_hex (url, out, 0);
/* double encoded URL? */
if (conf.double_decode)
decode_hex (decoded, out);
decode_hex (decoded, out, 0);
strip_newlines (out);
return trim_str (out);

View File

@ -249,6 +249,7 @@ free_cmd_args (void) {
free (nargv[i]);
free (nargv);
free (conf.iconfigfile);
free (conf.ws_auth_secret);
}
/* Append extra value to argv */

View File

@ -146,6 +146,8 @@ typedef struct GConf_
const char *ws_url; /* WebSocket URL */
const char *ping_interval; /* WebSocket ping interval in seconds */
const char *unix_socket; /* unix socket to bind to */
char *ws_auth_secret; /* WebSocket AUTH */
long ws_auth_expire; /* WebSocket AUTH JWT expire in seconds */
/* User flags */
int all_static_files; /* parse all static files */

View File

@ -973,6 +973,29 @@ u642str (uint64_t d, int width) {
return s;
}
/* Decodes the given URL-encoded string.
*
* On success, the decoded string is assigned to the output buffer. */
#define B16TOD(x) (((x) >= '0' && (x) <= '9') ? ((x) - '0') : (toupper((unsigned char) (x)) - 'A' + 10))
void
decode_hex (char *url, char *out, int decode_plus) {
char *ptr;
const char *c;
for (c = url, ptr = out; *c; c++) {
if (*c != '%' || !isxdigit ((unsigned char) c[1]) || !isxdigit ((unsigned char) c[2])) {
if (decode_plus && *c == '+') {
*ptr++ = ' ';
} else {
*ptr++ = *c;
}
} else {
*ptr++ = (char) ((B16TOD (c[1]) * 16) + (B16TOD (c[2])));
c += 2;
}
}
*ptr = 0;
}
/* Convert the given float to a string with the ability to add some
* padding.
*

View File

@ -105,6 +105,7 @@ off_t file_size (const char *filename);
size_t append_str (char **dest, const char *src);
uint32_t djb2 (const unsigned char *str);
uint64_t u64encode (uint32_t x, uint32_t y);
void decode_hex(char *url, char *out, int decode_plus);
void genstr (char *dest, size_t len);
void set_tz (void);
void strip_newlines (char *str);

View File

@ -437,6 +437,8 @@ ws_free_header_fields (WSHeaders *headers) {
free (headers->path);
if (headers->protocol)
free (headers->protocol);
if (headers->jwt)
free (headers->jwt);
if (headers->upgrade)
free (headers->upgrade);
if (headers->ws_accept)
@ -1129,13 +1131,48 @@ ws_verify_req_headers (WSHeaders *headers) {
return 0;
}
/* Extract JWT token from query string */
static char *
ws_extract_jwt_token (char *path) {
char decoded_token[8192] = { 0 }; // Adjust size as needed
char *query = NULL, *tokenParam = NULL, *end = NULL;
if (!path)
return NULL;
// Look for a query string in the path
query = strchr (path, '?');
if (!query)
return NULL;
query++; // Skip the '?'
// Look for the token parameter
tokenParam = strstr (query, "token=");
if (!tokenParam)
return NULL;
tokenParam += strlen ("token="); // Move pointer past "token="
// Decode the token
decode_hex (tokenParam, decoded_token, 1);
// Find the end of the token (next '&' or end of string)
end = strpbrk (decoded_token, "& ");
if (end)
*end = '\0';
// Return a dynamically allocated copy of the decoded token
return strdup (decoded_token);
}
/* From RFC2616, each header field consists of a name followed by a
* colon (":") and the field value. Field names are case-insensitive.
* The field value MAY be preceded by any amount of LWS, though a
* single SP is preferred */
static int
ws_set_header_fields (char *line, WSHeaders *headers) {
char *path = NULL, *method = NULL, *proto = NULL, *p, *value;
char *path = NULL, *method = NULL, *proto = NULL;
char *p = NULL, *value = NULL;
if (line[0] == '\n' || line[0] == '\r')
return 1;
@ -1143,10 +1180,14 @@ ws_set_header_fields (char *line, WSHeaders *headers) {
if ((strstr (line, "GET ")) || (strstr (line, "get "))) {
if ((path = ws_parse_request (line, &method, &proto)) == NULL)
return 1;
headers->path = path;
headers->method = method;
headers->protocol = proto;
/* Extract JWT token from path */
headers->jwt = ws_extract_jwt_token (path);
return 0;
}
@ -1161,15 +1202,16 @@ ws_set_header_fields (char *line, WSHeaders *headers) {
return 1;
*p = '\0';
if (strpbrk (line, " \t") != NULL) {
*p = ' ';
return 1;
}
while (isspace ((unsigned char) *value))
value++;
ws_set_header_key_value (headers, line, value);
return 0;
}
@ -1619,6 +1661,13 @@ ws_get_handshake (WSClient *client, WSServer *server) {
return ws_set_status (client, WS_CLOSE, bytes);
}
/* Ensure we can authenticate the connection */
if (wsconfig.auth_secret && wsconfig.auth (client->headers->jwt, wsconfig.auth_secret) != 1) {
LOG (("Unable to authenticate connection %d [%s]...\n", client->listener, client->remote_ip));
http_error (client, WS_UNAUTHORIZED_STR);
return ws_set_status (client, WS_CLOSE, bytes);
}
ws_set_handshake_headers (client->headers);
/* handshake response */
@ -2961,6 +3010,17 @@ ws_set_config_sslkey (const char *sslkey) {
wsconfig.sslkey = sslkey;
}
/* Set auth secret for auth. */
void
ws_set_config_auth_secret (const char *auth_secret) {
wsconfig.auth_secret = auth_secret;
}
void
ws_set_config_auth_cb (int (*auth_cb) (const char *jwt, const char *secret)) {
wsconfig.auth = auth_cb;
}
/* Create a new websocket server context. */
WSServer *
ws_init (const char *host, const char *port, void (*initopts) (void)) {

View File

@ -102,6 +102,7 @@
#include "gslist.h"
#define WS_BAD_REQUEST_STR "HTTP/1.1 400 Invalid Request\r\n\r\n"
#define WS_UNAUTHORIZED_STR "HTTP/1.1 401 Unauthorized\r\n\r\n"
#define WS_SWITCH_PROTO_STR "HTTP/1.1 101 Switching Protocols"
#define WS_TOO_BUSY_STR "HTTP/1.1 503 Service Unavailable\r\n\r\n"
@ -178,6 +179,7 @@ typedef struct WSHeaders_ {
char *agent;
char *path;
char *jwt;
char *method;
char *protocol;
char *host;
@ -272,6 +274,11 @@ typedef struct WSConfig_ {
const char *sslcert;
const char *sslkey;
const char *unix_socket;
const char *auth_secret;
/* Function pointer for JWT verification */
int (*auth) (const char *jwt, const char *secret);
int echomode;
int strict;
int max_frm_size;
@ -322,6 +329,8 @@ void ws_set_config_port (const char *port);
void ws_set_config_sslcert (const char *sslcert);
void ws_set_config_sslkey (const char *sslkey);
void ws_set_config_strict (int strict);
void ws_set_config_auth_secret (const char *auth_secret);
void ws_set_config_auth_cb (int (*auth_cb) (const char *jwt, const char *secret));
void ws_start (WSServer * server);
void ws_stop (WSServer * server);
WSServer *ws_init (const char *host, const char *port, void (*initopts) (void));

421
src/wsauth.c Normal file
View File

@ -0,0 +1,421 @@
/**
* wsauth.c - web socket authentication
* ______ ___
* / ____/___ / | _____________ __________
* / / __/ __ \/ /| |/ ___/ ___/ _ \/ ___/ ___/
* / /_/ / /_/ / ___ / /__/ /__/ __(__ |__ )
* \____/\____/_/ |_\___/\___/\___/____/____/
*
* The MIT License (MIT)
* Copyright (c) 2009-2024 Gerardo Orellana <hello @ goaccess.io>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include <errno.h>
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <inttypes.h>
#include <unistd.h>
#include <limits.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/evp.h>
#include "wsauth.h"
#include "base64.h"
#include "util.h"
#include "pdjson.h"
#include "settings.h"
#include "xmalloc.h"
char *
read_secret_from_file (const char *path) {
FILE *file = fopen (path, "r");
char *secret = xcalloc (1, MAX_SECRET_SIZE);
if (!file) {
perror ("Error opening secret file");
return NULL;
}
if (!secret) {
perror ("Error allocating memory");
fclose (file);
return NULL;
}
if (!fgets (secret, MAX_SECRET_SIZE, file)) {
perror ("Error reading secret file");
free (secret);
fclose (file);
return NULL;
}
fclose (file);
// Remove trailing newline, if present.
secret[strcspn (secret, "\n")] = '\0';
return secret;
}
/* Generate a new secret (HS256-compatible) and return it as a hex string. */
char *
generate_ws_auth_secret (void) {
char *secret_hex = NULL;
int secret_len = 32; // 256 bits
unsigned char secret_bytes[32];
if (RAND_bytes (secret_bytes, secret_len) != 1) {
fprintf (stderr, "Error generating random bytes\n");
return NULL;
}
secret_hex = xmalloc (secret_len * 2 + 1);
if (!secret_hex)
return NULL;
for (int i = 0; i < secret_len; i++) {
sprintf (&secret_hex[i * 2], "%02x", secret_bytes[i]);
}
secret_hex[secret_len * 2] = '\0';
return secret_hex;
}
static char *
create_jwt_payload (const char *sub, long iat, long exp) {
const char *aud = "goaccess_ws";
const char *scope = "report_access";
char *payload = NULL;
char hostname[HOST_NAME_MAX];
if (gethostname (hostname, sizeof (hostname)) != 0) {
perror ("gethostname");
// Fallback to a default issuer value if hostname retrieval fails.
strcpy (hostname, "goaccess");
}
// Allocate a buffer for the payload JSON.
// Adjust the size if you plan on including more data.
payload = xcalloc (1, MAX_JWT_PAYLOAD);
if (!payload)
return NULL;
// Build the JSON payload.
snprintf (payload, MAX_JWT_PAYLOAD,
"{\"iss\":\"%s\",\"sub\":\"%s\",\"iat\":%ld,\"exp\":%ld,\"aud\":\"%s\",\"scope\":\"%s\"}",
hostname, sub, iat, exp, aud, scope);
return payload;
}
char *
create_jwt_token (void) {
char *jwt = NULL, *payload = NULL, report_id[50];
time_t now = time (NULL);
struct tm *jwt_now_tm = localtime (&now);
// Configure token lifetime in seconds
long token_lifetime = conf.ws_auth_expire > 0 ? conf.ws_auth_expire : DEFAULT_EXPIRE_TIME;
long iat = now;
long exp = now + token_lifetime;
// Format the date as "YYYYMMDD" for the report ID
snprintf (report_id, sizeof (report_id), "goaccess_report_%04d%02d%02d",
jwt_now_tm->tm_year + 1900, jwt_now_tm->tm_mon + 1, jwt_now_tm->tm_mday);
if (!conf.ws_auth_secret)
return NULL;
payload = create_jwt_payload (report_id, iat, exp);
if (!payload) {
fprintf (stderr, "Failed to create JWT payload\n");
return NULL;
}
jwt = generate_jwt (conf.ws_auth_secret, payload);
free (payload);
return jwt;
}
static int
verify_jwt_signature (const char *jwt, const char *secret) {
char *token_dup = NULL, *header_part = NULL, *payload_part = NULL, *signature_part = NULL,
*signing_input = NULL, *computed_signature = NULL;
unsigned char *hmac_result = NULL;
unsigned int hmac_len = 0;
int valid = 0;
size_t signing_input_len = 0;
if (!jwt || !secret) {
return 0;
}
token_dup = strdup (jwt);
if (!token_dup) {
return 0;
}
header_part = strtok (token_dup, ".");
payload_part = strtok (NULL, ".");
signature_part = strtok (NULL, ".");
if (!header_part || !payload_part || !signature_part) {
free (token_dup);
return 0;
}
signing_input_len = strlen (header_part) + 1 + strlen (payload_part) + 1;
signing_input = malloc (signing_input_len);
if (!signing_input) {
free (token_dup);
return 0;
}
snprintf (signing_input, signing_input_len, "%s.%s", header_part, payload_part);
hmac_result = HMAC (EVP_sha256 (), secret, strlen (secret), (unsigned char *) signing_input,
strlen (signing_input), NULL, &hmac_len);
free (signing_input);
if (!hmac_result) {
free (token_dup);
return 0;
}
computed_signature = base64_encode (hmac_result, hmac_len);
if (!computed_signature) {
free (token_dup);
if (hmac_result)
OPENSSL_free (hmac_result);
return 0;
}
valid = (strcmp (computed_signature, signature_part) == 0);
free (computed_signature);
free (token_dup);
return valid;
}
static int
validate_jwt_claims (const char *payload_json) {
json_stream json;
enum json_type t = JSON_ERROR;
size_t len = 0, level = 0;
enum json_type ctx = JSON_ERROR;
char hostname[HOST_NAME_MAX] = { 0 };
char *curr_key = NULL;
/* Validation flags/values. */
int valid_iss = 0, valid_sub = 0, valid_aud = 0, valid_scope = 0;
long iat = 0, exp = 0;
time_t now = time (NULL);
/* Get hostname for the issuer check. */
if (gethostname (hostname, sizeof (hostname)) != 0) {
perror ("gethostname");
strcpy (hostname, "goaccess");
}
/* Open JSON payload as a stream and disable streaming mode. */
json_open_string (&json, payload_json);
json_set_streaming (&json, false);
/* The payload should be a JSON object. */
t = json_next (&json);
if (t != JSON_OBJECT) {
json_close (&json);
return 0;
}
/* Iterate over each token (key or value) in the JSON object. */
while ((t = json_next (&json)) != JSON_DONE && t != JSON_ERROR) {
ctx = json_get_context (&json, &level);
/* keys typically appear when (level % 2) != 0 and not inside an array.
* otherwise, the token is a value. */
if ((level % 2) != 0 && ctx != JSON_ARRAY) {
/* This token is a key. Duplicate it to use for matching. */
if (curr_key)
free (curr_key);
curr_key = xstrdup (json_get_string (&json, &len));
} else {
/* Assume this token is the value for the last encountered key. */
if (curr_key) {
char *val = xstrdup (json_get_string (&json, &len));
if (strcmp (curr_key, "iss") == 0) {
/* "iss" must equal the hostname. */
valid_iss = (strcmp (val, hostname) == 0);
} else if (strcmp (curr_key, "sub") == 0) {
/* "sub" must be non-empty. */
valid_sub = (val[0] != '\0');
} else if (strcmp (curr_key, "aud") == 0) {
valid_aud = (strcmp (val, "goaccess_ws") == 0);
} else if (strcmp (curr_key, "scope") == 0) {
valid_scope = (strcmp (val, "report_access") == 0);
} else if (strcmp (curr_key, "iat") == 0) {
iat = strtol (val, NULL, 10);
} else if (strcmp (curr_key, "exp") == 0) {
exp = strtol (val, NULL, 10);
}
free (val);
free (curr_key);
curr_key = NULL;
}
}
}
if (curr_key)
free (curr_key);
json_close (&json);
/* Final validation: All required properties must be valid and the token time window correct.
* iat must be > 0, exp must be after iat, and the current time must be between iat and exp.
*/
if (valid_iss && valid_sub && valid_aud && valid_scope &&
iat > 0 && exp > iat && now >= iat && now <= exp) {
return 1; // Valid JWT claims.
}
return 0; // One or more claim validations failed.
}
/* verifies the JWT signature.
* Returns 1 if valid, 0 if not.
*/
int
verify_jwt_token (const char *jwt, const char *secret) {
char *payload_part = NULL, *payload_json = NULL, *token_dup = NULL;
size_t payload_len = 0;
int valid_signature = 0, valid_claims = 0;
/* Step 1: Verify the signature */
valid_signature = verify_jwt_signature (jwt, secret);
if (!valid_signature) {
return 0;
}
/* Step 2: Extract the payload part from the JWT */
token_dup = strdup (jwt);
if (!token_dup) {
return 0;
}
strtok (token_dup, "."); // Skip header
payload_part = strtok (NULL, "."); // Get payload
if (!payload_part) {
free (token_dup);
return 0;
}
/* Step 3: Decode the base64url-encoded payload */
payload_json = base64_decode (payload_part, &payload_len);
free (token_dup);
if (!payload_json) {
return 0;
}
/* Null-terminate the payload JSON string for parsing */
payload_json = realloc (payload_json, payload_len + 1);
if (!payload_json) {
return 0;
}
payload_json[payload_len] = '\0';
/* Step 4: Validate the claims */
valid_claims = validate_jwt_claims (payload_json);
/* Clean up */
free (payload_json);
return valid_claims;
}
/* Generate a JWT signed with HMAC-SHA256.
* - secret: the secret key as a string (from conf.ws_auth_secret)
* - payload: a JSON string to be used as the JWT payload.
*
* The JWT header is fixed to {"alg":"HS256","typ":"JWT"}.
*
* The returned JWT is dynamically allocated and must be freed by the caller.
*/
char *
generate_jwt (const char *secret, const char *payload) {
char *encoded_payload = NULL, *encoded_header = NULL, *encoded_signature = NULL;
char *signing_input = NULL;
const char *header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
unsigned char *hmac_result = NULL;
unsigned int hmac_len = 0;
size_t jwt_len = 0, signing_input_len = 0;
char *jwt = NULL;
encoded_header = base64_encode ((const unsigned char *) header, strlen (header));
if (!encoded_header)
return NULL;
encoded_payload = base64_encode ((const unsigned char *) payload, strlen (payload));
if (!encoded_payload) {
free (encoded_header);
return NULL;
}
// Create the signing input: "<encoded_header>.<encoded_payload>"
signing_input_len = strlen (encoded_header) + 1 + strlen (encoded_payload) + 1;
signing_input = malloc (signing_input_len);
if (!signing_input) {
free (encoded_header);
free (encoded_payload);
return NULL;
}
snprintf (signing_input, signing_input_len, "%s.%s", encoded_header, encoded_payload);
// Compute HMAC-SHA256 signature
hmac_result =
HMAC (EVP_sha256 (), secret, strlen (secret), (unsigned char *) signing_input,
strlen (signing_input), NULL, &hmac_len);
if (!hmac_result) {
free (encoded_header);
free (encoded_payload);
free (signing_input);
return NULL;
}
// Base64url-encode the signature
encoded_signature = base64_encode (hmac_result, hmac_len);
if (!encoded_signature) {
free (encoded_header);
free (encoded_payload);
free (signing_input);
return NULL;
}
// Build the final JWT: "<encoded_header>.<encoded_payload>.<encoded_signature>"
jwt_len =
strlen (encoded_header) + 1 + strlen (encoded_payload) + 1 + strlen (encoded_signature) + 1;
jwt = malloc (jwt_len);
if (!jwt) {
free (encoded_header);
free (encoded_payload);
free (signing_input);
free (encoded_signature);
return NULL;
}
snprintf (jwt, jwt_len, "%s.%s.%s", encoded_header, encoded_payload, encoded_signature);
free (encoded_header);
free (encoded_payload);
free (signing_input);
free (encoded_signature);
return jwt;
}

43
src/wsauth.h Normal file
View File

@ -0,0 +1,43 @@
/**
* ______ ___
* / ____/___ / | _____________ __________
* / / __/ __ \/ /| |/ ___/ ___/ _ \/ ___/ ___/
* / /_/ / /_/ / ___ / /__/ /__/ __(__ |__ )
* \____/\____/_/ |_\___/\___/\___/____/____/
*
* The MIT License (MIT)
* Copyright (c) 2009-2024 Gerardo Orellana <hello @ goaccess.io>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef WSAUTH_H_INCLUDED
#define WSAUTH_H_INCLUDED
#define MAX_SECRET_SIZE 256
#define MAX_JWT_PAYLOAD 512
#define DEFAULT_EXPIRE_TIME 28800 // seconds
char *create_jwt_token (void);
char *generate_jwt (const char *secret, const char *payload);
char *generate_ws_auth_secret (void);
char *read_secret_from_file (const char *path);
int verify_jwt_token (const char *jwt, const char *secret);
#endif // for #ifndef WSAUTH_H