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