rails_api_auth 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +26 -3
- data/app/controllers/oauth2_controller.rb +17 -2
- data/app/services/edx_authenticator.rb +72 -0
- data/lib/rails_api_auth.rb +16 -0
- data/lib/rails_api_auth/version.rb +1 -1
- data/spec/dummy/app/controllers/access_once_controller.rb +10 -2
- data/spec/dummy/app/controllers/authenticated_controller.rb +10 -2
- data/spec/dummy/app/controllers/custom_authenticated_controller.rb +10 -2
- data/spec/dummy/config/application.rb +1 -0
- data/spec/dummy/config/environments/production.rb +5 -1
- data/spec/dummy/config/environments/test.rb +7 -2
- data/spec/dummy/config/initializers/rails_api_auth.rb +5 -0
- data/spec/models/login_spec.rb +2 -2
- data/spec/requests/access_once_spec.rb +14 -2
- data/spec/requests/authenticated_spec.rb +7 -1
- data/spec/requests/custom_authenticated_spec.rb +13 -4
- data/spec/requests/oauth2_spec.rb +32 -2
- data/spec/services/edx_authenticator_spec.rb +12 -0
- data/spec/support/shared_contexts/stubbed_edx_requests.rb +16 -0
- data/spec/support/shared_examples/authenticator_shared_requests.rb +5 -1
- data/spec/support/shared_examples/oauth2_edx_shared_requests.rb +93 -0
- data/spec/support/shared_examples/oauth2_shared_requests.rb +1 -3
- metadata +12 -14
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +0 -29
- data/spec/dummy/log/test.log +0 -20111
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 643e8f86cbe69072c9b057c1dae39e24fd49982d
|
4
|
+
data.tar.gz: ad2a2f90465fbeed37877dde1f758d5bf4ff7f77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0589164744d500bf57b6cd115e617bab91b1929e036b2fc0889330dd9f2548fa12341b701119ead95eec6c274f5edb23cc75aa1b00e79bb0035ebf6efa1195b9'
|
7
|
+
data.tar.gz: a0d1c552c7081eb31516b95b719938563f55f4688bec551878a66d78db096075d9df5c30d1576be4a82df68116392fca076a44d8f9864bdfd27d77ec6099c9b3
|
data/README.md
CHANGED
@@ -156,9 +156,15 @@ RailsApiAuth.tap do |raa|
|
|
156
156
|
raa.facebook_redirect_uri = '<your Facebook app redirect uri>'
|
157
157
|
|
158
158
|
# Google configurations
|
159
|
-
raa.google_client_id
|
159
|
+
raa.google_client_id = '<your Google client id>'
|
160
160
|
raa.google_client_secret = '<your Google client secret>'
|
161
|
-
raa.google_redirect_uri
|
161
|
+
raa.google_redirect_uri = '<your app redirect uri>'
|
162
|
+
|
163
|
+
# Edx configurations
|
164
|
+
raa.edx_client_id = '<your Edx client id>'
|
165
|
+
raa.edx_client_secret = '<your Edx client secret>'
|
166
|
+
raa.edx_domain = '<your Edx app domain>'
|
167
|
+
raa.edx_redirect_uri = 'your Edx app redirect uri'
|
162
168
|
|
163
169
|
# Force SSL for Oauth2Controller; defaults to `false` for the development environment, otherwise `true`
|
164
170
|
raa.force_ssl = false
|
@@ -166,9 +172,26 @@ end
|
|
166
172
|
|
167
173
|
```
|
168
174
|
|
175
|
+
### A note on Edx Oauth2 code flows
|
176
|
+
|
177
|
+
It is nesescary to include the Edx username in the request when making a call
|
178
|
+
rails_api_auth call /token. When rails_api_auth interfaces with Edx's
|
179
|
+
user api, the username is need to retrieve user data, not just a valid
|
180
|
+
oauth2 token.
|
181
|
+
|
182
|
+
E.g.
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
headers = {
|
186
|
+
username: "alice",
|
187
|
+
auth_code: "alices_authorization_code",
|
188
|
+
grant_type: "edx_auth_code"
|
189
|
+
}
|
190
|
+
```
|
191
|
+
|
169
192
|
## Contribution
|
170
193
|
|
171
|
-
See [CONTRIBUTING](https://github.com/simplabs/rails_api_auth/blob/master/CONTRIBUTING).
|
194
|
+
See [CONTRIBUTING](https://github.com/simplabs/rails_api_auth/blob/master/CONTRIBUTING.md).
|
172
195
|
|
173
196
|
## License
|
174
197
|
|
@@ -7,6 +7,7 @@ class Oauth2Controller < ApplicationController
|
|
7
7
|
|
8
8
|
force_ssl if: -> { RailsApiAuth.force_ssl }
|
9
9
|
|
10
|
+
# rubocop:disable MethodLength
|
10
11
|
def create
|
11
12
|
case params[:grant_type]
|
12
13
|
when 'password'
|
@@ -15,11 +16,14 @@ class Oauth2Controller < ApplicationController
|
|
15
16
|
authenticate_with_facebook(params[:auth_code])
|
16
17
|
when 'google_auth_code'
|
17
18
|
authenticate_with_google(params[:auth_code])
|
19
|
+
when 'edx_auth_code'
|
20
|
+
authenticate_with_edx(params[:username], params[:auth_code])
|
18
21
|
else
|
19
22
|
oauth2_error('unsupported_grant_type')
|
20
23
|
end
|
21
24
|
end
|
22
25
|
|
26
|
+
# rubocop:enable MethodLength
|
23
27
|
def destroy
|
24
28
|
oauth2_error('unsupported_token_type') && return unless params[:token_type_hint] == 'access_token'
|
25
29
|
|
@@ -48,7 +52,7 @@ class Oauth2Controller < ApplicationController
|
|
48
52
|
|
49
53
|
render json: { access_token: login.oauth2_token }
|
50
54
|
rescue FacebookAuthenticator::ApiError
|
51
|
-
|
55
|
+
head 502
|
52
56
|
end
|
53
57
|
|
54
58
|
def authenticate_with_google(auth_code)
|
@@ -58,7 +62,18 @@ class Oauth2Controller < ApplicationController
|
|
58
62
|
|
59
63
|
render json: { access_token: login.oauth2_token }
|
60
64
|
rescue GoogleAuthenticator::ApiError
|
61
|
-
|
65
|
+
head 502
|
66
|
+
end
|
67
|
+
|
68
|
+
def authenticate_with_edx(username, auth_code)
|
69
|
+
oauth2_error('no_authorization_code') && return unless auth_code.present?
|
70
|
+
oauth2_error('no_username') && return unless username.present?
|
71
|
+
|
72
|
+
login = EdxAuthenticator.new(username, auth_code).authenticate!
|
73
|
+
|
74
|
+
render json: { access_token: login.oauth2_token }
|
75
|
+
rescue EdxAuthenticator::ApiError
|
76
|
+
head 502
|
62
77
|
end
|
63
78
|
|
64
79
|
def oauth2_error(error)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
# Handles Edx authentication
|
4
|
+
#
|
5
|
+
# @!visibility private
|
6
|
+
class EdxAuthenticator < BaseAuthenticator
|
7
|
+
|
8
|
+
PROVIDER = 'edx'.freeze
|
9
|
+
DOMAIN = 'http://' + RailsApiAuth.edx_domain
|
10
|
+
TOKEN_URL = (DOMAIN + '/oauth2/access_token').freeze
|
11
|
+
PROFILE_URL = (DOMAIN + '/api/user/v1/accounts/%{username}').freeze
|
12
|
+
|
13
|
+
def initialize(username, auth_code)
|
14
|
+
@auth_code = auth_code
|
15
|
+
@username = username
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def connect_login_to_account(login, user)
|
21
|
+
login.update_attributes!(uid: user[:username], provider: PROVIDER)
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_login_from_account(user)
|
25
|
+
login_attributes = {
|
26
|
+
identification: user[:email],
|
27
|
+
uid: user[:username],
|
28
|
+
provider: PROVIDER
|
29
|
+
}
|
30
|
+
Login.create!(login_attributes)
|
31
|
+
end
|
32
|
+
|
33
|
+
def access_token
|
34
|
+
response = HTTParty.post(TOKEN_URL, token_options)
|
35
|
+
response.parsed_response['access_token']
|
36
|
+
end
|
37
|
+
|
38
|
+
# Override base authenticator
|
39
|
+
def get_request(url, headers)
|
40
|
+
response = HTTParty.get(url, headers: headers)
|
41
|
+
unless response.code == 200
|
42
|
+
Rails.logger.warn "#{self.class::PROVIDER} API request failed with status #{response.code}."
|
43
|
+
Rails.logger.debug "#{self.class::PROVIDER} API error response was:\n#{response.body}"
|
44
|
+
raise ApiError.new
|
45
|
+
end
|
46
|
+
response
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_user(access_token)
|
50
|
+
headers = { 'Authorization' => "Bearer #{access_token}" }
|
51
|
+
@edx_user ||= begin
|
52
|
+
get_request(user_url, headers).parsed_response.symbolize_keys
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def user_url
|
57
|
+
PROFILE_URL % { username: @username }
|
58
|
+
end
|
59
|
+
|
60
|
+
def token_options
|
61
|
+
@token_options ||= {
|
62
|
+
body: {
|
63
|
+
code: @auth_code,
|
64
|
+
client_id: RailsApiAuth.edx_client_id,
|
65
|
+
client_secret: RailsApiAuth.edx_client_secret,
|
66
|
+
redirect_uri: RailsApiAuth.edx_redirect_uri,
|
67
|
+
grant_type: 'authorization_code'
|
68
|
+
}
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
data/lib/rails_api_auth.rb
CHANGED
@@ -36,6 +36,22 @@ module RailsApiAuth
|
|
36
36
|
# The Google App's redirect URI.
|
37
37
|
mattr_accessor :google_redirect_uri
|
38
38
|
|
39
|
+
# @!attribute [rw] edx_client_id
|
40
|
+
# The Edx client ID.
|
41
|
+
mattr_accessor :edx_client_id
|
42
|
+
|
43
|
+
# @!attribute [rw] edx_client_secret
|
44
|
+
# The Edx client secret.
|
45
|
+
mattr_accessor :edx_client_secret
|
46
|
+
|
47
|
+
# @!attribute [rw] edx_redirect_uri
|
48
|
+
# The Edx App's redirect URI.
|
49
|
+
mattr_accessor :edx_redirect_uri
|
50
|
+
|
51
|
+
# @!attribute [rw] edx_domain
|
52
|
+
# The domain used for the Edx oauth2 provider
|
53
|
+
mattr_accessor :edx_domain
|
54
|
+
|
39
55
|
# @!attribute [rw] primary_key_type
|
40
56
|
# Configures database column type used for primary keys,
|
41
57
|
# currently only accepts :uuid
|
@@ -2,10 +2,18 @@ class AccessOnceController < ApplicationController
|
|
2
2
|
|
3
3
|
include RailsApiAuth::Authentication
|
4
4
|
|
5
|
-
|
5
|
+
if Rails::VERSION::MAJOR < 4
|
6
|
+
before_filter :consume_single_use_oauth2_token!
|
7
|
+
else
|
8
|
+
before_action :consume_single_use_oauth2_token!
|
9
|
+
end
|
6
10
|
|
7
11
|
def index
|
8
|
-
|
12
|
+
if Rails::VERSION::MAJOR < 4
|
13
|
+
render text: 'zuper content', status: 200
|
14
|
+
else
|
15
|
+
render plain: 'zuper content', status: 200
|
16
|
+
end
|
9
17
|
end
|
10
18
|
|
11
19
|
end
|
@@ -2,10 +2,18 @@ class AuthenticatedController < ApplicationController
|
|
2
2
|
|
3
3
|
include RailsApiAuth::Authentication
|
4
4
|
|
5
|
-
|
5
|
+
if Rails::VERSION::MAJOR < 4
|
6
|
+
before_filter :authenticate!
|
7
|
+
else
|
8
|
+
before_action :authenticate!
|
9
|
+
end
|
6
10
|
|
7
11
|
def index
|
8
|
-
|
12
|
+
if Rails::VERSION::MAJOR < 4
|
13
|
+
render text: 'zuper content', status: 200
|
14
|
+
else
|
15
|
+
render plain: 'zuper content', status: 200
|
16
|
+
end
|
9
17
|
end
|
10
18
|
|
11
19
|
end
|
@@ -2,10 +2,18 @@ class CustomAuthenticatedController < ApplicationController
|
|
2
2
|
|
3
3
|
include RailsApiAuth::Authentication
|
4
4
|
|
5
|
-
|
5
|
+
if Rails::VERSION::MAJOR < 4
|
6
|
+
before_filter :authenticate_with_account!
|
7
|
+
else
|
8
|
+
before_action :authenticate_with_account!
|
9
|
+
end
|
6
10
|
|
7
11
|
def index
|
8
|
-
|
12
|
+
if Rails::VERSION::MAJOR < 4
|
13
|
+
render text: 'zuper content', status: 200
|
14
|
+
else
|
15
|
+
render plain: 'zuper content', status: 200
|
16
|
+
end
|
9
17
|
end
|
10
18
|
|
11
19
|
private
|
@@ -22,7 +22,11 @@ Dummy::Application.configure do
|
|
22
22
|
|
23
23
|
# Disable serving static files from the `/public` folder by default since
|
24
24
|
# Apache or NGINX already handles this.
|
25
|
-
|
25
|
+
if Rails::VERSION::MAJOR < 5
|
26
|
+
config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
|
27
|
+
else
|
28
|
+
config.public_file_server.enabled = false
|
29
|
+
end
|
26
30
|
|
27
31
|
# Compress JavaScripts and CSS.
|
28
32
|
config.assets.js_compressor = :uglifier
|
@@ -13,8 +13,13 @@ Dummy::Application.configure do
|
|
13
13
|
config.eager_load = false
|
14
14
|
|
15
15
|
# Configure static file server for tests with Cache-Control for performance.
|
16
|
-
|
17
|
-
|
16
|
+
if Rails::VERSION::MAJOR < 5
|
17
|
+
config.serve_static_files = true
|
18
|
+
config.static_cache_control = 'public, max-age=3600'
|
19
|
+
else
|
20
|
+
config.public_file_server.enabled = true
|
21
|
+
config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }
|
22
|
+
end
|
18
23
|
|
19
24
|
# Show full error reports and disable caching.
|
20
25
|
config.consider_all_requests_local = true
|
@@ -8,4 +8,9 @@ RailsApiAuth.tap do |raa|
|
|
8
8
|
raa.google_client_id = 'google_client_id'
|
9
9
|
raa.google_client_secret = 'google_client_secret'
|
10
10
|
raa.google_redirect_uri = 'google_redirect_uri'
|
11
|
+
|
12
|
+
raa.edx_client_id = 'edx_client_id'
|
13
|
+
raa.edx_client_secret = 'edx_client_secret'
|
14
|
+
raa.edx_domain = 'edxdomain.org'
|
15
|
+
raa.edx_redirect_uri = 'edx_redirect_uri'
|
11
16
|
end
|
data/spec/models/login_spec.rb
CHANGED
@@ -7,8 +7,8 @@ describe Login do
|
|
7
7
|
|
8
8
|
describe 'validations' do
|
9
9
|
before do
|
10
|
-
Login.
|
11
|
-
Login.
|
10
|
+
allow_any_instance_of(Login).to receive(:ensure_oauth2_token).and_return(true)
|
11
|
+
allow_any_instance_of(Login).to receive(:assign_single_use_oauth2_token).and_return(true)
|
12
12
|
end
|
13
13
|
|
14
14
|
it { is_expected.to validate_presence_of(:identification) }
|
@@ -1,5 +1,11 @@
|
|
1
1
|
describe 'an access-once route' do
|
2
|
-
|
2
|
+
if Rails::VERSION::MAJOR < 5
|
3
|
+
# rubocop:disable Rails/HttpPositionalArguments
|
4
|
+
subject { get '/access-once', {}, headers }
|
5
|
+
# rubocop:enable Rails/HttpPositionalArguments
|
6
|
+
else
|
7
|
+
subject { get '/access-once', params: {}, headers: headers }
|
8
|
+
end
|
3
9
|
|
4
10
|
let(:login) { create(:login) }
|
5
11
|
let(:headers) do
|
@@ -52,7 +58,13 @@ describe 'an access-once route' do
|
|
52
58
|
|
53
59
|
context 'when accessed a second time with the same token' do
|
54
60
|
before do
|
55
|
-
|
61
|
+
if Rails::VERSION::MAJOR < 5
|
62
|
+
# rubocop:disable Rails/HttpPositionalArguments
|
63
|
+
get '/access-once', {}, headers
|
64
|
+
# rubocop:enable Rails/HttpPositionalArguments
|
65
|
+
else
|
66
|
+
get '/access-once', params: {}, headers: headers
|
67
|
+
end
|
56
68
|
end
|
57
69
|
|
58
70
|
it_behaves_like 'when access is not allowed'
|
@@ -1,5 +1,11 @@
|
|
1
1
|
describe 'an authenticated route' do
|
2
|
-
|
2
|
+
if Rails::VERSION::MAJOR < 5
|
3
|
+
# rubocop:disable Rails/HttpPositionalArguments
|
4
|
+
subject { get '/authenticated', {}, headers }
|
5
|
+
# rubocop:enable Rails/HttpPositionalArguments
|
6
|
+
else
|
7
|
+
subject { get '/authenticated', params: {}, headers: headers }
|
8
|
+
end
|
3
9
|
|
4
10
|
let(:headers) { {} }
|
5
11
|
|
@@ -1,11 +1,20 @@
|
|
1
1
|
describe 'a custom authenticated route' do
|
2
|
-
|
2
|
+
if Rails::VERSION::MAJOR < 5
|
3
|
+
# rubocop:disable Rails/HttpPositionalArguments
|
4
|
+
subject { get '/custom-authenticated', {}, headers }
|
5
|
+
# rubocop:enable Rails/HttpPositionalArguments
|
6
|
+
let(:headers) do
|
7
|
+
{ 'Authorization' => "Bearer #{login.oauth2_token}" }
|
8
|
+
end
|
9
|
+
else
|
10
|
+
subject { get '/custom-authenticated', params: {}, headers: headers }
|
11
|
+
let(:headers) do
|
12
|
+
{ HTTP_AUTHORIZATION: "Bearer #{login.oauth2_token}" }
|
13
|
+
end
|
14
|
+
end
|
3
15
|
|
4
16
|
let(:account) { create(:account) }
|
5
17
|
let(:login) { create(:login, account: account) }
|
6
|
-
let(:headers) do
|
7
|
-
{ 'Authorization' => "Bearer #{login.oauth2_token}" }
|
8
|
-
end
|
9
18
|
|
10
19
|
context 'when the block returns true' do
|
11
20
|
let(:account) { create(:account, first_name: 'user x') }
|
@@ -3,7 +3,13 @@ describe 'Oauth2 API' do
|
|
3
3
|
|
4
4
|
describe 'POST /token' do
|
5
5
|
let(:params) { { grant_type: 'password', username: login.identification, password: login.password } }
|
6
|
-
|
6
|
+
if Rails::VERSION::MAJOR < 5
|
7
|
+
# rubocop:disable Rails/HttpPositionalArguments
|
8
|
+
subject { post '/token', params, 'HTTPS' => ssl }
|
9
|
+
# rubocop:enable Rails/HttpPositionalArguments
|
10
|
+
else
|
11
|
+
subject { post '/token', params: params, headers: { 'HTTPS' => ssl } }
|
12
|
+
end
|
7
13
|
|
8
14
|
shared_examples 'when the request gets through' do
|
9
15
|
context 'for grant_type "password"' do
|
@@ -68,6 +74,21 @@ describe 'Oauth2 API' do
|
|
68
74
|
include_examples 'oauth2 shared contexts'
|
69
75
|
end
|
70
76
|
|
77
|
+
context 'for grant_type "edx_auth_code"' do
|
78
|
+
let(:authenticated_user_data) do
|
79
|
+
{
|
80
|
+
username: 'user',
|
81
|
+
email: email
|
82
|
+
}
|
83
|
+
end
|
84
|
+
let(:uid_mapped_field) { 'username' }
|
85
|
+
let(:grant_type) { 'edx_auth_code' }
|
86
|
+
let(:profile_url) { EdxAuthenticator::PROFILE_URL }
|
87
|
+
|
88
|
+
include_context 'stubbed edx requests'
|
89
|
+
include_examples 'oauth2 edx shared contexts'
|
90
|
+
end
|
91
|
+
|
71
92
|
context 'for an unknown grant type' do
|
72
93
|
let(:params) { { grant_type: 'UNKNOWN' } }
|
73
94
|
|
@@ -126,7 +147,16 @@ describe 'Oauth2 API' do
|
|
126
147
|
|
127
148
|
describe 'POST #destroy' do
|
128
149
|
let(:params) { { token_type_hint: 'access_token', token: login.oauth2_token } }
|
129
|
-
|
150
|
+
|
151
|
+
if Rails::VERSION::MAJOR < 5
|
152
|
+
# rubocop:disable Rails/HttpPositionalArguments
|
153
|
+
subject { get '/access-once', {}, headers }
|
154
|
+
subject { post '/revoke', params, 'HTTPS' => ssl }
|
155
|
+
# rubocop:enable Rails/HttpPositionalArguments
|
156
|
+
else
|
157
|
+
subject { get '/access-once', params: {}, headers: headers }
|
158
|
+
subject { post '/revoke', params: params, headers: { 'HTTPS' => ssl } }
|
159
|
+
end
|
130
160
|
|
131
161
|
shared_examples 'when the request gets through' do
|
132
162
|
it 'responds with status 200' do
|