Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RESTful JSON APIs access tokens and OAuth2.0 For Login #415

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,190 @@ External (see lib/sorcery/controller/submodules/external.rb):
* OAuth1 and OAuth2 support (currently twitter & facebook)
* configurable db field names and authentications table.

AccessToken (see lib/sorcery/model/submodules/access_token.rb)
* OAuth 2.0 For Login - RESTful JSON APIs.
* Please read instructions below.

== AccessToken Submodule (rails-api)

This submodule is intended to be used with rails as the backend server to
rich client-side applications.

See the rails-api project for building RESTful JSON APIs.

=== Supported Modes:

* *single_token*: One access token per user, shared between all user clients. Creates an access token on user creation.

* *session*: Allows multiple tokens per user, a user can have many clients acting on its behalf, with this mode each client can have its own access token. A maximum value can be defined using the 'max_per_user' configuration option.

=== Expiration:

Tokens expiration can be configured by setting the 'duration' value (in seconds),
this value will be used against token's creation time to know if the token has expired.

Expiration can also be evaluated against token's last actvity time by setting
'duration_from_last_activity' to true.

Expired tokens will be automatically deleted for each user after login.

Tokens are deleted on client logout.

==== Permanent Tokens:

All tokens are set to expire by default, permanent tokens can be created by setting
its 'expirable' attribute to false.

Example use case: mobile applications where the user never logs in/out.

=== Security Considerations:

* Use of TLS is *required* (HTTPS).

=== OAuth 2.0 For Login:

Outsources user authentication to OAuth 2.0 providers.

Flow: Implicit Grant [1]

==== How does it work?

Client-side application gets an access token from an OAuth 2.0 provider,
validates the received token, and sends the token with the rest of its
properties to this API server for 'login'.

The API server then attempts to login the user by sending a request to the
provider with the access token, if the access token is valid the API server
uses the user identifier included in the provider's response to find
and login (and optionaly create) the external user in the local database.

Please see links below for more information, in particular Ryan Boyd's
excellent talk about OAuth 2.0 [2] and Google's OAuth2 playground
(use the settings button to change the OAuth flow to Client-side,
you can also enter your own OAuth credentials) [4].

==== Notes:
- After login, the API server can then return one of its own api_access_token to the client application.
- Tested with the following providers: Google.
- The client-side application only needs to store the OAuth Client ID, the secret can be set in the API server configuration file.

==== References:

