The idle timeout is opt-in: when set with setSessionIdleTimeout(), a session expires after that period without a successful request and every successful request extends it. When unset (the default), sessions are not touched and keep the previous behavior: the absolute tokenTtl applied to created, or living until logout. Both deadlines can be combined and the earlier one wins. - SessionRec: add last_used, drop the unused refresh_token field - checkToken(): update last_used only while the idle timeout is enabled - tests: IdleTimeout, IdleActivityExtendsSession, IdleDisabledKeepsSession, HasSessionIdleTimeout
200 lines
5.7 KiB
C++
200 lines
5.7 KiB
C++
#include "pihttpserversessionauth.h"
|
|
|
|
#include "pidigest.h"
|
|
#include "pijson.h"
|
|
#include "piliterals_string.h"
|
|
|
|
|
|
namespace {
|
|
|
|
PIHTTP::MessageMutable jsonReply(PIHTTP::Code code, const PIJSON & j) {
|
|
return PIHTTP::MessageMutable::fromCode(code)
|
|
.addHeader(PIHTTP::Header::ContentType, "application/json")
|
|
.setBody(j.toJSON(PIJSON::Compact).toByteArray());
|
|
}
|
|
|
|
|
|
PIHTTP::MessageMutable errorReply(PIHTTP::Code code, const PIString & error) {
|
|
PIJSON j;
|
|
j["error"] = error;
|
|
return jsonReply(code, j);
|
|
}
|
|
|
|
|
|
bool parseToken(const PIString & auth_header, PIString & token) {
|
|
if (auth_header.left(7).toLowerCase() != "bearer "_a) return false;
|
|
PIString rest = auth_header.mid(7).trimmed();
|
|
if (rest.isEmpty()) return false;
|
|
token = rest;
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
PIHTTPServerSessionAuth::PIHTTPServerSessionAuth()
|
|
: token_length(PIDigest::hashLength(PIDigest::Type::BLAKE2b_512))
|
|
, login_path("/api/login"_a)
|
|
, logout_path("/api/logout"_a) {
|
|
setBearerAuthCallback([this](const PIString & token) -> PIHTTP::AuthInfo { return checkToken(token); });
|
|
registerLoginRoute();
|
|
registerLogoutRoute();
|
|
}
|
|
|
|
|
|
void PIHTTPServerSessionAuth::registerLoginRoute() {
|
|
registerPath(login_path, PIHTTP::Method::Post, [this](const PIHTTP::MessageConst & r) { return handleLogin(r); });
|
|
}
|
|
|
|
|
|
void PIHTTPServerSessionAuth::registerLogoutRoute() {
|
|
registerProtectedPath(logout_path, PIHTTP::Method::Post, [this](const PIHTTP::MessageConst & r, const PIHTTP::AuthInfo &) {
|
|
return handleLogout(r);
|
|
});
|
|
}
|
|
|
|
|
|
void PIHTTPServerSessionAuth::setLoginPath(const PIString & p) {
|
|
if (p.isEmpty() || p == login_path) return;
|
|
unregisterPath(login_path, PIHTTP::Method::Post);
|
|
login_path = p;
|
|
registerLoginRoute();
|
|
}
|
|
|
|
|
|
void PIHTTPServerSessionAuth::setLogoutPath(const PIString & p) {
|
|
if (p.isEmpty() || p == logout_path) return;
|
|
unregisterPath(logout_path, PIHTTP::Method::Post);
|
|
logout_path = p;
|
|
registerLogoutRoute();
|
|
}
|
|
|
|
|
|
PIHTTP::AuthInfo PIHTTPServerSessionAuth::checkToken(const PIString & token) {
|
|
PIMutexLocker locker(mutex);
|
|
SessionRec s = sessions.value(token);
|
|
if (s.isInvalid()) return false;
|
|
const PISystemTime now = PISystemTime::current();
|
|
if (hasTokenTtl() && now - s.created >= token_ttl) {
|
|
sessions.remove(token);
|
|
return false;
|
|
}
|
|
if (hasSessionIdleTimeout()) {
|
|
const PISystemTime base = s.last_used.isNull() ? s.created : s.last_used;
|
|
if (now - base >= session_idle_timeout) {
|
|
sessions.remove(token);
|
|
return false;
|
|
}
|
|
sessions[token].last_used = now;
|
|
}
|
|
return {true, s.user_id};
|
|
}
|
|
|
|
|
|
PIString PIHTTPServerSessionAuth::issueToken(int user_id) {
|
|
PIMutexLocker locker(mutex);
|
|
PIByteArray data = token_generator ? token_generator(token_length) : defaultTokenData(token_length);
|
|
if (data.isEmpty()) return PIString();
|
|
PIString token = data.toHex();
|
|
SessionRec & s(sessions[token]);
|
|
s.user_id = user_id;
|
|
s.created = PISystemTime::current();
|
|
s.last_used = s.created;
|
|
return token;
|
|
}
|
|
|
|
|
|
bool PIHTTPServerSessionAuth::revokeToken(const PIString & token) {
|
|
PIMutexLocker locker(mutex);
|
|
if (!sessions.contains(token)) return false;
|
|
sessions.remove(token);
|
|
return true;
|
|
}
|
|
|
|
|
|
void PIHTTPServerSessionAuth::revokeUserTokens(int user_id) {
|
|
PIMutexLocker locker(mutex);
|
|
sessions.removeWhere([user_id](const PIString &, const SessionRec & s) { return s.user_id == user_id; });
|
|
}
|
|
|
|
|
|
void PIHTTPServerSessionAuth::setTokenGenerator(TokenGenerator g) {
|
|
PIMutexLocker locker(mutex);
|
|
token_generator = std::move(g);
|
|
}
|
|
|
|
|
|
void PIHTTPServerSessionAuth::setTokenTtl(PISystemTime ttl) {
|
|
PIMutexLocker locker(mutex);
|
|
token_ttl = ttl;
|
|
}
|
|
|
|
|
|
PISystemTime PIHTTPServerSessionAuth::tokenTtl() const {
|
|
PIMutexLocker locker(mutex);
|
|
return token_ttl;
|
|
}
|
|
|
|
|
|
bool PIHTTPServerSessionAuth::hasTokenTtl() const {
|
|
PIMutexLocker locker(mutex);
|
|
return !token_ttl.isNull();
|
|
}
|
|
|
|
|
|
void PIHTTPServerSessionAuth::setSessionIdleTimeout(PISystemTime t) {
|
|
PIMutexLocker locker(mutex);
|
|
session_idle_timeout = t;
|
|
}
|
|
|
|
|
|
PISystemTime PIHTTPServerSessionAuth::sessionIdleTimeout() const {
|
|
PIMutexLocker locker(mutex);
|
|
return session_idle_timeout;
|
|
}
|
|
|
|
|
|
bool PIHTTPServerSessionAuth::hasSessionIdleTimeout() const {
|
|
PIMutexLocker locker(mutex);
|
|
return !session_idle_timeout.isNull();
|
|
}
|
|
|
|
|
|
PIHTTP::MessageMutable PIHTTPServerSessionAuth::handleLogin(const PIHTTP::MessageConst & request) {
|
|
PIJSON j = PIJSON::fromJSON(PIString::fromUTF8(request.body()));
|
|
if (!j.isObject() || !j.contains("login") || !j.contains("password") || j["login"].toString().isEmpty())
|
|
return errorReply(PIHTTP::Code::BadRequest, "invalid json");
|
|
PIString login = j["login"].toString();
|
|
PIString password = j["password"].toString();
|
|
// The credential check runs without the internal lock held: the subclass owns the user
|
|
// storage and may take its own locks.
|
|
const auto auth_info = checkCredentials(login, password);
|
|
if (!auth_info.authorized) return errorReply(PIHTTP::Code::Unauthorized, "invalid credentials");
|
|
PIString token = issueToken(auth_info.user_id);
|
|
if (token.isEmpty()) return errorReply(PIHTTP::Code::InternalServerError, "token generation failed");
|
|
PIJSON resp;
|
|
resp["token"] = token;
|
|
resp["user_id"] = auth_info.user_id;
|
|
return jsonReply(PIHTTP::Code::Ok, resp);
|
|
}
|
|
|
|
|
|
PIHTTP::MessageMutable PIHTTPServerSessionAuth::handleLogout(const PIHTTP::MessageConst & request) {
|
|
PIString token;
|
|
if (parseToken(request.headers().value(PIHTTP::Header::Authorization, ""), token)) revokeToken(token);
|
|
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::NoContent);
|
|
}
|
|
|
|
|
|
PIByteArray PIHTTPServerSessionAuth::defaultTokenData(uint len) {
|
|
static std::atomic<ullong> counter(0);
|
|
PIByteArray in = piSerialize(PISystemTime::current());
|
|
in << len << counter++;
|
|
PIByteArray out;
|
|
while (out.size() < len) {
|
|
out.append(PIDigest::calculate(in, PIDigest::Type::BLAKE2b_512));
|
|
in = out;
|
|
in << counter++;
|
|
}
|
|
return out.resized(len);
|
|
} |