Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save crifat/954a96ca1351d133a1d92f6fe0593fbc to your computer and use it in GitHub Desktop.

Select an option

Save crifat/954a96ca1351d133a1d92f6fe0593fbc to your computer and use it in GitHub Desktop.

Revisions

  1. @brunofacca brunofacca revised this gist Oct 14, 2016. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions rails_api_token_auth_with_tiddle.md
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,8 @@
    This gist attempts to explain how to implement token authentication in Rails, using
    [Devise](https://github.com/plataformatec/devise) and
    [Tiddle](https://github.com/adamniedzielski/tiddle). Tiddle was designed to
    provide token auth for API-only Rails apps. The following instructions will
    enable you to use it in Rails apps which have both APIs **and** web views.
    provide token auth for API-only Rails apps. However, the following instructions
    will enable you to use it in Rails apps which have both APIs **and** web views.

    ##Why Tiddle?

  2. @brunofacca brunofacca revised this gist Oct 14, 2016. 1 changed file with 105 additions and 57 deletions.
    162 changes: 105 additions & 57 deletions rails_api_token_auth_with_tiddle.md
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,38 @@
    This gist attempts to explain how to implement token authentication for APIs
    in Rails apps which also have web views, using the [Tiddle gem](https://github.com/adamniedzielski/tiddle).
    This gist attempts to explain how to implement token authentication in Rails, using
    [Devise](https://github.com/plataformatec/devise) and
    [Tiddle](https://github.com/adamniedzielski/tiddle). Tiddle was designed to
    provide token auth for API-only Rails apps. The following instructions will
    enable you to use it in Rails apps which have both APIs **and** web views.

    ##Why Tiddle?

    Devise is the obvious choice for authentication on Rails. However, token
    authentication was
    [deprecated in Devise 3.1](http://blog.plataformatec.com.br/2013/08/devise-3-1-now-with-more-secure-defaults/).
    There are a few gems that provide token authentication for Rails apps with
    Devise:

    - [Simple Token Authentication](https://github.com/gonzalo-bulnes/simple_token_authentication):
    Looks good if you need **very** simple token auth. However, it lacked some of
    the features my project required, such as supporting multiple tokens per user.
    - [Tiddle](https://github.com/adamniedzielski/tiddle/):
    Supports multiple tokens per user and its simple to implement.
    - [Devise Token Auth](https://github.com/lynndylanhurley/devise_token_auth):
    Looks a lot more robust than the above. If your project requires
    strong security and a full-featured token auth system, this is probably the
    best choice. However, the project I'm currently working on only requires
    simple token auth for a read-only API where security is not a major concern. I
    wanted to avoid unnecessary complexity, so this gem seemed like overkill.

    ##Prerequisites

    The following instructions assume that the Devise users model is called User.
    If it's not, just replace User for your model name.
    This tutorial assumes the following items:
    - You already have Devise configured to authenticate the web views of your
    Rails app. You want to use tokens to authenticate API (JSON) requests in
    the same Rails app.


    ## Installation and configuration

    1. Add to the Gemfile:
    `gem 'tiddle'`
    @@ -13,7 +43,7 @@ If it's not, just replace User for your model name.
    1. Generate the migration:
    `rails g model AuthenticationToken body:string user:references last_used_at:datetime ip_address:string user_agent:string`

    2. Edit the User model so it looks similar to:
    2. Edit `app/models/user.rb` , so it looks similar to:

    ````
    # app/models/user.rb
    @@ -25,88 +55,106 @@ If it's not, just replace User for your model name.
    end
    ````
    Note that we have:
    - Included `has_many :authentication_tokens`
    - Included `:token_authenticatable` in the list of devise modules
    - Added `has_many :authentication_tokens`
    - Added `:token_authenticatable` in the list of devise modules
    User is the default name for the model created by Devise to store it's users.
    You may have named it differently when installing Devise.
    4. Customize the Sessions Controller to handle token authentication for your API:
    4. Customize the Sessions Controller to handle token authentication for the API:
    1. Generate custom controllers:
    `rails generate devise:controllers User`
    Note that Devise controllers are contained inside the Devise gem by default. The above command will generate a set of Devise controller files in app/controllers/users so we can customize them.
    Note that Devise controllers are contained inside the Devise gem by
    default. The above command will generate a set of Devise controller
    files in `app/controllers/users/` so we can customize them.
    2. Edit `app/controllers/users/sessions_controller.rb` so it looks like this:
    2. Edit `app/controllers/users/sessions_controller.rb`, so it looks like this:
    ````
    module Api
    module V1
    class Users::SessionsController < Devise::SessionsController
    # API token authentication with the (Tiddle gem)
    # See ttps://github.com/adamniedzielski/tiddle/
    skip_before_action :verify_signed_out_user
    def create
    user = warden.authenticate!(auth_options)
    user = warden.authenticate!(:scope => :user)
    token = Tiddle.create_and_return_token(user, request)
    render json: { authentication_token: token }
    end
    def destroy
    Tiddle.expire_token(current_user, request) if current_user
    render json: {}
    if current_user && Tiddle.expire_token(current_user, request)
    head :ok
    else
    # Client tried to expire an invalid token
    render json: { error: 'invalid token' }, status: 401
    end
    end
    private
    # this is invoked before destroy and we have to override it
    def verify_signed_out_user
    end
    end
    end
    end
    ````
    Our custom controller will have specific routes, which will only be used for
    JSON (API) requests. The default routes created by `devise_for` will point
    to Devise's original controller, which will respond to HTML requests.
    JSON (API) requests. The default routes created by `devise_for` will remain
    unaltered, pointing to Devise's original session controller, which will
    keep being used by HTML requests (web view authentication).
    5. Create a route to our custom controller by including the following lines in
    5. Create routes to our custom controller by including the following lines in
    `config/routes.rb`:
    ````
    # Devise routes for API clients (custom sessions controller)
    devise_scope :user do
    post 'api/v1/sign_in', to: 'users/sessions#create'
    delete 'api/v1/sign_out', to: 'users/sessions#destroy'
    end
    # Devise routes for API clients (custom sessions controller)
    devise_scope :user do
    post 'api/v1/login', to: 'users/sessions#create'
    delete 'api/v1/logout', to: 'users/sessions#destroy'
    end
    # Devise routes for web clients (built-in sessions controller)
    devise_for :users
    # Devise routes for web clients (built-in sessions controller)
    devise_for :users
    ````
    Note that `devise_for :users` must be below our custom routes.
    Note that `devise_for :users` (which should already exist in routes.rb)
    must be below our custom routes.
    6. Include the following code to `app/controllers/application_controller.rb`:
    ````
    # Disable forgery protection for JSON requests (API clients)
    protect_from_forgery unless: -> { request.format.json? }
    # Require authentication for all JSON requests (API clients)
    before_action :authenticate_user!, if:
    lambda { |controller| controller.request.format.json? }
    ````
    6. Restart the rails server and test it with curl:
    6. Include the following lines to `app/controllers/application_controller.rb.`
    ````
    # Get an auth token (send credentials via POST, JSON):
    curl -X POST "http://localhost:3000/api/v1/sign_in.json" \
    -H "Content-Type:application/json" \
    -d '{ "user": { "email": "<email>", "password": "<password>" }}'
    # Prevent CSRF attacks, except for JSON requests (API clients)
    protect_from_forgery unless: -> { request.format.json? }
    # Require authentication and do not set a session cookie for JSON requests (API clients)
    before_action :authenticate_user!, :do_not_set_cookie, if: -> { request.format.json? }
    # Use the token to authenticate a request
    curl -X GET "http://127.0.0.1:3000/masters.json" \
    -H "Content-type: application/json" \
    -H "X-User-Email: <email>" \
    -H "X-User-Token: <token>"
    private
    #Sign-out (destroy the auth token):
    curl -X DELETE "http://localhost:3000/api/v1/sign_out.json" \
    -H "Content-Type:application/json" \
    -H "X-User-Email: <email>" \
    -H "X-User-Token: <token>"
    ````
    # Do not generate a session or session ID cookie
    # See https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb#L171
    def do_not_set_cookie
    request.session_options[:skip] = true
    end
    ````
    Make sure the `private` keyword (which is actually a method) and the
    `do_not_set_cookie` method definition are placed at the bottom of the
    controller to avoid making other (unrelated) methods private.
    ## Testing it with curl
    ````
    # Get an auth token (send credentials in JSON format):
    curl -X POST "http://localhost:3000/api/v1/login.json" \
    -H "Content-Type:application/json" \
    -d '{ "user": { "email": "<email>", "password": "<password>" }}'

    # Use the token to authenticate a request (send credentials as headers)
    curl -X GET "http://localhost:3000/api/v1/<resource name>.json" \
    -H "Content-type: application/json" \
    -H "X-User-Email: <email>" \
    -H "X-User-Token: <token>"

    # Log out (destroy the auth token):
    curl -X DELETE "http://localhost:3000/api/v1/sign_out.json" \
    -H "Content-Type:application/json" \
    -H "X-User-Email: <email>" \
    -H "X-User-Token: <token>"
    ````
  3. @brunofacca brunofacca created this gist Oct 7, 2016.
    112 changes: 112 additions & 0 deletions rails_api_token_auth_with_tiddle.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,112 @@
    This gist attempts to explain how to implement token authentication for APIs
    in Rails apps which also have web views, using the [Tiddle gem](https://github.com/adamniedzielski/tiddle).

    The following instructions assume that the Devise users model is called User.
    If it's not, just replace User for your model name.

    1. Add to the Gemfile:
    `gem 'tiddle'`

    2. Run `bundle install`

    3. Set up a model to store the authentication tokens:
    1. Generate the migration:
    `rails g model AuthenticationToken body:string user:references last_used_at:datetime ip_address:string user_agent:string`

    2. Edit the User model so it looks similar to:

    ````
    # app/models/user.rb
    class User < ActiveRecord::Base
    has_many :authentication_tokens
    devise :database_authenticatable, :registerable, :recoverable,
    :rememberable, :trackable, :validatable, :token_authenticatable
    end
    ````
    Note that we have:
    - Included `has_many :authentication_tokens`
    - Included `:token_authenticatable` in the list of devise modules
    4. Customize the Sessions Controller to handle token authentication for your API:
    1. Generate custom controllers:
    `rails generate devise:controllers User`
    Note that Devise controllers are contained inside the Devise gem by default. The above command will generate a set of Devise controller files in app/controllers/users so we can customize them.
    2. Edit `app/controllers/users/sessions_controller.rb` so it looks like this:
    ````
    module Api
    module V1
    class Users::SessionsController < Devise::SessionsController
    # API token authentication with the (Tiddle gem)
    # See ttps://github.com/adamniedzielski/tiddle/
    def create
    user = warden.authenticate!(auth_options)
    token = Tiddle.create_and_return_token(user, request)
    render json: { authentication_token: token }
    end
    def destroy
    Tiddle.expire_token(current_user, request) if current_user
    render json: {}
    end
    private
    # this is invoked before destroy and we have to override it
    def verify_signed_out_user
    end
    end
    end
    end
    ````
    Our custom controller will have specific routes, which will only be used for
    JSON (API) requests. The default routes created by `devise_for` will point
    to Devise's original controller, which will respond to HTML requests.
    5. Create a route to our custom controller by including the following lines in
    `config/routes.rb`:
    ````
    # Devise routes for API clients (custom sessions controller)
    devise_scope :user do
    post 'api/v1/sign_in', to: 'users/sessions#create'
    delete 'api/v1/sign_out', to: 'users/sessions#destroy'
    end
    # Devise routes for web clients (built-in sessions controller)
    devise_for :users
    ````
    Note that `devise_for :users` must be below our custom routes.
    6. Include the following code to `app/controllers/application_controller.rb`:
    ````
    # Disable forgery protection for JSON requests (API clients)
    protect_from_forgery unless: -> { request.format.json? }
    # Require authentication for all JSON requests (API clients)
    before_action :authenticate_user!, if:
    lambda { |controller| controller.request.format.json? }
    ````
    6. Restart the rails server and test it with curl:
    ````
    # Get an auth token (send credentials via POST, JSON):
    curl -X POST "http://localhost:3000/api/v1/sign_in.json" \
    -H "Content-Type:application/json" \
    -d '{ "user": { "email": "<email>", "password": "<password>" }}'
    # Use the token to authenticate a request
    curl -X GET "http://127.0.0.1:3000/masters.json" \
    -H "Content-type: application/json" \
    -H "X-User-Email: <email>" \
    -H "X-User-Token: <token>"
    #Sign-out (destroy the auth token):
    curl -X DELETE "http://localhost:3000/api/v1/sign_out.json" \
    -H "Content-Type:application/json" \
    -H "X-User-Email: <email>" \
    -H "X-User-Token: <token>"
    ````