Photo

Hi, I'm Aaron.

Authenticating against WordPress in Rails 3

N.B. Note the date on this article. It was written before Wordpress Version 5. I have not worked with Wordpress much since writing this, so I don’t know if it still works.

That said, I think the post still has value in its cross-application methodology.

I’ve been working on a Rails app that needs to integrate with an existing userbase found in a WordPress installation.

One problem I’ve encountered is that her main website that manages subscriptions and users is in WordPress. Users will register with that main website and may or may not have access to the Rails application, separately (depending on their subscription level). What I needed was a method to handle User Authentication in Rails that slaves to the subscription / account information held in WordPress. Ideally, this should all keep with the DRY principle to make it nice and Rails-y.

I wanted to avoid using any messy and potentially unsecure session cookie sharing, and I also wanted to avoid doing any database shadowing. I suspected that since all the data is up on databases on the same server, I should be able to just query it out, right?

Right!

Ok, so this was a little tricky and it took a couple hours to suss it all out, but here’s what I did. For this example, we’ll say that all of your databases are on the same host (mysql.host.com), though they don’t need to be.

Defining the settings

In your config/database.yml file, you need to specify separate entries for your WordPress database. Thanks to fnichol for this gist.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
development:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: rails_proj_development
  pool: 5
  username: rp_dev
  password: wootwoot
  host: localhost

wp_development:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: wordpress
  pool: 5
  username: wp_user
  password: hackable
  host: localhost
  table_prefix_name: wp_dev_

  # Warning: The database defined as "test" will be erased and
  # re-generated from your development database when you run "rake".
  # Do not set this db to the same as development or production.

test:
  adapter: mysql2
  encoding: utf8
 reconnect: false
  database: rails_proj_test
  pool: 5
  username: rp_test
  password: wootwoot
  host: localhost

wp_test:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: wordpress
  pool: 5
  username: wp_user
  password: hackable
  host: localhost
  table_prefix_name: wp_test_

production:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: rails_proj_production
  pool: 5
  username: rp_prod
  password: wootwoot
  host: localhost

wp_production:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: wordpress
  pool: 5
  username: wp_user
  password: hackable
  host: localhost
  table_prefix_name: wp_

Note that in the wp_* entries, I added a table_prefix_name entry. This will help keep things clean (inclusion alone does not do all the heavy lifting, but it puts it in an obvious place that we can reference later). The prefixes allow you to optionally put all your WordPresses into one database, prefixing them with different prefixes. I wouldn’t say this is necessarily advisable but it’s at least an option, if your configuration is already like that.

Creating the Model

We can use ActiveRecord to connect to this database, so that everything is seamless. You will need to create a different model for each table you want to connect to (this is consistent with the ActiveRecord pattern anyways). You can either create the model manually, using your favorite text editor, or you can use the rails generator, but be sure to pass it –skip-migration, since you don’t want it to create a duplicate table in your Rails DB.

Here is the model I used for “User”. (fnichol does one for Post on his gist).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class User < ActiveRecord::Base

  # define any "has_many" relationships here

  establish_connection "wp_#{Rails.env}"

  @prefix = Rails.application.config.database_configuration["wp_#{Rails.env}"]['table_prefix_name']

  set_table_name "#{@prefix}users"

  alias_attribute  "id",             "ID"
  alias_attribute  "login",          "user_login"
  alias_attribute  "pass",           "user_pass"
  alias_attribute  "nicename",       "user_nicename"
  alias_attribute  "email",          "user_email"
  alias_attribute  "url",            "user_url"
  alias_attribute  "created_at",     "user_registered"
  alias_attribute  "activation_key", "user_activation_key"
  alias_attribute  "status",         "user_status"
  alias_attribute  "display_name",   "display_name"

  def readonly?
    return true
  end

  def before_destroy
    raise ActiveRecord::ReadOnlyRecord
  end
end

So, first things first, do it just like you would any Rails AR model. Just make it a new class inheriting from AR:Base.

This first line creates the connection:

establish_connection "wp_#{Rails.env}"

If you used the naming scheme from the example database.yml file above, AND if your webhost handles Rails environments properly (mine sadly doesn’t 🙁 ), this should “just work.”

The prefix we created in the database.yml file earlier will now be pulled in:

@prefix = Rails.application.config.database_configuration["wp_#{Rails.env}"]['table_prefix_name']

We then use that prefix to specify the table name. If you were wanting to create a Post model, you would obviously just change it to posts. A taxonomy model might use terms.

set_table_name "#{@prefix}users"

