Safer Tokens extends ActiveRecord with neat API for generating and finding by random tokens. Among other gems, it stands out with its approach to security:
-
Lookup methods are timing-attack-proof
-
Tokens can be digested with BCrypt or SCrypt instead of storing them as a plain text. It is crucial when you grant someone read-only access to your database.
It works with ActiveRecord 3.0 onwards. Security features inspired by Devise.
Firstly, define token columns in ActiveRecord model. You may define multiple columns at once and provide options hash, as in following example:
class User < ActiveRecord::Base
token_in :email_confirmation, :password_reset, secure_with: :bcrypt
end
Note
|
If there’s any admin panel, make sure that either tokens are not displayed
there or they are digested with secure_with option.
|
Then manipulate tokens with methods of names inferred from those columns:
user = User.create!
token = user.set_password_reset!
found = User.use_password_reset token # find by token
found = User.expend_password_reset token # find and invalidate token
Find more examples in feature specs.
Safer Tokens assume that you got primary key in id
column.
Token consists of two segments:
- ID
-
ID of the record in the database used for database lookup.
- challenge
-
Value which is known to client. There is no need to set index on this column neither to enforce its uniqueness. Challenge is not used for database lookup and tokens are unique by definition in the scope in which
ID
is unique (at least in the whole table).
Segments are separated with single dash.
Separating database lookup from challenge comparison has a couple of benefits:
-
challenge can be encrypted or digested before storing it in the database
-
even if stored in cleartext, challenge comparison can be done with constant time algorithm (timing-attack-proof)
Two setter instance methods are defined per token column:
-
#set_<token_column_name>
-
#set_<token_column_name>!
Both generate new challenge and store it in adequate field.
Bang method saves the record with #save!
. This may result with raising
an exception. Bangless method leaves the record in dirty state.
Token is returned provided that id
is set (record is persisted) or nil
for
new records. Bang method enforces saving the record thus never returns nil
.
For destroyed records both method return proper but useless token.
Attribute reader is overriden and returns token if both id
and challenge can
be obtained (read: record is persisted and challenge is not digested) or nil
otherwise.
Attribute writer is not overriden but don’t use it. Any suggestions or contributions how to remove them are welcome warmly.
Safer Tokens defines two finder methods for every token column:
-
::use_<token_column_name>(token)
-
::expend_<token_column_name>(token)
Both attempt to find the record by ID
segment of the token, then compare
challenge in token with challenge stored in the record found. On success,
found record is returned. In addition, expend_*
invalidates the token which
requires saving (or destroing, deleting) that record.
Given example:
class User < ActiveRecord::Base
token_in :email_confirmation, :password_reset
end
Four finder class methods are defined on User
:
-
::use_email_confirmation
-
::expend_email_confirmation
-
::use_password_reset
-
::expend_password_reset
.
By default, token challenges are random 128-digit hexadecimal numbers. This is more than enough when it comes to challenge strength but poor for user experience if he’s supposed to rewrite it from SMS.
There are two ways to provide custom generator: inline lambda (which receives
model as a parameter) and symbol indicating instance method. Check
spec/features/generator_spec.rb
for examples.
Invalidation strategy describes what to do when token is expended. Strategy is
specified at token column definition with invalidate_with
option, for example:
class ApiToken < ActiveRecord::Base
token_in :token, :invalidate_with => :destroy
end
There are four invalidations strategies available:
:delete
-
Deletes the record using ActiveRecord::Persistence#delete, that is destroy callbacks are not triggered.
:destroy
-
Destroys the record using ActiveRecord::Persistence#destroy, destroy callbacks are triggered, record becomes frozen.
:new
-
Sets new challenge. Because new token is not returned, it does not play well with
:secure_with
option. :nullify
-
Nullifies challenge column value.
Random tokens are nothing more than unique, very strong passwords. Obtaining them by attacker naturally does not compromise users' accounts on other sites. However acquiring tokens e.g. for password reset or API access allows the attacker to hijack accounts. When you grant someone read-only access to your database, you may implicitly grant him write access this way.
For this reason you may want not to store tokens in cleartext but employ some key derivation function instead. BCrypt seems to be the safest choice, SCrypt is available too.
Have in mind that key derivation functions are computationally expensive because it makes brute-force attacks futile. While usually negligible, in some extreme cases the impact on application’s performance can be to strong. Using general purpose hash algorithms may help, but have in mind that they are not well suited for digesting passwords and having very long random challenge is the only way to keep them safe. This might be important with custom challenge generators. Neither custom cryptography providers nor HMAC-based ones are implemented yet. Contributions welcome.
- Cleartext
-
Dummy provider which stores challenges in cleartext. This is the default one.
- BCrypt
-
BCrypt is a key derivation function widely used in Ruby world. Rails #has_secure_password relies on it as well as Devise.
- SCrypt
-
Think of younger (born in 2009) brother of BCrypt (1999), even more computationally expensive.
Add this line to your application’s Gemfile:
gem "secure_token"
And then execute:
$ bundle
Or install it yourself as:
$ gem install secure_token
Bug reports and feature requests can be reported via GitHub’s issue tracker. Please don’t publish any security issues, rather mail them to skalee@gmail.com.
Pull requests are most welcome. Code style is maintained with help of EditorConfig. Please either use a compatible editor or review the settings (they’re human-readable and very short) and make your best to conform them. Tests are a must-have.
I want to complete most of following features before releasing version 1.0. Contributions are welcome.
-
Custom cryptography providers
-
At least one builtin cryptography provider using HMAC
-
At least one builtin cryptography provider which encrypts challenges (reversibly)
-
Customizing finder column (not only
id
) -
Customizing token separator (string which separates token segments)
-
Enforcing challenge presence (autogenerating them for new records)
-
Some callbacks maybe?