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.
- 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
|