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("Authorization");
67 if (it == msg.headers.end())
68 return {};
69 const auto& val = it->second;
70 constexpr std::string_view prefix = "Bearer ";
71 if (!val.starts_with(prefix))
72 return {};
73 return val.substr(prefix.size());
74}
75
76inline std::vector<boost::uuids::uuid> auth_compute_visible_party_ids(
77 const ores::database::context& ctx,
78 const boost::uuids::uuid& party_id) {
79 try {
80 refdata::repository::party_repository repo(ctx);
81 auto ids = repo.read_descendants(party_id);
82 if (ids.empty())
83 return {party_id};
84 return ids;
85 } catch (const std::exception& e) {
86 using namespace ores::logging;
87 BOOST_LOG_SEV(auth_handler_lg(), warn)
88 << "Failed to compute visible party IDs: " << e.what();
89 return {party_id};
90 }
91}
92
93inline std::optional<refdata::domain::party> auth_lookup_party(
94 const ores::database::context& ctx,
95 const boost::uuids::uuid& party_id) {
96 try {
98 auto parties = repo.read_latest(party_id);
99 if (!parties.empty())
100 return parties.front();
101 } catch (const std::exception& e) {
102 using namespace ores::logging;
103 BOOST_LOG_SEV(auth_handler_lg(), warn)
104 << "Failed to look up party: " << e.what();
105 }
106 return std::nullopt;
107}
108
109inline std::string auth_lookup_tenant_name(
110 const ores::database::context& ctx,
111 const boost::uuids::uuid& tenant_id) {
112 try {
113 repository::tenant_repository repo(ctx);
114 auto tenants = repo.read_latest(tenant_id);
115 if (!tenants.empty())
116 return tenants.front().name;
117 } catch (const std::exception& e) {
118 using namespace ores::logging;
119 BOOST_LOG_SEV(auth_handler_lg(), warn)
120 << "Failed to look up tenant name: " << e.what();
121 }
122 return {};
123}
124
125inline std::optional<ores::iam::domain::tenant> auth_lookup_tenant_by_hostname(
126 const ores::database::context& ctx, const std::string& hostname) {
127 try {
128 repository::tenant_repository repo(ctx);
129 auto tenants = repo.read_latest_by_hostname(hostname);
130 if (!tenants.empty())
131 return tenants.front();
132 } catch (const std::exception& e) {
133 using namespace ores::logging;
134 BOOST_LOG_SEV(auth_handler_lg(), warn)
135 << "Failed to look up tenant by hostname: " << e.what();
136 }
137 return std::nullopt;
138}
139
140inline bool auth_is_tenant_bootstrap_mode(
141 const ores::database::context& ctx,
142 const std::string& tenant_id_str) {
143 try {
144 auto tid_result = ores::utility::uuid::tenant_id::from_string(tenant_id_str);
145 if (!tid_result) return false;
146 auto tenant_ctx = ctx.with_tenant(*tid_result, "");
147 variability::service::system_settings_service sfs(tenant_ctx, tenant_id_str);
148 sfs.refresh();
149 return sfs.is_bootstrap_mode_enabled();
150 } catch (const std::exception& e) {
151 using namespace ores::logging;
152 BOOST_LOG_SEV(auth_handler_lg(), warn)
153 << "Failed to check tenant bootstrap mode: " << e.what();
154 }
155 return false;
156}
157
158} // namespace
159
160using ores::service::messaging::reply;
161using ores::service::messaging::decode;
162
163class auth_handler {
164public:
165 auth_handler(ores::nats::service::client& nats,
168 : nats_(nats), ctx_(std::move(ctx)), signer_(std::move(signer)) {
169 reload_token_settings();
170 }
171
172 void reload_token_settings() {
173 try {
175 svc.refresh();
176 token_settings_ = domain::token_settings::load(svc);
177 } catch (const std::exception& e) {
178 using namespace ores::logging;
179 BOOST_LOG_SEV(auth_handler_lg(), warn)
180 << "Failed to load token settings, using defaults: " << e.what();
181 }
182 }
183
184 void signup(ores::nats::message msg) {
185 using namespace ores::logging;
186 BOOST_LOG_SEV(auth_handler_lg(), debug) << "Handling " << msg.subject;
187 auto req = decode<signup_request>(msg);
188 if (!req) {
189 BOOST_LOG_SEV(auth_handler_lg(), warn)
190 << "Failed to decode: " << msg.subject;
191 return;
192 }
193 try {
194 service::account_service acct_svc(ctx_);
195 auto auth_svc =
196 std::make_shared<service::authorization_service>(ctx_);
197 service::account_setup_service setup_svc(acct_svc, auth_svc);
198 auto acct = setup_svc.create_account(
199 req->principal, req->email, req->password, ctx_.service_account());
200 BOOST_LOG_SEV(auth_handler_lg(), debug)
201 << "Completed " << msg.subject;
202 record_auth_event(ctx_, "signup_success", [&](auto& ev_repo) {
203 ev_repo.record_signup_success(
204 std::chrono::system_clock::now(),
205 acct.tenant_id.to_string(),
206 boost::uuids::to_string(acct.id),
207 acct.username);
208 });
209 reply(nats_, msg, signup_response{
210 .success = true,
211 .account_id = boost::uuids::to_string(acct.id)});
212 } catch (const std::exception& e) {
213 BOOST_LOG_SEV(auth_handler_lg(), error)
214 << msg.subject << " failed: " << e.what();
215 record_auth_event(ctx_, "signup_failure", [&](auto& ev_repo) {
216 ev_repo.record_signup_failure(
217 std::chrono::system_clock::now(),
218 "", req->principal, e.what());
219 });
220 reply(nats_, msg, signup_response{
221 .success = false, .message = e.what()});
222 }
223 }
224
225 void login(ores::nats::message msg) {
226 using namespace ores::logging;
227 BOOST_LOG_SEV(auth_handler_lg(), debug) << "Handling " << msg.subject;
228 auto req = decode<login_request>(msg);
229 if (!req) {
230 BOOST_LOG_SEV(auth_handler_lg(), warn)
231 << "Failed to decode: " << msg.subject;
232 return;
233 }
234 try {
235 // Parse principal: split username@hostname for tenant routing
236 std::string username = req->principal;
237 ores::database::context login_ctx = ctx_;
238 const auto at_pos = req->principal.rfind('@');
239 if (at_pos != std::string::npos) {
240 username = req->principal.substr(0, at_pos);
241 const auto hostname = req->principal.substr(at_pos + 1);
242 if (auto t = auth_lookup_tenant_by_hostname(ctx_, hostname)) {
243 auto tid_result =
245 if (tid_result)
246 login_ctx = ctx_.with_tenant(*tid_result, "");
247 }
248 }
249
250 service::account_service svc(login_ctx);
251 auto ip = boost::asio::ip::address_v4::loopback();
252 auto acct = svc.login(username, req->password, ip);
253
254 // Check if this non-system tenant needs its provisioning wizard
255 const bool in_tenant_bootstrap =
256 !acct.tenant_id.is_system() &&
257 auth_is_tenant_bootstrap_mode(
258 login_ctx, acct.tenant_id.to_string());
259
260 repository::account_party_repository ap_repo(login_ctx);
261 auto account_parties =
262 ap_repo.read_latest_by_account(acct.id);
263
264 // Every fully-provisioned account must have at least one party.
265 // A missing party is a misconfiguration — reject the login so the
266 // issue surfaces immediately rather than producing a confusing
267 // partial session.
268 if (account_parties.empty()) {
269 BOOST_LOG_SEV(auth_handler_lg(), warn)
270 << "Login rejected for " << username
271 << ": account has no party assignment";
272 throw std::runtime_error(
273 "Account has no party assignment. "
274 "Please contact your administrator.");
275 }
276
277 // Create a session record so that analytics, session listings,
278 // and logout end-time tracking all work correctly.
279 const auto now = std::chrono::system_clock::now();
280 boost::uuids::random_generator uuid_gen;
281 domain::session sess;
282 sess.id = uuid_gen();
283 sess.account_id = acct.id;
284 sess.tenant_id = acct.tenant_id;
285 sess.start_time = now;
286 sess.username = acct.username;
287 sess.protocol = domain::session_protocol::http;
288 sess.client_ip = ip;
289 // party_id set below once we know which party is active
290 try {
291 repository::session_repository sess_repo(login_ctx);
292 sess_repo.create(sess);
293 } catch (const std::exception& e) {
294 BOOST_LOG_SEV(auth_handler_lg(), warn)
295 << "Failed to create session record: " << e.what();
296 }
297 const auto session_id_str = boost::uuids::to_string(sess.id);
298
299 if (account_parties.size() == 1) {
300 const auto& party_id =
301 account_parties.front().party_id;
302 auto visible = auth_compute_visible_party_ids(
303 login_ctx, party_id);
304
306 claims.subject = boost::uuids::to_string(acct.id);
307 claims.issued_at = now;
308 claims.expires_at = now + std::chrono::seconds(
309 token_settings_.access_lifetime_s);
310 claims.username = acct.username;
311 claims.email = acct.email;
312 claims.tenant_id = acct.tenant_id.to_string();
313 claims.party_id = boost::uuids::to_string(party_id);
314 claims.session_id = session_id_str;
315 claims.session_start_time = now;
316 for (const auto& vid : visible)
317 claims.visible_party_ids.push_back(
318 boost::uuids::to_string(vid));
319 auto token = signer_.create_token(claims).value_or("");
320
321 login_response resp;
322 resp.success = true;
323 resp.token = token;
324 resp.account_id = boost::uuids::to_string(acct.id);
325 resp.tenant_id = acct.tenant_id.to_string();
326 resp.username = acct.username;
327 resp.email = acct.email;
328 resp.selected_party_id = boost::uuids::to_string(party_id);
329 resp.tenant_bootstrap_mode = in_tenant_bootstrap;
330 resp.access_lifetime_s = token_settings_.access_lifetime_s;
331 for (const auto& ap : account_parties) {
332 auto p = auth_lookup_party(login_ctx, ap.party_id);
333 resp.available_parties.push_back(party_summary{
334 .id = boost::uuids::to_string(ap.party_id),
335 .name = p ? p->full_name : std::string{},
336 .party_category =
337 p ? p->party_category : std::string{}
338 });
339 }
340 BOOST_LOG_SEV(auth_handler_lg(), debug)
341 << "Completed " << msg.subject;
342 record_auth_event(login_ctx, "login_success", [&](auto& ev_repo) {
343 ev_repo.record_login_success(
344 now,
345 acct.tenant_id.to_string(),
346 boost::uuids::to_string(acct.id),
347 acct.username,
348 session_id_str,
349 boost::uuids::to_string(party_id));
350 });
351 reply(nats_, msg, resp);
352 } else {
353 // Multiple parties: issue a short-lived select-party token.
355 claims.subject = boost::uuids::to_string(acct.id);
356 claims.issued_at = now;
357 claims.expires_at = now + std::chrono::seconds(
358 token_settings_.party_selection_lifetime_s);
359 claims.audience = "select_party_only";
360 claims.username = acct.username;
361 claims.email = acct.email;
362 claims.tenant_id = acct.tenant_id.to_string();
363 claims.session_id = session_id_str;
364 claims.session_start_time = now;
365 auto token = signer_.create_token(claims).value_or("");
366
367 login_response resp;
368 resp.success = true;
369 resp.token = token;
370 resp.account_id = boost::uuids::to_string(acct.id);
371 resp.tenant_id = acct.tenant_id.to_string();
372 resp.username = acct.username;
373 resp.email = acct.email;
374 resp.tenant_bootstrap_mode = in_tenant_bootstrap;
375 resp.access_lifetime_s = token_settings_.party_selection_lifetime_s;
376 for (const auto& ap : account_parties) {
377 auto p = auth_lookup_party(login_ctx, ap.party_id);
378 resp.available_parties.push_back(party_summary{
379 .id = boost::uuids::to_string(ap.party_id),
380 .name = p ? p->full_name : std::string{},
381 .party_category =
382 p ? p->party_category : std::string{}
383 });
384 }
385 BOOST_LOG_SEV(auth_handler_lg(), debug)
386 << "Completed " << msg.subject;
387 // Multi-party: login_success recorded after party selection
388 reply(nats_, msg, resp);
389 }
390 } catch (const std::exception& e) {
391 BOOST_LOG_SEV(auth_handler_lg(), error)
392 << msg.subject << " failed: " << e.what();
393 record_auth_event(ctx_, "login_failure", [&](auto& ev_repo) {
394 ev_repo.record_login_failure(
395 std::chrono::system_clock::now(),
396 "", req->principal, e.what());
397 });
398 login_response resp;
399 resp.success = false;
400 resp.error_message = e.what();
401 reply(nats_, msg, resp);
402 }
403 }
404
405 void public_key(ores::nats::message msg) {
406 using namespace ores::logging;
407 BOOST_LOG_SEV(auth_handler_lg(), debug) << "Handling " << msg.subject;
408 if (msg.reply_subject.empty()) return;
409 try {
410 auto pub_key = signer_.get_public_key_pem();
411 if (pub_key.empty())
412 throw std::runtime_error(
413 "No RSA private key configured for JWT signing. "
414 "Run generate_keys.sh in publish/bin/ to generate "
415 "the key, then restart the IAM service.");
416 const auto json =
417 std::string("{\"public_key\":") + rfl::json::write(pub_key) + "}";
419 BOOST_LOG_SEV(auth_handler_lg(), debug)
420 << "Completed " << msg.subject;
421 } catch (const std::exception& e) {
422 BOOST_LOG_SEV(auth_handler_lg(), error)
423 << msg.subject << " failed: " << e.what();
424 }
425 }
426
427 void logout(ores::nats::message msg) {
428 using namespace ores::logging;
429 BOOST_LOG_SEV(auth_handler_lg(), debug) << "Handling " << msg.subject;
430 auto token = auth_extract_bearer_token(msg);
431 try {
432 if (!token.empty()) {
433 auto claims_result = signer_.validate(token);
434 if (claims_result) {
435 boost::uuids::string_generator sg;
436 // Update the login_info online flag
437 try {
438 auto account_id = sg(claims_result->subject);
439 service::account_service svc(ctx_);
440 svc.logout(account_id);
441 } catch (const std::exception& e) {
442 BOOST_LOG_SEV(auth_handler_lg(), warn)
443 << "Failed to update logout state: " << e.what();
444 }
445 // Persist session end time using the IDs embedded in
446 // the JWT at login.
447 if (claims_result->session_id &&
448 claims_result->session_start_time) {
449 try {
450 const auto session_id =
451 sg(*claims_result->session_id);
452 repository::session_repository sess_repo(ctx_);
453 sess_repo.end_session(
454 session_id,
455 *claims_result->session_start_time,
456 std::chrono::system_clock::now(),
457 0, 0);
458 } catch (const std::exception& e) {
459 BOOST_LOG_SEV(auth_handler_lg(), warn)
460 << "Failed to end session record: "
461 << e.what();
462 }
463 }
464 }
465 }
466 if (!token.empty()) {
467 // Record logout event from the validated claims.
468 auto claims_result = signer_.validate_allow_expired(token);
469 if (claims_result) {
470 record_auth_event(ctx_, "logout", [&](auto& ev_repo) {
471 ev_repo.record_logout(
472 std::chrono::system_clock::now(),
473 claims_result->tenant_id.value_or(""),
474 claims_result->subject,
475 claims_result->username.value_or(""),
476 claims_result->session_id.value_or(""));
477 });
478 }
479 }
480 BOOST_LOG_SEV(auth_handler_lg(), debug)
481 << "Completed " << msg.subject;
482 reply(nats_, msg, logout_response{
483 .success = true, .message = "Logged out"});
484 } catch (const std::exception& e) {
485 BOOST_LOG_SEV(auth_handler_lg(), error)
486 << msg.subject << " failed: " << e.what();
487 reply(nats_, msg, logout_response{
488 .success = false, .message = e.what()});
489 }
490 }
491
492 void refresh(ores::nats::message msg) {
493 using namespace ores::logging;
494 BOOST_LOG_SEV(auth_handler_lg(), debug) << "Handling " << msg.subject;
495
496 const auto token = auth_extract_bearer_token(msg);
497 if (token.empty()) {
498 reply(nats_, msg, refresh_response{
499 .success = false, .message = "Missing Authorization header"});
500 return;
501 }
502
503 try {
504 // Validate token ignoring expiry — we still verify the signature.
505 auto claims_result = signer_.validate_allow_expired(token);
506 if (!claims_result) {
507 reply(nats_, msg, refresh_response{
508 .success = false, .message = "Invalid token"});
509 return;
510 }
511
512 // Enforce max session ceiling using session_start_time embedded
513 // in the token at login.
514 const auto now = std::chrono::system_clock::now();
515 if (claims_result->session_start_time) {
516 const auto session_age = now - *claims_result->session_start_time;
517 const auto max_session = std::chrono::seconds(
518 token_settings_.max_session_s);
519 if (session_age >= max_session) {
520 BOOST_LOG_SEV(auth_handler_lg(), info)
521 << "Max session exceeded for subject: "
522 << claims_result->subject;
523 record_auth_event(ctx_, "max_session_exceeded", [&](auto& ev_repo) {
524 ev_repo.record_max_session_exceeded(
525 now,
526 claims_result->tenant_id.value_or(""),
527 claims_result->subject,
528 claims_result->username.value_or(""),
529 claims_result->session_id.value_or(""));
530 });
531 reply(nats_, msg, refresh_response{
532 .success = false, .message = "max_session_exceeded"});
533 return;
534 }
535 }
536
537 // Issue a fresh token carrying the same identity claims.
538 security::jwt::jwt_claims new_claims;
539 new_claims.subject = claims_result->subject;
540 new_claims.issued_at = now;
541 new_claims.expires_at = now + std::chrono::seconds(
542 token_settings_.access_lifetime_s);
543 new_claims.username = claims_result->username;
544 new_claims.email = claims_result->email;
545 new_claims.tenant_id = claims_result->tenant_id;
546 new_claims.party_id = claims_result->party_id;
547 new_claims.session_id = claims_result->session_id;
548 new_claims.session_start_time = claims_result->session_start_time;
549 new_claims.roles = claims_result->roles;
550 new_claims.visible_party_ids = claims_result->visible_party_ids;
551
552 const auto new_token = signer_.create_token(new_claims).value_or("");
553 if (new_token.empty()) {
554 reply(nats_, msg, refresh_response{
555 .success = false, .message = "Token creation failed"});
556 return;
557 }
558
559 BOOST_LOG_SEV(auth_handler_lg(), debug)
560 << "Completed " << msg.subject << " for subject: "
561 << claims_result->subject;
562 record_auth_event(ctx_, "token_refresh", [&](auto& ev_repo) {
563 ev_repo.record_token_refresh(
564 now,
565 claims_result->tenant_id.value_or(""),
566 claims_result->subject,
567 claims_result->username.value_or(""),
568 claims_result->session_id.value_or(""));
569 });
570 reply(nats_, msg, refresh_response{
571 .success = true,
572 .token = new_token,
573 .access_lifetime_s = token_settings_.access_lifetime_s});
574
575 } catch (const std::exception& e) {
576 BOOST_LOG_SEV(auth_handler_lg(), error)
577 << msg.subject << " failed: " << e.what();
578 reply(nats_, msg, refresh_response{
579 .success = false, .message = e.what()});
580 }
581 }
582
583 void service_login(ores::nats::message msg) {
584 using namespace ores::logging;
585 BOOST_LOG_SEV(auth_handler_lg(), debug) << "Handling " << msg.subject;
586 auto req = decode<service_login_request>(msg);
587 if (!req) {
588 BOOST_LOG_SEV(auth_handler_lg(), warn)
589 << "Failed to decode: " << msg.subject;
590 reply(nats_, msg, service_login_response{
591 .success = false, .message = "Failed to decode request"});
592 return;
593 }
594 try {
595 repository::account_repository account_repo(ctx_);
596 const auto account_id = account_repo.check_service_credentials(
597 req->username, req->password);
598 if (!account_id) {
599 BOOST_LOG_SEV(auth_handler_lg(), warn)
600 << "Service login failed: invalid credentials for " << req->username;
601 reply(nats_, msg, service_login_response{
602 .success = false, .message = "Invalid credentials"});
603 return;
604 }
605
606 service::service_session_service sess_svc(ctx_);
607 auto sess = sess_svc.start_service_session(
608 req->username, "ores.service.binary");
609 if (!sess) {
610 BOOST_LOG_SEV(auth_handler_lg(), error)
611 << "Failed to start service session for " << req->username;
612 reply(nats_, msg, service_login_response{
613 .success = false, .message = "Failed to create session"});
614 return;
615 }
616
617 const auto now = std::chrono::system_clock::now();
619 claims.subject = boost::uuids::to_string(sess->account_id);
620 claims.issued_at = now;
621 claims.expires_at = now + std::chrono::seconds(
622 token_settings_.access_lifetime_s);
623 claims.username = req->username;
624 claims.tenant_id = sess->tenant_id.to_string();
625 claims.session_id = boost::uuids::to_string(sess->id);
626 claims.session_start_time = sess->start_time;
627
628 // Embed effective permission codes so handlers can enforce
629 // permissions without a round-trip to the database.
630 try {
631 service::authorization_service auth_svc(ctx_);
632 claims.roles =
633 auth_svc.get_effective_permissions(sess->account_id);
634 } catch (const std::exception& e) {
635 BOOST_LOG_SEV(auth_handler_lg(), warn)
636 << "Failed to load permissions for service account "
637 << req->username << ": " << e.what();
638 }
639
640 auto token = signer_.create_token(claims).value_or("");
641 if (token.empty()) {
642 reply(nats_, msg, service_login_response{
643 .success = false, .message = "Token creation failed"});
644 return;
645 }
646
647 BOOST_LOG_SEV(auth_handler_lg(), info)
648 << "Service login successful for " << req->username;
649 reply(nats_, msg, service_login_response{
650 .success = true,
651 .token = std::move(token),
652 .access_lifetime_s = token_settings_.access_lifetime_s});
653 } catch (const std::exception& e) {
654 BOOST_LOG_SEV(auth_handler_lg(), error)
655 << msg.subject << " failed: " << e.what();
656 reply(nats_, msg, service_login_response{
657 .success = false, .message = e.what()});
658 }
659 }
660
661private:
667 template <typename Func>
668 void record_auth_event(const ores::database::context& ctx,
669 const char* event_name, Func&& fn) {
670 try {
671 repository::auth_event_repository ev_repo(ctx);
672 fn(ev_repo);
673 } catch (const std::exception& ev_err) {
674 using namespace ores::logging;
675 BOOST_LOG_SEV(auth_handler_lg(), warn)
676 << "Failed to record " << event_name
677 << " event: " << ev_err.what();
678 }
679 }
680
684 domain::token_settings token_settings_;
685};
686
687} // namespace ores::iam::messaging
688#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:161
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:298
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