This gem adds a flexible OAuth2 (RFC 6749) server authorization and endpoints protection to your Grape API project with any ORM / ODM / PORO.
Implemented features (flows):
- Resource Owner Password Credentials
- Client Credentials
- Refresh token
- Token revocation
- Access Token Scopes
Supported token types:
- Bearer
In progress (check Contributing section if yoy want to help with it):
- Authorization Code Flow (partially implemented)
- Access Grants
- Implicit Grant
Please check the documentation for the version of Grape::OAuth2
you are using in:
https://github.com/nbulaj/grape_oauth2/releases
- See the Wiki
- Installation
- Configuration
- Usage examples
- Custom Access Token authenticator
- Custom scopes validation
- Custom token generator
- Process token on Refresh (protect against Replay Attacks)
- Errors (exceptions) handling
- Example App
- Contributing
- License
Grape::OAuth2 gem requires only Grape
and Rack::OAuth2
gems as the dependency.
Yes, no Rails, ActiveRecord or any other libs or huge frameworks 👍
If you are using bundler, first add 'grape_oauth2'
to your Gemfile:
gem 'grape', '~> 1.0'
gem 'grape_oauth2', '~> 0.2'
# or
gem 'grape_oauth2', git: 'https://github.com/nbulaj/grape_oauth2.git'
And run:
bundle install
If you running your Grape API with rackup
and using the gem from git source, then
you need to explicitly require bundler in the config.ru
:
require 'bundler/setup'
Bundler.setup
or run your app with bundle exec command:
> bundle exec rackup config.ru
[2016-11-19 02:35:33] INFO WEBrick 1.3.1
[2016-11-19 02:35:33] INFO ruby 2.3.1 (2016-04-26) [i386-mingw32]
[2016-11-19 02:35:33] INFO WEBrick::HTTPServer#start: pid=5472 port=9292
Main Grape::OAuth2
configuration must be placed in config/initializers/
(in case you are using Rails)
or in some place, that will be processed at the application startup:
Grape::OAuth2.configure do |config|
# Access Tokens lifetime (expires in)
config.access_token_lifetime = 7200 # in seconds (2.hours for Rails), `nil` if never expires
# Authorization Code lifetime
# config.authorization_code_lifetime = 7200 # in seconds (2.hours for Rails)
# Allowed OAuth2 Authorization Grants (default is %w(password client_credentials)
config.allowed_grant_types = %w(password client_credentials refresh_token)
# Issue access tokens with refresh token (default is false)
config.issue_refresh_token = true
# Process Access Token that was used for the Refresh Token Flow (default is :nothing).
# Could be a symbol (Access Token instance must respond to it)
# or block with refresh token as an argument.
# config.on_refresh = :nothing
# WWW-Authenticate Realm (default is "OAuth 2.0")
# config.realm = 'My API'
# Access Token authenticator block.
# config.token_authenticator do |request|
# AccessToken.authenticate(request.access_token) || request.invalid_token!
# end
# Scopes validator class (default is Grape::OAuth2::Scopes).
# config.scopes_validator_class_name = 'MyCustomValidator'
# Token generator class (default is Grape::OAuth2::UniqueToken).
# Must respond to `self.generate(payload = {}, options = {})`.
# config.token_generator_class_name = 'JWTGenerator'
# Classes for OAuth2 Roles
config.client_class_name = 'Application'
config.access_token_class_name = 'AccessToken'
config.resource_owner_class_name = 'User'
end
Currently implemented (partly on completely) grant types: password, client_credentials, refresh_token.
As you know, OAuth2 workflow implies the existence of the next three roles: Access Token, Client and Resource Owner.
So your project must include 3 classes (models) - AccessToken, Application and User for example. The gem needs to know
what classes it work, so you need to create them and configure Grape::OAuth2
.
resource_owner_class
must have a self.oauth_authenticate(client, username, password)
method, that returns an instance of the
class if authentication successful (username
and password
matches for example) and false
or nil
in other cases.
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
def self.oauth_authenticate(_client, username, password)
# find the user by it username
user = find_by(username: username)
return if user.nil?
# check the password
user.authenticate(password)
end
end
client_class
, access_token_class
and resource_owner_class
objects must contain a specific set of API (methods), that are
called by the gem. Grape::OAuth2
includes predefined mixins for the projects that use the ActiveRecord
or Sequel
ORMs,
and you can just include them into your models.
# app/models/access_token.rb
class AccessToken < ApplicationRecord
include Grape::OAuth2::ActiveRecord::AccessToken
end
# app/models/application.rb
class Application < ApplicationRecord
include Grape::OAuth2::ActiveRecord::Client
end
Migration for the simplest use case of the gem looks as follows:
ActiveRecord::Schema.define(version: 3) do
# All the columns are custom
create_table :users do |t|
t.string :name
t.string :username
t.string :password_digest
end
# Required columns: :key & :secret
create_table :applications do |t|
t.string :name
t.string :key
t.string :secret
t.timestamps null: false
end
add_index :applications, :key, unique: true
# Required columns: :client_id, :resource_owner_id, :token, :expires_at, :revoked_at, :refresh_token
create_table :access_tokens do |t|
t.integer :resource_owner_id
t.integer :client_id
t.string :token, null: false
t.string :refresh_token
t.string :scopes, default: ''
t.datetime :expires_at
t.datetime :revoked_at
t.datetime :created_at, null: false
end
add_index :access_tokens, :token, unique: true
add_index :access_tokens, :resource_owner_id
add_index :access_tokens, :client_id
add_index :access_tokens, :refresh_token, unique: true
end
# app/models/access_token.rb
class AccessToken < Sequel::Model
include Grape::OAuth2::Sequel::AccessToken
end
# app/models/application.rb
class Application < Sequel::Model
include Grape::OAuth2::Sequel::Client
end
Migration for the simplest use case of the gem looks as follows:
DB.create_table :applications do
primary_key :id
column :name, String, size: 255, null: false
column :key, String, size: 255, null: false, index: { unique: true }
column :secret, String, size: 255, null: false
column :redirect_uri, String
column :created_at, DateTime
column :updated_at, DateTime
end
DB.create_table :access_tokens do
primary_key :id
column :client_id, Integer
column :resource_owner_id, Integer, index: true
column :token, String, size: 255, null: false, index: { unique: true }
column :refresh_token, String, size: 255, index: { unique: true }
column :expires_at, DateTime
column :revoked_at, DateTime
column :created_at, DateTime, null: false
column :scopes, String, size: 255
end
DB.create_table :users do
primary_key :id
column :name, String, size: 255
column :username, String, size: 255
column :created_at, DateTime
column :updated_at, DateTime
column :password_digest, String, size: 255
end
# app/models/access_token.rb
class AccessToken
include Grape::OAuth2::Mongoid::AccessToken
end
# app/models/application.rb
class Application
include Grape::OAuth2::Mongoid::Client
end
# app/models/user.rb
class User
include Mongoid::Document
include Mongoid::Timestamps
field :username, type: String
field :password, type: String
def self.oauth_authenticate(_client, username, password)
find_by(username: username, password: password)
end
end
If you want to use Grape::OAuth2
gem, but your project doesn't use ActiveRecord
, Sequel
or Mongoid
, then you can
create at least 3 classes (models) to cover OAuth2 roles and define a specific set ot API for them as described below.
Class that represents an OAuth2 Client should contain the following API:
class Client
# ...
def self.authenticate(key, secret = nil)
# Should return a Client instance matching the
# key & secret provided (`secret` is optional).
end
end
For the class that represents an OAuth2 Access Token you must define the next API:
class AccessToken
# ...
def self.create_for(client, resource_owner, scopes = nil)
# Creates the record in the database for the provided client and
# resource owner with specific scopes (if present).
# Returns an instance of that record.
end
def self.authenticate(token, type: :access_token)
# Returns an Access Token instance matching the token provided.
# Access Token can be searched by token or refresh token value. In the
# first case :type option must be set to :access_token (default), in
# the second case - to the :refresh_token.
# Note that you MAY include expired access tokens in the result
# of this method so long as you implement an instance `#expired?`
# method.
end
def client
# Returns associated Client instance. Always must be present!
# For ORM objects it can be an association (`belongs_to :client` for ActiveRecord).
end
def resource_owner
# Returns associated Resource Owner instance.
# Can return `nil` (for Client Credentials flow as an example).
# For ORM objects it can be an association (`belongs_to :resource_owner` for ActiveRecord).
end
def scopes
# Returns Access Token authorised set of scopes. Can be a space-separated String,
# Array or any object, that responds to `to_a`.
end
def expired?
# true if the Access Token has reached its expiration.
end
def revoked?
# true if the Access Token was revoked.
end
def revoke!(revoked_at = Time.now)
# Revokes an Access Token (by setting its :revoked_at attribute to the
# specific time for example).
end
def to_bearer_token
# Returns a Hash of Bearer token attributes like the following:
# access_token: '', # - required
# refresh_token: '', # - optional
# token_type: 'bearer', # - required
# expires_in: '', # - required
# scope: '' # - optional
end
end
You can take a look at the Grape::OAuth2 mixins to understand what they are doing and what they are returning.
As was said before, Resource Owner class (User
model for example) must contain only one class method
(only for Password Authorization Grant): self.oauth_authenticate(client, username, password)
.
class User
# ...
def self.oauth_authenticate(client, username, password)
# Returns an instance of the User class with matching username
# and password. If there is no such User or password doesn't match
# then returns nil.
end
end
If you need a common OAuth2 authentication then you can use default gem endpoints for it. First of all you
will need to configure Grape::OAuth2 as described above (create models, migrations, configure the gem).
For ActiveRecord
it would be as follows:
# app/models/access_token.rb
class AccessToken < ApplicationRecord
include Grape::OAuth2::ActiveRecord::AccessToken
end
# app/models/application.rb
class Application < ApplicationRecord
include Grape::OAuth2::ActiveRecord::Client
end
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
# Don't forget to setup this method for your Resource Owner model!
def self.oauth_authenticate(_client, username, password)
user = find_by(username: username)
return if user.nil?
user.authenticate(password)
end
end
# config/oauth2.rb
Grape::OAuth2.configure do |config|
# Classes for OAuth2 Roles
config.client_class_name = 'Application'
config.access_token_class_name = 'AccessToken'
config.resource_owner_class_name = 'User'
end
And just inject Grape::OAuth2
into your main API class:
# app/twitter.rb
module Twitter
class API < Grape::API
version 'v1', using: :path
format :json
prefix :api
# Mount all endpoints by default.
# You can define a custom one you want to use by providing them
# as an argument:
# include Grape::OAuth2.api :token, :authorize
#
include Grape::OAuth2.api
# mount any other endpoints
# ...
end
end
The include Grape::OAuth2.api
could be replaced with the next (as it does the same):
# app/twitter.rb
module Twitter
class API < Grape::API
version 'v1', using: :path
format :json
prefix :api
# Add OAuth2 helpers
helpers Grape::OAuth2::Helpers::AccessTokenHelpers
# Inject token authentication middleware
use *Grape::OAuth2.middleware
# Mount default Grape::OAuth2 Token endpoint
mount Grape::OAuth2::Endpoints::Token
# Mount default Grape::OAuth2 Authorization endpoint
mount Grape::OAuth2::Endpoints::Authorize
# mount any other endpoints
# ...
end
end
And that is all! Use the next available routes to get the Access Token:
POST /oauth/token
POST /oauth/revoke
Now you can protect your endpoints with access_token_required!
method:
module Twitter
module Endpoints
class Status < Grape::API
resources :status do
get do
# public resource, no scopes required
access_token_required!
present(:status, current_user.status)
end
post do
# requires 'write' scope to exist in Access Token
access_token_required! :write
status = current_user.statuses.create!(body: 'Hi man!')
present(:status, status, with: V1::Entities::Status)
end
end
end
end
end
If you need to protect all the routes in the endpoint, but it's requires different scopes, than you can
add access_token_required!
helper to the before
filter and setup required scopes directly for the endpoints:
module Twitter
module Endpoints
class Status < Grape::API
before do
access_token_required!
end
resources :status do
# public endpoint - no scopes required
get do
present(:status, current_user.status)
end
# private endpoint - requires :write scope
put ':id', scopes: [:write] do
status = current_user.statuses.create!(body: 'Hi man!')
present(:status, status, with: V1::Entities::Status)
end
end
end
end
end
If you need to do some special things (check if key
starts with 'MyAPI' word for example) and don't want to
write your own authentication endpoints, then you can just override default authentication methods in models
(only if you are using gem mixins, in other cases you MUST write them by yourself):
# app/models/application.rb
class Application < ApplicationRecord
include Grape::OAuth2::ActiveRecord::Client
class << self
def self.authenticate(key, secret = nil)
# My custom condition for successful authentication
return nil unless key.start_with?('MyAPI')
if secret.present?
find_by(key: key, secret: secret)
else
find_by(key: key)
end
end
end
end
Besides, you can create your own API endpoints for OAuth2 authentication and use grape_oauth2
gem functionality.
In that case you will get a full control over the authentication proccess and can do anything in it. Just create
a common Grape API class, set optional OAuth2 params and process the request with the Grape::OAuth2::Generators::Token
generator for example (for issuing an access token):
# api/oauth2.rb
module MyAPI
class OAuth2 < Grape::API
helpers Grape::OAuth2::Helpers::OAuthParams
namespace :oauth do
params do
use :oauth_token_params
end
post :token do
token_response = Grape::OAuth2::Generators::Token.generate_for(env) do |request, response|
# You can use default authentication if you don't need to change this part:
# client = Grape::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! unless client
# You can use default Resource Owner authentication if you don't need to change this part:
# resource_owner = Grape::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 = MyAwesomeAccessToken.create(client: client,
resource_owner: resource_owner,
scope: request.scope)
response.access_token = Grape::OAuth2::Strategies::Base.expose_to_bearer_token(token)
end
# If request is successful, then return it
status token_response.status
token_response.headers.each do |key, value|
header key, value
end
body token_response.access_token
end
desc 'OAuth 2.0 Token Revocation'
params do
use :oauth_token_revocation_params
end
post :revoke do
# ...
end
end
end
end
If you don't want to use default Grape::OAuth2
Access Token authenticator then you can define your own in the
configuration (it must be a proc
or lambda
):
Grape::OAuth2.configure do |config|
config.token_authenticator do |request|
AccessToken.find_by(token: request.access_token) || request.invalid_token!
end
# or config.token_authenticator = lambda { |request| ... }
end
Don't forget to add the middleware to your root API class (use *Grape::OAuth2.middleware
, see below).
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 Access Token instance.
def initialize(scopes)
@scopes = scopes || []
# ...some custom processing of scopes if required ...
end
def valid_for?(access_token)
# custom scopes validation implementation...
end
end
And set that class as scopes validator in the Grape::OAuth2 config:
Grape::OAuth2.configure do |config|
# ...
config.scopes_validator_class_name = 'CustomScopesValidator'
end
If you want to generate your own tokens for Access Tokens and Authorization Codes then you need to write your own generator:
class SomeTokenGenerator
# @param payload [Hash]
# Access Token payload (attributes before creation for example)
#
# @param options [Hash]
# Options for Generator
#
def self.generate(payload = {}, options = {})
# Returns a generated token string.
end
end
And set it as a token generator class in the Grape::OAuth2 config:
Grape::OAuth2.configure do |config|
# ...
config.token_generator_class_name = 'SomeTokenGenerator'
end
If you want to do something with the original Access Token that was used with the Refresh Token Flow, then you need to
setup on_refresh
configuration option. By default Grape::OAuth2
gem does nothing on token refresh and that
option is set to :nothing
. You can set it to the symbol (in that case Access Token
instance must respond to it)
or block. Look at the examples:
Grape::OAuth2.configure do |config|
# ...
config.on_refresh = :destroy # will call :destroy method (`refresh_token.destroy`)
end
Grape::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
You can add any exception class from the rack-oauth2
gem (like Rack::OAuth2::Server::Resource::Bearer::Unauthorized
)
to the rescue_from
if you need to return some special response.
Example:
module Twitter
class API < Grape::API
include Grape::OAuth2.api
# ...
rescue_from Rack::OAuth2::Server::Authorize::BadRequest do |e|
error!({ status: e.status, description: e.description, error: e.error}, 400)
end
end
end
Do not forget to meet the OAuth 2.0 specification.
If you want to see the gem in action then you can look at sample app (deployable to Heroku).
Or you can take a look at the sample applications in the "spec/dummy" project directory.
You are very welcome to help improve grape_oauth2
if you have suggestions for features that other people can use.
To contribute:
- Fork the project.
- Create your feature branch (
git checkout -b my-new-feature
). - Implement your feature or bug fix.
- Add documentation for your feature or bug fix.
- Add tests for your feature or bug fix.
- Run
rake
to make sure all tests pass. - Commit your changes (
git commit -am 'Add new feature'
). - Push to the branch (
git push origin my-new-feature
). - Create new pull request.
Thanks.
Grape::OAuth2
gem is released under the MIT License.
Copyright (c) 2014-2016 Nikita Bulai ([email protected]).