Redirecting Users After Login by Role
The filter receives the user on success and a WP_Error on failure, and every role-based snippet that forgets the second case throws a fatal error on a wrong password. It also only covers the standard login form, which is not the form most plugins use.
The filter decides where wp-login.php sends a user after a successful login — and it is also called after a failed one. login_redirect passes three values: the URL WordPress intends to use, the URL the user asked for through redirect_to, and the user. On success the user is a WP_User. On failure it is a WP_Error, and a callback that reads ->roles without checking is the fatal error that turns a mistyped password into a white screen.
The safe shape
add_filter( 'login_redirect', 'ti_login_redirect', 10, 3 );
function ti_login_redirect( $redirect_to, $requested_redirect_to, $user ) {
if ( is_wp_error( $user ) || ! $user instanceof WP_User ) {
return $redirect_to;
}
$default = admin_url();
if ( ! empty( $requested_redirect_to ) && $requested_redirect_to !== $default && 'wp-admin/' !== $requested_redirect_to ) {
return $redirect_to;
}
if ( in_array( 'administrator', $user->roles, true ) || in_array( 'editor', $user->roles, true ) ) {
return admin_url();
}
if ( in_array( 'subscriber', $user->roles, true ) ) {
return home_url( '/account/' );
}
return $redirect_to;
}
Three decisions are encoded there. The first guard handles the failed-login case. The second respects a deliberate redirect_to — a user who followed a link to a protected page and was bounced to the login form should land on that page, not on a fixed destination. The comparison against admin_url() is what makes it work: a plain visit to wp-login.php already carries redirect_to=<admin URL> in a hidden field, so "not empty" alone would never let the role branches run. Overriding a real destination is the most common complaint from users of sites that added a role redirect. The third branch uses $user->roles, which is an array because a user can hold several roles, and checks membership rather than reading $user->roles[0].
The failure case in detail
wp-login.php applies the filter right after wp_signon() returns and before it branches on the result, which is why the third argument is whatever wp_signon() returned — including on a plain GET of the login page, where empty credentials yield a WP_Error and your callback runs on every view of the form. A WP_Error for a wrong password, an empty username, a locked account from a security plugin, or a two-factor interruption all arrive here. The is_wp_error() guard is not defensive decoration; it is the branch that runs every time someone mistypes.
A less visible variant of the same bug: $user->roles is only populated for the current site on a multisite install. A user who exists in the network but has no role on this site has an empty array, and a callback that assumes a role will fall through to a default it did not intend.
Which forms run this filter
| Login form | Filter |
|---|---|
wp-login.php |
login_redirect |
wp_login_form() printed by a theme |
login_redirect (it posts to wp-login.php) |
| WooCommerce My Account | woocommerce_login_redirect |
| Membership and login plugins | Plugin-specific, usually a setting |
Programmatic wp_signon() |
None — the caller decides |
A site with WooCommerce typically has two login forms and needs two filters. The My Account one receives the redirect URL and the user, in that order, and the user is always a WP_User because WooCommerce only calls it on success. Sites that moved their login to a custom page should check how that page submits: a form posting to wp-login.php with a redirect_to field runs the filter; a form handled by a plugin's own endpoint does not.
Interaction with the redirect loop
A redirect to a page the user cannot reach — a subscriber sent into wp-admin on a site where a plugin blocks that role from the admin and bounces it to the login page, or to a page protected by a membership rule — produces a second redirect back to the login form, which succeeds again and redirects again. Hiding the admin bar alone does not do this; blocking the admin does. The symptoms match the login redirect loop, and the cause is the destination, not the login. Send each role somewhere that role can open.
Security considerations
The destination should be validated, not trusted. For anything other than the admin default, wp_safe_redirect() is what wp-login.php uses with the value you return, and it rejects off-site URLs unless the host is in allowed_redirect_hosts. Return the admin URL for a user without edit_posts and core rewrites it to the profile page or the front page before redirecting, which is the behaviour a subscriber branch should expect. Returning a URL built from user input — a query string, a cookie — without wp_validate_redirect() reintroduces an open redirect that core had closed. Hard-coded destinations built with home_url() and admin_url() are the safe pattern, and keeping the login flow predictable is as much part of user roles as the capabilities themselves.
Verify
- Log in as each role from
wp-login.php: each lands where you intended. - Log in with a wrong password: the form shows the error with no fatal.
- Open a protected page while logged out, log in from the prompt: you land on that page, not the role default.
- Log in through the WooCommerce form, if the site has one: the role redirect does not apply unless you added the second filter.
- Check a user with two roles: the first matching branch wins, which should match the order you want.
Frequently asked
- On a failed login the third argument is a WP_Error, not a WP_User, and reading $user->roles on it is a fatal error. Check is_wp_error( $user ) or isset( $user->roles ) before touching roles, and return $redirect_to unchanged.
- The My Account form has its own filter, woocommerce_login_redirect, and most membership and login plugins have theirs. login_redirect runs only in wp-login.php. Hook the form the site actually uses.
- Usually. The second argument is the URL the user was trying to reach when they were sent to log in. Overriding it with a fixed dashboard URL means a user who clicked a link to a protected page lands on the dashboard instead of the page.