#include "pidigest.h" #include "pihttpclient.h" #include "pihttpservermodule.h" #include "pijson.h" #include "piliterals_string.h" #include "piliterals_time.h" #include "pimutex.h" #include "pisemaphore.h" #include "gtest/gtest.h" //! \~english Test session server owning a small user table. It demonstrates the intended usage: //! \c PIHTTPServerSessionAuth stores only sessions, while the client subclass owns the users and //! implements \a checkCredentials(). //! \~russian Тестовый сервер сессий с небольшой таблицей пользователей. Демонстрирует //! предполагаемое использование: \c PIHTTPServerSessionAuth хранит только сессии, а клиентский //! наследник владеет пользователями и реализует \a checkCredentials(). class TestUserServer: public PIHTTPServerSessionAuth { public: bool addUser(const PIString & login, const PIString & pass) { if (login.isEmpty()) return false; PIMutexLocker locker(users_mutex); if (users.contains(login)) return false; TestUser u; u.id = next_id++; u.pass = pass; users[login] = u; return true; } bool removeUser(const PIString & login) { int id = 0; { PIMutexLocker locker(users_mutex); if (!users.contains(login)) return false; id = users.value(login).id; users.remove(login); } revokeUserTokens(id); return true; } bool userExists(const PIString & login) const { PIMutexLocker locker(users_mutex); return users.contains(login); } int userCount() const { PIMutexLocker locker(users_mutex); return users.size(); } //! \~english Public wrapper around the protected \a revokeToken() for tests. //! \~russian Публичная обертка вокруг protected \a revokeToken() для тестов. bool revoke(const PIString & token) { return revokeToken(token); } //! \~english Number of active sessions in the internal table. //! \~russian Количество активных сессий во внутренней таблице. int sessionCount() { PIMutexLocker locker(lock()); return sessionTable().size(); } //! \~english Public wrapper around the protected \a cleanupExpired() for tests. //! \~russian Публичная обертка вокруг protected \a cleanupExpired() для тестов. void cleanup() { cleanupExpired(); } protected: struct TestUser { int id = 0; PIString pass; }; PIHTTP::AuthInfo checkCredentials(const PIString & login, const PIString & pass) override { PIMutexLocker locker(users_mutex); if (!users.contains(login)) return PIHTTP::AuthInfo(); const TestUser u = users.value(login); return u.pass == pass ? PIHTTP::AuthInfo{true, u.id} : PIHTTP::AuthInfo(); } mutable PIMutex users_mutex; PIMap users; int next_id = 1; }; //! \~english Test server that stores password hashes: \a checkCredentials() hashes the submitted //! password before delegating to the plain comparison of \c TestUserServer. //! \~russian Тестовый сервер, хранящий хеши паролей: \a checkCredentials() хеширует переданный //! пароль перед делегированием в обычное сравнение \c TestUserServer. class HashedUserServer: public TestUserServer { protected: PIHTTP::AuthInfo checkCredentials(const PIString & login, const PIString & pass) override { return TestUserServer::checkCredentials(login, PIDigest::calculate(pass.toByteArray(), PIDigest::Type::SHA2_256).toHex()); } }; class HttpServerSessionAuthTest: public ::testing::Test { protected: struct Reply { PIHTTP::Code code = PIHTTP::Code::Unknown; PIString body; PIString www_authenticate; PIString x_trace; PIString error; bool finished = false; bool transport_error = false; }; //! \~english Performs a request to "url" with optional JSON body and "Authorization" //! header value and waits for the reply with a 10 seconds timeout. //! \~russian Выполняет запрос к "url" с необязательным JSON-телом и значением заголовка //! "Authorization" и ждет ответ с таймаутом 10 секунд. static Reply request(PIHTTP::Method method, const PIString & url, const PIString & body, const PIString & auth) { // The reply state and semaphore are heap-allocated: the callbacks run in the // HTTP client thread pool and may outlive this function on a wait timeout. Reply * rep = new Reply(); PISemaphore * sem = new PISemaphore(); PIHTTP::MessageMutable req; if (auth.isNotEmpty()) req.addHeader(PIHTTP::Header::Authorization, auth); if (body.isNotEmpty()) { req.addHeader(PIHTTP::Header::ContentType, "application/json"); req.setBody(body.toByteArray()); } auto client = PIHTTPClient::create(url, method, req); client->onFinish([rep, sem](const PIHTTP::MessageConst & r) { rep->code = r.code(); rep->body = PIString::fromUTF8(r.body()); rep->www_authenticate = r.headers().value(PIHTTP::Header::WWWAuthenticate); rep->x_trace = r.headers().value("X-Trace"); rep->finished = true; sem->release(); }); client->onError([client, rep, sem](const PIHTTP::MessageConst &) { rep->error = client->lastError(); rep->transport_error = true; rep->finished = true; sem->release(); }); client->start(); // The client object is owned by the HTTP client thread pool after start() // and deleted there, so it must not be touched or deleted here. if (sem->tryAcquire(1, 10_s)) { Reply out = *rep; delete rep; delete sem; return out; } // On timeout the callbacks may still run in the client thread pool, // so "rep"/"sem" are intentionally leaked (the test fails anyway). Reply out; return out; } static PIString loginJson(const PIString & login, const PIString & password) { PIJSON j; j["login"] = login; j["password"] = password; return j.toJSON(PIJSON::Compact); } //! \~english Builds a "Bearer " authorization header value. //! \~russian Формирует значение заголовка "Authorization" схемы Bearer из токена. static PIString bearer(const PIString & token) { return "Bearer %1"_a.arg(token); } //! \~english Logs in with the given credentials and returns the issued token (empty on failure). //! \~russian Выполняет вход с данными учетными данными и возвращает выданный токен (пустой при неудаче). static PIString login(const PIString & url, const PIString & login, const PIString & password) { auto rep = request(PIHTTP::Method::Post, url, loginJson(login, password), ""); if (rep.code != PIHTTP::Code::Ok) return PIString(); return PIJSON::fromJSON(rep.body)["token"].toString(); } //! \~english Starts listening on the first free port from 18461 and keeps the server //! for the teardown. Returns \c false if no port is available. //! \~russian Запускает прослушивание на первом свободном порту от 18461 и сохраняет сервер //! для завершения теста. Возвращает \c false, если свободный порт не найден. bool start(PIHTTPServer * s) { server = s; for (port = 18461; port < 18561; ++port) { if (s->listenAll((ushort)port)) return true; } port = -1; return false; } PIString url(const char * path) const { return "http://127.0.0.1:%1"_a.arg(port) + path; } void TearDown() override { if (server) { server->stop(); delete server; server = nullptr; } } PIHTTPServer * server = nullptr; int port = -1; }; //! \~english Test server overriding the login reply and the token validation: every login //! reply gets an "X-Trace" header, and the token from "banned_token" is denied. //! \~russian Тестовый сервер, переопределяющий ответ логина и проверку токена: каждый ответ //! логина получает заголовок "X-Trace", а токен из "banned_token" отклоняется. class OverriddenServer: public TestUserServer { public: int login_calls = 0; PIString banned_token; protected: PIHTTP::AuthInfo checkToken(const PIString & token) override { if (!banned_token.isEmpty() && token == banned_token) return PIHTTP::AuthInfo(); return PIHTTPServerSessionAuth::checkToken(token); } PIHTTP::MessageMutable handleLogin(const PIHTTP::MessageConst & request) override { ++login_calls; return PIHTTPServerSessionAuth::handleLogin(request).addHeader("X-Trace", "overridden"); } }; TEST_F(HttpServerSessionAuthTest, LoginOk) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) { return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok")); }); ASSERT_TRUE(start(s)); auto rep = request(PIHTTP::Method::Post, url("/api/login"), loginJson("alice", "pass1"), ""); EXPECT_TRUE(rep.finished); EXPECT_FALSE(rep.transport_error); EXPECT_EQ(PIHTTP::Code::Ok, rep.code); PIString token = PIJSON::fromJSON(rep.body)["token"].toString(); EXPECT_FALSE(token.isEmpty()); EXPECT_EQ(1, PIJSON::fromJSON(rep.body)["user_id"].toInt()); auto sec = request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)); EXPECT_EQ(PIHTTP::Code::Ok, sec.code); EXPECT_EQ("ok"_a, sec.body); } TEST_F(HttpServerSessionAuthTest, LoginWrongCredentials) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); ASSERT_TRUE(start(s)); auto wrong_pass = request(PIHTTP::Method::Post, url("/api/login"), loginJson("alice", "pass2"), ""); EXPECT_TRUE(wrong_pass.finished); EXPECT_FALSE(wrong_pass.transport_error); EXPECT_EQ(PIHTTP::Code::Unauthorized, wrong_pass.code); EXPECT_TRUE(wrong_pass.body.contains("invalid credentials")); auto unknown = request(PIHTTP::Method::Post, url("/api/login"), loginJson("eve", "pass3"), ""); EXPECT_EQ(PIHTTP::Code::Unauthorized, unknown.code); EXPECT_TRUE(unknown.body.contains("invalid credentials")); } TEST_F(HttpServerSessionAuthTest, LoginBadJson) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); ASSERT_TRUE(start(s)); auto not_json = request(PIHTTP::Method::Post, url("/api/login"), "not a json", ""); EXPECT_TRUE(not_json.finished); EXPECT_FALSE(not_json.transport_error); EXPECT_EQ(PIHTTP::Code::BadRequest, not_json.code); EXPECT_TRUE(not_json.body.contains("invalid json")); auto missing_pass = request(PIHTTP::Method::Post, url("/api/login"), "{\"login\":\"alice\"}", ""); EXPECT_EQ(PIHTTP::Code::BadRequest, missing_pass.code); auto empty_login = request(PIHTTP::Method::Post, url("/api/login"), "{\"login\":\"\",\"password\":\"pass1\"}", ""); EXPECT_EQ(PIHTTP::Code::BadRequest, empty_login.code); } TEST_F(HttpServerSessionAuthTest, ProtectedNoToken) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) { return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok")); }); ASSERT_TRUE(start(s)); auto rep = request(PIHTTP::Method::Get, url("/secret"), "", ""); EXPECT_TRUE(rep.finished); EXPECT_FALSE(rep.transport_error); EXPECT_EQ(PIHTTP::Code::Unauthorized, rep.code); EXPECT_TRUE(rep.www_authenticate.contains("Bearer")); auto wrong = request(PIHTTP::Method::Get, url("/secret"), "", "Bearer deadbeef"); EXPECT_EQ(PIHTTP::Code::Unauthorized, wrong.code); } TEST_F(HttpServerSessionAuthTest, MultiUser) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); ASSERT_TRUE(s->addUser("bob", "pass2")); s->registerProtectedPath("/whoami", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &, const PIHTTP::AuthInfo & info) { return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIString::fromNumber(info.user_id).toByteArray()); }); ASSERT_TRUE(start(s)); PIString alice_token = login(url("/api/login"), "alice", "pass1"); PIString bob_token = login(url("/api/login"), "bob", "pass2"); ASSERT_FALSE(alice_token.isEmpty()); ASSERT_FALSE(bob_token.isEmpty()); auto alice = request(PIHTTP::Method::Get, url("/whoami"), "", bearer(alice_token)); EXPECT_EQ(PIHTTP::Code::Ok, alice.code); EXPECT_EQ("1"_a, alice.body); auto bob = request(PIHTTP::Method::Get, url("/whoami"), "", bearer(bob_token)); EXPECT_EQ(PIHTTP::Code::Ok, bob.code); EXPECT_EQ("2"_a, bob.body); } TEST_F(HttpServerSessionAuthTest, Logout) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); 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()); auto no_auth = request(PIHTTP::Method::Post, url("/api/logout"), "", ""); EXPECT_EQ(PIHTTP::Code::Unauthorized, no_auth.code); auto out = request(PIHTTP::Method::Post, url("/api/logout"), "", bearer(token)); EXPECT_TRUE(out.finished); EXPECT_FALSE(out.transport_error); EXPECT_EQ(PIHTTP::Code::NoContent, out.code); auto dead = request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)); EXPECT_EQ(PIHTTP::Code::Unauthorized, dead.code); auto again = request(PIHTTP::Method::Post, url("/api/logout"), "", bearer(token)); EXPECT_EQ(PIHTTP::Code::Unauthorized, again.code); } TEST_F(HttpServerSessionAuthTest, RevokeToken) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); 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); EXPECT_TRUE(s->revoke(token)); EXPECT_FALSE(s->revoke(token)); EXPECT_EQ(PIHTTP::Code::Unauthorized, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code); } TEST_F(HttpServerSessionAuthTest, RemoveUserRevokesTokens) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); ASSERT_TRUE(s->addUser("bob", "pass2")); s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) { return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok")); }); ASSERT_TRUE(start(s)); // Two sessions of the same user must both be revoked. PIString first = login(url("/api/login"), "alice", "pass1"); PIString second = login(url("/api/login"), "alice", "pass1"); ASSERT_FALSE(first.isEmpty()); ASSERT_FALSE(second.isEmpty()); EXPECT_TRUE(s->removeUser("alice")); EXPECT_EQ(1, s->userCount()); EXPECT_FALSE(s->removeUser("alice")); EXPECT_EQ(PIHTTP::Code::Unauthorized, request(PIHTTP::Method::Get, url("/secret"), "", bearer(first)).code); EXPECT_EQ(PIHTTP::Code::Unauthorized, request(PIHTTP::Method::Get, url("/secret"), "", bearer(second)).code); PIString bob_token = login(url("/api/login"), "bob", "pass2"); ASSERT_FALSE(bob_token.isEmpty()); EXPECT_EQ(PIHTTP::Code::Ok, request(PIHTTP::Method::Get, url("/secret"), "", bearer(bob_token)).code); } TEST_F(HttpServerSessionAuthTest, UserTable) { auto s = new TestUserServer(); EXPECT_TRUE(s->addUser("alice", "pass1")); EXPECT_FALSE(s->addUser("alice", "other")); EXPECT_FALSE(s->addUser("", "pass")); EXPECT_TRUE(s->userExists("alice")); EXPECT_FALSE(s->userExists("bob")); EXPECT_EQ(1, s->userCount()); } TEST_F(HttpServerSessionAuthTest, TokenTtl) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); s->setTokenTtl(100_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, 300_ms); EXPECT_EQ(PIHTTP::Code::Unauthorized, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code); } TEST_F(HttpServerSessionAuthTest, HasTokenTtl) { auto s = new TestUserServer(); EXPECT_FALSE(s->hasTokenTtl()); s->setTokenTtl(1_s); EXPECT_TRUE(s->hasTokenTtl()); EXPECT_EQ(PISystemTime::fromSeconds(1), s->tokenTtl()); s->setTokenTtl(0_s); EXPECT_FALSE(s->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, SessionCleanupOnLogin) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); s->setSessionIdleTimeout(200_ms); ASSERT_TRUE(start(s)); PIString first = login(url("/api/login"), "alice", "pass1"); ASSERT_FALSE(first.isEmpty()); EXPECT_EQ(1, s->sessionCount()); // The first session expires by idle timeout; the next login must reclaim it instead of // growing the table. PISemaphore pause; pause.tryAcquire(1, 400_ms); PIString second = login(url("/api/login"), "alice", "pass1"); ASSERT_FALSE(second.isEmpty()); EXPECT_EQ(1, s->sessionCount()); } TEST_F(HttpServerSessionAuthTest, ManualCleanup) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); s->setSessionIdleTimeout(200_ms); ASSERT_TRUE(start(s)); PIString token = login(url("/api/login"), "alice", "pass1"); ASSERT_FALSE(token.isEmpty()); EXPECT_EQ(1, s->sessionCount()); PISemaphore pause; pause.tryAcquire(1, 400_ms); // The protected cleanupExpired() is available to subclasses for periodic maintenance. s->cleanup(); EXPECT_EQ(0, s->sessionCount()); } TEST_F(HttpServerSessionAuthTest, TokenCollisionRejected) { auto s = new TestUserServer(); // A constant generator produces the same token on every call: the second login must be // refused (500) rather than reusing/overwriting the active session. s->setTokenGenerator([](uint) { return PIByteArray::fromHex("01020304"); }); ASSERT_TRUE(s->addUser("alice", "pass1")); ASSERT_TRUE(s->addUser("bob", "pass2")); ASSERT_TRUE(start(s)); auto first = request(PIHTTP::Method::Post, url("/api/login"), loginJson("alice", "pass1"), ""); EXPECT_EQ(PIHTTP::Code::Ok, first.code); EXPECT_EQ("01020304"_a, PIJSON::fromJSON(first.body)["token"].toString()); auto second = request(PIHTTP::Method::Post, url("/api/login"), loginJson("bob", "pass2"), ""); EXPECT_EQ(PIHTTP::Code::InternalServerError, second.code); EXPECT_TRUE(second.body.contains("token generation failed")); } TEST_F(HttpServerSessionAuthTest, CustomTokenGenerator) { auto s = new TestUserServer(); s->setTokenGenerator([](uint) { return PIByteArray::fromHex("01020304"); }); ASSERT_TRUE(s->addUser("alice", "pass1")); ASSERT_TRUE(start(s)); auto rep = request(PIHTTP::Method::Post, url("/api/login"), loginJson("alice", "pass1"), ""); EXPECT_EQ(PIHTTP::Code::Ok, rep.code); EXPECT_EQ("01020304"_a, PIJSON::fromJSON(rep.body)["token"].toString()); } TEST_F(HttpServerSessionAuthTest, CustomPasswordCheck) { auto s = new HashedUserServer(); PIString hash = PIDigest::calculate(PIString("pass1").toByteArray(), PIDigest::Type::SHA2_256).toHex(); ASSERT_TRUE(s->addUser("alice", hash)); ASSERT_TRUE(start(s)); auto ok = request(PIHTTP::Method::Post, url("/api/login"), loginJson("alice", "pass1"), ""); EXPECT_TRUE(ok.finished); EXPECT_FALSE(ok.transport_error); EXPECT_EQ(PIHTTP::Code::Ok, ok.code); auto bad = request(PIHTTP::Method::Post, url("/api/login"), loginJson("alice", "other"), ""); EXPECT_EQ(PIHTTP::Code::Unauthorized, bad.code); } TEST_F(HttpServerSessionAuthTest, LowercaseBearerScheme) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); 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()); auto rep = request(PIHTTP::Method::Get, url("/secret"), "", "bearer " + token); EXPECT_EQ(PIHTTP::Code::Ok, rep.code); EXPECT_EQ("ok"_a, rep.body); } TEST_F(HttpServerSessionAuthTest, ConfigurablePaths) { auto s = new TestUserServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); s->setLoginPath("/auth/signin"); s->setLogoutPath("/auth/signout"); EXPECT_EQ("/auth/signin"_a, s->loginPath()); EXPECT_EQ("/auth/signout"_a, s->logoutPath()); s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) { return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok")); }); ASSERT_TRUE(start(s)); // The default routes are gone. EXPECT_EQ(PIHTTP::Code::NotFound, request(PIHTTP::Method::Post, url("/api/login"), loginJson("alice", "pass1"), "").code); PIString token = login(url("/auth/signin"), "alice", "pass1"); ASSERT_FALSE(token.isEmpty()); EXPECT_EQ(PIHTTP::Code::Ok, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code); auto out = request(PIHTTP::Method::Post, url("/auth/signout"), "", bearer(token)); EXPECT_EQ(PIHTTP::Code::NoContent, out.code); EXPECT_EQ(PIHTTP::Code::Unauthorized, request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)).code); } TEST_F(HttpServerSessionAuthTest, Inheritance_LoginOverride) { auto s = new OverriddenServer(); ASSERT_TRUE(s->addUser("alice", "pass1")); s->registerProtectedPath("/secret", PIHTTP::Method::Get, [](const PIHTTP::MessageConst &) { return PIHTTP::MessageMutable::fromCode(PIHTTP::Code::Ok).setBody(PIByteArray::fromAscii("ok")); }); ASSERT_TRUE(start(s)); auto rep = request(PIHTTP::Method::Post, url("/api/login"), loginJson("alice", "pass1"), ""); EXPECT_TRUE(rep.finished); EXPECT_FALSE(rep.transport_error); EXPECT_EQ(PIHTTP::Code::Ok, rep.code); EXPECT_EQ(1, s->login_calls); EXPECT_EQ("overridden"_a, rep.x_trace); PIString token = PIJSON::fromJSON(rep.body)["token"].toString(); auto sec = request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)); EXPECT_EQ(PIHTTP::Code::Ok, sec.code); } TEST_F(HttpServerSessionAuthTest, Inheritance_CheckToken) { auto s = new OverriddenServer(); s->setTokenGenerator([](uint) { return PIByteArray::fromHex("00112233"); }); ASSERT_TRUE(s->addUser("alice", "pass1")); 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()); auto allowed = request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)); EXPECT_EQ(PIHTTP::Code::Ok, allowed.code); s->banned_token = token; auto banned = request(PIHTTP::Method::Get, url("/secret"), "", bearer(token)); EXPECT_EQ(PIHTTP::Code::Unauthorized, banned.code); }