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

auth0 wordpress plugin not redirecting users causing invalid state errors #871

Closed
aap-jmedema opened this issue Feb 6, 2023 · 4 comments
Assignees

Comments

@aap-jmedema
Copy link

Tying back to auth0 ticket https://support.auth0.com/tickets/01966888 and copying in most of the ticket and summarizing here so we haven't lost any context.

Our wordpress site is set up using https://www.somedomain.com as the main site URL and home URL. However, it also accepts traffic destined to https://somedomain.com (no www). When you access a page at https://somedomain.com/some-page WP automatically redirects to https://www.somedomain.com/some-page. However, when the Ultimate Member and Auth0 plugins are installed (a common scenario) and some-page is locked behind a non-wp-admin user login requirement, then the auth0 login process captures the request before that redirection and after login sends back the successful login notification to the incorrect URL (https://www.somedomain.com/some-page).

To replicate the issue:

1) Set up a basic webserver.
2) Install WP, set the site_url and wordpress_url to https://www.somedomain.com.
3) Install the Ultimate Member plugin, create a WP page with a permalink as wp-login.php.
4) Install the Auth0 plugin, setting the basics appropriate to your test environment. Do not set a custom domain (using an auth0 domain for the ULP is fine). Enable ULP and auth0_logout. For the setting "Original Login Form on wp-login.php", we have "When 'wle' query parameter is present". We never use the original login form, I'm simply documenting this here to clarify that the UM and auth0 plugins handle auth exclusively.
5) Create a non-admin user in auth0.
6) In auth0 cloud, make sure your application calls back to https://www.somedomain.com.
7) Test normal logins and logouts, everything should work as expected for https://www.somedomain.com. When you go to https://somedomain.com WP should automatically redirect you to https://www.somedomain.com.
8) Create another page titled "some-page". Assuming a standard WP setup, the url to access that page should be https://www.somedomain.com/some-page.
9) Configure some-page to require logging in. Bookmark the page and test that going to the page requires logging in as the non-admin user. Duplicate the bookmark and edit the new one to point to https://somedomain.com/some-page (no www). This is the "test bookmark".
10) Either open an incognito window or clear your cookies and caches. Use the test bookmark. After logging in through auth0 you should get an invalid state error.

I should note that Wordpress, without Ultimate Member, has only a basic login infrastructure. The user states are "not logged in" or "logged in as an admin", and in the latter case after login redirects you to https://somedomain.com/wp-admin regardless of your original page. The Ultimate Member plugin enables non-admin users, which in turn enables us to redirect users post-login to whatever page they were trying to access. It may be possible to replicate the invalid state issue without installing the UM plugin, I haven't tried to boil it down that far.

In order to understand the root cause of this invalid state issue, I'll walk through the login process:

1) User GETs https://somedomain.com/some-page. Normally, in a non-login scenario, WP would return a 301 permanently moved response with a location tag set to https://www.somedomain.com/some-page. But a login scenario gets hijacked.
2) WP parses the URL and determines that some-page is the page we care about, and the UM plugin looks up its settings and determines that this is a page requiring login. It sends a 302 temporarily moved to https://somedomain.com/wp-login.php?redirect_to=https://somedomain.com/some-page.
3) WP parses the URL and determines that this is a login scenario and fires off the login init hook. The Auth0 plugin takes up the processing.
4) Auth0 plugin checks its settings and knows this is a ULP scenario. It sends it's own 302 temporarily moved to https://someapp.auth0.com/ with it's own query string parameters. One of those parameters is the auth0_state parameter, which is a randomly-generated string, as documented in your pages and processes. In addition, that auth0_state value is set in a browser cookie called "auth_state". The auth0 code does not specify a cookie-domain parameter when it runs set-cookie (empty string), and the browser fills it in with the domain of the original request (meaning the cookie-domain is somedomain.com). It's also very important to note that redirect_to is no longer used and instead this process is using a new parameter redirect_uri=https://www.somedomain.com/index.php?auth0=1. This is where we break down.
5) The browser then does it's login with auth0 cloud successfully. There are a number of other 302 redirects to callback and resume that aren't relevant. Skipping ahead to the final 302 the location is https://www.somedomain.com/index.php?auth0=1 (the prior steps' redirect_uri parameter).
6) WP parses the URL and hands it off to auth0 again. At this point, the Auth0 plugin does it's final post-login security check, comparing the returned state querystring parameter with the original cookie in step 4. Unfortunately, by this time we are processing requests from the site www.somedomain.com instead of somedomain.com and therefore the original cookie is not accessible. Because the cookie comparison breaks down we're thrown an invalid state error by the auth0 plugin.

To fix this, any one of three things needs to be changed in the auth0 plugin:

