API Authentication for user logged in to a Web App server

前端 未结 5 905
忘了有多久
忘了有多久 2020-12-02 19:40

I am building a Web App and a separate API (so that users can share their collected data with someone if they want to) using Ruby on Rails. The users can log in on the web a

5条回答
  •  悲哀的现实
    2020-12-02 20:21

    You could consider the doorkeeper gem for your API authorization. I considered it but decided against it because of complexity and lacking documentation for my use cases. Put simply I couldn't get it working properly.

    There is a good article on authentication using warden without devise which should give you a good feel for the moving parts of an authentication system. Devise is not appropriate for API authentication and in fact Devise recently removed the one thing that could be useful for API's which was token based authentication, obviously API's are not part of their roadmap!

    I used the guidance in the article referenced above to create my own JSON only Warden strategy that uses an OAUTH 2 Owner Password Credentials Grant type (See RFC 6749) to generate and return a bearer token for use on future API requests. API clients can easily create the JSON to do this kind of authentication to obtain an authorization access token.

    I will provide some of the Rails code to get you started below, but you will have to integrate into your specific environment. No warranty offered :)

    Warden initializer:

    # config/initializers/warden.rb
    Dir["./app/strategies/warden/*.rb"].each { |file| require file }
    
    Rails.application.config.middleware.insert_after ActionDispatch::ParamsParser, Warden::Manager do |manager|
      manager.default_strategies :null_auth, :oauth_access_token, :oauth_owner_password
      manager.failure_app = UnauthorizedController
    end
    

    Warden strategy for OAUTH 2 password authentication:

    # app/strategies/warden/oauth_owner_password_strategy.rb
    module Warden
      class OauthOwnerPasswordStrategy < Strategies::Base
        def valid?
          return false if request.get?
    
          params['grant_type'] == 'password' && params['client_id'] == 'web' && ! params['username'].blank?
        end
    
        def authenticate!
          user = User.with_login(params['username']).first
          if user.nil? || user.confirmed_at.nil? || ! user.authenticate!(params['password'])
            # delay failures for up to 20ms to thwart timing based attacks
            sleep(SecureRandom.random_number(20) / 1000.0)
            fail! :message => 'strategies.password.failed'
          else
            success! user, store: false
          end
    
          # ADD HERE: log IP and timestamp of all authentication attempts
        end
      end
    
      Strategies.add(:oauth_owner_password, OauthOwnerPasswordStrategy)
    end
    

    Warden strategy for OAUTH 2 access token authentication:

    # app/strategies/warden/oauth_access_token_strategy.rb
    module Warden
      class OauthAccessTokenStrategy < Strategies::Base
        def valid?
          # must be a bearer token
          return false unless auth_header = request.headers['authorization']
          auth_header.split(' ')[0] == 'Bearer'
        end
    
        def authenticate!
          # Use a periodic cleaner instead
          # clean out all old tokens. DOES NOT RUN CALLBACKS!
          Token.expired.delete
    
          # lookup bearer token
          token = Token.active.first(purpose: 'access', token: request.headers['authorization'].split(' ')[1])
          if token && (user = token.user) && user.confirmed_at
            success! user, store: false
          else
            # delay failures for up to 20ms to thwart timing based attacks
            sleep(SecureRandom.random_number(20) / 1000.0)
            fail! message: 'strategies.oauth_access_token.failed'
          end
        end
      end
    
      Strategies.add(:oauth_access_token, OauthAccessTokenStrategy)
    end
    

    Null authentication strategy (can be useful in development, just set config.null_auth_user within config/environments/development.rb):

    # app/strategies/warden/null_auth_strategy.rb
    module Warden
      class NullAuthStrategy < Strategies::Base
        def valid?
          ! Rails.configuration.null_auth_user.blank?
        end
    
        def authenticate!
          user = User.with_login(params["username"]||Rails.configuration.null_auth_user).first
          if user.nil?
            fail! :message => "strategies.password.failed"
          else
            success! user, store: false
          end
        end
      end
    
      Strategies.add(:null_auth, NullAuthStrategy)
    end
    

    Warden failure application for JSON clients (uses a bare metal rails controller):

    # app/controllers/unauthorized_controller.rb
    class UnauthorizedController < ActionController::Metal
    
      def self.call(env)
        @respond ||= action(:respond)
        @respond.call(env)
      end
    
      def respond(env)
        self.status = 401
        self.content_type = 'json'
        self.response_body = { 'errors' => ['Authentication failure']}.to_json
      end
    end
    

    Add the following in your base API controller:

    before_filter :authenticate!
    
    protected
    
        helper_method :warden, :signed_in?, :current_user
    
        def warden
          request.env['warden']
        end
    
        def signed_in?
          !current_user.nil?
        end
    
        def current_user
          @current_user ||= warden.user
        end
    
        def authenticate!(*args)
          warden.authenticate!(*args)
          # ADD ANY POST AUTHENTICATION SETUP CODE HERE
        end
    

    A sessions controller:

    class SessionsController < ApiController
      skip_before_filter :authenticate!
    
      # TODO exceptions and errors should return unauthorized HTTP response.
      # see RFC for details
    
      def create
        # mandate the password strategy.
        # don't use session store (don't want session cookies on APIs)
        authenticate!(scope: :oauth_owner_password, store: false)
    
        if signed_in?
          # create access token
          token = Token.create! purpose: 'access',
                                user: current_user,
                                expires_in: Rails.configuration.session_lifetime
    
           # Ensure response is never cached
           response.headers["Cache-Control"] = "no-store"
           response.headers["Pragma"] = "no-cache"
           response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
    
          # send the OAuth response
          render json: {
              access_token: token.token,
              token_type: 'Bearer',
              expires_in: token.expires_in,
              scope: 'user'
          }
        end
      end
    
      def destroy
        Token.current.delete
        warden.logout
        head :no_content
      end
    end
    

    You will need to define your own User and Token models for tracking users and bearer tokens respectively, the Token model needs to have a scope called active to limit the result set to unexpired tokens. Token generation should use SecureRandom.urlsafe_base64

提交回复
热议问题