|
| 1 | +#include "auth_handler.hpp" |
| 2 | + |
| 3 | +#include "exception.hpp" |
| 4 | +#include "ga_strings.hpp" |
| 5 | +#include "logging.hpp" |
| 6 | +#include "session.hpp" |
| 7 | +#include "session_impl.hpp" |
| 8 | + |
| 9 | +namespace ga { |
| 10 | +namespace sdk { |
| 11 | + namespace { |
| 12 | + // Server gives 3 attempts to get the twofactor code right before it's invalidated |
| 13 | + static const uint32_t TWO_FACTOR_ATTEMPTS = 3; |
| 14 | + |
| 15 | + static bool is_twofactor_invalid_code_error(const std::string& msg) |
| 16 | + { |
| 17 | + return msg == "Invalid Two Factor Authentication Code"; |
| 18 | + } |
| 19 | + } // namespace |
| 20 | + |
| 21 | + // |
| 22 | + // Common auth handling |
| 23 | + // |
| 24 | + auth_handler::auth_handler(session& session, const std::string& action, std::shared_ptr<signer> signer) |
| 25 | + : m_session(session) |
| 26 | + , m_action(action) |
| 27 | + , m_attempts_remaining(TWO_FACTOR_ATTEMPTS) |
| 28 | + { |
| 29 | + try { |
| 30 | + init(action, signer, true); |
| 31 | + } catch (const std::exception& e) { |
| 32 | + set_error(e.what()); |
| 33 | + } |
| 34 | + } |
| 35 | + |
| 36 | + auth_handler::auth_handler(session& session, const std::string& action) |
| 37 | + : m_session(session) |
| 38 | + , m_action(action) |
| 39 | + , m_attempts_remaining(TWO_FACTOR_ATTEMPTS) |
| 40 | + { |
| 41 | + try { |
| 42 | + init(action, m_session.get_nonnull_impl()->get_signer(), false); |
| 43 | + } catch (const std::exception& e) { |
| 44 | + set_error(e.what()); |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + auth_handler::~auth_handler() {} |
| 49 | + |
| 50 | + void auth_handler::init(const std::string& action, std::shared_ptr<signer> signer, bool is_pre_login) |
| 51 | + { |
| 52 | + m_signer = signer; |
| 53 | + set_action(action); |
| 54 | + |
| 55 | + if (!is_pre_login && !m_session.is_watch_only()) { |
| 56 | + m_methods = m_session.get_enabled_twofactor_methods(); |
| 57 | + } |
| 58 | + m_state = m_methods.empty() ? state_type::make_call : state_type::request_code; |
| 59 | + } |
| 60 | + |
| 61 | + void auth_handler::set_action(const std::string& action) |
| 62 | + { |
| 63 | + m_action = action; |
| 64 | + m_is_hw_action = m_signer && m_signer->is_hw_device() |
| 65 | + && (action == "get_xpubs" || action == "sign_message" || action == "sign_tx" |
| 66 | + || action == "get_receive_address" || action == "create_transaction" || action == "get_balance" |
| 67 | + || action == "get_subaccounts" || action == "get_subaccount" || action == "get_transactions" |
| 68 | + || action == "get_unspent_outputs" || action == "get_expired_deposits"); |
| 69 | + } |
| 70 | + |
| 71 | + void auth_handler::set_error(const std::string& error_message) |
| 72 | + { |
| 73 | + GDK_LOG_SEV(log_level::debug) << m_action << " call exception: " << error_message; |
| 74 | + m_state = state_type::error; |
| 75 | + m_error = error_message; |
| 76 | + } |
| 77 | + |
| 78 | + void auth_handler::set_data() |
| 79 | + { |
| 80 | + m_twofactor_data |
| 81 | + = { { "action", m_action }, { "device", m_is_hw_action ? m_signer->get_hw_device() : nlohmann::json() } }; |
| 82 | + } |
| 83 | + |
| 84 | + void auth_handler::request_code(const std::string& method) |
| 85 | + { |
| 86 | + request_code_impl(method); |
| 87 | + m_attempts_remaining = TWO_FACTOR_ATTEMPTS; |
| 88 | + } |
| 89 | + |
| 90 | + void auth_handler::request_code_impl(const std::string& method) |
| 91 | + { |
| 92 | + GDK_RUNTIME_ASSERT(m_state == state_type::request_code); |
| 93 | + |
| 94 | + // For gauth request code is a no-op |
| 95 | + if (method != "gauth") { |
| 96 | + m_session.auth_handler_request_code(method, m_action, m_twofactor_data); |
| 97 | + } |
| 98 | + |
| 99 | + m_method = method; |
| 100 | + m_state = state_type::resolve_code; |
| 101 | + } |
| 102 | + |
| 103 | + void auth_handler::resolve_code(const std::string& code) |
| 104 | + { |
| 105 | + GDK_RUNTIME_ASSERT(m_state == state_type::resolve_code); |
| 106 | + m_code = code; |
| 107 | + m_state = state_type::make_call; |
| 108 | + } |
| 109 | + |
| 110 | + void auth_handler::operator()() |
| 111 | + { |
| 112 | + GDK_RUNTIME_ASSERT(m_state == state_type::make_call); |
| 113 | + try { |
| 114 | + |
| 115 | + if (m_code.empty() || m_method.empty()) { |
| 116 | + if (!m_twofactor_data.empty()) { |
| 117 | + // Remove any previous auth attempts |
| 118 | + m_twofactor_data.erase("method"); |
| 119 | + m_twofactor_data.erase("code"); |
| 120 | + } |
| 121 | + } else { |
| 122 | + m_twofactor_data["method"] = m_method; |
| 123 | + m_twofactor_data["code"] = m_code; |
| 124 | + } |
| 125 | + m_state = call_impl(); |
| 126 | + m_attempts_remaining = TWO_FACTOR_ATTEMPTS; |
| 127 | + } catch (const autobahn::call_error& e) { |
| 128 | + auto details = get_error_details(e); |
| 129 | + if (is_twofactor_invalid_code_error(details.second)) { |
| 130 | + // The caller entered the wrong code |
| 131 | + // FIXME: Error if the methods time limit is up or we are rate limited |
| 132 | + if (m_method != "gauth" && --m_attempts_remaining == 0) { |
| 133 | + // No more attempts left, caller should try the action again |
| 134 | + set_error(res::id_invalid_twofactor_code); |
| 135 | + } else { |
| 136 | + // Caller should try entering the code again |
| 137 | + m_state = state_type::resolve_code; |
| 138 | + } |
| 139 | + } else { |
| 140 | + details = remap_ga_server_error(details); |
| 141 | + set_error(details.second.empty() ? e.what() : details.second); |
| 142 | + } |
| 143 | + } catch (const user_error& e) { |
| 144 | + // Just set the undecorated error string as it should be an id for a |
| 145 | + // translatable string resource, displayed as appropriate by the client. |
| 146 | + set_error(e.what()); |
| 147 | + } catch (const std::exception& e) { |
| 148 | + set_error(m_action + std::string(" exception:") + e.what()); |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + nlohmann::json auth_handler::get_status() const |
| 153 | + { |
| 154 | + GDK_RUNTIME_ASSERT(m_state == state_type::error || m_error.empty()); |
| 155 | + |
| 156 | + std::string status_str; |
| 157 | + nlohmann::json status; |
| 158 | + |
| 159 | + switch (m_state) { |
| 160 | + case state_type::request_code: |
| 161 | + GDK_RUNTIME_ASSERT(!m_is_hw_action); |
| 162 | + |
| 163 | + // Caller should ask the user to pick 2fa and request a code |
| 164 | + status_str = "request_code"; |
| 165 | + status["methods"] = m_methods; |
| 166 | + break; |
| 167 | + case state_type::resolve_code: |
| 168 | + status_str = "resolve_code"; |
| 169 | + if (m_is_hw_action) { |
| 170 | + // Caller must interact with the hardware and return |
| 171 | + // the returning data to us |
| 172 | + status["method"] = m_signer->get_hw_device().value("name", std::string()); |
| 173 | + status["required_data"] = m_twofactor_data; |
| 174 | + } else { |
| 175 | + // Caller should resolve the code the user has entered |
| 176 | + status["method"] = m_method; |
| 177 | + if (m_method != "gauth") { |
| 178 | + status["attempts_remaining"] = m_attempts_remaining; |
| 179 | + } |
| 180 | + } |
| 181 | + break; |
| 182 | + case state_type::make_call: |
| 183 | + // Caller should make the call |
| 184 | + status_str = "call"; |
| 185 | + break; |
| 186 | + case state_type::done: |
| 187 | + // Caller should destroy the call and continue |
| 188 | + status_str = "done"; |
| 189 | + status["result"] = m_result; |
| 190 | + break; |
| 191 | + case state_type::error: |
| 192 | + // Caller should handle the error |
| 193 | + status_str = "error"; |
| 194 | + status["error"] = m_error; |
| 195 | + break; |
| 196 | + } |
| 197 | + GDK_RUNTIME_ASSERT(!status_str.empty()); |
| 198 | + status["status"] = status_str; |
| 199 | + status["action"] = m_action; |
| 200 | + status["device"] = m_is_hw_action ? m_signer->get_hw_device() : nlohmann::json(); |
| 201 | + return status; |
| 202 | + } |
| 203 | + |
| 204 | +} // namespace sdk |
| 205 | +} // namespace ga |
0 commit comments