/sorcery

Magical authentication for Rails 3

Primary LanguageRubyMIT LicenseMIT

<img src=“https://secure.travis-ci.org/NoamB/sorcery.png” />

sorcery

Magical Authentication for Rails 3. Supports ActiveRecord, Mongoid and MongoMapper.

Inspired by restful_authentication, Authlogic and Devise. Crypto code taken almost unchanged from Authlogic. OAuth code inspired by OmniAuth and Ryan Bates’s railscasts about it.

Philosophy

Sorcery is a stripped-down, bare-bones authentication library, with which you can write your own authentication flow. It was built with a few goals in mind:

  • Less is more - less than 20 public methods to remember for the entire feature-set make the lib easy to ‘get’.

  • No built-in or generated code - use the library’s methods inside *your own* MVC structures, and don’t fight to fix someone else’s.

  • Magic yes, Voodoo no - the lib should be easy to hack for most developers.

  • Configuration over Confusion - Centralized (1 file), Simple & short configuration as possible, not drowning in syntactic sugar.

  • Keep MVC cleanly separated - DB is for models, sessions are for controllers. Models stay unaware of sessions.

Hopefully, I’ve achieved this. If not, let me know.

Railscast: railscasts.com/episodes/283-authentication-with-sorcery

Example Rails 3 app using sorcery: github.com/NoamB/sorcery-example-app

Documentation: rubydoc.info/gems/sorcery/0.8.1/frames

Check out the tutorials in the github wiki!

API Summary

Below is a summary of the library methods. Most method names are self explaining and the rest are commented:

# core
require_login # this is a before filter
login(username,password,remember_me = false)
auto_login(user)# login without credentials
logout
logged_in?      # available to view
current_user    # available to view
redirect_back_or_to # used when a user tries to access a page while logged out, is asked to login, and we want to return him back to the page he originally wanted.
@user.external? # external users, such as facebook/twitter etc.
User.authenticates_with_sorcery!

# activity logging
current_users

# http basic auth
require_login_from_http_basic # this is a before filter

# external
login_at(provider) # sends the user to an external service (twitter etc.) to authenticate.
login_from(provider) # tries to login from the external provider's callback.
create_from(provider) # create the user in the local app db.

# remember me
auto_login(user, should_remember=false)  # login without credentials, optional remember_me
remember_me!
forget_me!

# reset password
User.load_from_reset_password_token(token)
@user.deliver_reset_password_instructions!
@user.change_password!(new_password)

# user activation
User.load_from_activation_token(token)
@user.activate!

Please see the tutorials in the github wiki for detailed usage information.

Installation:

If using bundler, first add ‘sorcery’ to your Gemfile:

gem "sorcery"

And run

bundle install

Otherwise simply

gem install sorcery

Rails 3 Configuration:

rails generate sorcery:install

This will generate the core migration file, the initializer file and the ‘User’ model class.

rails generate sorcery:install remember_me reset_password

This will generate the migrations files for remember_me and reset_password submodules and will create the initializer file (and add submodules to it), and create the ‘User’ model class.

rails generate sorcery:install --model Person

This will generate the core migration file, the initializer and change the model class (in the initializer and migration files) to the class ‘Person’ (and its pluralized version, ‘people’)

rails generate sorcery:install http_basic_auth external remember_me --migrations

This will generate only the migration files for the specified submodules and will add them to the initializer file.

Inside the initializer, the comments will tell you what each setting does.

DelayedJob Integration

By default emails are sent synchronously. You can send them asynchronously by using the [delayed_job gem](github.com/collectiveidea/delayed_job).