1) At the beginning of the auth0 authentication process (login_auto), if the current domain doesn't match site_url(), use wp_redirect() to reload the page using site_url() (which is what is used during authentication via get_wp_auth0_url()).  
2) Change get_authorize_params (line 475) to automatically change $redirect_to to have site_url's domain.  This would effectively be doing the same redirect that WP normally does in a non-login scenario and by doing this at the beginning of step 4 retain the cookie until it's needed in step 6.  I should note that the auth0 shortcode at wp_auth0_shortcode() attempts to do just this, however the ultimate member plugin doesn't use the auth0 shortcode and simply calls the login page.  
3) Change get_authorize_params (line 483) to automatically change redirect_uri parameter to be the original domain instead of site_url (the reverse of the auth0 shortcode which is possible because it's lower down in the processing stack).  Then after authentication succeeds (post-step-6) WP will handle the process with it's own 302 to the official site location.  However, this may run afoul of allowed callback URLs in the auth0 app settings.
4) Instead of using '' for the cookie-domain, set it specifically for whatever is used to generate the redirect_uri parameter instead of the current domain that is shortly never going to be used again. Auth0 just added the custom cookie feature in v4.5.0 for this purpose.  While we no longer receive invalid state errors and can log in across site domains the redirection back to some-page is still broken because at the redirection stage (v 4.5.0 WP_Auth0_LoginManager.php's lines 70 and 237) auth0 uses wp_safe_redirect instead of wp_redirect.  Because of this problem, the custom cookie setting is insufficient for a solution and one of the other above options must be implemented.  

I have implemented my fix #1 (redirecting to https://home_url() . $_SERVER['REQUEST_URI']) outside of auth0 as a separate plugin for now as a temporary workaround, which has been in place in production for 2 weeks without issues. Here is the plugin content for reference:

// todo: doesn't re-POST data yet
function aap_auth0_forward_www()
{
if ("https://" . $_SERVER['SERVER_NAME'] != get_home_url(null, null, 'https'))
{
wp_redirect( get_home_url(null, null, 'https') . $_SERVER['REQUEST_URI'] );
exit;
}
}
// note the higher priority to pre-empt auth0
add_action( 'plugins_loaded', 'aap_auth0_forward_www', 5, 0);

@evansims evansims self-assigned this Feb 8, 2023
@evansims
Copy link
Member

evansims commented Feb 8, 2023

Hi @aap-jmedema 👋 Thank you very much for the thoughtful breakdown you've provided here!

In terms of #4, my understanding from our previous discussions on that ticket were that the changes introduced in 4.5.0 addressed at least that aspect of it for you — is that correct, or did I misunderstand?

I think your custom plugin approach is great, and makes sense for your circumstances. I am admittedly hesitant to introduce redirection logic into the plugin that could have unintended side effects for other users, but this feels like a good custom approach scenario to me.

@aap-jmedema
Copy link
Author

Glad it was useful. Took me long enough :)

The custom cookie setting allowed us to stop getting an invalid state error, the biggest problem. However, fixing this revealed a smaller problem where redirect_to broken in this reproduction scenario. Currently, with 4.5.0, here are 4 scenarios for reference:

  1. Throw "https://somedomain.com" into your url bar and go. Core WP with no plugins enabled redirects to https://www.somedomain.com and if you click on the website's login button then login occurs normally without needing a custom cookie.

  2. Throw "https://somedomain.com/wp-login" into your url bar and go. Auth0 catches the authentication attempt before wp can do it's redirect, does the auth0 login stuff, and redirects back to https://www.somedomain.com's home page. This is the scenario that the custom cookie fixed.

  3. Throw "https://somedomain.com/some-page-requiring-auth" into your url bar and go. Auth0 catches the authentication attempt before wp can do it's redirect, does the auth0 login stuff, and redirects back to https://www.somedomain.com's home page instead of the redirect_to because auth0 is using wp_safe_redirect. Prior to v4.5.0 and a custom cookie setting the authentication would fail with an invalid state error. So we're making progress but not all the way there. If you want to break this into a new issue for the separate problem I'm fine with that. Or I can change the issue header, etc. Whatever works for you.

  4. Throw "https://somedomain.com/some-page-not-requiring-auth" into your url bar and go. When my workaround is not in place, and regardless of the status or version of auth0, this page will pull up as-is with no redirection. When my workaround is in place it will redirect the page to https://www.somedomain.com/some-page-not-requiring-auth, which is technically bad behavior but still works for me. I'm highlighting this scenario because I only discovered it this morning. I thought I had tested for it before opening the original ticket but I must have only tested scenario lost your password #1 in this comment and made a bad assumption. WP doesn't redirect to site_url in all scenarios, despite what I might have said in earlier posts/tickets.

That said, I don't think scenario #4 matters for our discussion. Because auth0 is taking it upon itself to redirect during the authentication process (scenario #3 in this post), it needs to handle the redirect_to data appropriately in some fashion.

I can appreciate your hesitance on changing the redirect logic. I'm not asking for an immediate fix, I have my workaround. Chew on the alternatives for a while and find the best solution that fits auth0's architecture. I don't think there are any side effects, and there are some indications that other auth0 devs didn't see any problems. The auth0 shortcode is trying to do this - it's using wp_redirect as opposed to wp_safe_redirect. Maybe that dev author will have some insight?

@aap-jmedema
Copy link
Author

Any update on this? This is a lower priority than my other ticket (#867).

@evansims
Copy link
Member

Hi @aap-jmedema 👋 Thanks for your patience. Please review my follow up to the other thread, as this issue is in the same basket. We would recommend forking the plugin to accomodate the necessary change, or migrating to v5.

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

No branches or pull requests

2 participants