#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace rublon { enum class MethodIds { OTP, SMS, PUSH, EMAIL, SmsLink, YOTP, PhoneCall }; constexpr auto MethodNames = make_array< std::string_view >("totp", "sms", "push", "email", "smsLink", "yotp", "phoneCall"); class MethodProxy { public: template < typename Method_t > MethodProxy(Method_t method) : _impl{std::move(method)} {} template < typename Handler_t > tl::expected< AuthenticationStatus, Error > fire(Session & session, const CoreHandlerInterface< Handler_t > & coreHandler, const Pam_t & pam) const { coreHandler.createWSConnection(session.transactionID()); return std::visit( [&](const auto & method) { log(LogLevel::Info, "Using '%s' method", method._name); return method.verify(coreHandler, pam); }, _impl); } private: std::variant< method::OTP, method::SMS, method::PUSH, method::EMAIL, method::SmsLink, method::YOTP, method::PhoneCall > _impl; }; class PostMethod : public AuthenticationStep { using base_t = AuthenticationStep; const char * uri = "/api/transaction/methodSSH"; MethodIds _method; int _prompts; bool _autopushPrompt; tl::expected< void, Error > readAccessToken(const Document & coreResponse) const { log(LogLevel::Debug, "Readding access token"); auto alloc = RapidJSONPMRStackAlloc< 256 >{}; const auto * rublonAccessToken = JSONPointer{"/result/token", &alloc}.Get(coreResponse); if(rublonAccessToken) _session.updateAccessToken(rublonAccessToken); return {}; } tl::expected< MethodProxy, Error > createMethod() const { log(LogLevel::Debug, "Creating method"); switch(_method) { case rublon::MethodIds::OTP: return MethodProxy{method::OTP{_session, _prompts}}; case rublon::MethodIds::SMS: return MethodProxy{method::SMS{_session, _prompts}}; case rublon::MethodIds::PUSH: return MethodProxy{method::PUSH{_session, _autopushPrompt}}; case rublon::MethodIds::EMAIL: return MethodProxy{method::EMAIL{_session}}; case rublon::MethodIds::SmsLink: return MethodProxy{method::SmsLink{_session}}; case rublon::MethodIds::YOTP: return MethodProxy{method::YOTP{_session, _prompts}}; case rublon::MethodIds::PhoneCall: return MethodProxy{method::PhoneCall{_session}}; default: return tl::unexpected{MethodError{MethodError::BadMethod}}; } } void addParams(Document & body) const { auto & alloc = body.GetAllocator(); body.AddMember("method", Value{MethodNames[static_cast< int >(_method)].data(), alloc}, alloc); } public: const char * _name = "Confirm Method"; PostMethod(Session & session, MethodIds method, int prompts, bool autopushPrompt) : base_t(session), _method{method}, _prompts{prompts}, _autopushPrompt{autopushPrompt} {} template < typename Hander_t > tl::expected< MethodProxy, Error > handle(const CoreHandlerInterface< Hander_t > & coreHandler) const { auto readAccessToken = [&](const auto & coreResponse) { return this->readAccessToken(coreResponse); }; auto createMethod = [&]() { return this->createMethod(); }; RapidJSONPMRStackAlloc< 2 * 1024 > alloc{}; Document body{rapidjson::kObjectType, &alloc}; this->addSystemToken(body); this->addTid(body); this->addParams(body); return coreHandler .request(alloc, uri, body) // .and_then(readAccessToken) .and_then(createMethod); } }; class MethodSelect { Session & _session; int _prompts; bool _autopushPrompt; std::pmr::memory_resource * _mr; // method name is really short, there is almost no chance that thos strings will allocate std::pmr::vector< std::pmr::string > _methodsAvailable; public: template < typename Array_t > MethodSelect(Session & session, const Array_t & methodsEnabledInCore, int prompts, bool autopushPrompt) : _session{session}, _prompts{prompts}, _autopushPrompt{autopushPrompt}, _mr{memory::default_resource()}, _methodsAvailable{_mr} { rublon::log(LogLevel::Debug, "Checking what methods from core are supported"); using namespace std::string_view_literals; _methodsAvailable.reserve(std::size(methodsEnabledInCore)); transform_if( std::begin(methodsEnabledInCore), std::end(methodsEnabledInCore), std::back_inserter(_methodsAvailable), [&](const auto & method) { return method.GetString(); }, [&](const auto & method) { return std::find(MethodNames.begin(), MethodNames.end(), method.GetString()) != MethodNames.end(); }); rublon::log(LogLevel::Debug, "User has %d methods available", _methodsAvailable.size()); } tl::expected< PostMethod, Error > create(Pam_t & pam) const { rublon::log(LogLevel::Debug, "prompting user to select method"); memory::Monotonic_2k_Resource memoryResource; std::pmr::map< int, MethodIds > methods_id{&memoryResource}; std::pmr::map< int, std::pmr::string > methods_names{&memoryResource}; int prompts = _prompts; if(_methodsAvailable.size() == 0) { log(LogLevel::Warning, "None of provided methods are supported by the connector"); return tl::unexpected(MethodError(MethodError::ErrorClass::NoMethodAvailable)); } pam.print("Select the authentication method to verify your identity: "); auto logMethodAvailable = [](auto & method) { // rublon::log(LogLevel::Debug, "Method %s found", method.c_str()); }; auto logMethodSkippedInteractive = [](auto & method) { rublon::log(LogLevel::Debug, "Method %s found, but skipped due non interactive mode", method.c_str()); }; auto logMethodNotAvailable = [](auto & method) { rublon::log(LogLevel::Debug, "Method %s found, but has no handler", method.c_str()); }; auto printAvailableMethods = [&]() -> tl::expected< int, MethodError > { rublon::log(LogLevel::Trace, "Printing available methods"); int i{}; for(const auto & method : _methodsAvailable) { if(method == "totp") { // skipped in non interactive if(_session.inNonInteractiveMode()) { logMethodSkippedInteractive(method); continue; } logMethodAvailable(method); if(_session.inInteractiveMode()) pam.print("%d: Passcode", i + 1); methods_id[++i] = MethodIds::OTP; methods_names[i] = "Passcode"; continue; } if(method == "email") { // available in non interactive logMethodAvailable(method); if(_session.inInteractiveMode()) pam.print("%d: Email Link", i + 1); methods_id[++i] = MethodIds::EMAIL; methods_names[i] = "Email Link"; continue; } if(method == "yotp") { // skipped in non interactive if(_session.inNonInteractiveMode()) { logMethodSkippedInteractive(method); continue; } logMethodAvailable(method); if(_session.inInteractiveMode()) pam.print("%d: YubiKey OTP Security Key", i + 1); methods_id[++i] = MethodIds::YOTP; methods_names[i] = "YubiKey OTP Security Key"; continue; } if(method == "sms") { // skipped in non interactive if(_session.inNonInteractiveMode()) { logMethodSkippedInteractive(method); continue; } logMethodAvailable(method); if(_session.inInteractiveMode()) pam.print("%d: SMS Passcode", i + 1); methods_id[++i] = MethodIds::SMS; methods_names[i] = "SMS Passcode"; continue; } if(method == "push") { // available in non interactive logMethodAvailable(method); if(_session.inInteractiveMode()) pam.print("%d: Mobile Push", i + 1); methods_id[++i] = MethodIds::PUSH; methods_names[i] = "Mobile Push"; continue; } if(method == "smsLink") { // available in non interactive logMethodAvailable(method); if(_session.inInteractiveMode()) pam.print("%d: SMS Link", i + 1); methods_id[++i] = MethodIds::SmsLink; methods_names[i] = "SMS Link"; continue; } if(method == "phoneCall") { // available in non interactive logMethodAvailable(method); if(_session.inInteractiveMode()) pam.print("%d: Phone Call", i + 1); methods_id[++i] = MethodIds::PhoneCall; methods_names[i] = "Phone Call"; continue; } logMethodNotAvailable(method); } if(i == 0) { log(LogLevel::Warning, "None of provided methods are supported by the connector"); return tl::unexpected(MethodError(MethodError::ErrorClass::NoMethodAvailable)); } return i; }; const auto toMethodError = [&](conv::Error /*methodid*/) -> MethodError { // NaN or out of range return MethodError{MethodError::BadUserInput}; }; const auto createMethod = [&](std::uint32_t methodid) -> tl::expected< PostMethod, MethodError > { rublon::log(LogLevel::Trace, "Create method %d", methods_id); auto hasMethod = methods_id.find(methodid) != methods_id.end(); if(!hasMethod) { log(LogLevel::Error, "User selected option %d, which is not correct", methodid); return tl::unexpected{MethodError(MethodError::BadMethod)}; } else { log(LogLevel::Info, "User selected option %d{%s}", methodid, methods_names.at(methodid).c_str()); return PostMethod{_session, methods_id.at(methodid), _prompts, _autopushPrompt}; } }; const auto askForMethod = [&](int methods_number) -> tl::expected< uint32_t, MethodError > { if(not _session.inInteractiveMode()) { log(LogLevel::Debug, "Non interactive method selection"); /// TODO refactor, this is ugly auto nonInteractiveMethodsPriority = make_array< MethodIds >(MethodIds::PUSH, MethodIds::EMAIL, MethodIds::SmsLink, MethodIds::PhoneCall); for(const auto methodid : nonInteractiveMethodsPriority) { for(auto [key, id] : methods_id) { if(id == methodid) { log(LogLevel::Debug, "Automatically selected authentication method: %s due to working in noninteractive mode", methods_names.at(key).c_str()); pam.print("Non-interctive mode enabled, choose method: %s", methods_names.at(key).c_str()); return key; } } } return tl::unexpected{MethodError(MethodError::BadMethod)}; } else { if(methods_number == 1) { pam.print("Automatically selected the only available authentication method: %s", methods_names.at(1).c_str()); return 1; } return pam.scan(conv::to_uint32, "\nSelect method [1-%d]: ", methods_number).transform_error(toMethodError); } }; auto reducePromptCount = [&](int selected_method) -> tl::expected< uint32_t, MethodError > { rublon::log(LogLevel::Trace, "User has %d prompts available", prompts); prompts--; return selected_method; }; auto disableAnyNewMethodPrompts = [&]() { prompts = 0; }; const auto printEnrolementInformation = [&]() { log(LogLevel::Warning, "Enrolement send to admin about new user: %s", pam.username()); pam.print("Please check email"); }; const auto handleErrors = [&](const MethodError & e) -> tl::expected< PostMethod, MethodError > { switch(e.errorClass) { case MethodError::NoMethodAvailable: disableAnyNewMethodPrompts(); printEnrolementInformation(); break; case MethodError::BadMethod: // User provided a number but the number if not found pam.print("Input is not a valid method number. Enter a valid number"); break; case MethodError::BadUserInput: // User provided id is invalid (NAN) pam.print("Input is not a number. Enter a valid number"); break; } return tl::unexpected(e); }; const auto toGenericError = [](const MethodError & e) -> Error { return Error{e}; }; return [&]() { while(true) { auto method = printAvailableMethods() // .and_then(reducePromptCount) .and_then(askForMethod) .and_then(createMethod) .or_else(handleErrors); if(not method && prompts > 0) continue; return method; }; }() .map_error(toGenericError); } }; } // namespace rublon