rails_api_auth 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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