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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +17 -6
  4. data/app/controllers/oauth2_controller.rb +12 -0
  5. data/app/models/login.rb +20 -22
  6. data/app/services/base_authenticator.rb +38 -0
  7. data/app/services/facebook_authenticator.rb +25 -44
  8. data/app/services/google_authenticator.rb +55 -0
  9. data/db/migrate/20150709221755_create_logins.rb +8 -2
  10. data/db/migrate/20150904110438_add_provider_to_login.rb +8 -0
  11. data/lib/rails_api_auth.rb +17 -4
  12. data/lib/rails_api_auth/authentication.rb +44 -7
  13. data/lib/rails_api_auth/engine.rb +4 -0
  14. data/lib/rails_api_auth/version.rb +1 -1
  15. data/spec/dummy/app/controllers/access_once_controller.rb +11 -0
  16. data/spec/dummy/app/controllers/authenticated_controller.rb +1 -3
  17. data/spec/dummy/app/controllers/custom_authenticated_controller.rb +1 -3
  18. data/spec/dummy/config/initializers/rails_api_auth.rb +4 -1
  19. data/spec/dummy/config/routes.rb +1 -0
  20. data/spec/dummy/db/development.sqlite3 +0 -0
  21. data/spec/dummy/db/schema.rb +3 -2
  22. data/spec/dummy/db/test.sqlite3 +0 -0
  23. data/spec/dummy/log/development.log +26 -23
  24. data/spec/dummy/log/test.log +9402 -11504
  25. data/spec/factories/logins.rb +2 -1
  26. data/spec/models/login_spec.rb +9 -17
  27. data/spec/requests/access_once_spec.rb +66 -0
  28. data/spec/requests/authenticated_spec.rb +1 -1
  29. data/spec/requests/custom_authenticated_spec.rb +1 -1
  30. data/spec/requests/oauth2_spec.rb +19 -83
  31. data/spec/services/facebook_authenticator_spec.rb +8 -48
  32. data/spec/services/google_authenticator_spec.rb +12 -0
  33. data/spec/support/shared_contexts/stubbed_facebook_requests.rb +12 -0
  34. data/spec/support/shared_contexts/stubbed_google_requests.rb +11 -0
  35. data/spec/support/shared_examples/authenticator_shared_requests.rb +38 -0
  36. data/spec/support/shared_examples/oauth2_shared_requests.rb +80 -0
  37. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: af65e9fe685271c6e68587e74a3e2c33f35dc12c
4
- data.tar.gz: 2e55c5fb7ca6be0c2362769e744df99a8a5d9f11
3
+ metadata.gz: 3dbff8f6350acb5554f8d582e5a79f0c1250e24c
4
+ data.tar.gz: 9377a8dffd3962280a540da62f6d33bf3f8c0cb1
5
5
  SHA512:
6
- metadata.gz: 50e2055e18d2f4ab5c28b0735446113a567d9f65a0a4877f5ebd09a9d210f45f96a0b76aa5b675108d40fef6e04f57e98ab6cd16e3c4c54631d2717f54099636
7
- data.tar.gz: 83f495155e33db882ca560daeaa0db2b21c00fdda8a6e5b838a93fd93b7cbb280ac3d90680c29b228214799ca87c32f4fdf06c1432b1ce23741d4dbc55dfd06f
6
+ metadata.gz: fc7e7e108167bfa18c20c730e3e0f10177b35fce04ccbebb1ce554fbf559c8959de147ec2b7a65ecc0f56bf2ab59a1cf254a938c1dda5b4cfb31febd08e9f1f9
7
+ data.tar.gz: e7febed892fa651de9c67bba6e586db365bbd431d29688c082d27d16a724af16c073b18b0b823c4cd97526e706ebc4ebb3e32514bf27806fd7b5a0c93997cac2
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015 simplabs GmbH
3
+ Copyright (c) 2015-2016 simplabs GmbH
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 'rails_auth_api'
18
+ gem 'rails_api_auth'
19
19
  ```
20
20
 
21
- to the application's `Gemfile` and run `bundle install`.
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 201
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
@@ -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 `facebook_uid`__ (if it represents a Facebook login)
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 `facebook_uid` are present as no password is
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 thinks like
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 :password_or_facebook_uid_present
40
+ validate :password_or_uid_present
41
41
 
42
42
  before_validation :ensure_oauth2_token
43
- before_validation :refresh_single_use_oauth2_token
43
+ before_validation :assign_single_use_oauth2_token
44
44
 
45
- # Refreshes the Bearer token. This will effectively log out all clients that
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
- # 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.
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 consume_single_use_oauth2_token!(token)
63
- raise InvalidOAuth2Token.new if token != single_use_oauth2_token
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 password_or_facebook_uid_present
80
- if password_digest.blank? && facebook_uid.blank?
81
- errors.add :base, 'either password_digest or facebook_uid must be present'
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 = SecureRandom.hex(125) if set_token
81
+ self.oauth2_token = generate_token if set_token
88
82
  end
89
83
 
90
- def refresh_single_use_oauth2_token
91
- self.single_use_oauth2_token = SecureRandom.hex(125)
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
- class ApiError < StandardError; end
9
-
10
- def initialize(auth_code)
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
- @login ||= Login.where(identification: facebook_user[:email]).first
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 create_login_from_fb_account
19
+ def create_login_from_account(user)
35
20
  login_attributes = {
36
- identification: facebook_user[:email],
37
- facebook_uid: facebook_user[:id]
21
+ identification: user[:email],
22
+ uid: user[:id],
23
+ provider: PROVIDER
38
24
  }
39
25
 
40
- @login = Login.create!(login_attributes)
26
+ Login.create!(login_attributes)
41
27
  end
42
28
 
43
- def facebook_user
44
- @facebook_user ||= begin
45
- access_token = facebook_request(fb_token_url).parsed_response['access_token']
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 facebook_request(url)
51
- response = HTTParty.get(url)
52
- raise ApiError.new if response.code != 200
53
- response
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 fb_token_url
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}"
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 fb_user_url(access_token)
66
- "#{RailsApiAuth.facebook_graph_url}/me?access_token=#{access_token}"
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
@@ -0,0 +1,8 @@
1
+ class AddProviderToLogin < ActiveRecord::Migration
2
+
3
+ def change
4
+ add_column :logins, :provider, :string
5
+ rename_column :logins, :facebook_uid, :uid
6
+ end
7
+
8
+ end
@@ -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