Coverage Report

Created: 2026-10-28 06:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/bmcweb/include/sessions.hpp
Line
Count
Source
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright OpenBMC Authors
3
#pragma once
4
5
#include "bmcweb_config.h"
6
7
#include "logging.hpp"
8
#include "ossl_random.hpp"
9
#include "utils/ip_utils.hpp"
10
11
#include <boost/asio/ip/address.hpp>
12
#include <nlohmann/json.hpp>
13
14
#include <chrono>
15
#include <csignal>
16
#include <cstddef>
17
#include <cstdint>
18
#include <functional>
19
#include <memory>
20
#include <optional>
21
#include <string>
22
#include <string_view>
23
#include <unordered_map>
24
#include <vector>
25
26
namespace persistent_data
27
{
28
29
// entropy: 20 characters, 62 possibilities.  log2(62^20) = 119 bits of
30
// entropy.  OWASP recommends at least 64
31
// https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
32
constexpr std::size_t sessionTokenSize = 20;
33
34
enum class SessionType
35
{
36
    None,
37
    Basic,
38
    Session,
39
    Cookie,
40
    MutualTLS
41
};
42
43
struct UserSession
44
{
45
    std::string uniqueId;
46
    std::string sessionToken;
47
    std::string username;
48
    std::string csrfToken;
49
    std::optional<std::string> clientId;
50
    std::string clientIp;
51
    std::chrono::time_point<std::chrono::steady_clock> lastUpdated;
52
    SessionType sessionType{SessionType::None};
53
    bool cookieAuth = false;
54
    bool isConfigureSelfOnly = false;
55
    std::string userRole;
56
    std::vector<std::string> userGroups;
57
58
    // There are two sources of truth for isConfigureSelfOnly:
59
    //  1. When pamAuthenticateUser() returns PAM_NEW_AUTHTOK_REQD.
60
    //  2. D-Bus User.Manager.GetUserInfo property UserPasswordExpired.
61
    // These should be in sync, but the underlying condition can change at any
62
    // time.  For example, a password can expire or be changed outside of
63
    // bmcweb.  The value stored here is updated at the start of each
64
    // operation and used as the truth within bmcweb.
65
66
    /**
67
     * @brief Fills object with data from UserSession's JSON representation
68
     *
69
     * This replaces nlohmann's from_json to ensure no-throw approach
70
     *
71
     * @param[in] j   JSON object from which data should be loaded
72
     *
73
     * @return a shared pointer if data has been loaded properly, nullptr
74
     * otherwise
75
     */
76
    static std::shared_ptr<UserSession> fromJson(
77
        const nlohmann::json::object_t& j)
78
0
    {
79
0
        std::shared_ptr<UserSession> userSession =
80
0
            std::make_shared<UserSession>();
81
0
        for (const auto& element : j)
82
0
        {
83
0
            const std::string* thisValue =
84
0
                element.second.get_ptr<const std::string*>();
85
0
            if (thisValue == nullptr)
86
0
            {
87
0
                BMCWEB_LOG_ERROR(
88
0
                    "Error reading persistent store.  Property {} was not of type string",
89
0
                    element.first);
90
0
                continue;
91
0
            }
92
0
            if (element.first == "unique_id")
93
0
            {
94
0
                userSession->uniqueId = *thisValue;
95
0
            }
96
0
            else if (element.first == "session_token")
97
0
            {
98
0
                userSession->sessionToken = *thisValue;
99
0
            }
100
0
            else if (element.first == "csrf_token")
101
0
            {
102
0
                userSession->csrfToken = *thisValue;
103
0
            }
104
0
            else if (element.first == "username")
105
0
            {
106
0
                userSession->username = *thisValue;
107
0
            }
108
0
            else if (element.first == "client_id")
109
0
            {
110
0
                userSession->clientId = *thisValue;
111
0
            }
112
0
            else if (element.first == "client_ip")
113
0
            {
114
0
                userSession->clientIp = *thisValue;
115
0
            }
116
117
0
            else
118
0
            {
119
0
                BMCWEB_LOG_ERROR(
120
0
                    "Got unexpected property reading persistent file: {}",
121
0
                    element.first);
122
0
                continue;
123
0
            }
124
0
        }
125
        // If any of these fields are missing, we can't restore the session, as
126
        // we don't have enough information.  These 4 fields have been present
127
        // in every version of this file in bmcwebs history, so any file, even
128
        // on upgrade, should have these present
129
0
        if (userSession->uniqueId.empty() || userSession->username.empty() ||
130
0
            userSession->sessionToken.empty() || userSession->csrfToken.empty())
131
0
        {
132
0
            BMCWEB_LOG_DEBUG("Session missing required security "
133
0
                             "information, refusing to restore");
134
0
            return nullptr;
135
0
        }
136
137
        // For now, sessions that were persisted through a reboot get their idle
138
        // timer reset.  This could probably be overcome with a better
139
        // understanding of wall clock time and steady timer time, possibly
140
        // persisting values with wall clock time instead of steady timer, but
141
        // the tradeoffs of all the corner cases involved are non-trivial, so
142
        // this is done temporarily
143
0
        userSession->lastUpdated = std::chrono::steady_clock::now();
144
0
        userSession->sessionType = SessionType::Session;
145
146
0
        return userSession;
147
0
    }
148
};
149
150
enum class MTLSCommonNameParseMode
151
{
152
    Invalid = 0,
153
    // This section approximately matches Redfish AccountService
154
    // CertificateMappingAttribute,  plus bmcweb defined OEM ones.
155
    // Note, IDs in this enum must be maintained between versions, as they are
156
    // persisted to disk
157
    Whole = 1,
158
    CommonName = 2,
159
    UserPrincipalName = 3,
160
161
    // Intentional gap for future DMTF-defined enums
162
163
    // OEM parsing modes for various OEMs
164
};
165
166
inline MTLSCommonNameParseMode getMTLSCommonNameParseMode(std::string_view name)
167
0
{
168
0
    if (name == "CommonName")
169
0
    {
170
0
        return MTLSCommonNameParseMode::CommonName;
171
0
    }
172
0
    if (name == "Whole")
173
0
    {
174
        // Not yet supported
175
        // return MTLSCommonNameParseMode::Whole;
176
0
    }
177
0
    if (name == "UserPrincipalName")
178
0
    {
179
0
        return MTLSCommonNameParseMode::UserPrincipalName;
180
0
    }
181
0
    return MTLSCommonNameParseMode::Invalid;
182
0
}
183
184
struct AuthConfigMethods
185
{
186
    // Authentication paths
187
    bool basic = BMCWEB_BASIC_AUTH;
188
    bool sessionToken = BMCWEB_SESSION_AUTH;
189
    bool xtoken = BMCWEB_XTOKEN_AUTH;
190
    bool cookie = BMCWEB_COOKIE_AUTH;
191
    bool tls = BMCWEB_MUTUAL_TLS_AUTH;
192
193
    // Whether or not unauthenticated TLS should be accepted
194
    // true = reject connections if mutual tls is not provided
195
    // false = allow connection, and allow user to use other auth method
196
    // Always default to false, because root certificates will not
197
    // be provisioned at startup
198
    bool tlsStrict = false;
199
200
    MTLSCommonNameParseMode mTLSCommonNameParsingMode =
201
        getMTLSCommonNameParseMode(
202
            BMCWEB_MUTUAL_TLS_COMMON_NAME_PARSING_DEFAULT);
203
204
    void fromJson(const nlohmann::json::object_t& j)
205
0
    {
206
0
        for (const auto& element : j)
207
0
        {
208
0
            const bool* value = element.second.get_ptr<const bool*>();
209
0
            if (value != nullptr)
210
0
            {
211
0
                if (element.first == "XToken")
212
0
                {
213
0
                    xtoken = *value;
214
0
                }
215
0
                else if (element.first == "Cookie")
216
0
                {
217
0
                    cookie = *value;
218
0
                }
219
0
                else if (element.first == "SessionToken")
220
0
                {
221
0
                    sessionToken = *value;
222
0
                }
223
0
                else if (element.first == "BasicAuth")
224
0
                {
225
0
                    basic = *value;
226
0
                }
227
0
                else if (element.first == "TLS")
228
0
                {
229
0
                    tls = *value;
230
0
                }
231
0
                else if (element.first == "TLSStrict")
232
0
                {
233
0
                    tlsStrict = *value;
234
0
                }
235
0
            }
236
0
            const uint64_t* intValue =
237
0
                element.second.get_ptr<const uint64_t*>();
238
0
            if (intValue != nullptr)
239
0
            {
240
0
                if (element.first == "MTLSCommonNameParseMode")
241
0
                {
242
0
                    MTLSCommonNameParseMode tmpMTLSCommonNameParseMode =
243
0
                        static_cast<MTLSCommonNameParseMode>(*intValue);
244
0
                    if (tmpMTLSCommonNameParseMode <=
245
0
                        MTLSCommonNameParseMode::UserPrincipalName)
246
0
                    {
247
0
                        mTLSCommonNameParsingMode = tmpMTLSCommonNameParseMode;
248
0
                    }
249
0
                    else
250
0
                    {
251
0
                        BMCWEB_LOG_WARNING(
252
0
                            "Json value of {} was out of range of the enum.  Ignoring",
253
0
                            *intValue);
254
0
                    }
255
0
                }
256
0
            }
257
0
        }
258
0
    }
259
};
260
261
class SessionStore
262
{
263
  public:
264
    std::shared_ptr<UserSession> generateUserSession(
265
        std::string_view username, const boost::asio::ip::address& clientIp,
266
        const std::optional<std::string>& clientId, SessionType sessionType,
267
        bool isConfigureSelfOnly = false)
268
0
    {
269
        // Only need csrf tokens for cookie based auth, token doesn't matter
270
0
        std::string sessionToken =
271
0
            bmcweb::getRandomIdOfLength(sessionTokenSize);
272
0
        std::string csrfToken = bmcweb::getRandomIdOfLength(sessionTokenSize);
273
0
        std::string uniqueId = bmcweb::getRandomIdOfLength(10);
274
275
        //
276
0
        if (sessionToken.empty() || csrfToken.empty() || uniqueId.empty())
277
0
        {
278
0
            BMCWEB_LOG_ERROR("Failed to generate session tokens");
279
0
            return nullptr;
280
0
        }
281
282
0
        auto session = std::make_shared<UserSession>(UserSession{
283
0
            uniqueId,
284
0
            sessionToken,
285
0
            std::string(username),
286
0
            csrfToken,
287
0
            clientId,
288
0
            redfish::ip_util::toString(clientIp),
289
0
            std::chrono::steady_clock::now(),
290
0
            sessionType,
291
0
            false,
292
0
            isConfigureSelfOnly,
293
0
            "",
294
0
            {}});
295
0
        auto it = authTokens.emplace(sessionToken, session);
296
        // Only need to write to disk if session isn't about to be destroyed.
297
0
        needWrite = sessionType != SessionType::Basic &&
298
0
                    sessionType != SessionType::MutualTLS;
299
0
        return it.first->second;
300
0
    }
301
302
    std::shared_ptr<UserSession> loginSessionByToken(std::string_view token)
303
0
    {
304
0
        applySessionTimeouts();
305
0
        if (token.size() != sessionTokenSize)
306
0
        {
307
0
            return nullptr;
308
0
        }
309
0
        auto sessionIt = authTokens.find(std::string(token));
310
0
        if (sessionIt == authTokens.end())
311
0
        {
312
0
            return nullptr;
313
0
        }
314
0
        std::shared_ptr<UserSession> userSession = sessionIt->second;
315
0
        userSession->lastUpdated = std::chrono::steady_clock::now();
316
0
        return userSession;
317
0
    }
318
319
    std::shared_ptr<UserSession> getSessionByUid(std::string_view uid)
320
0
    {
321
0
        applySessionTimeouts();
322
        // TODO(Ed) this is inefficient
323
0
        auto sessionIt = authTokens.begin();
324
0
        while (sessionIt != authTokens.end())
325
0
        {
326
0
            if (sessionIt->second->uniqueId == uid)
327
0
            {
328
0
                return sessionIt->second;
329
0
            }
330
0
            sessionIt++;
331
0
        }
332
0
        return nullptr;
333
0
    }
334
335
    void removeSession(const std::shared_ptr<UserSession>& session)
336
0
    {
337
0
        authTokens.erase(session->sessionToken);
338
0
        needWrite = true;
339
0
    }
340
341
    std::vector<std::string> getAllUniqueIds()
342
0
    {
343
0
        applySessionTimeouts();
344
0
        std::vector<std::string> ret;
345
0
        ret.reserve(authTokens.size());
346
0
        for (auto& session : authTokens)
347
0
        {
348
0
            ret.push_back(session.second->uniqueId);
349
0
        }
350
0
        return ret;
351
0
    }
352
353
    std::vector<std::string> getUniqueIdsBySessionType(SessionType type)
354
0
    {
355
0
        applySessionTimeouts();
356
0
357
0
        std::vector<std::string> ret;
358
0
        ret.reserve(authTokens.size());
359
0
        for (auto& session : authTokens)
360
0
        {
361
0
            if (type == session.second->sessionType)
362
0
            {
363
0
                ret.push_back(session.second->uniqueId);
364
0
            }
365
0
        }
366
0
        return ret;
367
0
    }
368
369
    std::vector<std::shared_ptr<UserSession>> getSessions()
370
0
    {
371
0
        std::vector<std::shared_ptr<UserSession>> sessions;
372
0
        sessions.reserve(authTokens.size());
373
0
        for (auto& session : authTokens)
374
0
        {
375
0
            sessions.push_back(session.second);
376
0
        }
377
0
        return sessions;
378
0
    }
379
380
    void removeSessionsByUsername(std::string_view username)
381
0
    {
382
0
        std::erase_if(authTokens, [username](const auto& value) {
383
0
            if (value.second == nullptr)
384
0
            {
385
0
                return false;
386
0
            }
387
0
            return value.second->username == username;
388
0
        });
389
0
    }
390
391
    void removeSessionsByUsernameExceptSession(
392
        std::string_view username, const std::shared_ptr<UserSession>& session)
393
0
    {
394
0
        std::erase_if(authTokens, [username, session](const auto& value) {
395
0
            if (value.second == nullptr)
396
0
            {
397
0
                return false;
398
0
            }
399
0
400
0
            return value.second->username == username &&
401
0
                   value.second->uniqueId != session->uniqueId;
402
0
        });
403
0
    }
404
405
    void updateAuthMethodsConfig(const AuthConfigMethods& config)
406
0
    {
407
0
        bool isTLSchanged = (authMethodsConfig.tls != config.tls);
408
0
        authMethodsConfig = config;
409
0
        needWrite = true;
410
0
        if (isTLSchanged)
411
0
        {
412
            // recreate socket connections with new settings
413
            // NOLINTNEXTLINE(misc-include-cleaner)
414
0
            std::raise(SIGHUP);
415
0
        }
416
0
    }
417
418
    AuthConfigMethods& getAuthMethodsConfig()
419
0
    {
420
0
        return authMethodsConfig;
421
0
    }
422
423
    bool needsWrite() const
424
0
    {
425
0
        return needWrite;
426
0
    }
427
    int64_t getTimeoutInSeconds() const
428
0
    {
429
0
        return std::chrono::seconds(timeoutInSeconds).count();
430
0
    }
431
432
    void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds)
433
0
    {
434
0
        timeoutInSeconds = newTimeoutInSeconds;
435
0
        needWrite = true;
436
0
    }
437
438
    static SessionStore& getInstance()
439
0
    {
440
0
        static SessionStore sessionStore;
441
0
        return sessionStore;
442
0
    }
443
444
    void applySessionTimeouts()
445
0
    {
446
0
        auto timeNow = std::chrono::steady_clock::now();
447
0
        if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1))
