Skip to content

Latest commit

 

History

History
412 lines (312 loc) · 12.9 KB

README.md

File metadata and controls

412 lines (312 loc) · 12.9 KB

Simple OAuth2 Logo

Build Status Gem Version Dependency Status Coverage Status Code Climate Inline docs security License git.legal

A flexible OAuth2 (RFC 6749) server authorization and endpoints protection to your API with any ORM.

Goal

The goal of this gem is provide a simple OAuth2 provider for different frameworks for example like Rails, Grape, Sinatra and e.t.c. Also this gem makes it easy to introduce OAuth2 provider functionality to your application.

Table of Contents

Installation

Add this line to your application's Gemfile:

gem 'simple_oauth2'

And then execute:

$ bundle

Or install it yourself as:

$ gem install simple_oauth2

Usage

The example below, it's a simple Rack application.

  1. You need to choose the mixin for your ORM and setup it. Please read the documentation about mixin. The list of mixins you can take a look at here.

If you want to use this gem, but you can't find the mixin that you need, so then you have to create at least 4 classes (models) to cover OAuth2 roles and define a specific set to API for them as described HERE.

You can take a look at the mixins to understand what they are doing and what they are returning.

  1. Important. You need to create a file and put it in some place, that will be processed at the application startup. Also you need to configure SimpleOAuth2 in order to provide authentication block.
  # config/initializers/simple_oauth2_config.rb

  Simple::OAuth2.configure do |config|
    config.resource_owner_authenticator do |request|
      User.find_by_id(request.params['user_id'])
    end
  end
  1. Include Simple::OAuth2 helpers.
  # config/application.rb

  load File.expand_path('initializers/simple_oauth2_config.rb', __FILE__)

  include Simple::OAuth2::Helpers

  module Application
  end
  1. Inject token authentication middleware into config.ru.
  # config.ru

  require 'config/application'

  use Simple::OAuth2.middleware
  1. And you need to define for example Token, Authorization and RevokeToken endpoints.
  # config/application.rb

  load File.expand_path('initializers/simple_oauth2_config.rb', __FILE__)

  include Simple::OAuth2::Helpers

  module Application
    class Authorization
      def call(env)
        response = Simple::OAuth2::Generators::Authorization.generate_for(env)

        status = response.status
        headers = response.headers
        body = JSON.generate(response.body)

        [status, headers, [body]]
      end
    end

    class Token
      def call(env)
        response = Simple::OAuth2::Generators::Token.generate_for(env)

        status = response.status
        headers = response.headers
        body = JSON.generate(response.body)

        [status, headers, [body]]
      end
    end

    class RevokeToken
      def call(env)
        params = Rack::Request.new(env).params
        Simple::OAuth2::Generators::Token.revoke(params['token'], env)
      end
    end
  end

Don't forget to define routes for them.

  # config.ru

  require 'config/application'

  use Simple::OAuth2.middleware

  map '/oauth/token' do
    run Application::Token
  end

  map '/oauth/revoke_token' do
    run Application::RevokeToken
  end

  map '/oauth/authorization' do
    run Application::Authorization
  end

That's all! Now we can run our app with bundle exec command.

  bundle exec rackup config.ru

Use the next available routes.

  POST /oauth/token
  POST /oauth/revoke_token
  POST /oauth/authorization

Helpers

Use the next available helpers.

  access_token_required! :read, :write
  current_access_token
  current_resource_owner

We can protect our endpoints with access_token_required! method. For example:

  # config/application.rb

  # ...

  module Application
    # ...

    class Images
      def call(env)
        # public resource, no scopes required
        access_token_required!
        # or, requires 'write', 'read' scopes to exist in AccessToken
        access_token_required! :write, :read

        [200, { 'content-type' => 'application/json' }, [Image.all.to_json]]
      end
    end
  end

This gem has ability to get ResourceOwner from the AccessToken found by access_token value passed with the request. Also we can get AccessToken instance found by access_token value passed with the request. For example:

  class Images
    # ...

    def call(env)
      images = current_access_token.resource_owner.images
      # or
      images = current_resource_owner.images

      [200, { 'content-type' => 'application/json' }, [images.to_json]]
    end
  end

Custom authentication endpoints

