Refactor reauthentication logic

The reauthentication logic differs from openvpn2
and the code is a bit hard to follow. Simplify
the code and make it behave like in openvpn2.

 - password is cached by default

 - password is purged when auth-nocache is presented in a local config or pushed

 - when AUTH_FAILED is received and we have no session-id, throw a fatal error

 - when AUTH_FAILED is received and user interaction is required for
   authentication (MFA), throw a fatal error

 - when AUTH_FAILED is received, user interaction is not required
   for authentication and either we have a cached password OR password is not
   needed, we reconnect.

Password is "needed" when non-empty password is provided.

User interaction is required for static/dynamic challenge and SAML.

Signed-off-by: Lev Stipakov <lev@openvpn.net>
This commit is contained in:
Lev Stipakov
2024-03-28 16:55:28 +02:00
committed by Jenkins-dev
parent d0cfea2a23
commit e34094e30d
12 changed files with 197 additions and 150 deletions

View File

@@ -795,13 +795,12 @@ OPENVPN_CLIENT_EXPORT Status OpenVPNClient::provide_creds(const ProvideCreds &cr
{
ClientCreds::Ptr cc = new ClientCreds();
cc->set_username(creds.username);
cc->save_username_for_session_id();
cc->set_password(creds.password);
cc->set_http_proxy_username(creds.http_proxy_user);
cc->set_http_proxy_password(creds.http_proxy_pass);
cc->set_response(creds.response);
cc->set_dynamic_challenge_cookie(creds.dynamicChallengeCookie, creds.username);
cc->set_replace_password_with_session_id(creds.replacePasswordWithSessionID);
cc->enable_password_cache(creds.cachePassword);
state->creds = cc;
}
catch (const std::exception &e)
@@ -972,15 +971,6 @@ OPENVPN_CLIENT_EXPORT void OpenVPNClient::connect_setup(Status &status, bool &se
#if defined(OPENVPN_EXTERNAL_TRANSPORT_FACTORY)
cc.extern_transport_factory = this;
#endif
// force Session ID use and disable password cache if static challenge is enabled
if (state->creds
&& !state->creds->get_replace_password_with_session_id()
&& !state->eval.autologin
&& !state->eval.staticChallenge.empty())
{
state->creds->set_replace_password_with_session_id(true);
state->creds->enable_password_cache(false);
}
// external PKI
#if !defined(USE_APPLE_SSL)

View File

@@ -119,19 +119,6 @@ struct ProvideCreds
// Dynamic challenge/response cookie
std::string dynamicChallengeCookie;
// If true, on successful connect, we will replace the password
// with the session ID we receive from the server (if provided).
// If false, the password will be cached for future reconnects
// and will not be replaced with a session ID, even if the
// server provides one.
bool replacePasswordWithSessionID = false;
// If true, and if replacePasswordWithSessionID is true, and if
// we actually receive a session ID from the server, cache
// the user-provided password for future use before replacing
// the active password with the session ID.
bool cachePassword = false;
};
// used to get session token from VPN core

View File

@@ -463,6 +463,7 @@ class ClientConnect : ClientProto::NotifyCallback,
// Errors below will cause the client to NOT retry the connection,
// or otherwise give the error special handling.
case Error::SESSION_EXPIRED:
case Error::AUTH_FAILED:
{
const std::string &reason = client->fatal_reason();
@@ -474,9 +475,13 @@ class ClientConnect : ClientProto::NotifyCallback,
}
else
{
ClientEvent::Base::Ptr ev = new ClientEvent::AuthFailed(reason);
ClientEvent::Base::Ptr ev;
if (client->fatal() == Error::SESSION_EXPIRED)
ev = new ClientEvent::SessionExpired(reason);
else
ev = new ClientEvent::AuthFailed(reason);
client_options->events().add_event(std::move(ev));
client_options->stats().error(Error::AUTH_FAILED);
client_options->stats().error(client->fatal());
if (client_options->retry_on_auth_failed())
queue_restart(5000);
else

View File

@@ -42,13 +42,7 @@ class ClientCreds : public RC<thread_unsafe_refcount>
public:
typedef RCPtr<ClientCreds> Ptr;
ClientCreds()
: allow_cache_password(false),
password_save_defined(false),
replace_password_with_session_id(false),
did_replace_password_with_session_id(false)
{
}
ClientCreds() = default;
void set_username(const std::string &username_arg)
{
@@ -58,7 +52,10 @@ class ClientCreds : public RC<thread_unsafe_refcount>
void set_password(const std::string &password_arg)
{
password = password_arg;
did_replace_password_with_session_id = false;
if (!password.empty())
{
password_needed_ = true;
}
}
void set_http_proxy_username(const std::string &username)
@@ -74,6 +71,10 @@ class ClientCreds : public RC<thread_unsafe_refcount>
void set_response(const std::string &response_arg)
{
response = response_arg;
if (!response.empty())
{
need_user_interaction_ = true;
}
}
void set_dynamic_challenge_cookie(const std::string &cookie, const std::string &username)
@@ -82,51 +83,31 @@ class ClientCreds : public RC<thread_unsafe_refcount>
dynamic_challenge.reset(new ChallengeResponse(cookie, username));
}
void set_replace_password_with_session_id(const bool value)
{
replace_password_with_session_id = value;
}
void enable_password_cache(const bool value)
{
allow_cache_password = value;
}
bool get_replace_password_with_session_id() const
{
return replace_password_with_session_id;
}
void set_session_id(const std::string &user, const std::string &sess_id)
{
// force Session ID use if dynamic challenge is enabled
if (dynamic_challenge && !replace_password_with_session_id)
replace_password_with_session_id = true;
if (replace_password_with_session_id)
if (dynamic_challenge)
{
if (allow_cache_password && !password_save_defined)
{
password_save = password;
password_save_defined = true;
}
password = sess_id;
response = "";
if (dynamic_challenge)
{
username = dynamic_challenge->get_username();
dynamic_challenge.reset();
}
else if (!user.empty())
username = user;
did_replace_password_with_session_id = true;
session_id_username = dynamic_challenge->get_username();
// for dynamic challenge we use dynamic password only once
dynamic_challenge.reset();
}
else if (!user.empty())
{
session_id_username = user;
}
// response is used only once
response.clear();
session_id = sess_id;
}
std::string get_username() const
{
if (dynamic_challenge)
return dynamic_challenge->get_username();
else if (!session_id_username.empty())
return session_id_username;
else
return username;
}
@@ -136,7 +117,12 @@ class ClientCreds : public RC<thread_unsafe_refcount>
if (dynamic_challenge)
return dynamic_challenge->construct_dynamic_password(response);
else if (response.empty())
return password;
{
if (!session_id.empty())
return session_id;
else
return password;
}
else
return ChallengeResponse::construct_static_password(password, response);
}
@@ -173,34 +159,44 @@ class ClientCreds : public RC<thread_unsafe_refcount>
bool session_id_defined() const
{
return did_replace_password_with_session_id;
}
// If we have a saved password that is not a session ID,
// restore it and wipe any existing session ID.
bool reset_to_cached_password()
{
if (password_save_defined)
{
password = password_save;
password_save.clear();
password_save_defined = false;
did_replace_password_with_session_id = false;
return true;
}
else
return false;
return !session_id.empty();
}
void purge_session_id()
{
if (!reset_to_cached_password())
session_id.clear();
session_id_username.clear();
}
void purge_user_pass()
{
username.clear();
password.clear();
}
void save_username_for_session_id()
{
if (session_id_username.empty())
{
password.clear();
did_replace_password_with_session_id = false;
session_id_username = username;
}
}
void set_need_user_interaction()
{
need_user_interaction_ = true;
}
bool need_user_interaction() const
{
return need_user_interaction_;
}
bool password_needed() const
{
return password_needed_;
}
std::string auth_info() const
{
std::string ret;
@@ -210,20 +206,27 @@ class ClientCreds : public RC<thread_unsafe_refcount>
}
else if (response.empty())
{
if (!username.empty())
ret += "Username";
else
ret += "UsernameEmpty";
ret += '/';
if (!password.empty())
if (!session_id_username.empty() || !username.empty())
{
if (did_replace_password_with_session_id)
ret += "SessionID";
else
ret += "Password";
ret += "Username";
}
else
{
ret += "UsernameEmpty";
}
ret += '/';
if (!session_id.empty())
{
ret += "SessionID";
}
else if (!password.empty())
{
ret += "Password";
}
else
{
ret += "PasswordEmpty";
}
}
else
{
@@ -241,23 +244,20 @@ class ClientCreds : public RC<thread_unsafe_refcount>
std::string http_proxy_user;
std::string http_proxy_pass;
// Password caching
bool allow_cache_password;
bool password_save_defined;
std::string password_save;
std::string session_id;
std::string session_id_username;
// Response to challenge
// Response to a challenge
std::string response;
// Need user interaction to authenticate - such as static/dynamic challenge or SAML
bool need_user_interaction_ = false;
// Non-empty password provided
bool password_needed_ = false;
// Info describing a dynamic challenge
ChallengeResponse::Ptr dynamic_challenge;
// If true, on successful connect, we will replace the password
// with the session ID we receive from the server.
bool replace_password_with_session_id;
// true if password has been replaced with Session ID
bool did_replace_password_with_session_id;
};
} // namespace openvpn

