Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RCORE-2253 Redirected user authenticated app requests cause user to be logged out and location is not updated #8011

Open
wants to merge 28 commits into
base: master
Choose a base branch
from

Conversation

michael-wb
Copy link
Contributor

@michael-wb michael-wb commented Aug 29, 2024

What, How & Why?

Updated the test http transport to enable HTTP redirection support in the Curl lib and added a test to verify the redirect operation. The Authorization header is not being included in the request when it is forwarded to the new location URL, leading the User to be logged out when a user authenticated appservices requests is performed. As a result, the handle_auth_failure() function was updated to request the location prior to refreshing the access token when an authenticated request fails on the first attempt, in order to ensure the client app is always connecting to the correct server hostname.

Added a test that is run when using the redirect server, that verifies the HTTP and websocket redirect operations since this support has been removed from App and has been enabled in the Curl lib used by the test harness.

The RedirectingHttpServer was updated to enable more control of the hostname and ws_hostname values returned by the location response and support was added to force a redirect response when an appservices request is received. An event hook was also added to be notified when a location request, http redirect, websocket redirect or error occurred.

Fixes #8008, #8012

☑️ ToDos

  • 📝 Changelog update
  • 🚦 Tests (or not relevant)
  • [ ] C-API, if public C++ API changed
  • [ ] bindgen/spec.yml, if public C++ API changed

@michael-wb michael-wb self-assigned this Aug 29, 2024
@cla-bot cla-bot bot added the cla: yes label Aug 29, 2024
@michael-wb michael-wb changed the title RCORE-2252 Add test to handle redirects in CURL test transport and add a redirect test run RCORE-2253 Redirected user authenticated app requests cause user to be logged out and location is not updated Aug 29, 2024
Copy link

coveralls-official bot commented Aug 30, 2024

Pull Request Test Coverage Report for Build michael.wilkersonbarker_1387

Details

  • 222 of 258 (86.05%) changed or added relevant lines in 5 files are covered.
  • 65 unchanged lines in 19 files lost coverage.
  • Overall coverage decreased (-0.005%) to 91.103%

Changes Missing Coverage Covered Lines Changed/Added Lines %
test/object-store/util/test_file.cpp 1 5 20.0%
test/object-store/sync/app.cpp 154 169 91.12%
test/object-store/util/sync/redirect_server.hpp 56 73 76.71%
Files with Coverage Reduction New Missed Lines %
src/realm/query_expression.hpp 1 93.81%
src/realm/sync/network/websocket.cpp 1 72.2%
src/realm/sync/noinst/server/server_history.cpp 1 62.92%
src/realm/util/serializer.cpp 1 90.43%
src/realm/uuid.cpp 1 98.48%
test/fuzz_tester.hpp 1 57.73%
test/test_table.cpp 1 99.51%
src/realm/array_blobs_big.cpp 2 98.58%
src/realm/list.cpp 2 87.37%
src/realm/mixed.cpp 2 86.46%
Totals Coverage Status
Change from base Build 2602: -0.005%
Covered Lines: 217392
Relevant Lines: 238621

💛 - Coveralls

@michael-wb michael-wb marked this pull request as ready for review August 30, 2024 01:55
Base automatically changed from mwb/remove-308-support to master August 30, 2024 16:09
An error occurred while trying to automatically change base from mwb/remove-308-support to master August 30, 2024 16:09
auto app_session = get_runtime_app_session();

// Skip this test if not using the redirect server
if (!get_redirector())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why wouldn't we just explicitly create a redirecting server for this test regardless of whether it's enabled globally?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated test to define its own copy of the redirect server.

@@ -282,6 +283,9 @@ std::string get_real_base_url();
// your test is talking to.
std::string get_admin_url();

// Returns a reference to the redirector if it is enabled.
std::optional<sync::RedirectingHttpServer>& get_redirector();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of pulling the redirect server in as an include dependency of anyone using the baas admin API why not just instantiate one where you need it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the function in baas_admin_api.hpp and the test is now declaring its own local redirect server.

, socket(service)
, http_server(socket, logger)
{
util::seed_prng_nondeterministically(random); // Throws
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to seed this non-deterministically? If you just use the catch seed then you can actually replay the random values when you get an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to util::seed_prng_nondeterministically(random) since I was having issues importing the Catch2 library for the redirect server command line tool. I reverted this change.

m_acceptor.open(m_endpoint.protocol());
m_acceptor.bind(m_endpoint);
m_endpoint = m_acceptor.local_endpoint();
reset_state();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this type need to be stateful?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The redirect server has state for force_http_redirect(), force_websocket_redirect(), and the event hook callback - this resets those values back to default (such as for the end of a test).
No longer needed since the redirector is being created in the test where it is used.

network::Endpoint ep;
if (listen_port != 0) {
// If a port is provided, then configure the endpoint with this port num
ep = network::Endpoint(network::make_address("0.0.0.0"), listen_port);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to supply an explicit listen port?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was for the redirect server command line tool - removed.

});
});
}

std::string trim_url(std::string_view str)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It trims the whitespace from both ends and the trailing / from the redirect location URL in the constructor.
Since this was primarily for the redirect server command line tool, it has been removed.

return m_redirect_to_base_url;
}

