rails_api_auth 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -2
  3. data/app/controllers/oauth2_controller.rb +9 -9
  4. data/app/lib/login_not_found.rb +3 -0
  5. data/app/models/login.rb +55 -8
  6. data/app/services/facebook_authenticator.rb +15 -10
  7. data/db/migrate/20150709221755_create_logins.rb +2 -1
  8. data/lib/rails_api_auth.rb +26 -0
  9. data/lib/rails_api_auth/authentication.rb +67 -11
  10. data/lib/rails_api_auth/engine.rb +1 -4
  11. data/lib/rails_api_auth/version.rb +1 -1
  12. data/spec/dummy/app/controllers/authenticated_controller.rb +2 -2
  13. data/spec/dummy/app/controllers/custom_authenticated_controller.rb +21 -0
  14. data/spec/dummy/app/models/account.rb +5 -0
  15. data/spec/dummy/config/application.rb +0 -2
  16. data/spec/dummy/config/environments/development.rb +2 -2
  17. data/spec/dummy/config/environments/production.rb +1 -1
  18. data/spec/dummy/config/environments/test.rb +1 -1
  19. data/spec/dummy/config/initializers/assets.rb +1 -1
  20. data/spec/dummy/config/initializers/cookies_serializer.rb +1 -1
  21. data/spec/dummy/config/initializers/filter_parameter_logging.rb +1 -1
  22. data/spec/dummy/config/initializers/rails_api_auth.rb +8 -0
  23. data/spec/dummy/config/initializers/secret_token.rb +1 -0
  24. data/spec/dummy/config/initializers/session_store.rb +1 -1
  25. data/spec/dummy/config/routes.rb +2 -1
  26. data/spec/dummy/config/secrets.yml +1 -1
  27. data/spec/dummy/db/development.sqlite3 +0 -0
  28. data/spec/dummy/db/migrate/20150803185817_create_accounts.rb +12 -0
  29. data/spec/dummy/db/schema.rb +9 -9
  30. data/spec/dummy/db/test.sqlite3 +0 -0
  31. data/spec/dummy/log/development.log +21 -11
  32. data/spec/dummy/log/test.log +11767 -6995
  33. data/spec/factories/accounts.rb +6 -0
  34. data/spec/factories/logins.rb +2 -2
  35. data/spec/models/login_spec.rb +10 -10
  36. data/spec/requests/authenticated_spec.rb +29 -26
  37. data/spec/requests/custom_authenticated_spec.rb +45 -0
  38. data/spec/requests/oauth2_spec.rb +30 -34
  39. data/spec/services/facebook_authenticator_spec.rb +15 -17
  40. data/spec/spec_helper.rb +10 -3
  41. metadata +17 -38
  42. data/app/controllers/rails_api_auth/application_controller.rb +0 -7
  43. data/config/initializers/facebook.rb +0 -6
  44. data/lib/tasks/rails_api_auth_tasks.rake +0 -4
  45. data/spec/dummy/db/migrate/20150709221900_create_users.rb +0 -11
  46. data/spec/dummy/db/production.sqlite3 +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b165743edc12371fd7870892ad34aacf10bfe984
4
- data.tar.gz: ced13aee635701bb2385267b33b952237602217a
3
+ metadata.gz: af65e9fe685271c6e68587e74a3e2c33f35dc12c
4
+ data.tar.gz: 2e55c5fb7ca6be0c2362769e744df99a8a5d9f11
5
5
  SHA512:
