20#ifndef ORES_IAM_MESSAGING_AUTH_HANDLER_HPP
21#define ORES_IAM_MESSAGING_AUTH_HANDLER_HPP
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"
55namespace ores::iam::messaging {
59inline auto& auth_handler_lg() {
60 static auto instance = ores::logging::make_logger(
61 "ores.iam.messaging.auth_handler");
66 auto it = msg.
headers.find(std::string(ores::nats::headers::authorization));
69 const auto& val = it->second;
70 if (!val.starts_with(ores::nats::headers::bearer_prefix))
72 return val.substr(ores::nats::headers::bearer_prefix.size());
75inline std::vector<boost::uuids::uuid> auth_compute_visible_party_ids(
77 const boost::uuids::uuid& party_id) {
79 refdata::repository::party_repository repo(ctx);
80 auto ids = repo.read_descendants(party_id);
84 }
catch (
const std::exception& e) {
86 BOOST_LOG_SEV(auth_handler_lg(), warn)
87 <<
"Failed to compute visible party IDs: " << e.what();
92inline std::optional<refdata::domain::party> auth_lookup_party(
94 const boost::uuids::uuid& party_id) {
97 auto parties = repo.read_latest(party_id);
99 return parties.front();
100 }
catch (
const std::exception& e) {
102 BOOST_LOG_SEV(auth_handler_lg(), warn)
103 <<
"Failed to look up party: " << e.what();
108inline std::string auth_lookup_tenant_name(
110 const boost::uuids::uuid& tenant_id) {
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) {
118 BOOST_LOG_SEV(auth_handler_lg(), warn)
119 <<
"Failed to look up tenant name: " << e.what();
124inline std::optional<ores::iam::domain::tenant> auth_lookup_tenant_by_hostname(
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) {
133 BOOST_LOG_SEV(auth_handler_lg(), warn)
134 <<
"Failed to look up tenant by hostname: " << e.what();
139inline bool auth_is_tenant_bootstrap_mode(
141 const std::string& tenant_id_str) {
144 if (!tid_result)
return false;
145 auto tenant_ctx = ctx.
with_tenant(*tid_result,
"");
148 return sfs.is_bootstrap_mode_enabled();
149 }
catch (
const std::exception& e) {
151 BOOST_LOG_SEV(auth_handler_lg(), warn)
152 <<
"Failed to check tenant bootstrap mode: " << e.what();
159using ores::service::messaging::reply;
160using ores::service::messaging::decode;
161using ores::service::messaging::log_handler_entry;
169 : nats_(nats), ctx_(
std::move(ctx)), signer_(
std::move(signer)) {
170 reload_token_settings();
173 void reload_token_settings() {
178 }
catch (
const std::exception& e) {
180 BOOST_LOG_SEV(auth_handler_lg(), warn)
181 <<
"Failed to load token settings, using defaults: " << e.what();
186 [[maybe_unused]]
const auto correlation_id =
187 log_handler_entry(auth_handler_lg(), msg);
188 auto req = decode<signup_request>(msg);
190 BOOST_LOG_SEV(auth_handler_lg(), warn)
191 <<
"Failed to decode: " << msg.
subject;
195 service::account_service acct_svc(ctx_);
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),
210 reply(nats_, msg, signup_response{
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());
221 reply(nats_, msg, signup_response{
222 .success =
false, .message = e.what()});
227 [[maybe_unused]]
const auto correlation_id =
228 log_handler_entry(auth_handler_lg(), msg);
229 auto req = decode<login_request>(msg);
231 BOOST_LOG_SEV(auth_handler_lg(), warn)
232 <<
"Failed to decode: " << msg.
subject;
237 std::string username = req->principal;
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)) {
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);
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());
261 repository::account_party_repository ap_repo(login_ctx);
262 auto account_parties =
263 ap_repo.read_latest_by_account(acct.id);
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.");
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;
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();
294 const auto session_id_str = boost::uuids::to_string(sess.id);
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);
303 claims.
subject = boost::uuids::to_string(acct.id);
305 claims.
expires_at = now + std::chrono::seconds(
306 token_settings_.access_lifetime_s);
308 claims.
email = acct.email;
309 claims.
tenant_id = acct.tenant_id.to_string();
310 claims.
party_id = boost::uuids::to_string(party_id);
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(
"");
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{},
338 p ? p->party_category : std::string{},
339 .business_center_code =
340 p ? p->business_center_code : std::string{}
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(
348 acct.tenant_id.to_string(),
349 boost::uuids::to_string(acct.id),
352 boost::uuids::to_string(party_id));
354 reply(nats_, msg, resp);
358 claims.
subject = boost::uuids::to_string(acct.id);
360 claims.
expires_at = now + std::chrono::seconds(
361 token_settings_.party_selection_lifetime_s);
362 claims.
audience =
"select_party_only";
364 claims.
email = acct.email;
365 claims.
tenant_id = acct.tenant_id.to_string();
368 auto token = signer_.create_token(claims).value_or(
"");
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{},
386 p ? p->party_category : std::string{},
387 .business_center_code =
388 p ? p->business_center_code : std::string{}
391 BOOST_LOG_SEV(auth_handler_lg(), debug)
392 <<
"Completed " << msg.
subject;
394 reply(nats_, msg, resp);
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());
405 resp.success =
false;
406 resp.error_message = e.what();
407 reply(nats_, msg, resp);
412 [[maybe_unused]]
const auto correlation_id =
413 log_handler_entry(auth_handler_lg(), msg);
416 auto pub_key = signer_.get_public_key_pem();
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.");
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();
434 [[maybe_unused]]
const auto correlation_id =
435 log_handler_entry(auth_handler_lg(), msg);
436 auto token = auth_extract_bearer_token(msg);
438 if (!token.empty()) {
439 auto claims_result = signer_.validate(token);
441 boost::uuids::string_generator sg;
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();
453 if (claims_result->session_id &&
454 claims_result->session_start_time) {
456 const auto session_id =
457 sg(*claims_result->session_id);
458 repository::session_repository sess_repo(ctx_);
459 sess_repo.end_session(
461 *claims_result->session_start_time,
462 std::chrono::system_clock::now(),
464 }
catch (
const std::exception& e) {
465 BOOST_LOG_SEV(auth_handler_lg(), warn)
466 <<
"Failed to end session record: "
472 if (!token.empty()) {
474 auto claims_result = signer_.validate_allow_expired(token);
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(
""));
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()});
499 [[maybe_unused]]
const auto correlation_id =
500 log_handler_entry(auth_handler_lg(), msg);
502 const auto token = auth_extract_bearer_token(msg);
504 reply(nats_, msg, refresh_response{
505 .success =
false, .message =
"Missing Authorization header"});
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"});
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(
532 claims_result->tenant_id.value_or(
""),
533 claims_result->subject,
534 claims_result->username.value_or(
""),
535 claims_result->session_id.value_or(
""));
537 reply(nats_, msg, refresh_response{
538 .success =
false, .message =
"max_session_exceeded"});
545 new_claims.
subject = claims_result->subject;
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;
555 new_claims.
roles = claims_result->roles;
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"});
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(
571 claims_result->tenant_id.value_or(
""),
572 claims_result->subject,
573 claims_result->username.value_or(
""),
574 claims_result->session_id.value_or(
""));
576 reply(nats_, msg, refresh_response{
579 .access_lifetime_s = token_settings_.access_lifetime_s});
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()});
590 [[maybe_unused]]
const auto correlation_id =
591 log_handler_entry(auth_handler_lg(), msg);
592 auto req = decode<service_login_request>(msg);
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"});
601 repository::account_repository account_repo(ctx_);
602 const auto account_id = account_repo.check_service_credentials(
603 req->username, req->password);
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"});
612 service::service_session_service sess_svc(ctx_);
613 auto sess = sess_svc.start_service_session(
614 req->username,
"ores.service.binary");
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"});
623 const auto now = std::chrono::system_clock::now();
625 claims.
subject = boost::uuids::to_string(sess->account_id);
627 claims.
expires_at = now + std::chrono::seconds(
628 token_settings_.access_lifetime_s);
630 claims.
tenant_id = sess->tenant_id.to_string();
631 claims.
session_id = boost::uuids::to_string(sess->id);
637 service::authorization_service auth_svc(ctx_);
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();
646 auto token = signer_.create_token(claims).value_or(
"");
648 reply(nats_, msg, service_login_response{
649 .success =
false, .message =
"Token creation failed"});
653 BOOST_LOG_SEV(auth_handler_lg(), info)
654 <<
"Service login successful for " << req->username;
655 reply(nats_, msg, service_login_response{
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()});
673 template <
typename Func>
675 const char* event_name, Func&& fn) {
677 repository::auth_event_repository ev_repo(ctx);
679 }
catch (
const std::exception& ev_err) {
681 BOOST_LOG_SEV(auth_handler_lg(), warn)
682 <<
"Failed to record " << event_name
683 <<
" event: " << ev_err.what();
690 domain::token_settings token_settings_;
@ 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