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(
"Authorization");
69 const auto& val = it->second;
70 constexpr std::string_view prefix =
"Bearer ";
71 if (!val.starts_with(prefix))
73 return val.substr(prefix.size());
76inline std::vector<boost::uuids::uuid> auth_compute_visible_party_ids(
78 const boost::uuids::uuid& party_id) {
80 refdata::repository::party_repository repo(ctx);
81 auto ids = repo.read_descendants(party_id);
85 }
catch (
const std::exception& e) {
87 BOOST_LOG_SEV(auth_handler_lg(), warn)
88 <<
"Failed to compute visible party IDs: " << e.what();
93inline std::optional<refdata::domain::party> auth_lookup_party(
95 const boost::uuids::uuid& party_id) {
98 auto parties = repo.read_latest(party_id);
100 return parties.front();
101 }
catch (
const std::exception& e) {
103 BOOST_LOG_SEV(auth_handler_lg(), warn)
104 <<
"Failed to look up party: " << e.what();
109inline std::string auth_lookup_tenant_name(
111 const boost::uuids::uuid& tenant_id) {
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) {
119 BOOST_LOG_SEV(auth_handler_lg(), warn)
120 <<
"Failed to look up tenant name: " << e.what();
125inline std::optional<ores::iam::domain::tenant> auth_lookup_tenant_by_hostname(
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) {
134 BOOST_LOG_SEV(auth_handler_lg(), warn)
135 <<
"Failed to look up tenant by hostname: " << e.what();
140inline bool auth_is_tenant_bootstrap_mode(
142 const std::string& tenant_id_str) {
145 if (!tid_result)
return false;
146 auto tenant_ctx = ctx.
with_tenant(*tid_result,
"");
149 return sfs.is_bootstrap_mode_enabled();
150 }
catch (
const std::exception& e) {
152 BOOST_LOG_SEV(auth_handler_lg(), warn)
153 <<
"Failed to check tenant bootstrap mode: " << e.what();
160using ores::service::messaging::reply;
161using ores::service::messaging::decode;
168 : nats_(nats), ctx_(
std::move(ctx)), signer_(
std::move(signer)) {
169 reload_token_settings();
172 void reload_token_settings() {
177 }
catch (
const std::exception& e) {
179 BOOST_LOG_SEV(auth_handler_lg(), warn)
180 <<
"Failed to load token settings, using defaults: " << e.what();
186 BOOST_LOG_SEV(auth_handler_lg(), debug) <<
"Handling " << msg.
subject;
187 auto req = decode<signup_request>(msg);
189 BOOST_LOG_SEV(auth_handler_lg(), warn)
190 <<
"Failed to decode: " << msg.
subject;
194 service::account_service acct_svc(ctx_);
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),
209 reply(nats_, msg, signup_response{
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());
220 reply(nats_, msg, signup_response{
221 .success =
false, .message = e.what()});
227 BOOST_LOG_SEV(auth_handler_lg(), debug) <<
"Handling " << msg.
subject;
228 auto req = decode<login_request>(msg);
230 BOOST_LOG_SEV(auth_handler_lg(), warn)
231 <<
"Failed to decode: " << msg.
subject;
236 std::string username = req->principal;
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)) {
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);
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());
260 repository::account_party_repository ap_repo(login_ctx);
261 auto account_parties =
262 ap_repo.read_latest_by_account(acct.id);
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.");
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;
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();
297 const auto session_id_str = boost::uuids::to_string(sess.id);
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);
306 claims.
subject = boost::uuids::to_string(acct.id);
308 claims.
expires_at = now + std::chrono::seconds(
309 token_settings_.access_lifetime_s);
311 claims.
email = acct.email;
312 claims.
tenant_id = acct.tenant_id.to_string();
313 claims.
party_id = boost::uuids::to_string(party_id);
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(
"");
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{},
337 p ? p->party_category : std::string{}
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(
345 acct.tenant_id.to_string(),
346 boost::uuids::to_string(acct.id),
349 boost::uuids::to_string(party_id));
351 reply(nats_, msg, resp);
355 claims.
subject = boost::uuids::to_string(acct.id);
357 claims.
expires_at = now + std::chrono::seconds(
358 token_settings_.party_selection_lifetime_s);
359 claims.
audience =
"select_party_only";
361 claims.
email = acct.email;
362 claims.
tenant_id = acct.tenant_id.to_string();
365 auto token = signer_.create_token(claims).value_or(
"");
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{},
382 p ? p->party_category : std::string{}
385 BOOST_LOG_SEV(auth_handler_lg(), debug)
386 <<
"Completed " << msg.
subject;
388 reply(nats_, msg, resp);
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());
399 resp.success =
false;
400 resp.error_message = e.what();
401 reply(nats_, msg, resp);
407 BOOST_LOG_SEV(auth_handler_lg(), debug) <<
"Handling " << msg.
subject;
410 auto pub_key = signer_.get_public_key_pem();
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.");
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();
429 BOOST_LOG_SEV(auth_handler_lg(), debug) <<
"Handling " << msg.
subject;
430 auto token = auth_extract_bearer_token(msg);
432 if (!token.empty()) {
433 auto claims_result = signer_.validate(token);
435 boost::uuids::string_generator sg;
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();
447 if (claims_result->session_id &&
448 claims_result->session_start_time) {
450 const auto session_id =
451 sg(*claims_result->session_id);
452 repository::session_repository sess_repo(ctx_);
453 sess_repo.end_session(
455 *claims_result->session_start_time,
456 std::chrono::system_clock::now(),
458 }
catch (
const std::exception& e) {
459 BOOST_LOG_SEV(auth_handler_lg(), warn)
460 <<
"Failed to end session record: "
466 if (!token.empty()) {
468 auto claims_result = signer_.validate_allow_expired(token);
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(
""));
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()});
494 BOOST_LOG_SEV(auth_handler_lg(), debug) <<
"Handling " << msg.
subject;
496 const auto token = auth_extract_bearer_token(msg);
498 reply(nats_, msg, refresh_response{
499 .success =
false, .message =
"Missing Authorization header"});
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"});
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(
526 claims_result->tenant_id.value_or(
""),
527 claims_result->subject,
528 claims_result->username.value_or(
""),
529 claims_result->session_id.value_or(
""));
531 reply(nats_, msg, refresh_response{
532 .success =
false, .message =
"max_session_exceeded"});
539 new_claims.
subject = claims_result->subject;
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;
549 new_claims.
roles = claims_result->roles;
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"});
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(
565 claims_result->tenant_id.value_or(
""),
566 claims_result->subject,
567 claims_result->username.value_or(
""),
568 claims_result->session_id.value_or(
""));
570 reply(nats_, msg, refresh_response{
573 .access_lifetime_s = token_settings_.access_lifetime_s});
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()});
585 BOOST_LOG_SEV(auth_handler_lg(), debug) <<
"Handling " << msg.
subject;
586 auto req = decode<service_login_request>(msg);
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"});
595 repository::account_repository account_repo(ctx_);
596 const auto account_id = account_repo.check_service_credentials(
597 req->username, req->password);
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"});
606 service::service_session_service sess_svc(ctx_);
607 auto sess = sess_svc.start_service_session(
608 req->username,
"ores.service.binary");
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"});
617 const auto now = std::chrono::system_clock::now();
619 claims.
subject = boost::uuids::to_string(sess->account_id);
621 claims.
expires_at = now + std::chrono::seconds(
622 token_settings_.access_lifetime_s);
624 claims.
tenant_id = sess->tenant_id.to_string();
625 claims.
session_id = boost::uuids::to_string(sess->id);
631 service::authorization_service auth_svc(ctx_);
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();
640 auto token = signer_.create_token(claims).value_or(
"");
642 reply(nats_, msg, service_login_response{
643 .success =
false, .message =
"Token creation failed"});
647 BOOST_LOG_SEV(auth_handler_lg(), info)
648 <<
"Service login successful for " << req->username;
649 reply(nats_, msg, service_login_response{
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()});
667 template <
typename Func>
669 const char* event_name, Func&& fn) {
671 repository::auth_event_repository ev_repo(ctx);
673 }
catch (
const std::exception& ev_err) {
675 BOOST_LOG_SEV(auth_handler_lg(), warn)
676 <<
"Failed to record " << event_name
677 <<
" event: " << ev_err.what();
684 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: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