std::string location_hostname() const
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like something a caller of this type should already know?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns the values that the redirect server /location endpoint will use so it can be displayed when using the redirect server command line tool - removed.

// value) and open a websocket conneciton to the actual server.
// NOTE: the websocket will never connect if both http and websockets are
// redirecting and will just keep getting the redirect close code.
void force_websocket_redirect(bool force = true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this is always called with an argument, do you need a default argument value here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary (thought was that calling force_websocket_redirect() would do just that - removed default

// NOTE: some http transport redirect implementations may strip the authorization
// header from the request after it is redirected and the user will be logged out
// from the client app as a result.
void force_http_redirect(bool remote = true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this is always called with an argument, do you need a default argument value here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary (thought was that calling force_http_redirect() would do just that - removed default

@@ -853,7 +854,8 @@ void App::log_in_with_credentials(const AppCredentials& credentials, const std::
completion(user, error);
});
},
false);
// Update location to make sure we're talking to the latest server before logging in
true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think getting logged out is the existing behavior that's always been there, do we really want to change that now?

Copy link
Contributor Author

@michael-wb michael-wb Sep 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do get logged out, but you will never be able to logged in again due to the authorization header being stripped from the /profile request, which will log you out while you are trying to log in.
By updating the location when you attempt to log in, the client app will have the latest server location info and the login attempt should be successful, instead of failing when trying to query the user's profile.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So has redirection after a region migration always been totally broken? Like you cannot recover? Because I think all the SDKs HTTP implementations have been stripping the authorization header out when following a redirect this whole time...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely an edge case and I doubt any customers have hit this specific use case:
they have to do a deployment change while the app is already running; and the user has to have already performed some operation that updated the location prior to the deployment change, such as logging in.
After the deployment change (and the requests start getting redirected), any app services request like updating the access token will log the user out and they won't be able to successfully log in again.

Fortunately, restarting the app will also resolve the issue, since it will require the location to be updated before sending any app services requests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about this offline - instead of making every call to log_in_with_credentials() pre-emptively request a location update, we're going to request a location update if the call to get the user's profile fails with a 401 unauthorized error. That way this error handling gets a bit slower in this edge case, but the behavior of all other log_in_with_credentials() should stay the same.

Copy link
Contributor Author

@michael-wb michael-wb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of your comments are around extra code that was added when running the redirect server using the command line tool. While it was helpful for creating the test, it sounds like we don't want the tool, so many of those changes in this PR have been reverted.

@@ -853,7 +854,8 @@ void App::log_in_with_credentials(const AppCredentials& credentials, const std::
completion(user, error);
});
},
false);
// Update location to make sure we're talking to the latest server before logging in
true);
Copy link
Contributor Author

@michael-wb michael-wb Sep 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do get logged out, but you will never be able to logged in again due to the authorization header being stripped from the /profile request, which will log you out while you are trying to log in.
By updating the location when you attempt to log in, the client app will have the latest server location info and the login attempt should be successful, instead of failing when trying to query the user's profile.

auto app_session = get_runtime_app_session();

// Skip this test if not using the redirect server
if (!get_redirector())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated test to define its own copy of the redirect server.

, socket(service)
, http_server(socket, logger)
{
util::seed_prng_nondeterministically(random); // Throws
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to util::seed_prng_nondeterministically(random) since I was having issues importing the Catch2 library for the redirect server command line tool. I reverted this change.

return;
}

conn->http_server.async_receive_request([this, conn](HTTPRequest req, std::error_code ec) {
static bool use_301 = false; // send 301 on first redirect response
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is actually switching between sending 301 and 308 after every message on line 353 to verify both redirect statuses are supported. It just initially starts out with 301.

I updated this to a member variable and added a comment about the code.

});
});
}

std::string trim_url(std::string_view str)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It trims the whitespace from both ends and the trailing / from the redirect location URL in the constructor.
Since this was primarily for the redirect server command line tool, it has been removed.

// NOTE: some http transport redirect implementations may strip the authorization
// header from the request after it is redirected and the user will be logged out
// from the client app as a result.
void force_http_redirect(bool remote = true)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary (thought was that calling force_http_redirect() would do just that - removed default

// value) and open a websocket conneciton to the actual server.
// NOTE: the websocket will never connect if both http and websockets are
// redirecting and will just keep getting the redirect close code.
void force_websocket_redirect(bool force = true)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary (thought was that calling force_websocket_redirect() would do just that - removed default

return m_redirect_to_base_url;
}

std::string location_hostname() const
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns the values that the redirect server /location endpoint will use so it can be displayed when using the redirect server command line tool - removed.

@@ -282,6 +283,9 @@ std::string get_real_base_url();
// your test is talking to.
std::string get_admin_url();

// Returns a reference to the redirector if it is enabled.
std::optional<sync::RedirectingHttpServer>& get_redirector();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the function in baas_admin_api.hpp and the test is now declaring its own local redirect server.

test/object-store/util/sync/baas_admin_api.cpp Outdated Show resolved Hide resolved
@jedelbo
Copy link
Contributor

jedelbo commented Oct 18, 2024

@jbreams should this just be closed or is there anything valuable to preserve?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
3 participants