View File

@@ -92,6 +92,7 @@ enum Type
RELAY_ERROR,
COMPRESS_ERROR,
NTLM_MISSING_CRYPTO,
SESSION_EXPIRED,
N_TYPES
};
@@ -153,7 +154,8 @@ inline const char *event_name(const Type type)
"EPKI_INVALID_ALIAS",
"RELAY_ERROR",
"COMPRESS_ERROR",
"NTLM_MISSING_CRYPTO"};
"NTLM_MISSING_CRYPTO",
"SESSION_EXPIRED"};
static_assert(N_TYPES == array_size(names), "event names array inconsistency");
if (type < N_TYPES)
@@ -451,6 +453,14 @@ struct AuthFailed : public ReasonBase
}
};
struct SessionExpired : public ReasonBase
{
SessionExpired(std::string reason)
: ReasonBase(SESSION_EXPIRED, std::move(reason))
{
}
};
struct CertVerifyFail : public ReasonBase
{
CertVerifyFail(std::string reason)

View File

@@ -175,7 +175,6 @@ class ClientOptions : public RC<thread_unsafe_refcount>
bool synchronous_dns_lookup = false;
bool disable_client_cert = false;
int default_key_direction = -1;
bool autologin_sessions = false;
bool allow_local_lan_access = false;
PeerInfo::Set::Ptr extra_peer_info;
@@ -234,7 +233,7 @@ class ClientOptions : public RC<thread_unsafe_refcount>
// creds
userlocked_username = pcc.userlockedUsername();
autologin = pcc.autologin();
autologin_sessions = (autologin && config.autologin_sessions);
autologin_sessions = (autologin && clientconf.autologinSessions);
// digest factory
DigestFactory::Ptr digest_factory(new CryptoDigestFactory<SSLLib::CryptoAPI>());
@@ -540,15 +539,11 @@ class ClientOptions : public RC<thread_unsafe_refcount>
{
cc->set_username(userlocked_username);
cc->set_password(pcc.embeddedPassword());
cc->enable_password_cache(true);
cc->set_replace_password_with_session_id(true);
submit_creds(cc);
creds_locked = true;
}
else if (autologin_sessions)
{
// autologin sessions require replace_password_with_session_id
cc->set_replace_password_with_session_id(true);
submit_creds(cc);
creds_locked = true;
}
@@ -687,7 +682,6 @@ class ClientOptions : public RC<thread_unsafe_refcount>
std::unordered_set<std::string> settings_ignoreWithWarning = {
"allow-compression", /* TODO: maybe check against our client option compression setting? */
"allow-recursive-routing",
"auth-nocache",
"auth-retry",
"block-outside-dns", /* Core will decide on its own when to block outside dns, so this is not 100% identical in behaviour, so still warn */
"compat-mode",
@@ -1178,6 +1172,13 @@ class ClientOptions : public RC<thread_unsafe_refcount>
cli_config->echo = clientconf.echo;
cli_config->info = clientconf.info;
cli_config->autologin_sessions = autologin_sessions;
// if the previous client instance had session-id, it must be used by the new instance too
if (creds && creds->session_id_defined())
{
cli_config->proto_context_config->set_xmit_creds(true);
}
return cli_config;
}
@@ -1202,7 +1203,10 @@ class ClientOptions : public RC<thread_unsafe_refcount>
// if no username is defined in creds and userlocked_username is defined
// in profile, set the creds username to be the userlocked_username
if (!creds_arg->username_defined() && !userlocked_username.empty())
{
creds_arg->set_username(userlocked_username);
creds_arg->save_username_for_session_id();
}
creds = creds_arg;
}
}

