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
@@ -4,8 +4,9 @@ FactoryGirl.define do
4
4
  password { Faker::Lorem.word }
5
5
 
6
6
  trait :facebook do
7
- facebook_uid { Faker::Number.number }
7
+ uid { Faker::Number.number }
8
8
  password nil
9
+ provider 'facebook'
9
10
  end
10
11
  end
11
12
  end
@@ -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', facebook_uid: '123')
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 '#consume_single_use_oauth2_token!' do
45
- subject { described_class.new(single_use_oauth2_token: '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
- context 'when the supplied token is valid' do
52
- it 'resets the single use oauth2 token' do
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
- context 'when the supplied token is invalid' do
64
- it 'raises an InvalidOAuth2Token' do
65
- expect { subject.consume_single_use_oauth2_token!('invalid token') }.to raise_error(Login::InvalidOAuth2Token)
66
- end
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
@@ -18,7 +18,7 @@ describe 'an authenticated route' do
18
18
  it "responds with the actual action's status" do
19
19
  subject
20
20
 
21
- expect(response).to have_http_status(201)
21
+ expect(response).to have_http_status(200)
22
22
  end
23
23
 
24
24
  it "responds with the actual action's body" do
@@ -19,7 +19,7 @@ describe 'a custom authenticated route' do
19
19
  it "responds with the actual action's status" do
20
20
  subject
21
21
 
22
- expect(response).to have_http_status(201)
22
+ expect(response).to have_http_status(200)
23
23
  end
24
24
 
25
25
  it "responds with the actual action's body" do
@@ -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(:secret) { described_class::FB_APP_SECRET }
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: facebook_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
- before do
53
- stub_request(:get, 'https://graph.facebook.com/oauth/access_token?client_id=app_id&client_secret=app_secret&code=authcode&redirect_uri=redirect_uri').to_return({ body: '{ "access_token": "access_token" }' })
54
- stub_request(:get, 'https://graph.facebook.com/me?access_token=access_token').to_return({ body: JSON.generate(facebook_data), headers: { 'Content-Type' => 'application/json' } })
55
- end
56
-
57
- context 'when a login with for the Facebook account exists' do
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
- describe '#authenticate!' do
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
- subject { described_class.new(auth_code).authenticate! }
16
-
17
- before do
18
- stub_request(:get, 'https://graph.facebook.com/oauth/access_token?client_id=app_id&client_secret=app_secret&code=authcode&redirect_uri=redirect_uri').to_return({ body: '{ "access_token": "access_token" }' })
19
- stub_request(:get, 'https://graph.facebook.com/me?access_token=access_token').to_return(response_with_fb_user)
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
+ describe GoogleAuthenticator do
2
+ let(:uid_mapped_field) { 'sub' }
3
+
4
+ let(:authenticated_user_data) do
5
+ {
6
+ email: 'user@gmail.com',
7
+ sub: '789789789789'
8
+ }
9
+ end
10
+ include_context 'stubbed google requests'
11
+ it_behaves_like 'a authenticator'
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