ORE Studio 0.0.4
Loading...
Searching...
No Matches
auth_handler.hpp
1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 *
3 * Copyright (C) 2026 Marco Craveiro <marco.craveiro@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU General Public License as published by the Free Software
7 * Foundation; either version 3 of the License, or (at your option) any later
8 * version.
9 *
10 * This program is distributed in the hope that it will be useful, but WITHOUT
11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 * details.
14 *
15 * You should have received a copy of the GNU General Public License along with
16 * this program; if not, write to the Free Software Foundation, Inc., 51
17 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 *
19 */
20#ifndef ORES_IAM_MESSAGING_AUTH_HANDLER_HPP
21#define ORES_IAM_MESSAGING_AUTH_HANDLER_HPP
22
23#include <chrono>
24#include <stdexcept>
25#include <boost/asio/ip/address.hpp>
26#include <boost/uuid/random_generator.hpp>
27#include <boost/uuid/string_generator.hpp>
28#include <boost/uuid/uuid_io.hpp>
29#include "ores.logging/make_logger.hpp"
30#include "ores.nats/domain/message.hpp"
31#include "ores.nats/service/client.hpp"
32#include "ores.database/domain/context.hpp"
33#include "ores.security/jwt/jwt_authenticator.hpp"
34#include "ores.security/jwt/jwt_claims.hpp"
35#include <rfl/json.hpp>
36#include "ores.service/messaging/handler_helpers.hpp"
37#include "ores.iam.api/messaging/login_protocol.hpp"
38#include "ores.iam.api/messaging/signup_protocol.hpp"
39#include "ores.iam.api/domain/session.hpp"
40#include "ores.iam.core/repository/account_party_repository.hpp"
41#include "ores.iam.core/repository/account_repository.hpp"
42#include "ores.iam.core/repository/session_repository.hpp"
43#include "ores.iam.core/repository/tenant_repository.hpp"
44#include "ores.refdata.core/repository/party_repository.hpp"
45#include "ores.iam.core/service/account_service.hpp"
46#include "ores.iam.core/service/authorization_service.hpp"
47#include "ores.iam.core/service/account_setup_service.hpp"
48#include "ores.utility/uuid/tenant_id.hpp"
49#include "ores.variability.core/service/system_settings_service.hpp"
50#include "ores.iam.core/domain/token_settings.hpp"
51#include "ores.iam.core/repository/auth_event_repository.hpp"
52#include "ores.iam.core/service/service_session_service.hpp"
53#include "ores.database/repository/bitemporal_operations.hpp"
54
55namespace ores::iam::messaging {
56
57namespace {
58
59inline auto& auth_handler_lg() {
60 static auto instance = ores::logging::make_logger(
61 "ores.iam.messaging.auth_handler");
62 return instance;
63}
64
65inline std::string auth_extract_bearer_token(const ores::nats::message& msg) {
66 auto it = msg.headers.find(std::string(ores::nats::headers::authorization));
67 if (it == msg.headers.end())
68 return {};
69 const auto& val = it->second;
70 if (!val.starts_with(ores::nats::headers::bearer_prefix))
71 return {};
72 return val.substr(ores::nats::headers::bearer_prefix.size());
73}
74
75inline std::vector<boost::uuids::uuid> auth_compute_visible_party_ids(
76 const ores::database::context& ctx,
77 const boost::uuids::uuid& party_id) {
78 try {
79 refdata::repository::party_repository repo(ctx);
80 auto ids = repo.read_descendants(party_id);
81 if (ids.empty())
82 return {party_id};
83 return ids;
84 } catch (const std::exception& e) {
85 using namespace ores::logging;
86 BOOST_LOG_SEV(auth_handler_lg(), warn)
87 << "Failed to compute visible party IDs: " << e.what();
88 return {party_id};
89 }
90}
91
92inline std::optional<refdata::domain::party> auth_lookup_party(
93 const ores::database::context& ctx,
94 const boost::uuids::uuid& party_id) {
95 try {
97 auto parties = repo.read_latest(party_id);
98 if (!parties.empty())
99 return parties.front();
100 } catch (const std::exception& e) {
101 using namespace ores::logging;
102 BOOST_LOG_SEV(auth_handler_lg(), warn)
103 << "Failed to look up party: " << e.what();
104 }
105 return std::nullopt;
106}
107
108inline std::string auth_lookup_tenant_name(
109 const ores::database::context& ctx,
110 const boost::uuids::uuid& tenant_id) {
111 try {
112 repository::tenant_repository repo(ctx);
113 auto tenants = repo.read_latest(tenant_id);
114 if (!tenants.empty())
115 return tenants.front().name;
116 } catch (const std::exception& e) {
117 using namespace ores::logging;
118 BOOST_LOG_SEV(auth_handler_lg(), warn)
119 << "Failed to look up tenant name: " << e.what();
120 }
121 return {};
122}
123
124inline std::optional<ores::iam::domain::tenant> auth_lookup_tenant_by_hostname(
125 const ores::database::context& ctx, const std::string& hostname) {
126 try {
127 repository::tenant_repository repo(ctx);
128 auto tenants = repo.read_latest_by_hostname(hostname);
129 if (!tenants.empty())
130 return tenants.front();
131 } catch (const std::exception& e) {
132 using namespace ores::logging;
133 BOOST_LOG_SEV(auth_handler_lg(), warn)
134 << "Failed to look up tenant by hostname: " << e.what();
135 }
136 return std::nullopt;
137}
138
139inline bool auth_is_tenant_bootstrap_mode(
140 const ores::database::context& ctx,
141 const std::string& tenant_id_str) {
142 try {
143 auto tid_result = ores::utility::uuid::tenant_id::from_string(tenant_id_str);
144 if (!tid_result) return false;
145 auto tenant_ctx = ctx.with_tenant(*tid_result, "");
146 variability::service::system_settings_service sfs(tenant_ctx, tenant_id_str);
147 sfs.refresh();
148 return sfs.is_bootstrap_mode_enabled();
149 } catch (const std::exception& e) {
150 using namespace ores::logging;
151 BOOST_LOG_SEV(auth_handler_lg(), warn)
152 << "Failed to check tenant bootstrap mode: " << e.what();
153 }
154 return false;
155}
156
157} // namespace
158
159using ores::service::messaging::reply;
160using ores::service::messaging::decode;
161using ores::service::messaging::log_handler_entry;
162using namespace ores::logging;
163
164class auth_handler {
165public:
166 auth_handler(ores::nats::service::client& nats,
169 : nats_(nats), ctx_(std::move(ctx)), signer_(std::move(signer)) {
170 reload_token_settings();
171 }
172
173 void reload_token_settings() {
174 try {
176 svc.refresh();
177 token_settings_ = domain::token_settings::load(svc);
178 } catch (const std::exception& e) {
179 using namespace ores::logging;
180 BOOST_LOG_SEV(auth_handler_lg(), warn)
181 << "Failed to load token settings, using defaults: " << e.what();
182 }
183 }
184
185 void signup(ores::nats::message msg) {
186 [[maybe_unused]] const auto correlation_id =
187 log_handler_entry(auth_handler_lg(), msg);
188 auto req = decode<signup_request>(msg);
189 if (!req) {
190 BOOST_LOG_SEV(auth_handler_lg(), warn)
191 << "Failed to decode: " << msg.subject;
192 return;
193 }
194 try {
195 service::account_service acct_svc(ctx_);
196 auto auth_svc =
197 std::make_shared<service::authorization_service>(ctx_);
198 service::account_setup_service setup_svc(acct_svc, auth_svc);
199 auto acct = setup_svc.create_account(
200 req->principal, req->email, req->password, ctx_.service_account());
201 BOOST_LOG_SEV(auth_handler_lg(), debug)
202 << "Completed " << msg.subject;
203 record_auth_event(ctx_, "signup_success", [&](auto& ev_repo) {
204 ev_repo.record_signup_success(
205 std::chrono::system_clock::now(),
206 acct.tenant_id.to_string(),
207 boost::uuids::to_string(acct.id),
208 acct.username);
209 });
210 reply(nats_, msg, signup_response{
211 .success = true,
212 .account_id = boost::uuids::to_string(acct.id)});
213 } catch (const std::exception& e) {
214 BOOST_LOG_SEV(auth_handler_lg(), error)
215 << msg.subject << " failed: " << e.what();
216 record_auth_event(ctx_, "signup_failure", [&](auto& ev_repo) {
217 ev_repo.record_signup_failure(
218 std::chrono::system_clock::now(),
219 "", req->principal, e.what());
220 });
221 reply(nats_, msg, signup_response{
222 .success = false, .message = e.what()});
223 }
224 }
225
226 void login(ores::nats::message msg) {
227 [[maybe_unused]] const auto correlation_id =
228 log_handler_entry(auth_handler_lg(), msg);
229 auto req = decode<login_request>(msg);
230 if (!req) {
231 BOOST_LOG_SEV(auth_handler_lg(), warn)
232 << "Failed to decode: " << msg.subject;
233 return;
234 }
235 try {
236 // Parse principal: split username@hostname for tenant routing
237 std::string username = req->principal;
238 ores::database::context login_ctx = ctx_;
239 const auto at_pos = req->principal.rfind('@');
240 if (at_pos != std::string::npos) {
241 username = req->principal.substr(0, at_pos);
242 const auto hostname = req->principal.substr(at_pos + 1);
243 if (auto t = auth_lookup_tenant_by_hostname(ctx_, hostname)) {
244 auto tid_result =
246 if (tid_result)
247 login_ctx = ctx_.with_tenant(*tid_result, "");
248 }
249 }
250
251 service::account_service svc(login_ctx);
252 auto ip = boost::asio::ip::address_v4::loopback();
253 auto acct = svc.login(username, req->password, ip);
254
255 // Check if this non-system tenant needs its provisioning wizard
256 const bool in_tenant_bootstrap =
257 !acct.tenant_id.is_system() &&
258 auth_is_tenant_bootstrap_mode(
259 login_ctx, acct.tenant_id.to_string());
260
261 repository::account_party_repository ap_repo(login_ctx);
262 auto account_parties =
263 ap_repo.read_latest_by_account(acct.id);
264
265 if (account_parties.empty()) {
266 BOOST_LOG_SEV(auth_handler_lg(), warn)
267 << "Login rejected for " << username
268 << ": account has no party assignment";
269 throw std::runtime_error(
270 "Account has no party assignment. "
271 "Please contact your administrator.");
272 }
273
274 // Create a session record so that analytics, session listings,
275 // and logout end-time tracking all work correctly.
276 const auto now = std::chrono::system_clock::now();
277 boost::uuids::random_generator uuid_gen;
278 domain::session sess;
279 sess.id = uuid_gen();
280 sess.account_id = acct.id;
281 sess.tenant_id = acct.tenant_id;
282 sess.start_time = now;
283 sess.username = acct.username;
284 sess.protocol = domain::session_protocol::http;
285 sess.client_ip = ip;
286 // party_id set below once we know which party is active
287 try {
288 repository::session_repository sess_repo(login_ctx);
289 sess_repo.create(sess);
290 } catch (const std::exception& e) {
291 BOOST_LOG_SEV(auth_handler_lg(), warn)
292 << "Failed to create session record: " << e.what();
293 }
294 const auto session_id_str = boost::uuids::to_string(sess.id);
295
296 if (account_parties.size() == 1) {
297 const auto& party_id =
298 account_parties.front().party_id;
299 auto visible = auth_compute_visible_party_ids(
300 login_ctx, party_id);
301
303 claims.subject = boost::uuids::to_string(acct.id);
304 claims.issued_at = now;
305 claims.expires_at = now + std::chrono::seconds(
306 token_settings_.access_lifetime_s);
307 claims.username = acct.username;
308 claims.email = acct.email;
309 claims.tenant_id = acct.tenant_id.to_string();
310 claims.party_id = boost::uuids::to_string(party_id);
311 claims.session_id = session_id_str;
312 claims.session_start_time = now;
313 for (const auto& vid : visible)
314 claims.visible_party_ids.push_back(
315 boost::uuids::to_string(vid));
316 auto token = signer_.create_token(claims).value_or("");
317
318 login_response resp;
319 resp.success = true;
320 resp.token = token;
321 resp.account_id = boost::uuids::to_string(acct.id);
322 resp.tenant_id = acct.tenant_id.to_string();
323 resp.username = acct.username;
324 resp.email = acct.email;
325 resp.selected_party_id = boost::uuids::to_string(party_id);
326 resp.tenant_bootstrap_mode = in_tenant_bootstrap;
327 resp.access_lifetime_s = token_settings_.access_lifetime_s;
328 resp.session_id = session_id_str;
329 for (const auto& ap : account_parties) {
330 auto p = auth_lookup_party(login_ctx, ap.party_id);
331 if (ap.party_id == party_id)
332 resp.party_setup_required =
333 p && p->status == "Inactive";
334 resp.available_parties.push_back(party_summary{
335 .id = boost::uuids::to_string(ap.party_id),
336 .name = p ? p->full_name : std::string{},
337 .party_category =
338 p ? p->party_category : std::string{},
339 .business_center_code =
340 p ? p->business_center_code : std::string{}
341 });
342 }
343 BOOST_LOG_SEV(auth_handler_lg(), debug)
344 << "Completed " << msg.subject;
345 record_auth_event(login_ctx, "login_success", [&](auto& ev_repo) {
346 ev_repo.record_login_success(
347 now,
348 acct.tenant_id.to_string(),
349 boost::uuids::to_string(acct.id),
350 acct.username,
351 session_id_str,
352 boost::uuids::to_string(party_id));
353 });
354 reply(nats_, msg, resp);
355 } else {
356 // Multiple parties: issue a short-lived select-party token.
358 claims.subject = boost::uuids::to_string(acct.id);
359 claims.issued_at = now;
360 claims.expires_at = now + std::chrono::seconds(
361 token_settings_.party_selection_lifetime_s);
362 claims.audience = "select_party_only";
363 claims.username = acct.username;
364 claims.email = acct.email;
365 claims.tenant_id = acct.tenant_id.to_string();
366 claims.session_id = session_id_str;
367 claims.session_start_time = now;
368 auto token = signer_.create_token(claims).value_or("");
369
370 login_response resp;
371 resp.success = true;
372 resp.token = token;
373 resp.account_id = boost::uuids::to_string(acct.id);
374 resp.tenant_id = acct.tenant_id.to_string();
375 resp.username = acct.username;
376 resp.email = acct.email;
377 resp.tenant_bootstrap_mode = in_tenant_bootstrap;
378 resp.access_lifetime_s = token_settings_.party_selection_lifetime_s;
379 resp.session_id = session_id_str;
380 for (const auto& ap : account_parties) {
381 auto p = auth_lookup_party(login_ctx, ap.party_id);
382 resp.available_parties.push_back(party_summary{
383 .id = boost::uuids::to_string(ap.party_id),
384 .name = p ? p->full_name : std::string{},
385 .party_category =
386 p ? p->party_category : std::string{},
387 .business_center_code =
388 p ? p->business_center_code : std::string{}
389 });
390 }
391 BOOST_LOG_SEV(auth_handler_lg(), debug)
392 << "Completed " << msg.subject;
393 // Multi-party: login_success recorded after party selection
394 reply(nats_, msg, resp);
395 }
396 } catch (const std::exception& e) {
397 BOOST_LOG_SEV(auth_handler_lg(), error)
398 << msg.subject << " failed: " << e.what();
399 record_auth_event(ctx_, "login_failure", [&](auto& ev_repo) {
400 ev_repo.record_login_failure(
401 std::chrono::system_clock::now(),
402 "", req->principal, e.what());
403 });
404 login_response resp;
405 resp.success = false;
406 resp.error_message = e.what();
407 reply(nats_, msg, resp);
408 }
409 }
410
411 void public_key(ores::nats::message msg) {
412 [[maybe_unused]] const auto correlation_id =
413 log_handler_entry(auth_handler_lg(), msg);
414 if (msg.reply_subject.empty()) return;
415 try {
416 auto pub_key = signer_.get_public_key_pem();
417 if (pub_key.empty())
418 throw std::runtime_error(
419 "No RSA private key configured for JWT signing. "
420 "Run generate_keys.sh in publish/bin/ to generate "
421 "the key, then restart the IAM service.");
422 const auto json =
423 std::string("{\"public_key\":") + rfl::json::write(pub_key) + "}";
425 BOOST_LOG_SEV(auth_handler_lg(), debug)
426 << "Completed " << msg.subject;
427 } catch (const std::exception& e) {
428 BOOST_LOG_SEV(auth_handler_lg(), error)
429 << msg.subject << " failed: " << e.what();
430 }
431 }
432
433 void logout(ores::nats::message msg) {
434 [[maybe_unused]] const auto correlation_id =
435 log_handler_entry(auth_handler_lg(), msg);
436 auto token = auth_extract_bearer_token(msg);
437 try {
438 if (!token.empty()) {
439 auto claims_result = signer_.validate(token);
440 if (claims_result) {
441 boost::uuids::string_generator sg;
442 // Update the login_info online flag
443 try {
444 auto account_id = sg(claims_result->subject);
445 service::account_service svc(ctx_);
446 svc.logout(account_id);
447 } catch (const std::exception& e) {
448 BOOST_LOG_SEV(auth_handler_lg(), warn)
449 << "Failed to update logout state: " << e.what();
450 }
451 // Persist session end time using the IDs embedded in
452 // the JWT at login.
453 if (claims_result->session_id &&
454 claims_result->session_start_time) {
455 try {
456 const auto session_id =
457 sg(*claims_result->session_id);
458 repository::session_repository sess_repo(ctx_);
459 sess_repo.end_session(
460 session_id,
461 *claims_result->session_start_time,
462 std::chrono::system_clock::now(),
463 0, 0);
464 } catch (const std::exception& e) {
465 BOOST_LOG_SEV(auth_handler_lg(), warn)
466 << "Failed to end session record: "
467 << e.what();
468 }
469 }
470 }
471 }
472 if (!token.empty()) {
473 // Record logout event from the validated claims.
474 auto claims_result = signer_.validate_allow_expired(token);
475 if (claims_result) {
476 record_auth_event(ctx_, "logout", [&](auto& ev_repo) {
477 ev_repo.record_logout(
478 std::chrono::system_clock::now(),
479 claims_result->tenant_id.value_or(""),
480 claims_result->subject,
481 claims_result->username.value_or(""),
482 claims_result->session_id.value_or(""));
483 });
484 }
485 }
486 BOOST_LOG_SEV(auth_handler_lg(), debug)
487 << "Completed " << msg.subject;
488 reply(nats_, msg, logout_response{
489 .success = true, .message = "Logged out"});
490 } catch (const std::exception& e) {
491 BOOST_LOG_SEV(auth_handler_lg(), error)
492 << msg.subject << " failed: " << e.what();
493 reply(nats_, msg, logout_response{
494 .success = false, .message = e.what()});
495 }
496 }
497
498 void refresh(ores::nats::message msg) {
499 [[maybe_unused]] const auto correlation_id =
500 log_handler_entry(auth_handler_lg(), msg);
501
502 const auto token = auth_extract_bearer_token(msg);
503 if (token.empty()) {
504 reply(nats_, msg, refresh_response{
505 .success = false, .message = "Missing Authorization header"});
506 return;
507 }
508
509 try {
510 // Validate token ignoring expiry — we still verify the signature.
511 auto claims_result = signer_.validate_allow_expired(token);
512 if (!claims_result) {
513 reply(nats_, msg, refresh_response{
514 .success = false, .message = "Invalid token"});
515 return;
516 }
517
518 // Enforce max session ceiling using session_start_time embedded
519 // in the token at login.
520 const auto now = std::chrono::system_clock::now();
521 if (claims_result->session_start_time) {
522 const auto session_age = now - *claims_result->session_start_time;
523 const auto max_session = std::chrono::seconds(
524 token_settings_.max_session_s);
525 if (session_age >= max_session) {
526 BOOST_LOG_SEV(auth_handler_lg(), info)
527 << "Max session exceeded for subject: "
528 << claims_result->subject;
529 record_auth_event(ctx_, "max_session_exceeded", [&](auto& ev_repo) {
530 ev_repo.record_max_session_exceeded(
531 now,
532 claims_result->tenant_id.value_or(""),
533 claims_result->subject,
534 claims_result->username.value_or(""),
535 claims_result->session_id.value_or(""));
536 });
537 reply(nats_, msg, refresh_response{
538 .success = false, .message = "max_session_exceeded"});
539 return;
540 }
541 }
542
543 // Issue a fresh token carrying the same identity claims.
544 security::jwt::jwt_claims new_claims;
545 new_claims.subject = claims_result->subject;
546 new_claims.issued_at = now;
547 new_claims.expires_at = now + std::chrono::seconds(
548 token_settings_.access_lifetime_s);
549 new_claims.username = claims_result->username;
550 new_claims.email = claims_result->email;
551 new_claims.tenant_id = claims_result->tenant_id;
552 new_claims.party_id = claims_result->party_id;
553 new_claims.session_id = claims_result->session_id;
554 new_claims.session_start_time = claims_result->session_start_time;
555 new_claims.roles = claims_result->roles;
556 new_claims.visible_party_ids = claims_result->visible_party_ids;
557
558 const auto new_token = signer_.create_token(new_claims).value_or("");
559 if (new_token.empty()) {
560 reply(nats_, msg, refresh_response{
561 .success = false, .message = "Token creation failed"});
562 return;
563 }
564
565 BOOST_LOG_SEV(auth_handler_lg(), debug)
566 << "Completed " << msg.subject << " for subject: "
567 << claims_result->subject;
568 record_auth_event(ctx_, "token_refresh", [&](auto& ev_repo) {
569 ev_repo.record_token_refresh(
570 now,
571 claims_result->tenant_id.value_or(""),
572 claims_result->subject,
573 claims_result->username.value_or(""),
574 claims_result->session_id.value_or(""));
575 });
576 reply(nats_, msg, refresh_response{
577 .success = true,
578 .token = new_token,
579 .access_lifetime_s = token_settings_.access_lifetime_s});
580
581 } catch (const std::exception& e) {
582 BOOST_LOG_SEV(auth_handler_lg(), error)
583 << msg.subject << " failed: " << e.what();
584 reply(nats_, msg, refresh_response{
585 .success = false, .message = e.what()});
586 }
587 }
588
589 void service_login(ores::nats::message msg) {
590 [[maybe_unused]] const auto correlation_id =
591 log_handler_entry(auth_handler_lg(), msg);
592 auto req = decode<service_login_request>(msg);
593 if (!req) {
594 BOOST_LOG_SEV(auth_handler_lg(), warn)
595 << "Failed to decode: " << msg.subject;
596 reply(nats_, msg, service_login_response{
597 .success = false, .message = "Failed to decode request"});
598 return;
599 }
600 try {
601 repository::account_repository account_repo(ctx_);
602 const auto account_id = account_repo.check_service_credentials(
603 req->username, req->password);
604 if (!account_id) {
605 BOOST_LOG_SEV(auth_handler_lg(), warn)
606 << "Service login failed: invalid credentials for " << req->username;
607 reply(nats_, msg, service_login_response{
608 .success = false, .message = "Invalid credentials"});
609 return;
610 }
611
612 service::service_session_service sess_svc(ctx_);
613 auto sess = sess_svc.start_service_session(
614 req->username, "ores.service.binary");
615 if (!sess) {
616 BOOST_LOG_SEV(auth_handler_lg(), error)
617 << "Failed to start service session for " << req->username;
618 reply(nats_, msg, service_login_response{
619 .success = false, .message = "Failed to create session"});
620 return;
621 }
622
623 const auto now = std::chrono::system_clock::now();
625 claims.subject = boost::uuids::to_string(sess->account_id);
626 claims.issued_at = now;
627 claims.expires_at = now + std::chrono::seconds(
628 token_settings_.access_lifetime_s);
629 claims.username = req->username;
630 claims.tenant_id = sess->tenant_id.to_string();
631 claims.session_id = boost::uuids::to_string(sess->id);
632 claims.session_start_time = sess->start_time;
633
634 // Embed effective permission codes so handlers can enforce
635 // permissions without a round-trip to the database.
636 try {
637 service::authorization_service auth_svc(ctx_);
638 claims.roles =
639 auth_svc.get_effective_permissions(sess->account_id);
640 } catch (const std::exception& e) {
641 BOOST_LOG_SEV(auth_handler_lg(), warn)
642 << "Failed to load permissions for service account "
643 << req->username << ": " << e.what();
644 }
645
646 auto token = signer_.create_token(claims).value_or("");
647 if (token.empty()) {
648 reply(nats_, msg, service_login_response{
649 .success = false, .message = "Token creation failed"});
650 return;
651 }
652
653 BOOST_LOG_SEV(auth_handler_lg(), info)
654 << "Service login successful for " << req->username;
655 reply(nats_, msg, service_login_response{
656 .success = true,
657 .token = std::move(token),
658 .access_lifetime_s = token_settings_.access_lifetime_s});
659 } catch (const std::exception& e) {
660 BOOST_LOG_SEV(auth_handler_lg(), error)
661 << msg.subject << " failed: " << e.what();
662 reply(nats_, msg, service_login_response{
663 .success = false, .message = e.what()});
664 }
665 }
666
667private:
673 template <typename Func>
674 void record_auth_event(const ores::database::context& ctx,
675 const char* event_name, Func&& fn) {
676 try {
677 repository::auth_event_repository ev_repo(ctx);
678 fn(ev_repo);
679 } catch (const std::exception& ev_err) {
680 using namespace ores::logging;
681 BOOST_LOG_SEV(auth_handler_lg(), warn)
682 << "Failed to record " << event_name
683 << " event: " << ev_err.what();
684 }
685 }
686
690 domain::token_settings token_settings_;
691};
692
693} // namespace ores::iam::messaging
694#endif
STL namespace.
@ http
HTTP/REST API with JWT authentication.
std::span< const std::byte > as_bytes(std::string_view s) noexcept
Reinterprets a string's character data as a read-only byte span.
Definition message.hpp:81
Implements logging infrastructure for ORE Studio.
Definition boost_severity.hpp:28
Context for the operations on a postgres database.
Definition context.hpp:47
context with_tenant(utility::uuid::tenant_id tenant_id, std::string actor) const
Creates a new context with a different tenant ID (no party).
Definition context.hpp:162
static token_settings load(variability::service::system_settings_service &svc)
Loads token settings from the system settings service.
Definition token_settings.cpp:24
A received NATS message.
Definition message.hpp:40
std::string subject
The subject the message was published to.
Definition message.hpp:44
std::unordered_map< std::string, std::string > headers
NATS message headers (NATS 2.2+).
Definition message.hpp:66
std::string reply_subject
The reply-to subject, empty for one-way publishes.
Definition message.hpp:51
NATS client: connection, pub/sub, request/reply, and JetStream.
Definition client.hpp:73
void publish(std::string_view subject, std::span< const std::byte > data, std::unordered_map< std::string, std::string > headers={})
Publish a message to a subject.
Definition client.cpp:343
Reads and writes parties to data storage.
Definition party_repository.hpp:37
JWT authentication primitive.
Definition jwt_authenticator.hpp:45
Represents the claims extracted from a JWT token.
Definition jwt_claims.hpp:33
std::string subject
Subject claim - typically the account ID.
Definition jwt_claims.hpp:37
std::optional< std::string > session_id
Optional session ID for tracking sessions.
Definition jwt_claims.hpp:81
std::chrono::system_clock::time_point issued_at
Time when the token was issued.
Definition jwt_claims.hpp:57
std::optional< std::string > tenant_id
Optional tenant ID (UUID string).
Definition jwt_claims.hpp:97
std::optional< std::string > party_id
Optional party ID (UUID string, nil UUID if no party selected).
Definition jwt_claims.hpp:104
std::optional< std::chrono::system_clock::time_point > session_start_time
Optional session start time for efficient database updates.
Definition jwt_claims.hpp:90
std::string audience
Intended audience for the token.
Definition jwt_claims.hpp:47
std::vector< std::string > visible_party_ids
List of visible party IDs (UUID strings) for the session.
Definition jwt_claims.hpp:112
std::optional< std::string > username
Optional username claim.
Definition jwt_claims.hpp:67
std::chrono::system_clock::time_point expires_at
Time when the token expires.
Definition jwt_claims.hpp:52
std::vector< std::string > roles
User roles/permissions.
Definition jwt_claims.hpp:62
std::optional< std::string > email
Optional email claim.
Definition jwt_claims.hpp:72
static std::expected< tenant_id, std::string > from_uuid(const boost::uuids::uuid &uuid)
Creates a tenant_id from a boost UUID.
Definition tenant_id.cpp:47
static std::expected< tenant_id, std::string > from_string(std::string_view str)
Creates a tenant_id from a string representation.
Definition tenant_id.cpp:57
Service for managing typed system settings.
Definition system_settings_service.hpp:41