rails_api_auth 0.0.3 → 0.0.4
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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +17 -6
- data/app/controllers/oauth2_controller.rb +12 -0
- data/app/models/login.rb +20 -22
- data/app/services/base_authenticator.rb +38 -0
- data/app/services/facebook_authenticator.rb +25 -44
- data/app/services/google_authenticator.rb +55 -0
- data/db/migrate/20150709221755_create_logins.rb +8 -2
- data/db/migrate/20150904110438_add_provider_to_login.rb +8 -0
- data/lib/rails_api_auth.rb +17 -4
- data/lib/rails_api_auth/authentication.rb +44 -7
- data/lib/rails_api_auth/engine.rb +4 -0
- data/lib/rails_api_auth/version.rb +1 -1
- data/spec/dummy/app/controllers/access_once_controller.rb +11 -0
- data/spec/dummy/app/controllers/authenticated_controller.rb +1 -3
- data/spec/dummy/app/controllers/custom_authenticated_controller.rb +1 -3
- data/spec/dummy/config/initializers/rails_api_auth.rb +4 -1
- data/spec/dummy/config/routes.rb +1 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/schema.rb +3 -2
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +26 -23
- data/spec/dummy/log/test.log +9402 -11504
- data/spec/factories/logins.rb +2 -1
- data/spec/models/login_spec.rb +9 -17
- data/spec/requests/access_once_spec.rb +66 -0
- data/spec/requests/authenticated_spec.rb +1 -1
- data/spec/requests/custom_authenticated_spec.rb +1 -1
- data/spec/requests/oauth2_spec.rb +19 -83
- data/spec/services/facebook_authenticator_spec.rb +8 -48
- data/spec/services/google_authenticator_spec.rb +12 -0
- data/spec/support/shared_contexts/stubbed_facebook_requests.rb +12 -0
- data/spec/support/shared_contexts/stubbed_google_requests.rb +11 -0
- data/spec/support/shared_examples/authenticator_shared_requests.rb +38 -0
- data/spec/support/shared_examples/oauth2_shared_requests.rb +80 -0
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3dbff8f6350acb5554f8d582e5a79f0c1250e24c
|
4
|
+
data.tar.gz: 9377a8dffd3962280a540da62f6d33bf3f8c0cb1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc7e7e108167bfa18c20c730e3e0f10177b35fce04ccbebb1ce554fbf559c8959de147ec2b7a65ecc0f56bf2ab59a1cf254a938c1dda5b4cfb31febd08e9f1f9
|
7
|
+
data.tar.gz: e7febed892fa651de9c67bba6e586db365bbd431d29688c082d27d16a724af16c073b18b0b823c4cd97526e706ebc4ebb3e32514bf27806fd7b5a0c93997cac2
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -5,20 +5,23 @@
|
|
5
5
|
Rails API Auth is a lightweight Rails Engine that __implements the _"Resource
|
6
6
|
Owner Password Credentials Grant"_ OAuth 2.0 flow
|
7
7
|
([RFC 6749](http://tools.ietf.org/html/rfc6749#section-4.3)) as well as
|
8
|
-
Facebook authentication for API projects__.
|
8
|
+
Facebook and Google authentication for API projects__.
|
9
9
|
|
10
10
|
It uses __Bearer tokens__ ([RFC 6750](http://tools.ietf.org/html/rfc6750)) to
|
11
11
|
authorize requests coming in from clients.
|
12
12
|
|
13
13
|
## Installation
|
14
14
|
|
15
|
-
To install the engine simply add
|
15
|
+
To install the engine simply add to the application's `Gemfile`
|
16
16
|
|
17
17
|
```ruby
|
18
|
-
gem '
|
18
|
+
gem 'rails_api_auth'
|
19
19
|
```
|
20
20
|
|
21
|
-
|
21
|
+
and run:
|
22
|
+
```bash
|
23
|
+
bundle install
|
24
|
+
```
|
22
25
|
|
23
26
|
__Rails API Auth also adds a migration__ to the application so run
|
24
27
|
|
@@ -54,7 +57,7 @@ class UsersController < ApplicationController
|
|
54
57
|
def create
|
55
58
|
user = User.new(user_params)
|
56
59
|
if user.save && user.create_login(login_params)
|
57
|
-
head
|
60
|
+
head 200
|
58
61
|
else
|
59
62
|
head 422 # you'd actually want to return validation errors here
|
60
63
|
end
|
@@ -136,6 +139,8 @@ end
|
|
136
139
|
|
137
140
|
```
|
138
141
|
|
142
|
+
See the [demo project](https://github.com/simplabs/rails_api_auth-demo) for further details.
|
143
|
+
|
139
144
|
## Configuration
|
140
145
|
|
141
146
|
The Engine can be configured by simply setting some attributes on its main
|
@@ -145,10 +150,16 @@ module:
|
|
145
150
|
RailsApiAuth.tap do |raa|
|
146
151
|
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
152
|
|
153
|
+
# Facebook configurations
|
148
154
|
raa.facebook_app_id = '<your Facebook app id>'
|
149
155
|
raa.facebook_app_secret = '<your Facebook app secret>'
|
150
|
-
raa.facebook_graph_url = 'https://graph.facebook.com'
|
151
156
|
raa.facebook_redirect_uri = '<your Facebook app redirect uri>'
|
157
|
+
|
158
|
+
# Google configurations
|
159
|
+
raa.google_client_id = '<your Google client id>'
|
160
|
+
raa.google_client_secret = '<your Google client secret>'
|
161
|
+
raa.google_redirect_uri = '<your app redirect uri>'
|
162
|
+
|
152
163
|
end
|
153
164
|
|
154
165
|
```
|
@@ -11,6 +11,8 @@ class Oauth2Controller < ApplicationController
|
|
11
11
|
authenticate_with_credentials(params[:username], params[:password])
|
12
12
|
when 'facebook_auth_code'
|
13
13
|
authenticate_with_facebook(params[:auth_code])
|
14
|
+
when 'google_auth_code'
|
15
|
+
authenticate_with_google(params[:auth_code])
|
14
16
|
else
|
15
17
|
oauth2_error('unsupported_grant_type')
|
16
18
|
end
|
@@ -47,6 +49,16 @@ class Oauth2Controller < ApplicationController
|
|
47
49
|
render nothing: true, status: 502
|
48
50
|
end
|
49
51
|
|
52
|
+
def authenticate_with_google(auth_code)
|
53
|
+
oauth2_error('no_authorization_code') && return unless auth_code.present?
|
54
|
+
|
55
|
+
login = GoogleAuthenticator.new(auth_code).authenticate!
|
56
|
+
|
57
|
+
render json: { access_token: login.oauth2_token }
|
58
|
+
rescue GoogleAuthenticator::ApiError
|
59
|
+
render nothing: true, status: 502
|
60
|
+
end
|
61
|
+
|
50
62
|
def oauth2_error(error)
|
51
63
|
render json: { error: error }, status: 400
|
52
64
|
end
|
data/app/models/login.rb
CHANGED
@@ -6,15 +6,15 @@
|
|
6
6
|
# The __`Login` model has `identification` and `password` attributes__ (in fact
|
7
7
|
# it uses Rails'
|
8
8
|
# [`has_secure_password`](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password))
|
9
|
-
# __as well as a `
|
9
|
+
# __as well as a `uid`__ (a Facebook uid or Google sub)
|
10
10
|
# attribute. As opposed to the standard `has_secure_password` behavior it
|
11
11
|
# doesn't validate that the password must be present but instead validates that
|
12
|
-
# __either__ the `password` or the `
|
13
|
-
# required in the case that Facebook is used for authentication.
|
12
|
+
# __either__ the `password` or the `uid` are present as no password is
|
13
|
+
# required in the case that Facebook or Google is used for authentication.
|
14
14
|
#
|
15
15
|
# The `Login` model also stores the Bearer token in the `oauth2_token`
|
16
16
|
# attribute. The model also stores an additional Bearer token, the
|
17
|
-
# `single_use_oauth2_token`, that can be used for implementing
|
17
|
+
# `single_use_oauth2_token`, that can be used for implementing things like
|
18
18
|
# password reset where you need to make sure that the provided token can only
|
19
19
|
# be used once.
|
20
20
|
class Login < ActiveRecord::Base
|
@@ -37,12 +37,12 @@ class Login < ActiveRecord::Base
|
|
37
37
|
validates :oauth2_token, presence: true
|
38
38
|
validates :single_use_oauth2_token, presence: true
|
39
39
|
validates :password, length: { maximum: 72 }, confirmation: true
|
40
|
-
validate :
|
40
|
+
validate :password_or_uid_present
|
41
41
|
|
42
42
|
before_validation :ensure_oauth2_token
|
43
|
-
before_validation :
|
43
|
+
before_validation :assign_single_use_oauth2_token
|
44
44
|
|
45
|
-
# Refreshes the
|
45
|
+
# Refreshes the random token. This will effectively log out all clients that
|
46
46
|
# possess the previous token.
|
47
47
|
#
|
48
48
|
# @raise [ActiveRecord::RecordInvalid] if the model is invalid
|
@@ -51,17 +51,11 @@ class Login < ActiveRecord::Base
|
|
51
51
|
save!
|
52
52
|
end
|
53
53
|
|
54
|
-
#
|
55
|
-
# will raise an exception if the token is not valid or has already been
|
56
|
-
# consumed.
|
54
|
+
# Refreshes the single use Oauth 2.0 token.
|
57
55
|
#
|
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
56
|
# @raise [ActiveRecord::RecordInvalid] if the model is invalid
|
62
|
-
def
|
63
|
-
|
64
|
-
refresh_single_use_oauth2_token
|
57
|
+
def refresh_single_use_oauth2_token!
|
58
|
+
assign_single_use_oauth2_token
|
65
59
|
save!
|
66
60
|
end
|
67
61
|
|
@@ -76,19 +70,23 @@ class Login < ActiveRecord::Base
|
|
76
70
|
|
77
71
|
private
|
78
72
|
|
79
|
-
def
|
80
|
-
if password_digest.blank? &&
|
81
|
-
errors.add :base, 'either password_digest or
|
73
|
+
def password_or_uid_present
|
74
|
+
if password_digest.blank? && uid.blank?
|
75
|
+
errors.add :base, 'either password_digest or uid must be present'
|
82
76
|
end
|
83
77
|
end
|
84
78
|
|
85
79
|
def ensure_oauth2_token(force = false)
|
86
80
|
set_token = oauth2_token.blank? || force
|
87
|
-
self.oauth2_token =
|
81
|
+
self.oauth2_token = generate_token if set_token
|
88
82
|
end
|
89
83
|
|
90
|
-
def
|
91
|
-
self.single_use_oauth2_token =
|
84
|
+
def assign_single_use_oauth2_token
|
85
|
+
self.single_use_oauth2_token = generate_token
|
86
|
+
end
|
87
|
+
|
88
|
+
def generate_token
|
89
|
+
SecureRandom.hex(125)
|
92
90
|
end
|
93
91
|
|
94
92
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class BaseAuthenticator
|
2
|
+
|
3
|
+
class ApiError < StandardError; end
|
4
|
+
|
5
|
+
def initialize(auth_code)
|
6
|
+
@auth_code = auth_code
|
7
|
+
end
|
8
|
+
|
9
|
+
def authenticate!
|
10
|
+
user = get_user(access_token)
|
11
|
+
login = find_login(user)
|
12
|
+
|
13
|
+
if login.present?
|
14
|
+
connect_login_to_account(login, user)
|
15
|
+
else
|
16
|
+
login = create_login_from_account(user)
|
17
|
+
end
|
18
|
+
|
19
|
+
login
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def find_login(user)
|
25
|
+
Login.where(identification: user[:email]).first
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_request(url)
|
29
|
+
response = HTTParty.get(url)
|
30
|
+
unless response.code == 200
|
31
|
+
Rails.logger.warn "#{self.class::PROVIDER} API request failed with status #{response.code}."
|
32
|
+
Rails.logger.debug "#{self.class::PROVIDER} API error response was:\n#{response.body}"
|
33
|
+
raise ApiError.new
|
34
|
+
end
|
35
|
+
response
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -1,69 +1,50 @@
|
|
1
1
|
require 'httparty'
|
2
|
+
require 'pry'
|
2
3
|
|
3
4
|
# Handles Facebook authentication
|
4
5
|
#
|
5
6
|
# @!visibility private
|
6
|
-
class FacebookAuthenticator
|
7
|
+
class FacebookAuthenticator < BaseAuthenticator
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@auth_code = auth_code
|
12
|
-
end
|
13
|
-
|
14
|
-
def authenticate!
|
15
|
-
if login.present?
|
16
|
-
connect_login_to_fb_account
|
17
|
-
else
|
18
|
-
create_login_from_fb_account
|
19
|
-
end
|
20
|
-
|
21
|
-
login
|
22
|
-
end
|
9
|
+
PROVIDER = 'facebook'.freeze
|
10
|
+
TOKEN_URL = 'https://graph.facebook.com/v2.4/oauth/access_token?client_id=%{client_id}&client_secret=%{client_secret}&code=%{auth_code}&redirect_uri=%{redirect_uri}'.freeze
|
11
|
+
PROFILE_URL = 'https://graph.facebook.com/v2.4/me?fields=email,name&access_token=%{access_token}'.freeze
|
23
12
|
|
24
13
|
private
|
25
14
|
|
26
|
-
def login
|
27
|
-
|
28
|
-
end
|
29
|
-
|
30
|
-
def connect_login_to_fb_account
|
31
|
-
login.update_attributes!(facebook_uid: facebook_user[:id])
|
15
|
+
def connect_login_to_account(login, user)
|
16
|
+
login.update_attributes!(uid: user[:id], provider: PROVIDER)
|
32
17
|
end
|
33
18
|
|
34
|
-
def
|
19
|
+
def create_login_from_account(user)
|
35
20
|
login_attributes = {
|
36
|
-
identification:
|
37
|
-
|
21
|
+
identification: user[:email],
|
22
|
+
uid: user[:id],
|
23
|
+
provider: PROVIDER
|
38
24
|
}
|
39
25
|
|
40
|
-
|
26
|
+
Login.create!(login_attributes)
|
41
27
|
end
|
42
28
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
facebook_request(fb_user_url(access_token)).parsed_response.symbolize_keys
|
47
|
-
end
|
29
|
+
def access_token
|
30
|
+
response = get_request(token_url)
|
31
|
+
response.parsed_response.symbolize_keys[:access_token]
|
48
32
|
end
|
49
33
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
34
|
+
def get_user(access_token)
|
35
|
+
@facebook_user ||= begin
|
36
|
+
parsed_response = get_request(user_url(access_token)).parsed_response
|
37
|
+
parsed_response.symbolize_keys
|
38
|
+
end
|
54
39
|
end
|
55
40
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
url << "&redirect_uri=#{RailsApiAuth.facebook_redirect_uri}"
|
60
|
-
url << "&client_secret=#{RailsApiAuth.facebook_app_secret}"
|
61
|
-
url << "&code=#{@auth_code}"
|
62
|
-
end
|
41
|
+
def token_url
|
42
|
+
url_options = { client_id: RailsApiAuth.facebook_app_id, client_secret: RailsApiAuth.facebook_app_secret, auth_code: @auth_code, redirect_uri: RailsApiAuth.facebook_redirect_uri }
|
43
|
+
TOKEN_URL % url_options
|
63
44
|
end
|
64
45
|
|
65
|
-
def
|
66
|
-
|
46
|
+
def user_url(access_token)
|
47
|
+
PROFILE_URL % { access_token: access_token }
|
67
48
|
end
|
68
49
|
|
69
50
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
# Handles Google authentication
|
4
|
+
#
|
5
|
+
# @!visibility private
|
6
|
+
class GoogleAuthenticator < BaseAuthenticator
|
7
|
+
|
8
|
+
PROVIDER = 'google'.freeze
|
9
|
+
TOKEN_URL = 'https://www.googleapis.com/oauth2/v3/token'.freeze
|
10
|
+
PROFILE_URL = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect?access_token=%{access_token}'.freeze
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def connect_login_to_account(login, user)
|
15
|
+
login.update_attributes!(uid: user[:sub], provider: PROVIDER)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_login_from_account(user)
|
19
|
+
login_attributes = {
|
20
|
+
identification: user[:email],
|
21
|
+
uid: user[:sub],
|
22
|
+
provider: PROVIDER
|
23
|
+
}
|
24
|
+
|
25
|
+
Login.create!(login_attributes)
|
26
|
+
end
|
27
|
+
|
28
|
+
def access_token
|
29
|
+
response = HTTParty.post(TOKEN_URL, token_options)
|
30
|
+
response.parsed_response['access_token']
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_user(access_token)
|
34
|
+
@google_user ||= begin
|
35
|
+
get_request(user_url(access_token)).parsed_response.symbolize_keys
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def user_url(access_token)
|
40
|
+
PROFILE_URL % { access_token: access_token }
|
41
|
+
end
|
42
|
+
|
43
|
+
def token_options
|
44
|
+
@token_options ||= {
|
45
|
+
body: {
|
46
|
+
code: @auth_code,
|
47
|
+
client_id: RailsApiAuth.google_client_id,
|
48
|
+
client_secret: RailsApiAuth.google_client_secret,
|
49
|
+
redirect_uri: RailsApiAuth.google_redirect_uri,
|
50
|
+
grant_type: 'authorization_code'
|
51
|
+
}
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -1,17 +1,23 @@
|
|
1
1
|
class CreateLogins < ActiveRecord::Migration
|
2
2
|
|
3
3
|
def change
|
4
|
-
create_table :logins do |t|
|
4
|
+
create_table :logins, primary_key_options(:id) do |t|
|
5
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
|
+
t.references :user, primary_key_options(:type)
|
12
12
|
|
13
13
|
t.timestamps
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
private
|
18
|
+
|
19
|
+
def primary_key_options(option_name)
|
20
|
+
RailsApiAuth.primary_key_type ? { option_name => RailsApiAuth.primary_key_type } : {}
|
21
|
+
end
|
22
|
+
|
17
23
|
end
|
data/lib/rails_api_auth.rb
CHANGED
@@ -20,12 +20,25 @@ module RailsApiAuth
|
|
20
20
|
# The Facebook App secret.
|
21
21
|
mattr_accessor :facebook_app_secret
|
22
22
|
|
23
|
-
# @!attribute [rw] facebook_graph_url
|
24
|
-
# The Facebook grahp URL.
|
25
|
-
mattr_accessor :facebook_graph_url
|
26
|
-
|
27
23
|
# @!attribute [rw] facebook_redirect_uri
|
28
24
|
# The Facebook App's redirect URI.
|
29
25
|
mattr_accessor :facebook_redirect_uri
|
30
26
|
|
27
|
+
# @!attribute [rw] google_client_id
|
28
|
+
# The Google client ID.
|
29
|
+
mattr_accessor :google_client_id
|
30
|
+
|
31
|
+
# @!attribute [rw] google_client_secret
|
32
|
+
# The Google client secret.
|
33
|
+
mattr_accessor :google_client_secret
|
34
|
+
|
35
|
+
# @!attribute [rw] google_redirect_uri
|
36
|
+
# The Google App's redirect URI.
|
37
|
+
mattr_accessor :google_redirect_uri
|
38
|
+
|
39
|
+
# @!attribute [rw] primary_key_type
|
40
|
+
# Configures database column type used for primary keys,
|
41
|
+
# currently only accepts :uuid
|
42
|
+
mattr_accessor :primary_key_type
|
43
|
+
|
31
44
|
end
|