rails_api_auth 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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