feat: add optional sliding idle timeout to PIHTTPServerSessionAuth
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
This commit is contained in:
@@ -74,10 +74,19 @@ PIHTTP::AuthInfo PIHTTPServerSessionAuth::checkToken(const PIString & token) {
|
||||
PIMutexLocker locker(mutex);
|
||||
SessionRec s = sessions.value(token);
|
||||
if (s.isInvalid()) return false;
|
||||
if (hasTokenTtl() && PISystemTime::current() - s.created >= token_ttl) {
|
||||
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};
|
||||
}
|
||||
|
||||
@@ -88,8 +97,9 @@ PIString PIHTTPServerSessionAuth::issueToken(int user_id) {
|
||||
if (data.isEmpty()) return PIString();
|
||||
PIString token = data.toHex();
|
||||
SessionRec & s(sessions[token]);
|
||||
s.user_id = user_id;
|
||||
s.created = PISystemTime::current();
|
||||
s.user_id = user_id;
|
||||
s.created = PISystemTime::current();
|
||||
s.last_used = s.created;
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -132,6 +142,24 @@ bool PIHTTPServerSessionAuth::hasTokenTtl() const {
|
||||
}
|
||||
|
||||
|
||||
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())
|
||||
|
||||
@@ -103,6 +103,25 @@ public:
|
||||
//! \~russian Возвращает, установлено ли время жизни сессии.
|
||||
bool hasTokenTtl() const;
|
||||
|
||||
//! \~english Enables the idle timeout: a session is denied and removed after "t" without a
|
||||
//! successful request, and every successful request extends it. A null time disables the
|
||||
//! idle timeout (the default), so sessions are not extended and live until \a setTokenTtl()
|
||||
//! expires them or they are revoked. When both are set, the session ends at whichever
|
||||
//! deadline comes first.
|
||||
//! \~russian Включает idle-таймаут: сессия отклоняется и удаляется после "t" без успешного
|
||||
//! запроса, а каждый успешный запрос продлевает её. Нулевое время отключает idle-таймаут
|
||||
//! (по умолчанию): сессии не продлеваются и живут, пока их не ограничит \a setTokenTtl()
|
||||
//! или не отзовут. Если заданы оба, сессия завершается по более раннему сроку.
|
||||
void setSessionIdleTimeout(PISystemTime t);
|
||||
|
||||
//! \~english Returns the idle timeout set by \a setSessionIdleTimeout().
|
||||
//! \~russian Возвращает idle-таймаут, установленный \a setSessionIdleTimeout().
|
||||
PISystemTime sessionIdleTimeout() const;
|
||||
|
||||
//! \~english Returns whether the idle timeout is enabled.
|
||||
//! \~russian Возвращает, включен ли idle-таймаут.
|
||||
bool hasSessionIdleTimeout() const;
|
||||
|
||||
//! \~english Changes the route path used for login. The old route is unregistered and the
|
||||
//! new one registered immediately; call it before \a listen() to avoid racing with requests.
|
||||
//! \~russian Изменяет путь маршрута логина. Старый маршрут снимается, новый регистрируется
|
||||
@@ -134,9 +153,11 @@ protected:
|
||||
//! \~english Session creation time.
|
||||
//! \~russian Время создания сессии.
|
||||
PISystemTime created;
|
||||
//! \~english Reserved for future refresh-token support; not used yet.
|
||||
//! \~russian Зарезервировано для будущей поддержки refresh-токенов; пока не используется.
|
||||
PIString refresh_token;
|
||||
//! \~english Time of the last successful \a checkToken(). Updated only while the
|
||||
//! idle timeout is enabled; used as the base of the idle timeout.
|
||||
//! \~russian Время последнего успешного \a checkToken(). Обновляется только при
|
||||
//! включенном idle-таймауте; служит базой idle-таймаута.
|
||||
PISystemTime last_used;
|
||||
};
|
||||
|
||||
//! \~english Resolves a Bearer token to the \c AuthInfo of its owner, applying the
|
||||
@@ -215,6 +236,7 @@ private:
|
||||
mutable PIMutex mutex;
|
||||
PIMap<PIString, SessionRec> sessions;
|
||||
PISystemTime token_ttl;
|
||||
PISystemTime session_idle_timeout;
|
||||
uchar token_length;
|
||||
TokenGenerator token_generator;
|
||||
PIString login_path;
|
||||
|
||||
@@ -429,6 +429,84 @@ TEST_F(HttpServerSessionAuthTest, HasTokenTtl) {
|
||||
}
|
||||
|
||||
|
||||
TEST_F(HttpServerSessionAuthTest, HasSessionIdleTimeout) {
|
||||
auto s = new TestUserServer();
|
||||
EXPECT_FALSE(s->hasSessionIdleTimeout());
|
||||
s->setSessionIdleTimeout(1_s);
|
||||
EXPECT_TRUE(s->hasSessionIdleTimeout());
|
||||
EXPECT_EQ(PISystemTime::fromSeconds(1), s->sessionIdleTimeout());
|
||||
s->setSessionIdleTimeout(0_s);
|
||||
EXPECT_FALSE(s->hasSessionIdleTimeout());
|
||||
}
|
||||
|
||||
|
||||
TEST_F(HttpServerSessionAuthTest, IdleTimeout) {
|
||||
auto s = new TestUserServer();
|
||||
ASSERT_TRUE(s->addUser("alice", "pass1"));
|
||||
s->setSessionIdleTimeout(500_ms);
|
||||
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
|
||||
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
|
||||
});
|
||||
ASSERT_TRUE(start(s));
|
||||
|
||||
PIString token = login(url("/api/login"), "alice", "pass1");
|
||||
ASSERT_FALSE(token.isEmpty());
|
||||
EXPECT_EQ(PIHTTP::Code::Ok, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code);
|
||||
|
||||
PISemaphore pause;
|
||||
pause.tryAcquire(1, 900_ms);
|
||||
|
||||
EXPECT_EQ(PIHTTP::Code::Unauthorized, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code);
|
||||
}
|
||||
|
||||
|
||||
TEST_F(HttpServerSessionAuthTest, IdleActivityExtendsSession) {
|
||||
auto s = new TestUserServer();
|
||||
ASSERT_TRUE(s->addUser("alice", "pass1"));
|
||||
s->setSessionIdleTimeout(500_ms);
|
||||
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
|
||||
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
|
||||
});
|
||||
ASSERT_TRUE(start(s));
|
||||
|
||||
PIString token = login(url("/api/login"), "alice", "pass1");
|
||||
ASSERT_FALSE(token.isEmpty());
|
||||
|
||||
// Requests spaced by ~200 ms keep the session alive for a total period well beyond the
|
||||
// 500 ms idle timeout: without sliding this session would already be expired.
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
EXPECT_EQ(PIHTTP::Code::Ok, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code);
|
||||
PISemaphore pause;
|
||||
pause.tryAcquire(1, 200_ms);
|
||||
}
|
||||
EXPECT_EQ(PIHTTP::Code::Ok, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code);
|
||||
|
||||
// Once the client stops, the idle timeout does expire the session.
|
||||
PISemaphore pause;
|
||||
pause.tryAcquire(1, 900_ms);
|
||||
EXPECT_EQ(PIHTTP::Code::Unauthorized, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code);
|
||||
}
|
||||
|
||||
|
||||
TEST_F(HttpServerSessionAuthTest, IdleDisabledKeepsSession) {
|
||||
auto s = new TestUserServer();
|
||||
ASSERT_TRUE(s->addUser("alice", "pass1"));
|
||||
// The idle timeout is not configured, so the session must not be extended or expired.
|
||||
s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) {
|
||||
return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok"));
|
||||
});
|
||||
ASSERT_TRUE(start(s));
|
||||
|
||||
PIString token = login(url("/api/login"), "alice", "pass1");
|
||||
ASSERT_FALSE(token.isEmpty());
|
||||
|
||||
PISemaphore pause;
|
||||
pause.tryAcquire(1, 900_ms);
|
||||
|
||||
EXPECT_EQ(PIHTTP::Code::Ok, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code);
|
||||
}
|
||||
|
||||
|
||||
TEST_F(HttpServerSessionAuthTest, CustomTokenGenerator) {
|
||||
auto s = new TestUserServer();
|
||||
s->setTokenGenerator([](uint) { return PIByteArray::fromHex("01020304"); });
|
||||
|
||||
Reference in New Issue
Block a user