1. {https://tools.ietf.org/html/draft-ietf-oauth-v2-30#section-1.3.2}[https://tools.ietf.org/html/draft-ietf-oauth-v2-30#section-1.3.2]
2. {Google I/O 2012 - OAuth 2.0 for Identity and Data Access}[http://www.youtube.com/watch?v=YLHyeSuBspI]
3. {https://developers.google.com/accounts/docs/OAuth2}[https://developers.google.com/accounts/docs/OAuth2]
4. {https://developers.google.com/oauthplayground}[https://developers.google.com/oauthplayground]


=== Setup Example (OAuth 2.0 For Login):

* Update: demo with rails-api and Google's OAuth 2.0 For Login: https://github.com/fzagarzazu/sorcery_access_token_demo

* Installation
rails generate sorcery:install access_token external

* Example configuration file config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:access_token, :external]
config.restful_json_api = true
config.external_providers = [:google]
config.google.key = "client_id"
config.google.secret = "client_secret"
config.user_config do |user|
user.username_attribute_names = [:email]
user.authentications_class = Authentication
user.access_token_mode = 'single_token'
user.access_token_duration = 10.minutes.to_i
user.access_token_duration_from_last_activity = true
user.access_token_register_last_activity = true
end

* Example user model:


class User < ActiveRecord::Base
authenticates_with_sorcery! do |config|
config.authentications_class = Authentication
end
attr_accessible :email, :password, :password_confirmation, :authentications_attributes
has_many :access_tokens, :dependent => :delete_all
has_many :authentications, :dependent => :destroy

accepts_nested_attributes_for :authentications

end

* Example authentication model

class Authentication < ActiveRecord::Base
attr_accessible :user_id, :provider, :uid
belongs_to :user
end


* Example sessions controller


class SessionsController < ApplicationController
skip_before_filter :require_login, :except => [:destroy]

def create
# Login
if params[:provider] && params[:access_token_hash]
login_or_create_from_client_side(params[:provider], params[:access_token_hash])
elsif params[:email] && params[:password]
login(params[:email], params[:password])
end

# Response
if @api_access_token
render :json => {:access_token => @api_access_token.token }
else
head :unauthorized
end
end

def destroy
logout
head :ok
end
end

* Example application controller

class ApplicationController < ActionController::API
before_filter :require_login
end


=== Notes:

* It should be compatible with the following submodules:
activity_logging, brute_force_protection, external, reset_password, user_activation

* curl examples:

# Examples, remember to use HTTPS.

# GET request
curl -v -H "Accept: application/json" -H "Content-type: application/json" \
-X GET http://localhost:3000/apples?access_token=DdQoWMGzsVdPAL5egq67

# POST login request with email and password credentials
curl -v -H "Accept: application/json" -H "Content-type: application/json" \
-X POST -d '{"email": "[email protected]", "password": "secret_passwd"}' http://localhost:3000/sessions

# POST login request with provider's access token
curl -v -H "Accept: application/json" -H "Content-type: application/json" \
-X POST -d '{"provider": "google", "access_token_hash": { \
"access_token": "9d2H0I9AEa834B45cRTfBEcA82bF010B53BFGT83eFDB6097", \
"token_type": "Bearer", "expires_in": 3600 } }' http://localhost:3000/sessions

* Mongoid and MongoMapper

Support included but not completely tested, please review code before using it.

* Code review, feedback and contributions much appreciated.


== Next Planned Features:

Expand Down
15 changes: 15 additions & 0 deletions lib/generators/sorcery/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ def configure_initializer_file
generate "model #{model_class_name} --skip-migration"
insert_into_file "app/models/#{model_class_name.underscore}.rb", " authenticates_with_sorcery!\n", :after => "class #{model_class_name} < ActiveRecord::Base\n"
end

if submodules && submodules.include?("access_token")
generate_access_token_model
end

end

# Copy the migrations files to db/migrate folder
Expand Down Expand Up @@ -72,6 +77,16 @@ def self.next_migration_number(dirname)
def model_class_name
options[:model] ? options[:model].classify : "User"
end

def generate_access_token_model
access_token_class_name = 'AccessToken'
access_token_model_file = "app/models/#{access_token_class_name.underscore}.rb"
template "models/access_token.rb", access_token_model_file

insert_into_file("app/models/#{model_class_name.underscore}.rb",
"\n has_many :access_tokens, :dependent => :delete_all\n",
:after => " authenticates_with_sorcery!")
end
end
end
end
36 changes: 36 additions & 0 deletions lib/generators/sorcery/templates/initializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
#
# config.cookie_domain =

# -- RESTful JSON API ( access_token ) --
# (required by access_token submodule)
#
# config.restful_json_api = false

# -- session timeout --
# How long in seconds to keep the session alive.
Expand Down Expand Up @@ -419,6 +423,38 @@
# Default: `:uid`
#
# user.provider_uid_attribute_name =

# -- access_token --
# NOTE requires config.restful_json_api to be true
#

# Set access token mode:
# - 'single_token' : A single access token per user, shared between all
# user clients, a new access token is generated on each login
# iff previous expired.
# - 'session' : Allows multiple access tokens per user,
# deletes user expired access tokens on each login.
#
# user.access_token_mode = 'single_token'

# Set access token duration (in seconds), if present this value is used
# to evaluate token expiration.
#
# user.access_token_duration = nil

# Set access token expiration to be evaluated against token last activity time
#
# user.access_token_duration_from_last_activity = false

# Set max number of access token allowed per user for 'session' mode,
# past this value, on each login the returned access token is a valid
# stored token.
#
# user.access_token_max_per_user = nil

# Always register last activity time of token
#
# user.access_token_register_last_activity = false
end

# This line must come after the 'user config' block.
Expand Down
16 changes: 16 additions & 0 deletions lib/generators/sorcery/templates/migration/access_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class SorceryAccessToken < ActiveRecord::Migration
def self.up
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use change method here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kirs sure

create_table :access_tokens do |t|
t.string :token, :default => nil
t.boolean :expirable, :default => true
t.datetime :last_activity_at
t.references :<%= model_class_name.downcase %>

t.timestamps
end
end

def self.down
drop_table :access_tokens
end
end
Loading