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

Add multiple customization/advance options #2

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ itself. A config file can look like:

```
nthreads = 4;
auth_per_second = 2;
totp_generations = 1;
path_prefix = "";
auth_path = "/auth";
login_path = "/login";
logout_path = "/logout";
secret = "some-random-string-that-is-relatively-long-used-for-cookie-minting";
webs = (
{
Expand All @@ -32,17 +38,31 @@ webs = (
password = "password123!";
totp = "base32otpsecretgoeshere";
duration = 3600;
},
{
username = "user2";
password = "password123!";
totp = "base32otpsecretgoeshere";
algorithm = 512;
digits = 6;
period = 30;
duration = 3600;
}
);
},
{
hostname = "anotherweb.com";
template = "customtemplate";
path_prefix = "/all-paths-will-prefix-with-this"
auth_path = "/something-else"
totp_only = true;
totp_generations = 0;
users = (
{
username = "user2";
password = "password123456";
username = "more-like-remainder-here";
totp = "base32otpsecretgoeshere";
digits = 6;
period = 30;
duration = 7200;
}
);
Expand All @@ -57,7 +77,12 @@ at startup, and this will cause logout of all users on a server restart.

`hostname` must match the hostname for the vhost in the nginx configuration. Then
for each entry a list of users can be defined with their username, password and
totp secret (base32 encoded string). The duration is the cookie lifetime in seconds.
totp secret (base32 encoded string). Can also specify optional advanced option for
TOTP digits, period and algorithm (1 = HMAC-SHA1, 256 = HMAC-SHA256, 512 = HMAC-SHA512).
The duration is the cookie lifetime in seconds.

For TOTP Only mode, all users (totp secrets) are tried, any one matches will pass
the authentication.

The authenticator supports templates. By default there's one called "gradient", but
more can be added. The templates are built in, so one must recompile the binary
Expand Down
121 changes: 97 additions & 24 deletions server.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <mutex>
#include <regex>
#include <memory>
#include <cmath>
#include <unordered_map>
#include <fstream>
#include <fcgio.h>
Expand All @@ -37,18 +38,31 @@ int nthreads = 4;
// 2 would mean the last 2 and future 2 are valid, and so on.
unsigned totp_generations = 1;

const char* default_path_prefix = "";
const char* default_auth_path = "/auth";
const char* default_login_path = "/login";
const char* default_logout_path = "/logout";

#define MAX_REQ_SIZE (4*1024)
#define RET_ERR(x) { std::cerr << x << std::endl; return 1; }

typedef std::unordered_map<std::string, std::string> StrMap;

struct cred_t {
std::string password, totp; // Pass and TOTP (binary)
uint16_t algorithm; // TOTP Algorithm -> HMAC-SHA<X>
uint8_t digits; // Digits of TOTP
uint32_t period; // Period of TOTP
unsigned sduration; // Duration of a valid session (seconds)
};

struct web_t {
std::string webtemplate; // Template to use
std::string auth_path; // Path for nginx auth endpoint
std::string login_path; // Path for login endpoint
std::string logout_path; // Path for logout endpoint
bool totp_only; // Only TOTP, without username/password
unsigned totp_generations; // see comment for global totp_generations
std::unordered_map<std::string, cred_t> users; // User to credential
};

Expand Down Expand Up @@ -108,14 +122,30 @@ class AuthenticationServer {
return (hmac == hmac_calc);
}

bool validate_cred(std::string &user, std::string pass, unsigned totp, const web_t *wcfg) {
if (wcfg->totp_only) {
for (auto pair : wcfg->users)
if (totp_valid(pair.second, totp, wcfg->totp_generations)) {
user = pair.first;
return true;
}
return false;
}
else {
return wcfg->users.count(user) &&
wcfg->users.at(user).password == pass &&
totp_valid(wcfg->users.at(user), totp, wcfg->totp_generations);
}
}

std::string process_req(web_req *req, const web_t *wcfg) {
std::string rpage = req->getvars["follow_page"];
if (rpage.empty())
rpage = req->postvars["follow_page"];
if (rpage.empty())
rpage = "/"; // Make sure we never return empty location, default to index

if (req->uri == "/auth") {
if (req->uri == wcfg->auth_path) {
// Read cookie and validate the authorization
bool authed = check_cookie(req->cookies["authentication-token"], wcfg);
if (authed)
Expand All @@ -125,12 +155,12 @@ class AuthenticationServer {
return "Status: 401\r\nContent-Type: text/plain\r\n"
"Content-Length: 21\r\n\r\nAuthentication Denied";
}
else if (req->uri == "/login") {
else if (req->uri == wcfg->login_path) {
// Die hard if someone's bruteforcing this
if (rl->check(req->ip64)) {
std::cerr << "Rate limit hit for ip id " << req->ip64 << std::endl;
return "Status: 429\r\nContent-Type: text/plain\r\n"
"Content-Length: 34\r\n\r\nToo many requests, request blocked";
"Content-Length: 34\r\n\r\nToo many requests, request blocked";
}
rl->consume(req->ip64);

Expand All @@ -139,17 +169,17 @@ class AuthenticationServer {
std::string user = req->postvars["username"];
std::string pass = req->postvars["password"];
unsigned totp = atoi(req->postvars["totp"].c_str());
std::cerr << "Login attempt for user " << user << std::endl;
if (wcfg->totp_only)
std::cerr << "Login attempt at " << req->host << std::endl;
else
std::cerr << "Login attempt for user " << user << " at " << req->host << std::endl;
// Validate the authentication to issue a cookie or throw an error
if (wcfg->users.count(user) &&
wcfg->users.at(user).password == pass &&
totp_valid(wcfg->users.at(user).totp, totp, totp_generations)) {

if (validate_cred(user, pass, totp, wcfg)) {
std::cerr << "Login with user " << user << " successful" << std::endl;

// Render a redirect page to the redirect address (+cookie)
std::string token = create_cookie(user);
return "Status: 302\r\nSet-Cookie: authentication-token=" + token +
return "Status: 302\r\nSet-Cookie: authentication-token=" + token + "; Path=/" +
"\r\nLocation: " + stripnl(rpage) + "\r\n\r\n";
}
else
Expand All @@ -161,18 +191,18 @@ class AuthenticationServer {
return "Status: 500\r\nContent-Type: text/plain\r\n"
"Content-Length: 23\r\n\r\nCould not find template";
else {
std::string page = templates.at(wcfg->webtemplate)(req->host, rpage, lerror);
std::string page = templates.at(wcfg->webtemplate)(req->host, rpage, wcfg->login_path, wcfg->totp_only, lerror);
return "Status: 200\r\nContent-Type: text/html\r\n"
"Content-Length: " + std::to_string(page.size()) + "\r\n\r\n" + page;
}
}
else if (req->uri == "/logout") {
else if (req->uri == wcfg->logout_path) {
// Just redirect to the page (if present, otherwise login) deleting cookie
return "Status: 302\r\nSet-Cookie: authentication-token=null\r\n"
"Location: /login\r\n\r\n";
}
return "Status: 404\r\nContent-Type: text/plain\r\n"
"Content-Length: 48\r\nNot found, valid endpoints: /auth /login /logout\r\n\r\n";
"Content-Length: 48\r\nNot found, valid endpoints: " + wcfg->auth_path + " " + wcfg->login_path + " " + wcfg->logout_path + " " + "\r\n\r\n";
}

public:
Expand All @@ -193,15 +223,15 @@ class AuthenticationServer {
cthread.join();
}

bool totp_valid(std::string key, unsigned input, unsigned generations) {
uint32_t ct = time(0) / 30UL;
for (int i = -(signed)generations; i < (signed)generations; i++)
if (totp_calc(key, ct + i) == input)
bool totp_valid(cred_t user, unsigned input, unsigned generations) {
uint32_t ct = time(0) / user.period;
for (int i = -(signed)generations; i <= (signed)generations; i++)
if (totp_calc(user.totp, user.algorithm, user.digits, ct + i) == input)
return true;
return false;
}

static unsigned totp_calc(std::string key, uint32_t epoch) {
static unsigned totp_calc(std::string key, uint16_t algorithm, uint8_t digits, uint32_t epoch) {
// Key comes in binary format already!
// Concatenate the epoc in big endian fashion
uint8_t msg [8] = {
Expand All @@ -212,15 +242,31 @@ class AuthenticationServer {
(uint8_t)(epoch & 255)
};

std::string hashs = hmac_sha1(key, std::string((char*)msg, sizeof(msg)));
std::string hashs;
unsigned lastbyte;
switch (algorithm) {
case 256:
hashs = hmac_sha256(key, std::string((char*)msg, sizeof(msg)));
lastbyte = 31;
break;
case 512:
hashs = hmac_sha512(key, std::string((char*)msg, sizeof(msg)));
lastbyte = 63;
break;
case 1:
default:
hashs = hmac_sha1(key, std::string((char*)msg, sizeof(msg)));
lastbyte = 19;
break;
}
uint8_t *hash = (uint8_t*)hashs.c_str();

// The last nibble of the hash is an offset:
unsigned off = hash[19] & 15;
unsigned off = hash[lastbyte] & 15;
// The result is a substr in hash at that offset (pick 32 bits)
uint32_t value = (hash[off] << 24) | (hash[off+1] << 16) | (hash[off+2] << 8) | hash[off+3];
value &= 0x7fffffff;
return value % 1000000;
return value % ((uint32_t)pow(10, digits));
}

// Receives requests and processes them by replying via a side http call.
Expand Down Expand Up @@ -313,6 +359,11 @@ int main(int argc, char **argv) {
config_lookup_int(&cfg, "auth_per_second", (int*)&auths_per_second);
// Number of generations to consider valid for an OTP code
config_lookup_int(&cfg, "totp_generations", (int*)&totp_generations);
// Default pathes
config_lookup_string(&cfg, "path_prefix", &default_path_prefix);
config_lookup_string(&cfg, "auth_path", &default_auth_path);
config_lookup_string(&cfg, "login_path", &default_login_path);
config_lookup_string(&cfg, "logout_path", &default_logout_path);
// Secret holds the server secret used to create cookies
const char *secret = "";
config_lookup_string(&cfg, "secret", &secret);
Expand All @@ -328,26 +379,48 @@ int main(int argc, char **argv) {
config_setting_t *webentry = config_setting_get_elem(webs_cfg, i);
config_setting_t *hostname = config_setting_get_member(webentry, "hostname");
config_setting_t *wtemplate = config_setting_get_member(webentry, "template");
config_setting_t *totp_only = config_setting_get_member(webentry, "totp_only");
config_setting_t *totp_gens = config_setting_get_member(webentry, "totp_generations");
config_setting_t *path_prefix = config_setting_get_member(webentry, "path_prefix");
config_setting_t *auth_path = config_setting_get_member(webentry, "auth_path");
config_setting_t *login_path = config_setting_get_member(webentry, "login_path");
config_setting_t *logout_path = config_setting_get_member(webentry, "logout_path");
config_setting_t *users_cfg = config_setting_lookup(webentry, "users");

if (!webentry || !hostname || !wtemplate || !users_cfg)
RET_ERR("hostname, template and users must be present in the web group");

web_t wentry = { .webtemplate = config_setting_get_string(wtemplate)};
const char* wpath_prefix = !path_prefix ? default_path_prefix : config_setting_get_string(path_prefix);

web_t wentry = {
.webtemplate = config_setting_get_string(wtemplate),
.auth_path = std::string(wpath_prefix).append(!auth_path ? default_auth_path : config_setting_get_string(auth_path)),
.login_path = std::string(wpath_prefix).append(!login_path ? default_login_path : config_setting_get_string(login_path)),
.logout_path = std::string(wpath_prefix).append(!logout_path ? default_logout_path : config_setting_get_string(logout_path)),
.totp_only = !totp_only ? false : config_setting_get_bool(totp_only) == CONFIG_TRUE,
.totp_generations = !totp_gens ? totp_generations : (unsigned)config_setting_get_int(totp_gens), };

for (int j = 0; j < config_setting_length(users_cfg); j++) {
config_setting_t *userentry = config_setting_get_elem(users_cfg, j);
config_setting_t *user = config_setting_get_member(userentry, "username");
config_setting_t *pass = config_setting_get_member(userentry, "password");
config_setting_t *totp = config_setting_get_member(userentry, "totp");
config_setting_t *algo = config_setting_get_member(userentry, "algorithm");
config_setting_t *digi = config_setting_get_member(userentry, "digits");
config_setting_t *peri = config_setting_get_member(userentry, "period");
config_setting_t *durt = config_setting_get_member(userentry, "duration");

if (!user || !pass || !totp || !durt)
RET_ERR("username, password, totp and duration must be present in the user group");
if (!wentry.totp_only && !pass)
RET_ERR("either set web group to TOTP only mode or password must be present in the user group");
if (!user || !totp || !durt)
RET_ERR("username, totp and duration must be present in the user group");

wentry.users[config_setting_get_string(user)] = cred_t {
.password = config_setting_get_string(pass),
.password = !pass ? "" : config_setting_get_string(pass),
.totp = b32dec(b32pad(config_setting_get_string(totp))),
.algorithm = !algo ? 1 : (uint16_t)config_setting_get_int(algo),
.digits = !digi ? 6 : (uint8_t)config_setting_get_int(digi),
.period = !peri ? 30UL : (uint32_t)config_setting_get_int(peri),
.sduration = (unsigned)config_setting_get_int(durt), };
}

Expand Down
6 changes: 4 additions & 2 deletions templ.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

# Produce a usable header
assetsh = b"#include <string>\n#include <unordered_map>\n"
assetsh += b"typedef std::string(*t_templatefn)(std::string, std::string, bool);\n"
assetsh += b"typedef std::string(*t_templatefn)(std::string, std::string, std::string, bool, bool);\n"
assetsh += b"extern const std::unordered_map<std::string, t_templatefn> templates;\n"

# Read template HTML files and generate templates.cc asset
Expand All @@ -17,9 +17,11 @@
cont = cont.replace(b"\\", b"\\\\").replace(b'"', b'\\"').replace(b"\n", b"\\n")
cont = cont.replace(b"{{hostname}}", b'" + hostname + "')
cont = cont.replace(b"{{follow_page}}", b'" + follow_page + "')
cont = cont.replace(b"{{login_path}}", b'" + login_path + "')
cont = re.sub(b"{{nototponly}}(.*){{/nototponly}}", b'" + (totp_only ? "" : "\\1") + "', cont)
cont = re.sub(b"{{loginfailed}}(.*){{/loginfailed}}", b'" + (err ? "\\1" : "") + "', cont)

assets += b"std::string login_%d(std::string hostname, std::string follow_page, bool err) {\n" % i
assets += b"std::string login_%d(std::string hostname, std::string follow_page, std::string login_path, bool totp_only, bool err) {\n" % i
assets += b"return \"" + cont + b"\";\n}\n"

fnentries.append(b" {\"%s\", %s},\n" % (f.split(".")[0].encode("utf-8"), b"login_%d" % i))
Expand Down
6 changes: 3 additions & 3 deletions templates/gradient.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@
<body>
<div class="login">
<h1>Login</h1>
<form method="post" action="login">
<input type="text" name="username" placeholder="Username" required="required" />
<input type="password" name="password" placeholder="Password" required="required" />
<form method="post" action="{{login_path}}">
{{nototponly}}<input type="text" name="username" placeholder="Username" required="required" />
<input type="password" name="password" placeholder="Password" required="required" />{{/nototponly}}
<input type="text" name="totp" placeholder="OTP token" required="required" />
<input type="hidden" name="follow_page" value="{{follow_page}}" />
{{loginfailed}}<h4 class="failedp">Login failed</h4>{{/loginfailed}}
Expand Down
14 changes: 14 additions & 0 deletions util.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ static std::string hmac_sha1(std::string key, std::string msg) {
return std::string((char*)hash, sizeof(hash));
}

static std::string hmac_sha256(std::string key, std::string msg) {
uint8_t hash[32];
unsigned hsize = sizeof(hash);
HMAC(EVP_sha256(), key.c_str(), key.size(), (uint8_t*)msg.c_str(), msg.size(), hash, &hsize);
return std::string((char*)hash, sizeof(hash));
}

static std::string hmac_sha512(std::string key, std::string msg) {
uint8_t hash[64];
unsigned hsize = sizeof(hash);
HMAC(EVP_sha512(), key.c_str(), key.size(), (uint8_t*)msg.c_str(), msg.size(), hash, &hsize);
return std::string((char*)hash, sizeof(hash));
}

static std::string randstr() {
char buf[256];
RAND_bytes((uint8_t*)buf, sizeof(buf));
Expand Down