- cleanupExpired() drops sessions expired by tokenTtl or sessionIdleTimeout and is called opportunistically on issuing a token, bounding the session table; it is protected and self-locking so subclasses can run it periodically - issueToken() refuses a token already used by an active session instead of silently merging two sessions (login replies 500 on a generator collision) - document that the token generator must be collision-resistant - tests: SessionCleanupOnLogin, ManualCleanup, TokenCollisionRejected
266 lines
18 KiB
C++
266 lines
18 KiB
C++
//! \addtogroup HTTPServer
|
||
//! \~\file pihttpserversessionauth.h
|
||
//! \brief HTTP server with bearer session management
|
||
//! \~english HTTP server with bearer session management
|
||
//! \~russian HTTP-сервер с управлением bearer-сессиями
|
||
/*
|
||
PIP - Platform Independent Primitives
|
||
HTTP server with bearer session management
|
||
Andrey Bychkov andrey@signalmodelling.ru
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU Lesser General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU Lesser General Public License for more details.
|
||
|
||
You should have received a copy of the GNU Lesser General Public License
|
||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#ifndef PIHTTPSERVERSESSIONAUTH_H
|
||
#define PIHTTPSERVERSESSIONAUTH_H
|
||
|
||
#include "pihttpserverbearerauth.h"
|
||
#include "pimap.h"
|
||
#include "pimutex.h"
|
||
#include "pisystemtime.h"
|
||
|
||
//! \~\ingroup HTTPServer
|
||
//! \~\brief
|
||
//! \~english Protected HTTP server that manages bearer sessions in memory. It does not store
|
||
//! user accounts: credential verification is delegated to \a checkCredentials(), implemented
|
||
//! by a client subclass; this class only stores issued tokens and resolves them to \c AuthInfo.
|
||
//! The \c POST /api/login route authenticates a user and replies with a token, the
|
||
//! \c POST /api/logout route revokes the token of the request (the route paths are configurable
|
||
//! with \a setLoginPath() / \a setLogoutPath()). Application routes must be registered with
|
||
//! \a registerProtectedPath(); their handlers receive the \c AuthInfo of the token owner.
|
||
//! \~russian Защищенный HTTP-сервер, управляющий bearer-сессиями в памяти. Он не хранит учетные
|
||
//! записи пользователей: проверка учетных данных делегируется \a checkCredentials(), реализуемому
|
||
//! клиентским наследником; этот класс хранит только выданные токены и разрешает их в \c AuthInfo.
|
||
//! Маршрут \c POST /api/login аутентифицирует пользователя и отвечает токеном, маршрут
|
||
//! \c POST /api/logout отзывает токен запроса (пути маршрутов настраиваются через
|
||
//! \a setLoginPath() / \a setLogoutPath()). Маршруты приложения регистрируются через
|
||
//! \a registerProtectedPath(); их обработчики получают \c AuthInfo владельца токена.
|
||
//! \note
|
||
//! \~english \a checkCredentials() is called without the internal class lock held, so an
|
||
//! override may take its own locks and use its own user storage. \c PIMutex is recursive.
|
||
//! \~russian \a checkCredentials() вызывается без захваченного внутреннего замка класса,
|
||
//! поэтому переопределение может брать собственные замки и использовать свое хранилище
|
||
//! пользователей. \c PIMutex рекурсивен.
|
||
//! \note
|
||
//! \~english The class is designed for inheritance: subclasses must implement
|
||
//! \a checkCredentials() and may override \a checkToken() to extend token validation,
|
||
//! \a issueToken() to customize session issuing and \a handleLogin()/\a handleLogout() to
|
||
//! customize the login/logout route replies.
|
||
//! \~russian Класс рассчитан на наследование: подклассы должны реализовать
|
||
//! \a checkCredentials() и могут переопределять \a checkToken() для расширения проверки токенов,
|
||
//! \a issueToken() для кастомизации выдачи сессий и \a handleLogin()/\a handleLogout() для
|
||
//! кастомизации ответов маршрутов login/logout.
|
||
class PIP_HTTP_SERVER_EXPORT PIHTTPServerSessionAuth: public PIHTTPServerBearerAuth {
|
||
PIOBJECT_SUBCLASS(PIHTTPServerSessionAuth, PIHTTPServerBearerAuth)
|
||
|
||
public:
|
||
//! \~english Token data generator: receives the requested length in bytes and returns
|
||
//! that many random bytes; the token is passed to the client as a hex string.
|
||
//! \~russian Генератор данных токена: получает запрашиваемую длину в байтах и возвращает
|
||
//! столько случайных байтов; токен передается клиенту как hex-строка.
|
||
using TokenGenerator = std::function<PIByteArray(uint len)>;
|
||
|
||
//! \~english Creates a session server and registers the \c POST /api/login and
|
||
//! \c POST /api/logout routes.
|
||
//! \~russian Создает сервер сессий и регистрирует маршруты \c POST /api/login и
|
||
//! \c POST /api/logout.
|
||
PIHTTPServerSessionAuth();
|
||
|
||
//! \~english Destroys the server.
|
||
//! \~russian Удаляет сервер.
|
||
virtual ~PIHTTPServerSessionAuth() = default;
|
||
|
||
//! \~english Sets the token data generator. When no generator is set, a built-in
|
||
//! generator based on \c PIDigest and the current time is used; for production use
|
||
//! a cryptographically secure generator is recommended. The generator must be
|
||
//! collision-resistant: \a issueToken() refuses to issue a token that is already used
|
||
//! by an active session (a collision makes the login reply \c 500 instead of silently
|
||
//! merging two sessions).
|
||
//! \~russian Устанавливает генератор данных токена. Когда генератор не установлен,
|
||
//! используется встроенный генератор на основе \c PIDigest и текущего времени;
|
||
//! для production рекомендуется криптографически стойкий генератор. Генератор должен
|
||
//! быть устойчив к коллизиям: \a issueToken() отказывается выдавать токен, уже занятый
|
||
//! активной сессией (коллизия приводит к ответу логина \c 500 вместо молчаливого
|
||
//! объединения двух сессий).
|
||
void setTokenGenerator(TokenGenerator g);
|
||
|
||
//! \~english Sets the session time-to-live: an expired token is denied and removed
|
||
//! lazily on the next request. A null time means tokens live until logout.
|
||
//! \~russian Устанавливает время жизни сессии: истекший токен отклоняется и удаляется
|
||
//! лениво при следующем запросе. Нулевое время означает, что токены живут до logout.
|
||
void setTokenTtl(PISystemTime ttl);
|
||
|
||
//! \~english Returns the session time-to-live set by \a setTokenTtl().
|
||
//! \~russian Возвращает время жизни сессии, установленное \a setTokenTtl().
|
||
PISystemTime tokenTtl() const;
|
||
|
||
//! \~english Returns whether a session time-to-live is set.
|
||
//! \~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 Изменяет путь маршрута логина. Старый маршрут снимается, новый регистрируется
|
||
//! сразу; вызывайте до \a listen(), чтобы избежать гонки с запросами.
|
||
void setLoginPath(const PIString & p);
|
||
|
||
//! \~english Changes the route path used for logout. The old route is unregistered and the
|
||
//! new one registered immediately; call it before \a listen() to avoid racing with requests.
|
||
//! \~russian Изменяет путь маршрута выхода. Старый маршрут снимается, новый регистрируется
|
||
//! сразу; вызывайте до \a listen(), чтобы избежать гонки с запросами.
|
||
void setLogoutPath(const PIString & p);
|
||
|
||
//! \~english Returns the login route path (default \c "/api/login").
|
||
//! \~russian Возвращает путь маршрута логина (по умолчанию \c "/api/login").
|
||
const PIString & loginPath() const { return login_path; }
|
||
|
||
//! \~english Returns the logout route path (default \c "/api/logout").
|
||
//! \~russian Возвращает путь маршрута выхода (по умолчанию \c "/api/logout").
|
||
const PIString & logoutPath() const { return logout_path; }
|
||
|
||
protected:
|
||
//! \~english Stored session data.
|
||
//! \~russian Хранимые данные сессии.
|
||
struct SessionRec {
|
||
bool isInvalid() const { return created.isNull(); }
|
||
//! \~english Id of the session owner.
|
||
//! \~russian Идентификатор владельца сессии.
|
||
int user_id = 0;
|
||
//! \~english Session creation time.
|
||
//! \~russian Время создания сессии.
|
||
PISystemTime created;
|
||
//! \~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
|
||
//! session time-to-live (an expired session is removed). Returns an \c AuthInfo with
|
||
//! \c authorized \c false for unknown or expired tokens. Called for every protected
|
||
//! request; override to extend token validation (e.g. a revocation list).
|
||
//! \~russian Разрешает Bearer-токен в \c AuthInfo его владельца, применяя время жизни
|
||
//! сессии (истекшая сессия удаляется). Для неизвестного или истекшего токена возвращает
|
||
//! \c AuthInfo с \c authorized \c false. Вызывается для каждого защищенного запроса;
|
||
//! переопределяется для расширения проверки токенов (например, список отзыва).
|
||
virtual PIHTTP::AuthInfo checkToken(const PIString & token);
|
||
|
||
//! \~english Issues a new session token for the user with id "user_id" using the token
|
||
//! generator and stores the session. Returns an empty string when the token generator
|
||
//! produces no data.
|
||
//! \~russian Выдает новый токен сессии для пользователя с идентификатором "user_id"
|
||
//! генератором токенов и сохраняет сессию. Возвращает пустую строку, когда генератор
|
||
//! токенов не выдал данных.
|
||
virtual PIString issueToken(int user_id);
|
||
|
||
//! \~english Verifies the credentials submitted at the login route and resolves them to an
|
||
//! \c AuthInfo (with the application user id). Must be implemented by a client subclass that
|
||
//! owns the user storage. The access is denied when \c AuthInfo::authorized is \c false.
|
||
//! Called without the internal class lock held.
|
||
//! \~russian Проверяет учетные данные, переданные в маршрут логина, и разрешает их в
|
||
//! \c AuthInfo (с идентификатором пользователя приложения). Должен быть реализован
|
||
//! клиентским наследником, владеющим хранилищем пользователей. Доступ запрещается, когда
|
||
//! в \c AuthInfo::authorized \c false. Вызывается без захваченного внутреннего замка класса.
|
||
virtual PIHTTP::AuthInfo checkCredentials(const PIString & login, const PIString & password) = 0;
|
||
|
||
//! \~english \c POST /api/login route handler: verifies credentials and issues a token.
|
||
//! Override to customize the login reply.
|
||
//! \~russian Обработчик маршрута \c POST /api/login: проверяет учетные данные и
|
||
//! выдает токен. Переопределяется для кастомизации ответа логина.
|
||
virtual PIHTTP::MessageMutable handleLogin(const PIHTTP::MessageConst & request);
|
||
|
||
//! \~english \c POST /api/logout route handler: revokes the token of the request.
|
||
//! Override to customize the logout reply.
|
||
//! \~russian Обработчик маршрута \c POST /api/logout: отзывает токен запроса.
|
||
//! Переопределяется для кастомизации ответа выхода.
|
||
virtual PIHTTP::MessageMutable handleLogout(const PIHTTP::MessageConst & request);
|
||
|
||
//! \~english Revokes a single session token. Returns \c false when the token is unknown.
|
||
//! \~russian Отзывает одну сессию по токену. Возвращает \c false, если токен неизвестен.
|
||
bool revokeToken(const PIString & token);
|
||
|
||
//! \~english Revokes all sessions of the user with id "user_id". Call it from a subclass
|
||
//! when a user is removed.
|
||
//! \~russian Отзывает все сессии пользователя с идентификатором "user_id". Вызывайте из
|
||
//! наследника при удалении пользователя.
|
||
void revokeUserTokens(int user_id);
|
||
|
||
//! \~english Returns the internal class lock. Guard any direct access to the session table
|
||
//! with it.
|
||
//! \~russian Возвращает внутренний замок класса. Любое прямое обращение к таблице сессий
|
||
//! защищайте им.
|
||
PIMutex & lock() { return mutex; }
|
||
|
||
//! \~english Returns the session table (token to session record). Guard with \a lock().
|
||
//! \~russian Возвращает таблицу сессий (токен на запись сессии). Защищайте \a lock().
|
||
PIMap<PIString, SessionRec> & sessionTable() { return sessions; }
|
||
|
||
//! \~english Removes sessions expired by \a setTokenTtl() or \a setSessionIdleTimeout().
|
||
//! Called opportunistically on token issuing to bound the session table; subclasses may
|
||
//! also call it periodically (e.g. from a timer thread). Acquires the internal lock itself.
|
||
//! \~russian Удаляет сессии, истекшие по \a setTokenTtl() или \a setSessionIdleTimeout().
|
||
//! Вызывается оппортунистически при выдаче токена, чтобы ограничить таблицу сессий;
|
||
//! наследники также могут вызывать её периодически (например, из потока таймера).
|
||
//! Захватывает внутренний замок самостоятельно.
|
||
void cleanupExpired();
|
||
|
||
private:
|
||
//! \~english Built-in token data generator based on \c PIDigest and the current time.
|
||
//! \~russian Встроенный генератор данных токена на основе \c PIDigest и текущего времени.
|
||
PIByteArray defaultTokenData(uint len);
|
||
|
||
//! \~english Registers the login route at the current \a login_path.
|
||
//! \~russian Регистрирует маршрут логина по текущему \a login_path.
|
||
void registerLoginRoute();
|
||
|
||
//! \~english Registers the logout route at the current \a logout_path.
|
||
//! \~russian Регистрирует маршрут выхода по текущему \a logout_path.
|
||
void registerLogoutRoute();
|
||
|
||
//! \~english Same as \a cleanupExpired() but assumes \a mutex is already held.
|
||
//! \~russian То же, что \a cleanupExpired(), но предполагает, что \a mutex уже захвачен.
|
||
void cleanupExpiredLocked();
|
||
|
||
mutable PIMutex mutex;
|
||
PIMap<PIString, SessionRec> sessions;
|
||
PISystemTime token_ttl;
|
||
PISystemTime session_idle_timeout;
|
||
uchar token_length;
|
||
TokenGenerator token_generator;
|
||
PIString login_path;
|
||
PIString logout_path;
|
||
};
|
||
|
||
|
||
#endif // PIHTTPSERVERSESSIONAUTH_H
|