You can create your own API endpoints for OAuth2 authentication and use simple_oauth2 gem functionality. In that case you will get a full control over the authentication proccess and can do anything in it.

  # For Token endpoint
  #
  class Token
    def call(env)
      response = Simple::OAuth2::Generators::Token.generate_for(env) do |request, response|
        # You can use default authentication if you don't need to change this part:
        # client = Simple::OAuth2::Strategies::Base.authenticate_client(request)

        # Or write your custom client authentication:
        client = Application.find_by(key: request.client_id, active: true) || request.invalid_client!

        # You can use default Resource Owner authentication if you don't need to change this part:
        # resource_owner = Simple::OAuth2::Strategies::Base.authenticate_resource_owner(client, request)

        # Or define your custom resource owner authentication:
        resource_owner = User.find_by(username: request.username)
        request.invalid_grant! if resource_owner.nil? || resource_owner.inactive?

        # You can create an Access Token as you want:
        token = AccessToken.create(
          client: client,
          resource_owner: resource_owner,
          scope: request.scope
        )

        response.access_token = Simple::OAuth2::Strategies::Base.expose_to_bearer_token(token)
      end

      status = response.status
      headers = response.headers
      body = JSON.generate(response.body)

      [status, headers, [body]]
    end
  end
  # For Authorization endpoint
  #
  class Authorization
    def call(env)
      response = Simple::OAuth2::Generators::Authorization.generate_for(env) do |request, response|
        # ...
      end

      # ...
    end
  end

Custom AccessToken authenticator

If you don't want to use default Simple::OAuth2 AccessToken authenticator then you can define your own in the configuration (it must be a proc or lambda):

  Simple::OAuth2.configure do |config|
    config.token_authenticator do |request|
      AccessToken.find_by(token: request.access_token) || request.invalid_token!
    end
  end

Custom scopes validation

If you want to control the process of scopes validation (for protected endpoints for example) then you must implement your own class that will implement the following API:

  class CustomScopesValidator
    # `scopes' is the set of required scopes that must be present in the AccessToken instance.

    def self.valid?(access_scopes, scopes)
      # custom scopes validation implementation...
    end
  end

And set that class as scopes validator in the Simple::OAuth2 config:

  Simple::OAuth2.configure do |config|
    # ...

    config.scopes_validator_class_name = 'CustomScopesValidator'
  end

Custom token generator

If you want to use JSON Web Tokens as a value for your Access Tokens, than you need to implement your custom Token Generator. First of all add jwt gem to your Gemfile

If will do the main work for us in accordance to RFC 7519 standard. Now define custom token generator class:

  class JWTGenerator
    HMAC_SECRET = '1d62ada3461$a091c38c95c!0388c8a1a2'.freeze

    # `payload` is a model attributes hash (in case of using some ORM)
    #
    def self.generate(payload = {}, options = {})
      JWT.encode(payload, HMAC_SECRET, 'HS256')

      # You can provide custom secrets if you need:
      #   JWT.encode(payload, options[:secret], 'HS256')
      #
      # or skip any encrypting at all:
      #   JWT.encode(payload, nil, 'none')
      #
      # @see https://github.com/jwt/ruby-jwt for more examples
    end
  end

And set it as a token generator class in the Simple::OAuth2 config:

  Simple::OAuth2.configure do |config|
    # ...

    config.token_generator_class_name = 'JWTGenerator'
  end

Process token on Refresh (protect against Replay Attacks)

If you want to do something with the original AccessToken that was used with the RefreshToken Flow, then you need to setup on_refresh configuration option.

By default Simple::OAuth2 gem does nothing on token refresh and that option is set to :nothing. You can set it to the symbol (in that case AccessToken instance must respond to it) or block. Look at the examples:

  Simple::OAuth2.configure do |config|
    # ...

    config.on_refresh = :destroy # will call :destroy method (`refresh_token.destroy`)
  end
  Simple::OAuth2.configure do |config|
    # ...

    config.on_refresh do |refresh_token|
      refresh_token.destroy

      MyAwesomeLogger.info("Token ##{refresh_token.id} was destroyed on refresh!")
    end
  end

[CORS] Cross Origin Resource Sharing

The most common solution for Rack-based applications is to use rack-cors gem. It's a Rack middleware that will set required HTTP headers for you in order to be able to make Cross Domain requests to your application. Add rack-cors to you Gemfile:

  gem 'rack-cors', require: 'rack/cors'

In config.ru of your project configure Rack::Cors as follows:

  # config.ru

  require 'rack/cors'

  # ...

  use Rack::Cors do
    allow do
      origins '*'
      resource '*', headers: :any, methods: [:get, :post, :put, :delete, :options]
    end
  end

You can make any other CORS configuration, please read the gem docs.

List of mixins

Bugs and Feedback

Bug reports and feedback are welcome on GitHub at https://github.com/simple-oauth2/simple_oauth2/issues.

Contributing

  1. Fork the project.
  2. Create your feature branch (git checkout -b my-new-feature).
  3. Implement your feature or bug fix.
  4. Add documentation for your feature or bug fix.
  5. Add tests for your feature or bug fix.
  6. Run rake and rubocop to make sure all tests pass.
  7. Commit your changes (git commit -am 'Add new feature').
  8. Push to the branch (git push origin my-new-feature).
  9. Create new pull request.

Thanks.

License

The gem is available as open source under the terms of the MIT License.

Copyright (c) 2016-2017 Volodimir Partytskyi (volodimir.partytskyi@gmail.com).