user2176575 user2176575 - 5 months ago 65
AngularJS Question

How to setup angular-rails 4.2 - devise registration/authentication?

Can you give an advice or recommend some resources related to this topic? I understand how to it in a theory. But I also heard about jwt etc. What are the best practices to implement device/angular/rails role based auth/registration?

Answer

The short answer is to read this blog post which goes into details of how the concept is minimally implemented


This would be a long code answer, but I plan to write separate blog post on how to implement it in much more details...

but for now, here is how I implemented it in some project...

First the angular app part, you can use something like Satellizer which plays nicely...

here is the angular auth module in the front-end app

# coffeescript
config = (
  $authProvider
  $stateProvider
) ->
  $authProvider.httpInterceptor = true # to automatically add the headers for auth
  $authProvider.baseUrl = "http://path.to.your.api/"
  $authProvider.loginRedirect = '/profile' # front-end route after login
  $authProvider.logoutRedirect = '/' # front-end route after logout
  $authProvider.signupRedirect = '/sign_in' 
  $authProvider.loginUrl = '/auth/sign_in' # api route for sign_in
  $authProvider.signupUrl = '/auth/sign_up' # api route for sign_up
  $authProvider.loginRoute = 'sign_in' # front-end route for login
  $authProvider.signupRoute = 'sign_up' # front-end route for sign_up
  $authProvider.signoutRoute = 'sign_out' # front-end route for sign_out
  $authProvider.tokenRoot = 'data' 
  $authProvider.tokenName = 'token'
  $authProvider.tokenPrefix = 'front-end-prefix-in-localstorage'
  $authProvider.authHeader = 'Authorization'
  $authProvider.authToken = 'Bearer'
  $authProvider.storage = 'localStorage'

  # state configurations for the routes
  $stateProvider
    .state 'auth',
      url: '/'
      abstract: true
      templateUrl: 'modules/auth/auth.html'
      data:
        permissions:
          only: ['guest']
          redirectTo: 'profile'


    .state 'auth.sign_up',
      url: $authProvider.signupRoute
      views:
        'sign_up@auth':
          templateUrl: 'modules/auth/sign_up.html'
          controller: 'AuthenticationCtrl'
          controllerAs: 'vm'

    .state 'auth.sign_in',
      url: $authProvider.loginRoute
      views:
        'sign_in@auth':
          templateUrl: 'modules/auth/sign_in.html'
          controller: 'AuthenticationCtrl'
          controllerAs: 'vm'

this is the basic configurations for satellizer... as for the authentication controller... it's something like following

  @signIn = (email, password, remember_me) ->
    $auth.login
      email: email
      password: password
      remember_me: remember_me
    .then(success, error)
    return

  @signUp = (name, email, password) ->
    $auth.signup
      name: name
      email: email
      password: password
    .then(success, error)
    return

this is the basics for authenticating


as for the backend (RoR API) you should first allow CORS for the front-end app. and add gem 'jwt' to your gemfile.

second implement the API controller and the authentication controller

for example it might look something like the following

class Api::V1::ApiController < ApplicationController
  # The API responds only to JSON
  respond_to :json
  before_action :authenticate_user!
  protected

  def authenticate_user!
    http_authorization_header?
    authenticate_request
    set_current_user
  end

  # Bad Request if http authorization header missing
  def http_authorization_header?
    fail BadRequestError, 'errors.auth.missing_header' unless authorization_header
    true
  end

  def authenticate_request
    decoded_token ||= AuthenticationToken.decode(authorization_header)
    @auth_token   ||= AuthenticationToken.where(id: decoded_token['id']).
      first unless decoded_token.nil?

    fail UnauthorizedError, 'errors.auth.invalid_token' if @auth_token.nil?
  end

  def set_current_user
    @current_user ||= @auth_token.user
  end

  # JWT's are stored in the Authorization header using this format:
  # Bearer some_random_string.encoded_payload.another_random_string
  def authorization_header
    return @authorization_header if defined? @authorization_header

    @authorization_header =
      begin
        if request.headers['Authorization'].present?
          request.headers['Authorization'].split(' ').last
        else
          nil
        end
      end
  end
end


class Api::V1::AuthenticationsController < Api::V1::ApiController
  skip_before_action :authenticate_user!, only: [:sign_up, :sign_in]

  def sign_in
    # getting the current user from sign in request
    @current_user ||= User.find_by_credentials(auth_params)
    fail UnauthorizedError, 'errors.auth.invalid_credentials' unless @current_user

    generate_auth_token(auth_params)

    render :authentication, status: 201
  end

  def sign_out
    # this auth token is assigned via api controller from headers
    @auth_token.destroy!
    head status: 204
  end
  def generate_auth_token(params)
    @auth_token = AuthenticationToken.generate(@current_user, params[:remember_me])
  end
end

The AuthenticationToken is a model used to keep track of the JWT tokens ( for session management like facebook)

here is the implementation for the AuthenticationToken model

class AuthenticationToken < ActiveRecord::Base
  ## Relations
  belongs_to :user

  ## JWT wrappers
  def self.encode(payload)
    AuthToken.encode(payload)
  end

  def self.decode(token)
    AuthToken.decode(token)
  end

  # generate and save new authentication token for the user
  def self.generate(user, remember_me = false)
    @auth_token = user.authentication_tokens.create
    @auth_token.token = AuthToken.generate(@auth_token.id, remember_me)
    @auth_token.save!
    @auth_token
  end

  # check if a token can be used or not
  # used by background job to clear the authentication collection
  def expired?
    AuthToken.decode(token).nil?
  end
end

it uses a wrapper called AuthToken which wraps the JWT functionality here is it's implementation

# wrapper around JWT to encapsulate it's code
# and exception handling and don't polute the AuthenticationToken model
class AuthToken
  def self.encode(payload)
    JWT.encode(payload, Rails.application.secrets.secret_key_base)
  end

  def self.decode(token)
    payload = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
  rescue JWT::ExpiredSignature
    # It will raise an error if it is not a token that was generated
    # with our secret key or if the user changes the contents of the payload
    Rails.logger.info "Expired Token"
    nil
  rescue
    Rails.logger.warn "Invalid Token"
    nil
  end

  def self.generate(token_id, remember_me = false)
    exp = remember_me ? 6.months.from_now : 6.hours.from_now
    payload = { id: token_id.to_s, exp: exp.to_i }
    self.encode(payload)
  end
end