View File

@@ -160,7 +160,6 @@ class Session : ProtoContextCallbackInterface,
cli_events(config.cli_events),
echo(config.echo),
info(config.info),
autologin_sessions(config.autologin_sessions),
pushed_options_limit(config.pushed_options_limit),
pushed_options_filter(config.pushed_options_filter),
inactive_timer(io_context_arg),
@@ -580,9 +579,7 @@ class Session : ProtoContextCallbackInterface,
#else
OPENVPN_LOG("Session token: [redacted]");
#endif
autologin_sessions = true;
proto_context.conf().set_xmit_creds(true);
creds->set_replace_password_with_session_id(true);
creds->set_session_id(username, sess_id);
}
}
@@ -696,26 +693,61 @@ class Session : ProtoContextCallbackInterface,
if (msg.length() >= 13)
reason = string::trim_left_copy(std::string(msg, 12));
// If session token problem (such as expiration), and we have a cached
// password, retry with it. Otherwise, fail without retry.
if (string::starts_with(reason, "SESSION:")
&& ((creds && creds->reset_to_cached_password())
|| autologin_sessions))
{
if (creds && creds->session_id_defined())
creds->purge_session_id();
log_reason = "SESSION_AUTH_FAILED";
}
else if (string::starts_with(reason, "TEMP"))
if (string::starts_with(reason, "TEMP"))
{
log_reason = "AUTH_FAILED_TEMP:" + parse_auth_failed_temp(std::string(reason, 4));
}
else
{
fatal_ = Error::AUTH_FAILED;
fatal_reason_ = std::move(reason);
log_reason = "AUTH_FAILED";
bool password_defined = false;
bool session_id_defined = false;
if (creds)
{
password_defined = creds->password_defined();
session_id_defined = creds->session_id_defined();
// authentication failure, purge auth-token
creds->purge_session_id();
}
// do we have a session-id?
if (session_id_defined)
{
bool reconnect = false;
// if there was an OOB auth (server pushed AUTH_PENDING) throw a fatal error since we need a user input
if (!creds->need_user_interaction())
{
// reconnect if we have a password OR password is not needed
if (!creds->password_needed() || password_defined)
{
reconnect = true;
}
}
OPENVPN_LOG("need_user_interaction: " << creds->need_user_interaction() << ", pw_needed: " << creds->password_needed() << ", pw_defined: " << password_defined << ", reconnect: " << reconnect);
if (reconnect)
{
log_reason = "SESSION_AUTH_FAILED";
}
else
{
// we don't have a password and we need a user input, throw a fatal error and let the client to re-authenticate
fatal_ = Error::SESSION_EXPIRED;
fatal_reason_ = reason;
log_reason = "SESSION_EXPIRED";
}
}
else
{
// no session-id, throw fatal error
fatal_ = Error::AUTH_FAILED;
fatal_reason_ = reason;
log_reason = "AUTH_FAILED";
}
}
if (notify_callback)
{
OPENVPN_LOG(log_reason);
@@ -756,8 +788,6 @@ class Session : ProtoContextCallbackInterface,
}
}
if (notify_callback && timeout > 0)
{
notify_callback->client_proto_auth_pending_timeout(timeout);
@@ -798,9 +828,17 @@ class Session : ProtoContextCallbackInterface,
// requires the VPN tunnel to be ready.
ClientEvent::Base::Ptr ev;
if (info_pre)
{
ev = new ClientEvent::Info(msg.substr(std::strlen("INFO_PRE,")));
if ((string::starts_with(ev->render(), "WEB_AUTH:") || string::starts_with(ev->render(), "CR_TEXT:")) && creds)
{
creds->set_need_user_interaction();
}
}
else
{
ev = new ClientEvent::Info(msg.substr(std::strlen("INFO,")));
}
// INFO_PRE is like INFO but it is never buffered
if (info_hold && !info_pre)
@@ -913,6 +951,12 @@ class Session : ProtoContextCallbackInterface,
// modify proto config (cipher, auth, key-derivation and compression methods)
proto_context.process_push(received_options, *proto_context_options);
// process pushed auth-nocache
if (creds && proto_context.conf().auth_nocache)
{
creds->purge_user_pass();
}
// initialize tun/routing
tun = tun_factory->new_tun_client_obj(io_context, *this, transport.get());
tun->tun_start(received_options, *transport, proto_context.dc_settings());
@@ -1059,6 +1103,14 @@ class Session : ProtoContextCallbackInterface,
{
proto_context.write_auth_string(creds->get_password(), buf);
}
// save username for auth-token, which might be pushed later
creds->save_username_for_session_id();
if (proto_context.conf().auth_nocache)
{
creds->purge_user_pass();
}
}
else
{
@@ -1451,7 +1503,6 @@ class Session : ProtoContextCallbackInterface,
bool echo;
bool info;
bool autologin_sessions;
Error::Type fatal_ = Error::UNDEF;
std::string fatal_reason_;

View File

@@ -94,6 +94,7 @@ enum Type
EARLY_NEG_INVALID, // Early protoctol negotiation information invalid/parse error
NTLM_MISSING_CRYPTO, // crypto primitives requires for NTLM are unavailable
UNUSED_OPTIONS, // unused/unknown options found in configuration
SESSION_EXPIRED, // authentication error when using session-id and password is not cached
// key event errors
KEV_NEGOTIATE_ERROR,
@@ -180,6 +181,7 @@ inline const char *name(const size_t type)
"EARLY_NEG_INVALID",
"NTLM_MISSING_CRYPTO",
"UNUSED_OPTIONS_ERROR",
"SESSION_EXPIRED",
"KEV_NEGOTIATE_ERROR",
"KEV_PENDING_ERROR",
"N_KEV_EXPIRE",

View File

@@ -483,16 +483,12 @@ class OMI : public OMICore, public ClientAPI::LogReceiver
// response contains only challenge text
creds->response = auth_password;
}
creds->cachePassword = !auth_nocache;
creds->replacePasswordWithSessionID = true;
}
else if (type == "Auth")
{
creds.reset(new ClientAPI::ProvideCreds);
creds->username = username;
creds->password = password;
creds->replacePasswordWithSessionID = true;
creds->cachePassword = !auth_nocache;
}
else if (type == "HTTP Proxy")
{

View File

@@ -440,6 +440,8 @@ class ProtoContext : public logging::LoggingMixin<OPENVPN_DEBUG_PROTO, logging::
// instead of possible modifications caused by NCP
std::string initial_options;
bool auth_nocache = false;
void load(const OptionList &opt,
const ProtoContextCompressionOptions &pco,
const int default_key_direction,
@@ -1203,6 +1205,11 @@ class ProtoContext : public logging::LoggingMixin<OPENVPN_DEBUG_PROTO, logging::
load_duration_parm(keepalive_timeout, "ping-restart", opt, 1, false, false);
}
}
if ((type == LOAD_COMMON_CLIENT) || (type == LOAD_COMMON_CLIENT_PUSHED))
{
auth_nocache = opt.exists("auth-nocache");
}
}
std::string relay_prefix(const char *optname) const

