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
data/spec/factories/logins.rb
CHANGED
data/spec/models/login_spec.rb
CHANGED
@@ -12,7 +12,7 @@ describe Login do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
it "doesn't validate presence of password when Facebook UID is present" do
|
15
|
-
login = described_class.new(identification: 'test@example.com', oauth2_token: 'token',
|
15
|
+
login = described_class.new(identification: 'test@example.com', oauth2_token: 'token', uid: '123', provider: 'facebook')
|
16
16
|
|
17
17
|
expect(login).to be_valid
|
18
18
|
end
|
@@ -41,29 +41,21 @@ describe Login do
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
describe '#
|
45
|
-
subject { described_class.new(single_use_oauth2_token: '
|
44
|
+
describe '#refresh_single_use_oauth2_token!' do
|
45
|
+
subject { described_class.new(single_use_oauth2_token: 'oldtoken') }
|
46
46
|
|
47
47
|
before do
|
48
48
|
allow(subject).to receive(:save!)
|
49
49
|
end
|
50
50
|
|
51
|
-
|
52
|
-
|
53
|
-
expect { subject.consume_single_use_oauth2_token!(subject.single_use_oauth2_token) }.to change(subject, :single_use_oauth2_token)
|
54
|
-
end
|
55
|
-
|
56
|
-
it 'saves the model' do
|
57
|
-
expect(subject).to receive(:save!)
|
58
|
-
|
59
|
-
subject.refresh_oauth2_token!
|
60
|
-
end
|
51
|
+
it 'force-resets the single oauth2 token' do
|
52
|
+
expect { subject.refresh_single_use_oauth2_token! }.to change(subject, :single_use_oauth2_token)
|
61
53
|
end
|
62
54
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
55
|
+
it 'saves the model' do
|
56
|
+
expect(subject).to receive(:save!)
|
57
|
+
|
58
|
+
subject.refresh_single_use_oauth2_token!
|
67
59
|
end
|
68
60
|
end
|
69
61
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
describe 'an access-once route' do
|
2
|
+
subject { get '/access-once', {}, headers }
|
3
|
+
|
4
|
+
let(:login) { create(:login) }
|
5
|
+
let(:headers) do
|
6
|
+
{ 'Authorization' => "Bearer #{login.single_use_oauth2_token}" }
|
7
|
+
end
|
8
|
+
|
9
|
+
context 'when a valid Bearer token is present' do
|
10
|
+
it 'assigns the authenticated login to @current_login' do
|
11
|
+
subject
|
12
|
+
|
13
|
+
expect(assigns[:current_login]).to eq(login)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "responds with the actual action's status" do
|
17
|
+
subject
|
18
|
+
|
19
|
+
expect(response).to have_http_status(200)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "responds with the actual action's body" do
|
23
|
+
subject
|
24
|
+
|
25
|
+
expect(response.body).to eql('zuper content')
|
26
|
+
end
|
27
|
+
|
28
|
+
it "changes the login's single_use_oauth2_token" do
|
29
|
+
expect { subject }.to change { login.reload.single_use_oauth2_token }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
shared_examples 'when access is not allowed' do
|
34
|
+
it 'does not assign the authenticated login to @current_login' do
|
35
|
+
subject
|
36
|
+
|
37
|
+
expect(assigns[:current_login]).to be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'responds with status 401' do
|
41
|
+
subject
|
42
|
+
|
43
|
+
expect(response).to have_http_status(401)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'responds with an empty body' do
|
47
|
+
subject
|
48
|
+
|
49
|
+
expect(response.body.strip).to be_empty
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when accessed a second time with the same token' do
|
54
|
+
before do
|
55
|
+
get '/access-once', {}, headers
|
56
|
+
end
|
57
|
+
|
58
|
+
it_behaves_like 'when access is not allowed'
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'when no valid Bearer token is present' do
|
62
|
+
let(:headers) { {} }
|
63
|
+
|
64
|
+
it_behaves_like 'when access is not allowed'
|
65
|
+
end
|
66
|
+
end
|
@@ -39,95 +39,31 @@ describe 'Oauth2 API' do
|
|
39
39
|
end
|
40
40
|
|
41
41
|
context 'for grant_type "facebook_auth_code"' do
|
42
|
-
let(:
|
43
|
-
let(:params) { { grant_type: 'facebook_auth_code', auth_code: 'authcode' } }
|
44
|
-
let(:facebook_email) { login.identification }
|
45
|
-
let(:facebook_data) do
|
42
|
+
let(:authenticated_user_data) do
|
46
43
|
{
|
47
44
|
id: '1238190321',
|
48
|
-
email:
|
45
|
+
email: email
|
49
46
|
}
|
50
47
|
end
|
48
|
+
let(:uid_mapped_field) { 'id' }
|
49
|
+
let(:grant_type) { 'facebook_auth_code' }
|
50
|
+
let(:profile_url) { FacebookAuthenticator::PROFILE_URL }
|
51
|
+
include_context 'stubbed facebook requests'
|
52
|
+
it_behaves_like 'oauth2 shared contexts'
|
53
|
+
end
|
51
54
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
it 'connects the login to the Facebook account' do
|
59
|
-
subject
|
60
|
-
|
61
|
-
expect(login.reload.facebook_uid).to eq(facebook_data[:id])
|
62
|
-
end
|
63
|
-
|
64
|
-
it 'responds with status 200' do
|
65
|
-
subject
|
66
|
-
|
67
|
-
expect(response).to have_http_status(200)
|
68
|
-
end
|
69
|
-
|
70
|
-
it "responds with the login's OAuth 2.0 token" do
|
71
|
-
subject
|
72
|
-
|
73
|
-
expect(response.body).to be_json_eql({ access_token: login.oauth2_token }.to_json)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
context 'when no login for the Facebook account exists' do
|
78
|
-
let(:facebook_email) { Faker::Internet.email }
|
79
|
-
|
80
|
-
it 'responds with status 200' do
|
81
|
-
subject
|
82
|
-
|
83
|
-
expect(response).to have_http_status(200)
|
84
|
-
end
|
85
|
-
|
86
|
-
it 'creates a login for the Facebook account' do
|
87
|
-
expect { subject }.to change { Login.where(identification: facebook_email).count }.by(1)
|
88
|
-
end
|
89
|
-
|
90
|
-
it "responds with the login's OAuth 2.0 token" do
|
91
|
-
subject
|
92
|
-
login = Login.where(identification: facebook_email).first
|
93
|
-
|
94
|
-
expect(response.body).to be_json_eql({ access_token: login.oauth2_token }.to_json)
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
context 'when no Facebook auth code is sent' do
|
99
|
-
let(:params) { { grant_type: 'facebook_auth_code' } }
|
100
|
-
|
101
|
-
it 'responds with status 400' do
|
102
|
-
subject
|
103
|
-
|
104
|
-
expect(response).to have_http_status(400)
|
105
|
-
end
|
106
|
-
|
107
|
-
it 'responds with a "no_authorization_code" error' do
|
108
|
-
subject
|
109
|
-
|
110
|
-
expect(response.body).to be_json_eql({ error: 'no_authorization_code' }.to_json)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
context 'when Facebook responds with an error' do
|
115
|
-
before do
|
116
|
-
stub_request(:get, 'https://graph.facebook.com/me?access_token=access_token').to_return(status: 422)
|
117
|
-
end
|
118
|
-
|
119
|
-
it 'responds with status 502' do
|
120
|
-
subject
|
121
|
-
|
122
|
-
expect(response).to have_http_status(502)
|
123
|
-
end
|
124
|
-
|
125
|
-
it 'responds with an empty response body' do
|
126
|
-
subject
|
127
|
-
|
128
|
-
expect(response.body.strip).to eql('')
|
129
|
-
end
|
55
|
+
context 'for grant_type "google_auth_code"' do
|
56
|
+
let(:authenticated_user_data) do
|
57
|
+
{
|
58
|
+
sub: '1238190321',
|
59
|
+
email: email
|
60
|
+
}
|
130
61
|
end
|
62
|
+
let(:uid_mapped_field) { 'sub' }
|
63
|
+
let(:grant_type) { 'google_auth_code' }
|
64
|
+
let(:profile_url) { GoogleAuthenticator::PROFILE_URL }
|
65
|
+
include_context 'stubbed google requests'
|
66
|
+
it_behaves_like 'oauth2 shared contexts'
|
131
67
|
end
|
132
68
|
|
133
69
|
context 'for an unknown grant type' do
|
@@ -1,52 +1,12 @@
|
|
1
1
|
describe FacebookAuthenticator do
|
2
|
-
|
3
|
-
let(:auth_code) { 'authcode' }
|
4
|
-
let(:email) { 'email@facebook.com' }
|
5
|
-
let(:facebook_data) do
|
6
|
-
{
|
7
|
-
id: '1238190321',
|
8
|
-
email: email
|
9
|
-
}
|
10
|
-
end
|
11
|
-
let(:response_with_fb_token) { { body: '{ "access_token": "access_token" }' } }
|
12
|
-
let(:response_with_fb_user) { { body: JSON.generate(facebook_data), headers: { 'Content-Type' => 'application/json' } } }
|
13
|
-
let(:login) { double('login') }
|
2
|
+
let(:uid_mapped_field) { 'id' }
|
14
3
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
context 'when no login for the Facebook account exists' do
|
23
|
-
let(:login_attributes) do
|
24
|
-
{
|
25
|
-
identification: facebook_data[:email],
|
26
|
-
facebook_uid: facebook_data[:id]
|
27
|
-
}
|
28
|
-
end
|
29
|
-
|
30
|
-
before do
|
31
|
-
allow(Login).to receive(:create!).with(login_attributes).and_return(login)
|
32
|
-
end
|
33
|
-
|
34
|
-
it 'returns a login created from the Facebook account' do
|
35
|
-
expect(subject).to eql(login)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
context 'when a login for the Facebook account exists already' do
|
40
|
-
before do
|
41
|
-
expect(Login).to receive(:where).with(identification: facebook_data[:email]).and_return([login])
|
42
|
-
allow(login).to receive(:update_attributes!).with(facebook_uid: facebook_data[:id])
|
43
|
-
end
|
44
|
-
|
45
|
-
it 'connects the login to the Facebook account' do
|
46
|
-
expect(login).to receive(:update_attributes!).with(facebook_uid: facebook_data[:id])
|
47
|
-
|
48
|
-
subject
|
49
|
-
end
|
50
|
-
end
|
4
|
+
let(:authenticated_user_data) do
|
5
|
+
{
|
6
|
+
email: 'user@facebook.com',
|
7
|
+
id: '123123123123'
|
8
|
+
}
|
51
9
|
end
|
10
|
+
include_context 'stubbed facebook requests'
|
11
|
+
it_behaves_like 'a authenticator'
|
52
12
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
shared_context 'stubbed facebook requests' do
|
2
|
+
let(:auth_code) { 'authcode' }
|
3
|
+
let(:access_token) { 'CAAMvEGOZAxB8BAODGpIWO9meEXEpvigfIRs5j7LIi1Uef8xvTz4vpayfP6rxn0Om3jZAmvEojZB9HNWD44PgSSwFyD7bKsJ3EaNMKwYpZBRqjm25HfwUzF3pOVRXp9cdquT1afm7bj4mnb4WFFo7TxLcgO848FaAKZBdxwefJlPneVUSpquEh2TZAVWghndnPO9ON7QTqXhAZDZD' }
|
4
|
+
let(:response_with_fb_token) { { body: JSON.generate({ access_token: access_token, token_type: 'bearer', expires_in: 5169402 }), headers: { 'Content-Type' => 'application/json' } } }
|
5
|
+
let(:response_with_fb_user) { { body: JSON.generate(authenticated_user_data), headers: { 'Content-Type' => 'application/json' } } }
|
6
|
+
let(:token_parameters) { { client_id: 'app_id', client_secret: 'app_secret', auth_code: auth_code, redirect_uri: 'redirect_uri' } }
|
7
|
+
|
8
|
+
before do
|
9
|
+
stub_request(:get, FacebookAuthenticator::TOKEN_URL % token_parameters).to_return(response_with_fb_token)
|
10
|
+
stub_request(:get, FacebookAuthenticator::PROFILE_URL % { access_token: access_token }).to_return(response_with_fb_user)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
shared_context 'stubbed google requests' do
|
2
|
+
let(:auth_code) { 'authcode' }
|
3
|
+
let(:response_with_token) { { body: '{ "access_token": "access_token" }, "token_type": "Bearer", "expires_in": 3600' } }
|
4
|
+
let(:response_with_user) { { body: JSON.generate(authenticated_user_data), headers: { 'Content-Type' => 'application/json' } } }
|
5
|
+
|
6
|
+
before do
|
7
|
+
stub_request(:post, GoogleAuthenticator::TOKEN_URL).
|
8
|
+
with(body: hash_including(grant_type: 'authorization_code')).to_return(response_with_token)
|
9
|
+
stub_request(:get, GoogleAuthenticator::PROFILE_URL % { access_token: 'access_token' }).to_return(response_with_user)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
shared_examples 'a authenticator' do
|
2
|
+
describe '#authenticate!' do
|
3
|
+
let(:login) { double('login') }
|
4
|
+
|
5
|
+
subject { described_class.new(auth_code).authenticate! }
|
6
|
+
|
7
|
+
context "when no login for the #{described_class::PROVIDER} account exists" do
|
8
|
+
let(:login_attributes) do
|
9
|
+
{
|
10
|
+
identification: authenticated_user_data[:email],
|
11
|
+
uid: authenticated_user_data[uid_mapped_field.to_sym],
|
12
|
+
provider: described_class::PROVIDER
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
before do
|
17
|
+
allow(Login).to receive(:create!).with(login_attributes).and_return(login)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "returns a login created from the #{described_class::PROVIDER} account" do
|
21
|
+
expect(subject).to eql(login)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "when a login for the #{described_class::PROVIDER} account exists already" do
|
26
|
+
before do
|
27
|
+
expect(Login).to receive(:where).with(identification: authenticated_user_data[:email]).and_return([login])
|
28
|
+
allow(login).to receive(:update_attributes!).with(uid: authenticated_user_data[uid_mapped_field.to_sym], provider: described_class::PROVIDER)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "connects the login to the #{described_class::PROVIDER} account" do
|
32
|
+
expect(login).to receive(:update_attributes!).with(uid: authenticated_user_data[uid_mapped_field.to_sym], provider: described_class::PROVIDER)
|
33
|
+
|
34
|
+
subject
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
shared_context 'oauth2 shared contexts' do
|
2
|
+
let(:params) { { grant_type: grant_type, auth_code: 'authcode' } }
|
3
|
+
let(:access_token) { 'access_token' }
|
4
|
+
let(:email) { login.identification }
|
5
|
+
|
6
|
+
context 'when a login with for the service account exists' do
|
7
|
+
it 'connects the login to the service account' do
|
8
|
+
subject
|
9
|
+
|
10
|
+
expect(login.reload.uid).to eq(authenticated_user_data[uid_mapped_field.to_sym])
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'responds with status 200' do
|
14
|
+
subject
|
15
|
+
|
16
|
+
expect(response).to have_http_status(200)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "responds with the login's OAuth 2.0 token" do
|
20
|
+
subject
|
21
|
+
|
22
|
+
expect(response.body).to be_json_eql({ access_token: login.oauth2_token }.to_json)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'when no login for the service account exists' do
|
27
|
+
let(:email) { Faker::Internet.email }
|
28
|
+
|
29
|
+
it 'responds with status 200' do
|
30
|
+
subject
|
31
|
+
|
32
|
+
expect(response).to have_http_status(200)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'creates a login for the service account' do
|
36
|
+
expect { subject }.to change { Login.where(identification: email).count }.by(1)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "responds with the login's OAuth 2.0 token" do
|
40
|
+
subject
|
41
|
+
login = Login.where(identification: email).first
|
42
|
+
|
43
|
+
expect(response.body).to be_json_eql({ access_token: login.oauth2_token }.to_json)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when no service auth code is sent' do
|
48
|
+
let(:params) { { grant_type: grant_type } }
|
49
|
+
|
50
|
+
it 'responds with status 400' do
|
51
|
+
subject
|
52
|
+
|
53
|
+
expect(response).to have_http_status(400)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'responds with a "no_authorization_code" error' do
|
57
|
+
subject
|
58
|
+
|
59
|
+
expect(response.body).to be_json_eql({ error: 'no_authorization_code' }.to_json)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'when service responds with an error' do
|
64
|
+
before do
|
65
|
+
stub_request(:get, profile_url % { access_token: access_token }).to_return(status: 422)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'responds with status 502' do
|
69
|
+
subject
|
70
|
+
|
71
|
+
expect(response).to have_http_status(502)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'responds with an empty response body' do
|
75
|
+
subject
|
76
|
+
|
77
|
+
expect(response.body.strip).to eql('')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|