6
- metadata.gz: 70632344b8bdec1043b8f5899e98166426301af14aaddd5dcea1322454a53e00e8f28a43f4c115ec00ec17fa1a396c8ded1393abcc98a0c6a00e1f05bea36649
7
- data.tar.gz: 2bc812bf057509fe0df986881be6446fe2fe9430c6268dbfca3d7e7ea2589be3fa695d2a055abb0112558bed86446c3b163f18781070d0783d9f5d52dc755d64
6
+ metadata.gz: 50e2055e18d2f4ab5c28b0735446113a567d9f65a0a4877f5ebd09a9d210f45f96a0b76aa5b675108d40fef6e04f57e98ab6cd16e3c4c54631d2717f54099636
7
+ data.tar.gz: 83f495155e33db882ca560daeaa0db2b21c00fdda8a6e5b838a93fd93b7cbb280ac3d90680c29b228214799ca87c32f4fdf06c1432b1ce23741d4dbc55dfd06f
data/README.md CHANGED
@@ -1,3 +1,160 @@
1
- = RailsApiAuth
1
+ # RailsApiAuth
2
2
 
3
- This project rocks and uses MIT-LICENSE.
3
+ [![Build Status](https://travis-ci.org/simplabs/rails_api_auth.svg)](https://travis-ci.org/simplabs/rails_api_auth)
4
+
5
+ Rails API Auth is a lightweight Rails Engine that __implements the _"Resource
6
+ Owner Password Credentials Grant"_ OAuth 2.0 flow
7
+ ([RFC 6749](http://tools.ietf.org/html/rfc6749#section-4.3)) as well as
8
+ Facebook authentication for API projects__.
9
+
10
+ It uses __Bearer tokens__ ([RFC 6750](http://tools.ietf.org/html/rfc6750)) to
11
+ authorize requests coming in from clients.
12
+
13
+ ## Installation
14
+
15
+ To install the engine simply add
16
+
17
+ ```ruby
18
+ gem 'rails_auth_api'
19
+ ```
20
+
21
+ to the application's `Gemfile` and run `bundle install`.
22
+
23
+ __Rails API Auth also adds a migration__ to the application so run
24
+
25
+ ```bash
26
+ rake db:migrate
27
+ ```
28
+
29
+ as well to migrate the database.
30
+
31
+ ## Usage
32
+
33
+ __Rails API Auth stores a user's credentials as well as the tokens in a `Login`
34
+ model__ so that this data remains separated from the application's `User` model
35
+ (or `Account` or whatever the application chose to store profile data in).
36
+
37
+ After installing the engine you can add the relation from your user model to
38
+ the `Login` model:
39
+
40
+ ```ruby
41
+ class User < ActiveRecord::Base
42
+
43
+ has_one :login # this could be has_many as well of course
44
+
45
+ end
46
+ ```
47
+
48
+ When creating a new `User` in the host application, make sure to create a
49
+ related `Login` as well, e.g.:
50
+
51
+ ```ruby
52
+ class UsersController < ApplicationController
53
+
54
+ def create
55
+ user = User.new(user_params)
56
+ if user.save && user.create_login(login_params)
57
+ head 201
58
+ else
59
+ head 422 # you'd actually want to return validation errors here
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def user_params
66
+ params.require(:user).permit(:first_name, :last_name)
67
+ end
68
+
69
+ def login_params
70
+ params.require(:user).permit(:identification, :password, :password_confirmation)
71
+ end
72
+
73
+ end
74
+ ```
75
+
76
+ __The engine adds 2 routes to the application__ that implement the endpoints
77
+ for acquiring and revoking Bearer tokens:
78
+
79
+ ```
80
+ token POST /token(.:format) oauth2#create
81
+ revoke POST /revoke(.:format) oauth2#destroy
82
+ ```
83
+
84
+ These endpoints are fully implemented in the engine and will issue or revoke
85
+ Bearer tokens.
86
+
87
+ In order to authorize incoming requests the engine provides the
88
+ __`authenticate!` helper that can be used in controllers__ to make sure the
89
+ request includes a valid Bearer token in the `Authorization` header (e.g.
90
+ `Authorization: Bearer d5086ac8457b9db02a13`):
91
+
92
+ ```ruby
93
+ class AuthenticatedController < ApplicationController
94
+
95
+ include RailsApiAuth::Authentication
96
+
97
+ before_action :authenticate!
98
+
99
+ def index
100
+ render json: { success: true }
101
+ end
102
+
103
+ end
104
+
105
+ ```
106
+
107
+ If no valid Bearer token is provided the client will see a 401 response.
108
+
109
+ The engine also provides the `current_login` helper method that will return the
110
+ `Login` model authorized with the sent Bearer token.
111
+
112
+ You can also invoke `authenticate!` with a block to perform additional checks
113
+ on the current login, e.g. making sure the login's associated account has a
114
+ certain role:
115
+
116
+ ```ruby
117
+ class AuthenticatedController < ApplicationController
118
+
119
+ include RailsApiAuth::Authentication
120
+
121
+ before_action :authenticate_admin!
122
+
123
+ def index
124
+ render json: { success: true }
125
+ end
126
+
127
+ private
128
+
129
+ def authenticate_admin!
130
+ authenticate! do
131
+ current_login.account.admin?
132
+ end
133
+ end
134
+
135
+ end
136
+
137
+ ```
138
+
139
+ ## Configuration
140
+
141
+ The Engine can be configured by simply setting some attributes on its main
142
+ module:
143
+
144
+ ```ruby
145
+ RailsApiAuth.tap do |raa|
146
+ raa.user_model_relation = :account # this will set up the belongs_to relation from the Login model to the Account model automatically (of course if your application uses a User model this would be :user)
147
+
148
+ raa.facebook_app_id = '<your Facebook app id>'
149
+ raa.facebook_app_secret = '<your Facebook app secret>'
150
+ raa.facebook_graph_url = 'https://graph.facebook.com'
151
+ raa.facebook_redirect_uri = '<your Facebook app redirect uri>'
152
+ end
153
+
154
+ ```
155
+
156
+ ## License
157
+
158
+ Rails API Auth is developed by and &copy;
159
+ [simplabs GmbH](http://simplabs.com) and contributors. It is released under the
160
+ [MIT License](https://github.com/simplabs/ember-simple-auth/blob/master/LICENSE).
@@ -1,7 +1,8 @@
1
1
  require 'login_not_found'
2
2
 
3
- class FacebookApiError < StandardError; end
4
-
3
+ # The controller that implements the engine's endpoints.
4
+ #
5
+ # @!visibility private
5
6
  class Oauth2Controller < ApplicationController
6
7
 
7
8
  def create
@@ -18,7 +19,7 @@ class Oauth2Controller < ApplicationController
18
19
  def destroy
19
20
  oauth2_error('unsupported_token_type') && return unless params[:token_type_hint] == 'access_token'
20
21
 
21
- login = Login.find_by(oauth2_token: params[:token]) || LoginNotFound.new
22
+ login = Login.where(oauth2_token: params[:token]).first || LoginNotFound.new
22
23
  login.refresh_oauth2_token!
23
24
 
24
25
  head 200
@@ -26,8 +27,8 @@ class Oauth2Controller < ApplicationController
26
27
 
27
28
  private
28
29
 
29
- def authenticate_with_credentials(email, password)
30
- login = Login.find_by(email: email) || LoginNotFound.new
30
+ def authenticate_with_credentials(identification, password)
31
+ login = Login.where(identification: identification).first || LoginNotFound.new
31
32
 
32
33
  if login.authenticate(password)
33
34
  render json: { access_token: login.oauth2_token }
@@ -39,12 +40,11 @@ class Oauth2Controller < ApplicationController
39
40
  def authenticate_with_facebook(auth_code)
40
41
  oauth2_error('no_authorization_code') && return unless auth_code.present?
41
42
 
42
- login = FacebookAuthenticator.new(auth_code).authenticate
43
+ login = FacebookAuthenticator.new(auth_code).authenticate!
43
44
 
44
45
  render json: { access_token: login.oauth2_token }
45
-
46
- rescue FacebookApiError
47
- render nothing: true, status: 500
46
+ rescue FacebookAuthenticator::ApiError
47
+ render nothing: true, status: 502
48
48
  end
49
49
 
50
50
  def oauth2_error(error)
@@ -1,3 +1,6 @@
1
+ # Null Object that is used when no login is found for a Bearer token.
2
+ #
3
+ # @!visibility private
1
4
  class LoginNotFound
2
5
 
3
6
  def authenticate(_)
data/app/models/login.rb CHANGED
@@ -1,32 +1,79 @@
1
- require 'email_validator'
2
-
1
+ # The `Login` __model encapsulates login credentials and the associated Bearer
2
+ # tokens__. Rails API Auth uses this separate model so that login data and
3
+ # user/profile data doesn't get mixed up and the Engine remains clearly
4
+ # separeated from the code of the host application.
5
+ #
6
+ # The __`Login` model has `identification` and `password` attributes__ (in fact
7
+ # it uses Rails'
8
+ # [`has_secure_password`](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password))
9
+ # __as well as a `facebook_uid`__ (if it represents a Facebook login)
10
+ # attribute. As opposed to the standard `has_secure_password` behavior it
11
+ # doesn't validate that the password must be present but instead validates that
12
+ # __either__ the `password` or the `facebook_uid` are present as no password is
13
+ # required in the case that Facebook is used for authentication.
14
+ #
15
+ # The `Login` model also stores the Bearer token in the `oauth2_token`
16
+ # attribute. The model also stores an additional Bearer token, the
17
+ # `single_use_oauth2_token`, that can be used for implementing thinks like
18
+ # password reset where you need to make sure that the provided token can only
19
+ # be used once.
3
20
  class Login < ActiveRecord::Base
4
21
 
5
- class AlreadyVerifiedError < StandardError; end
6
- class InvalidSingleUseOAuth2Token < StandardError; end
22
+ # This is raised when an invalid token or a token that has already been
23
+ # consumed is consumed.
24
+ class InvalidOAuth2Token < StandardError; end
25
+
26
+ if RailsApiAuth.user_model_relation
27
+ belongs_to RailsApiAuth.user_model_relation, foreign_key: :user_id
28
+ end
7
29
 
8
- has_secure_password validations: false
30
+ if Rails::VERSION::MAJOR >= 4
31
+ has_secure_password validations: false
32
+ else
33
+ has_secure_password
34
+ end
9
35
 
10
- validates :email, presence: true, email: true
36
+ validates :identification, presence: true
11
37
  validates :oauth2_token, presence: true
12
38
  validates :single_use_oauth2_token, presence: true
13
- validates :password, length: { maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED }, confirmation: true
39
+ validates :password, length: { maximum: 72 }, confirmation: true
14
40
  validate :password_or_facebook_uid_present
15
41
 
16
42
  before_validation :ensure_oauth2_token
17
43
  before_validation :refresh_single_use_oauth2_token
18
44
 
45
+ # Refreshes the Bearer token. This will effectively log out all clients that
46
+ # possess the previous token.
47
+ #
48
+ # @raise [ActiveRecord::RecordInvalid] if the model is invalid
19
49
  def refresh_oauth2_token!
20
50
  ensure_oauth2_token(true)
21
51
  save!
22
52
  end
23
53
 
54
+ # This validates the passed single use Bearer token and assigns a new one. It
55
+ # will raise an exception if the token is not valid or has already been
56
+ # consumed.
57
+ #
58
+ # @param token [String] the single use token to consume
59
+ # @raise [InvalidOAuth2Token] if the token is not valid or has already been
60
+ # consumed
61
+ # @raise [ActiveRecord::RecordInvalid] if the model is invalid
24
62
  def consume_single_use_oauth2_token!(token)
25
- raise InvalidSingleUseOAuth2Token.new if token != single_use_oauth2_token
63
+ raise InvalidOAuth2Token.new if token != single_use_oauth2_token
26
64
  refresh_single_use_oauth2_token
27
65
  save!
28
66
  end
29
67
 
68
+ if Rails::VERSION::MAJOR == 3
69
+ # @!visibility private
70
+ def errors
71
+ super.tap do |errors|
72
+ errors.delete(:password_digest)
73
+ end
74
+ end
75
+ end
76
+
30
77
  private
31
78
 
32
79
  def password_or_facebook_uid_present
@@ -1,12 +1,17 @@
1
1
  require 'httparty'
2
2
 
3
+ # Handles Facebook authentication
4
+ #
5
+ # @!visibility private
3
6
  class FacebookAuthenticator
4
7
 
8
+ class ApiError < StandardError; end
9
+
5
10
  def initialize(auth_code)
6
11
  @auth_code = auth_code
7
12
  end
8
13
 
9
- def authenticate
14
+ def authenticate!
10
15
  if login.present?
11
16
  connect_login_to_fb_account
12
17
  else
@@ -19,7 +24,7 @@ class FacebookAuthenticator
19
24
  private
20
25
 
21
26
  def login
22
- @login ||= Login.find_by(email: facebook_user[:email])
27
+ @login ||= Login.where(identification: facebook_user[:email]).first
23
28
  end
24
29
 
25
30
  def connect_login_to_fb_account
@@ -28,8 +33,8 @@ class FacebookAuthenticator
28
33
 
29
34
  def create_login_from_fb_account
30
35
  login_attributes = {
31
- email: facebook_user[:email],
32
- facebook_uid: facebook_user[:id]
36
+ identification: facebook_user[:email],
37
+ facebook_uid: facebook_user[:id]
33
38
  }
34
39
 
35
40
  @login = Login.create!(login_attributes)
@@ -44,21 +49,21 @@ class FacebookAuthenticator
44
49
 
45
50
  def facebook_request(url)
46
51
  response = HTTParty.get(url)
47
- raise FacebookApiError.new if response.code != 200
52
+ raise ApiError.new if response.code != 200
48
53
  response
49
54
  end
50
55
 
51
56
  def fb_token_url
52
- "#{Rails.application.config.x.facebook.graph_url}/oauth/access_token".tap do |url|
53
- url << "?client_id=#{Rails.application.config.x.facebook.app_id}"
54
- url << "&redirect_uri=#{Rails.application.config.x.facebook.redirect_uri}"
55
- url << "&client_secret=#{Rails.application.config.x.facebook.app_secret}"
57
+ "#{RailsApiAuth.facebook_graph_url}/oauth/access_token".tap do |url|
58
+ url << "?client_id=#{RailsApiAuth.facebook_app_id}"
59
+ url << "&redirect_uri=#{RailsApiAuth.facebook_redirect_uri}"
60
+ url << "&client_secret=#{RailsApiAuth.facebook_app_secret}"
56
61
  url << "&code=#{@auth_code}"
57
62
  end
58
63
  end
59
64
 
60
65
  def fb_user_url(access_token)
61
- "#{Rails.application.config.x.facebook.graph_url}/me?access_token=#{access_token}"
66
+ "#{RailsApiAuth.facebook_graph_url}/me?access_token=#{access_token}"
62
67
  end
63
68
 
64
69
  end
@@ -2,11 +2,12 @@ class CreateLogins < ActiveRecord::Migration
2
2
 
3
3
  def change
4
4
  create_table :logins do |t|
5
- t.string :email, null: false
5
+ t.string :identification, null: false
6
6
  t.string :password_digest, null: true
7
7
  t.string :oauth2_token, null: false
8
8
  t.string :facebook_uid
9
9
  t.string :single_use_oauth2_token
10
+
10
11
  t.references :user
11
12
 
12
13
  t.timestamps
@@ -1,5 +1,31 @@
1
1
  require 'rails_api_auth/engine'
2
2
 
3
+ # The engine's main module.
3
4
  module RailsApiAuth
4
5
 
6
+ # @!attribute [rw] user_model_relation
7
+ # Defines the `Login` model's `belongs_to` relation to the host application's
8
+ # `User` model (or `Account` or whatever the application stores user data
9
+ # in).
10
+ #
11
+ # E.g. is this is set to `:profile`, the `Login` model will have a
12
+ # `belongs_to :profile` relation.
13
+ mattr_accessor :user_model_relation
14
+
15
+ # @!attribute [rw] facebook_app_id
16
+ # The Facebook App ID.
17
+ mattr_accessor :facebook_app_id
18
+
19
+ # @!attribute [rw] facebook_app_secret
20
+ # The Facebook App secret.
21
+ mattr_accessor :facebook_app_secret
22
+
23
+ # @!attribute [rw] facebook_graph_url
24
+ # The Facebook grahp URL.
25
+ mattr_accessor :facebook_graph_url
26
+
27
+ # @!attribute [rw] facebook_redirect_uri
28
+ # The Facebook App's redirect URI.
29
+ mattr_accessor :facebook_redirect_uri
30
+
5
31
  end
@@ -1,27 +1,83 @@
1
1
  module RailsApiAuth
2
2
 
3
+ # Module that defines attributes and method for use in controllers. This
4
+ # module would typically be included in the `ApplicationController`.
3
5
  module Authentication
4
6
 
5
- class RequestForbidden < StandardError; end
6
-
7
7
  extend ActiveSupport::Concern
8
8
 
9
+ # @!attribute [r] current_login
10
+ # The login that was authenticated from the Bearer token in the
11
+ # `Authorization` header (if one was successfully authenticated).
12
+
13
+ # @!method authenticate!(&block)
14
+ # Ensures that the `Authorization` header is present with a valid Bearer
15
+ # token.
16
+ #
17
+ # If a valid bearer token is present this method will retrieve the
18
+ # respective `Login` model and store it in `current_login`. If no or an
19
+ # invalid Bearer token is present this will result in a 401 response.
20
+ #
21
+ # This method will typically be called as a `before_action`:
22
+ #
23
+ # ```ruby
24
+ # class AuthenticatedController < ApplicationController
25
+ #
26
+ # include RailsApiAuth::Authentication
27
+ #
28
+ # before_filter :authenticate!
29
+ #
30
+ # def index
31
+ # render text: 'zuper content', status: 201
32
+ # end
33
+ #
34
+ # end
35
+ # ```
36
+ #
37
+ # You can also call this method with a block to perform additional checks
38
+ # on the login retrieved for the Bearer token. When the block returns a
39
+ # truthy value authentication is successful, when the block returns a falsy
40
+ # value the client will see a 401 response:
41
+ #
42
+ # ```ruby
43
+ # class AuthenticatedController < ApplicationController
44
+ #
45
+ # include RailsApiAuth::Authentication
46
+ #
47
+ # before_filter :authenticate_admin!
48
+ #
49
+ # def index
50
+ # render text: 'zuper content', status: 201
51
+ # end
52
+ #
53
+ # private
54
+ #
55
+ # def authenticate_with_account!
56
+ # authenticate! do
57
+ # current_login.account.first_name == 'user x'
58
+ # end
59
+ # end
60
+ #
61
+ # end
62
+ # ```
63
+ #
64
+ # @see #current_login
65
+
9
66
  included do
10
67
  attr_reader :current_login
11
68
 
12
- rescue_from RequestForbidden, with: :deny_access
13
-
14
69
  private
15
70
 
16
- def deny_access
17
- head 403
18
- end
19
-
20
- def authenticate!
21
- auth_header = request.headers[:authorization]
71
+ def authenticate!(&block)
72
+ auth_header = request.headers['Authorization']
22
73
  token = auth_header ? auth_header.split(' ').last : ''
23
- @current_login ||= Login.find_by!(oauth2_token: token)
74
+ @current_login = Login.where(oauth2_token: token).first!
24
75
 
76
+ if block_given?
77
+ head 401 unless block.call
78
+ else
79
+ @current_login
80
+ end
25
81
  rescue ActiveRecord::RecordNotFound
26
82
  head 401
27
83
  end