448
0
        {
449
0
            lastTimeoutUpdate = timeNow;
450
0
            auto authTokensIt = authTokens.begin();
451
0
            while (authTokensIt != authTokens.end())
452
0
            {
453
0
                if (timeNow - authTokensIt->second->lastUpdated >=
454
0
                    timeoutInSeconds)
455
0
                {
456
0
                    authTokensIt = authTokens.erase(authTokensIt);
457
458
0
                    needWrite = true;
459
0
                }
460
0
                else
461
0
                {
462
0
                    authTokensIt++;
463
0
                }
464
0
            }
465
0
        }
466
0
    }
467
468
    SessionStore(const SessionStore&) = delete;
469
    SessionStore& operator=(const SessionStore&) = delete;
470
    SessionStore(SessionStore&&) = delete;
471
    SessionStore& operator=(const SessionStore&&) = delete;
472
0
    ~SessionStore() = default;
473
474
    std::unordered_map<std::string, std::shared_ptr<UserSession>,
475
                       std::hash<std::string>, bmcweb::ConstantTimeCompare>
476
        authTokens;
477
478
    std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
479
    bool needWrite{false};
480
    std::chrono::seconds timeoutInSeconds;
481
    AuthConfigMethods authMethodsConfig;
482
483
  private:
484
0
    SessionStore() : timeoutInSeconds(1800) {}
485
};
486
487
} // namespace persistent_data