WordPress – SSL login page without SSL admin

In WordPress (v3.3), FORCE_SSL_LOGIN will transmit login credentials over SSL, however, the login page itself may be accessed over HTTP (as opposed to HTTPS). If the login page is accessed via HTTP, the user has no (easy) way of verifying that the page is legitimate and that the credentials will in fact be sent over SSL (i.e. the address bar doesn’t display the ‘https’ and ‘lock symbol’). If one is also using FORCE_SSL_ADMIN, this problem is easily solved – simply access your login page over SSL.

I do not want an SSL admin page (FORCE_SSL_ADMIN=false), but want to access the login page via SSL (i.e. actually have the URL start with https – not just send the credentials over SSL), and this is where you run into a bit of a problem. If the login page is accessed over HTTPS, a successful login redirects to the SSL admin section and sets a secure cookie.

To briefly go over what occurs:

wp-login.php determines where to ‘redirect’ the page after the login ($redirect_to). If no redirect is specified in the URL, you should end up on the admin section. However, if the login page is accessed via SSL, the redirect points to the SSL version of the admin page (this is easy enough to resolve with a web server redirect). The problem is that WordPress assumes that the admin section will be accessed only via SSL and sets a secure cookie. If you set a secure cookie, the cookie cannot be accessed from a non SSL page.

Specifically, the function admin_url() will set $redirect_to to the SSL admin page if is_ssl() is true (and it is true if the login page is accessed via SSL). Deciding whether to disable the secure cookie is done in the following if statement (lines 565 and 566):

if (!$secure_cookie && is_ssl() && force_ssl_login() && !force_ssl_admin() && ( 0 !== strpos($redirect_to, 'https') ) && ( 0 === strpos($redirect_to, 'http') ) )
$secure_cookie = false;

Since admin_url() starts with https, the last two conditions evaluate to false, and $secure_cookie remains an empty string. $secure_cookie is passed to wp_signon(), which will then use the value of is_ssl() and set a secure cookie.

There are two easy solutions to this, unfortunately, both require editing wp-login.php:

  1. Change line 537 (568 for WP 3.4; 565 for WP 3.4.1; 574 for WP 3.5) of wp-login.php:
    From: $secure_cookie = '';
    To: $secure_cookie = false;
  2. Explictly request admin_url() to return the non SSL url by changing line 557 (589 for WP 3.4; 588 for WP 3.4.1; 597 for WP 3.5) of wp-login.php:
    From: $redirect_to = admin_url();
    To: $redirect_to = admin_url('','http');
case 'login' :
default:
        $secure_cookie = '';
        $interim_login = isset($_REQUEST['interim-login']);

        // If the user wants ssl but the session is not ssl, force a secure cookie.
        if ( !empty($_POST['log']) && !force_ssl_admin() ) {
                $user_name = sanitize_user($_POST['log']);
                if ( $user = get_userdatabylogin($user_name) ) {
                        if ( get_user_option('use_ssl', $user->ID) ) {
                                $secure_cookie = true;
                                force_ssl_admin(true);
                        }
                }
        }

        if ( isset( $_REQUEST['redirect_to'] ) ) {
                $redirect_to = $_REQUEST['redirect_to'];
                // Redirect to https if user wants ssl
                if ( $secure_cookie && false !== strpos($redirect_to, 'wp-admin') )
                        $redirect_to = preg_replace('|^http://|', 'https://', $redirect_to);
        } else {
                $redirect_to = admin_url();
        }

In my case, I prefer the second solution, as it saves on one redirect.

Finally, to force the login URL to always be over SSL, and all other paths to not be over SSL, some nginx rewrite rules:

server {
#SSL server
	listen *:443 ssl;
	#ssl_certificate, ssl_certificate_key, server_name, listen, root, logs, etc

	location = /wp-login.php {
		include /etc/nginx/fastcgi_params;
		fastcgi_pass 127.0.0.1:XXXX;
	}

	location / {
		rewrite ^ http://$host$request_uri permanent;
	}
}

server {
#Non-SSL server
        #server_name, listen, root, logs, etc

        rewrite ^/wp-login.php$ https://$host/wp-login.php;

	##other directives
}

By cyberx86

Just a random guy who dabbles with assorted technologies yet works in a completely unrelated field.