After implementing the ‘delayed_job` into your project add the code below at the end of the `config/initializers/sorcery.rb` file. After that all emails will be sent asynchronously.

module Sorcery
  module Model
    module InstanceMethods
      def generic_send_email(method, mailer)
        config = sorcery_config
        mail = config.send(mailer).delay.send(config.send(method), self)
      end
    end
  end
end

Single Table Inheritance (STI) Support

STI is supported via a single setting in config/initializers/sorcery.rb.

Full Features List by module:

Core (see lib/sorcery/model.rb and lib/sorcery/controller.rb):

  • login/logout, optional return user to requested url on login, configurable redirect for non-logged-in users.

  • password encryption, algorithms: bcrypt(default), md5, sha1, sha256, sha512, aes256, custom(yours!), none. Configurable stretches and salt.

  • configurable attribute names for username, password and email.

  • allow multiple fields to serve as username.

User Activation (see lib/sorcery/model/submodules/user_activation.rb):

  • User activation by email with optional success email.

  • configurable attribute names.

  • configurable mailer, method name, and attribute name.

  • configurable temporary token expiration.

  • Optionally prevent non-active users to login.

Reset Password (see lib/sorcery/model/submodules/reset_password.rb):

  • Reset password with email verification.

  • configurable mailer, method name, and attribute name.

  • configurable temporary token expiration.

  • configurable time between emails (hammering protection).

Remember Me (see lib/sorcery/model/submodules/remember_me.rb):

  • Remember me with configurable expiration.

  • configurable attribute names.

Session Timeout (see lib/sorcery/controller/submodules/session_timeout.rb):

  • Configurable session timeout.

  • Optionally session timeout will be calculated from last user action.

Brute Force Protection (see lib/sorcery/model/submodules/brute_force_protection.rb):

  • Brute force login hammering protection.

  • configurable logins before lock and lock duration.

Basic HTTP Authentication (see lib/sorcery/controller/submodules/http_basic_auth.rb):

  • A before filter for requesting authentication with HTTP Basic.

  • automatic login from HTTP Basic.

  • automatic login is disabled if session key changed.

Activity Logging (see lib/sorcery/model/submodules/activity_logging.rb):

  • automatic logging of last login, last logout and last activity time.

  • an easy method of collecting the list of currently logged in users.

  • configurable timeout by which to decide whether to include a user in the list of logged in users.

External (see lib/sorcery/controller/submodules/external.rb):

  • OAuth1 and OAuth2 support (currently twitter & facebook)

  • configurable db field names and authentications table.

AccessToken (see lib/sorcery/model/submodules/access_token.rb)

  • OAuth 2.0 For Login - RESTful JSON APIs.

  • Please read instructions below.

AccessToken Submodule (rails-api)

This submodule is intended to be used with rails as the backend server to rich client-side applications.

See the rails-api project for building RESTful JSON APIs.

Supported Modes:

  • single_token: One access token per user, shared between all user clients. Creates an access token on user creation.

  • session: Allows multiple tokens per user, a user can have many clients acting on its behalf, with this mode each client can have its own access token. A maximum value can be defined using the ‘max_per_user’ configuration option.

Expiration:

Tokens expiration can be configured by setting the ‘duration’ value (in seconds), this value will be used against token’s creation time to know if the token has expired.

Expiration can also be evaluated against token’s last actvity time by setting ‘duration_from_last_activity’ to true.

Expired tokens will be automatically deleted for each user after login.

Tokens are deleted on client logout.

Permanent Tokens:

All tokens are set to expire by default, permanent tokens can be created by setting its ‘expirable’ attribute to false.

Example use case: mobile applications where the user never logs in/out.

Security Considerations:

  • Use of TLS is required (HTTPS).

OAuth 2.0 For Login:

Outsources user authentication to OAuth 2.0 providers.

Flow: Implicit Grant [1]

How does it work?

Client-side application gets an access token from an OAuth 2.0 provider, validates the received token, and sends the token with the rest of its properties to this API server for ‘login’.

The API server then attempts to login the user by sending a request to the provider with the access token, if the access token is valid the API server uses the user identifier included in the provider’s response to find and login (and optionaly create) the external user in the local database.

Please see links below for more information, in particular Ryan Boyd’s excellent talk about OAuth 2.0 [2] and Google’s OAuth2 playground (use the settings button to change the OAuth flow to Client-side, you can also enter your own OAuth credentials) [4].

Notes:

  • After login, the API server can then return one of its own api_access_token to the client application.

  • Tested with the following providers: Google.

  • The client-side application only needs to store the OAuth Client ID, the secret can be set in the API server configuration file.

References:

  1. tools.ietf.org/html/draft-ietf-oauth-v2-30#section-1.3.2

  2. Google I/O 2012 - OAuth 2.0 for Identity and Data Access

  3. developers.google.com/accounts/docs/OAuth2

  4. developers.google.com/oauthplayground

Setup Example (OAuth 2.0 For Login):

  • Update: demo with rails-api and Google’s OAuth 2.0 For Login: github.com/fzagarzazu/sorcery_access_token_demo

  • Installation rails generate sorcery:install access_token external

  • Example configuration file config/initializers/sorcery.rb

    Rails.application.config.sorcery.submodules = [:access_token, :external]
    config.restful_json_api = true
    config.external_providers = [:google]
    config.google.key = "client_id"
    config.google.secret = "client_secret"
    config.user_config do |user|
      user.username_attribute_names = [:email]
      user.authentications_class = Authentication
      user.access_token_mode = 'single_token'
      user.access_token_duration = 10.minutes.to_i
      user.access_token_duration_from_last_activity = true
      user.access_token_register_last_activity = true
    end
    
  • Example user model:

    class User < ActiveRecord::Base
      authenticates_with_sorcery! do |config|
        config.authentications_class = Authentication
      end
      attr_accessible :email, :password, :password_confirmation, :authentications_attributes
      has_many :access_tokens, :dependent => :delete_all
      has_many :authentications, :dependent => :destroy
    
      accepts_nested_attributes_for :authentications
    
    end
    
  • Example authentication model

    class Authentication < ActiveRecord::Base
      attr_accessible :user_id, :provider, :uid
      belongs_to :user
    end
    
  • Example sessions controller

    class SessionsController < ApplicationController
      skip_before_filter :require_login, :except => [:destroy]
    
      def create
        # Login
        if params[:provider] && params[:access_token_hash]
          login_or_create_from_client_side(params[:provider], params[:access_token_hash])
        elsif params[:email] && params[:password]
          login(params[:email], params[:password])
        end
    
        # Response
        if @api_access_token
          render :json => {:access_token => @api_access_token.token }
        else
          head :unauthorized
        end
      end
    
      def destroy
        logout
        head :ok
      end
    end
    
  • Example application controller

    class ApplicationController < ActionController::API
      before_filter :require_login
    end
    

Notes:

  • It should be compatible with the following submodules: activity_logging, brute_force_protection, external, reset_password, user_activation

  • curl examples:

    # Examples, remember to use HTTPS.
    
    # GET request
    curl -v -H "Accept: application/json" -H "Content-type: application/json" \
    -X GET http://localhost:3000/apples?access_token=DdQoWMGzsVdPAL5egq67
    
    # POST login request with email and password credentials
    curl -v -H "Accept: application/json" -H "Content-type: application/json" \
    -X POST -d '{"email": "johndoe@example.com", "password": "secret_passwd"}'  http://localhost:3000/sessions
    
    # POST login request with provider's access token
    curl -v -H "Accept: application/json" -H "Content-type: application/json" \
    -X POST -d '{"provider": "google", "access_token_hash": { \
      "access_token": "9d2H0I9AEa834B45cRTfBEcA82bF010B53BFGT83eFDB6097", \
      "token_type": "Bearer", "expires_in": 3600 } }'  http://localhost:3000/sessions
  • Mongoid and MongoMapper

Support included but not completely tested, please review code before using it.

  • Code review, feedback and contributions much appreciated.

Next Planned Features:

I’ve got some thoughts which include (unordered):

  • Passing a block to encrypt, allowing the developer to define his own mix of salting and encrypting

  • Forgot username, maybe as part of the reset_password module

  • Scoping logins (to a subdomain or another arbitrary field)

  • Allowing storing the salt and crypted password in the same DB field for extra security

  • Other reset password strategies (security questions?)

  • Other brute force protection strategies (captcha)

Have an idea? Let me know, and it might get into the gem!

Backward compatibility

While the lib is young and evolving fast I’m breaking backward compatibility quite often. I’m constantly finding better ways to do things and throwing away old ways. To let you know when things are changing in a non-compatible way, I’m bumping the minor version of the gem. The patch version changes are backward compatible.

In short, an app that works with x.3.1 should be able to upgrade to x.3.2 with no code changes. The same cannot be said about upgrading to x.4.0 and above, however.

Upgrading

Important notes while upgrading:

  • If upgrading from <= 0.6.1 to >= 0.7.0 you need to change ‘username_attribute_name’ to ‘username_attribute_names’ in initializer.

  • If upgrading from <= v0.5.1 to >= v0.5.2 you need to explicitly set your user_class model in the initializer file.

    # This line must come after the 'user config' block.
    config.user_class = User
    
  • Sinatra support existed until v0.7.0 (including), but was dropped later due to being a maintenance nightmare.

Contributing to sorcery

Your feedback is very welcome and will make this gem much much better for you, me and everyone else. Besides feedback on code, features, suggestions and bug reports, you may want to actually make an impact on the code. For this:

  • Fork it.

  • Fix it.

  • Test it.

  • Commit it.

  • Send me a pull request so I’ll… Pull it.

If you feel sorcery has made your life easier, and you would like to express your thanks via a donation, my paypal email is in the contact details.

Contact

Feel free to ask questions using these contact details: email: nbenari@gmail.com ( also for paypal ) twitter: @nbenari

Copyright © 2010 Noam Ben Ari (nbenari@gmail.com). See LICENSE.txt for further details.