View File

@@ -32,6 +32,7 @@
#include <openvpn/common/wstring.hpp>
#include <openvpn/win/winerr.hpp>
#include <openvpn/win/reg.hpp>
#include <openvpn/win/npinfo.hpp>
namespace openvpn {
namespace Win {

View File

@@ -1015,7 +1015,6 @@ int openvpn_client(int argc, char *argv[], const std::string *profile_content)
std::string appCustomProtocols;
bool eval = false;
bool self_test = false;
bool cachePassword = false;
bool disableClientCert = false;
bool proxyAllowCleartextAuth = false;
int defaultKeyDirection = -1;
@@ -1078,9 +1077,6 @@ int openvpn_client(int argc, char *argv[], const std::string *profile_content)
case 'T':
self_test = true;
break;
case 'C':
cachePassword = true;
break;
case 'x':
disableClientCert = true;
break;
@@ -1375,8 +1371,6 @@ int openvpn_client(int argc, char *argv[], const std::string *profile_content)
creds.http_proxy_pass = proxyPassword;
creds.response = response;
creds.dynamicChallengeCookie = dynamicChallengeCookie;
creds.replacePasswordWithSessionID = true;
creds.cachePassword = cachePassword;
ClientAPI::Status creds_status = client.provide_creds(creds);
if (creds_status.error)
OPENVPN_THROW_EXCEPTION("creds error: " << creds_status.message);