15 comments

  1. That’s a nice trick to enable only the login attempts via SSL. Currently I enabled SSL for all the administration that is not necessary for me too.

    The code could be improved a little to make it work for any domain. For example, the first rewrite (inside SSL server) could be…

    rewrite ^ http://$host$request_uri permanent;

    And the second rewrite (inside Non-SSL server) could be…

    rewrite ^/wp-login.php$ https://$host$request_uri;
    1. Glad you found it useful. Thanks for the suggestion – I will make the recommended change. Using $host is definitely more versatile. The Non-SSL server though, needs to end in /wp-login.php. (It is worth noting that for unusual setups that use a port number, $host wouldn’t work, but in general, it is better).

      1. Thanks for adding my tip. I’m no expert in Nginx. I thought $host was better. I highly appreciate your listening skill. 🙂

        I wish there is a way to subscribe to the comments here. I came back here after a long time (after seeing you in ServerFault). It’s hard to believe that your day is “completely unrelated to technology”. Anyway, thanks for all you do there. I’ve already to all your comment in SF, though. 🙂 You have amazing insights on AWS, Nginx, PHP-FPM, etc.

        1. You should be able to subscribe to comments actually – there is a comments feed, just no icon for it. Most RSS readers should pick up on it (it is a standard WordPress feed). I tried to keep things simpler and uncluttered – and figured that one RSS icon was enough – I might have to reconsider that.

          As for my day – I hardly see a computer at work – and their use is mostly for presentations or word processing. Helping people out, on the other hand, is quite integral to my work, and is something I enjoy. Combine that with some problem solving and you can see why I enjoy SF.

          1. Thanks for the link to comments feed. I’d better subscribe to all the comments in this site than subscribing to only one post. Thanks again for your time to help me.

    1. Exactly right, if you want what the above article is proposing. You do not want to access the admin page over SSL – so we set that to false (FORCE_SSL_ADMIN=false). We do want the login to be over SSL – so we set that to true (FORCE_SSL_LOGIN=true).

    1. I don’t use WP Multisite, (so I am not familiar with how it works), on a single site (e.g. this one), the following happens on logout:

      http://www.thatsgeeky.com/wp-login.php?action=logout&_wpnonce=xxxxxxxxxx :
      302 redirect: https://www.thatsgeeky.com/wp-login.php?action=logout&_wpnonce=xxxxxxxxxx
      
      https://www.thatsgeeky.com/wp-login.php?action=logout&_wpnonce=xxxxxxxxxx :
      302 redirect: wp-login.php?loggedout=true
      
      ...and we end up on: https://www.thatsgeeky.com/wp-login.php?loggedout=true

      You can see that WordPress pointed to a non-HTTPS URL, but nginx redirected it.

      I would expect the same thing to happen on the main site of a WP-multisite setup (i.e. if you access http://example.com/wp-login.php – it should redirect to https regardless of the query parameters). Now, if you have a username in there, you probably need to modify the location block:

      location /wp-login.php {}

      becomes:

      location ~* /wp-login.php$ {}

      (Note: you might have to escape that forward slash – location ~* \/wp-login.php$)

      The difference is that the ~* makes it a regex match (instead of looking at the start of the url). So, /wp-login.php$ says anything ending in (the $ sign) /wp-login.php. (You could use ~ instead of ~* – the former is case-sensitive, the latter, case-insensitive).

      The server block I currently use is a bit different than the ones above. For HTTPS, I have:

      server {
      	listen *:443 ssl;
      	ssl_certificate /path/to/thatsgeeky.com.crt;
      	ssl_certificate_key /path/to/thatsgeeky.com.key;
              server_name thatsgeeky.com www.thatsgeeky.com;
      	root   /path/to/thatsgeeky.com/web;
      	error_log /path/to/thatsgeeky.com/error.log;
      	access_log /path/to/thatsgeeky.com/access.log main;
      
      	location = /wp-login.php {
      		include /etc/nginx/fastcgi_params;
      		fastcgi_pass 127.0.0.1:9011;
      	}
      
      	location / {
      		if ($http_referer !~ ^(https://www.thatsgeeky.com/) ){
      			rewrite ^ http://www.thatsgeeky.com$request_uri permanent;
      		}
      	}
      	location = / {
      		rewrite ^ http://www.thatsgeeky.com permanent;
      	}
      }

      For my main server block, there is only one line relevant to the login page:

      rewrite ^/wp-login.php$ https://www.thatsgeeky.com/wp-login.php;
  2. Excellent article, just what i was looking for. However, i’m unable to get this to work. I believe it may be due to the fact that i’m trying to use a shared SSL from bluehost.com and had to set my Website URL to secure.bluehost.com/~polishp2/blog which is the path my blog is installed. I’m able to get an SSL login page at https://secure.bluehost.com/~polishp2/blog/wp-login.php but it only works with HTTP for the admin section. I cannot use that session to administer via HTTP. This is an issue since the host limits data passed via SSL and you can’t view all media. Any ideas?

    1. Glad you found it useful. By ‘it only works with HTTP for the admin section’ do you mean that a login from HTTPS does not take you to the admin section (as in it just redirects you back to the login page)? Did you try the fully WordPress approach (that is, both the login page and the admin page being SSL – i.e. FORCE_SSL_LOGIN=true and FORCE_SSL_ADMIN=true)? The approach proposed here builds on the functionality established by those parameters, so if the default approach does not work for you, neither will the approach taken by this article. Offhand, it sounds like either you are using a secure cookie (set by $secure_cookie and only usable over HTTPS (so not accessible to the HTTP session)), or that the page is redirecting to the wrong location. Hope it works out for you.

  3. Sure a nice howto, but I should point out that even if you do the login over SSL and then continue to use plain HTTP for the communication, your session cookie can be easily be extracted, even if you don’t give away your password, you have given away your current privilege.

    1. A very good point. In many cases, cookie hijacking is possible if part of the site is not over SSL – so even if the full admin backend is over SSL, but the front-end site is not, cookie hijacking may be possible. For me, I have switched over to SSL on all pages for around a year now. Most servers are at the point where encryption will not add appreciable load, and the benefits definitely outweigh the costs.

Leave a comment

Your email address will not be published. Required fields are marked *