The lines in the middle are just aliases to make the model more Rails-y. The only critical one is the first — you need to alias ID to be id so that Rails can locate the id field when doing relationships.

The last part is optional, though I recommend including it. Since Rails is a guest in the house of WordPress, you don’t want it knocking over vases, making out with WordPress’s daughter, or eating all the expired cupcakes and cookies in their cupboards like I used to do to my best friends in grade school (only that last part, not the second one and the first one only once).

Example:

irb(main):005:0> u = User.last
=> #
irb(main):006:0> u.url = "foo"
=> "foo"
irb(main):007:0> u.save
ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
...(stacktrace)...

irb(main):008:0> u.destroy
ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
...(stacktrace)...
irb(main):009:0> u2 = User.new
=> #
irb(main):010:0> u2.user_login = "test"
=> "test"
irb(main):011:0> u2.save
ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord

Keeping the foreign table read-only will help ensure your Rails App doesn’t accidentally eat a Swiss Cake Roll that wasn’t yet expired.

Authenticating Against the Database

Password “hashing” is the process of encrypting the password to make it so bad guys and curious monkeys can’t easily tell what your passwords are.

It gets a little tricky here. If your wordpress was created AFTER version 2.5, then the account passwords will be hashed using a third-party library called “Phpass“. If they were created before, they will use an MD5 hash.

Since you are more than likely upgraded to the current version (because you are an awesome and smart wordpress user), bear in mind that even if your old accounts were created prior to v2.5 (because you’re old school, too!), if those passwords are changed or if new users are created, the new passwords will be hashed with Phpass. (If you login to your blog with your account, it will automatically update your password to the more secure encryption).

Like I said, tricky.

Either way, you’ll need Phpass for ruby. Fortunately, someone has already made a Gem for it: phpass-ruby. Add gem 'phpass-ruby' to your Gemfile, then bundle install, and you should be all set with that.

MD5 digests can be done using

Digest::MD5.hexdigest("plain text password").

Consider this an incomplete solution, yet at least a starting point.

Add this block to your User model.

1
2
3
4
def check_password plaintext_password
  require 'phpass'
  return Phpass.new(8).check(plaintext_password, pass)
end

I’m still researching the code to see if this will need to factor in the presence of any salts that you specify in your wp-config.php. Supposedly those salts are factored into the passwords, but in pluggable.php, the only place I see wp_salt() being used is in cookies and nonces. All this means is that you shouldn’t have to do anything extra provided your password is updated.

Testing it out

Once you’ve got that done. Test it out by jumping into your rails console.

Loading development environment (Rails 3.0.3)
irb(main):001:0> u = User.last
=> #
irb(main):002:0> u.check_password "bad_password"
=> false
irb(main):003:0> u.check_password "good_password"
=> true

If you cannot get the password to verify and you are absolutely positive you’re using the correct password, it likely means that your password is old-skool and needs to be updated. An MD5 password will appear in your database as only 0-9 and a-f, if you see any other characters, then it’s been modernized) You can make it hip by simply logging into your dashboard; WordPress will fix it when it checks.

If you’re getting errors about a “table not existing”, you can check to see if it’s using the correct table_name with this console command:

irb(main):008:0> User.table_name
=> "wp_dev_users"

My model took some tweaking here mainly because my host has a very dysfunctional relationship with its dev/test/prod environments.

Pitfall of Belongs_to

I have tested it with a has_many (which requires no modification to the table), but not with a belongs_to. Since the latter requires that the table actually have an additional reference column for the foreign key, do this at your own risk. WordPress may modify the user table without you knowing and that could break your relationships.

My suggestion would be to create a proxy ownership model (“liaison”?) that belongs_to your User model. That proxy ownership model could then handle all other Rails relationships autonomously from the WordPress database. It makes your AREL a little less pretty (eg. some_group.liaisons.first.nicename, assuming a Group model has_many :liaisons)

Applying it

In the real world context, you can now use it in whatever authentication framework you like. Since it’s using ActiveRecord, you can employ it any place you would if it was in a Rails DB instead.

Don’t forget you also have access to the other details in the WP user table, such as “nice name”, their e-mail, etc. If you want to include their profile details, you can create a usermeta model:

1
2
3
4
5
6
7
8
9
10
 
class User < ActiveRecord::Base
  has_many :usermeta
  ...
end

class Usermeta < ActiveRecord::Base
  belongs_to :user
  ...
end

Usermeta already contains a user_id column in Rails-friendly format. You’ll have to write some model methods to extract all the data easily. I suggest making that one readonly as well.