rails_api_auth 0.0.2

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 (65) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +3 -0
  4. data/Rakefile +20 -0
  5. data/app/controllers/oauth2_controller.rb +54 -0
  6. data/app/controllers/rails_api_auth/application_controller.rb +7 -0
  7. data/app/lib/login_not_found.rb +9 -0
  8. data/app/models/login.rb +47 -0
  9. data/app/services/facebook_authenticator.rb +64 -0
  10. data/config/initializers/facebook.rb +6 -0
  11. data/config/routes.rb +4 -0
  12. data/db/migrate/20150709221755_create_logins.rb +16 -0
  13. data/lib/rails_api_auth.rb +5 -0
  14. data/lib/rails_api_auth/authentication.rb +32 -0
  15. data/lib/rails_api_auth/engine.rb +19 -0
  16. data/lib/rails_api_auth/version.rb +5 -0
  17. data/lib/tasks/rails_api_auth_tasks.rake +4 -0
  18. data/spec/dummy/README.rdoc +28 -0
  19. data/spec/dummy/Rakefile +6 -0
  20. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  21. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  22. data/spec/dummy/app/controllers/application_controller.rb +7 -0
  23. data/spec/dummy/app/controllers/authenticated_controller.rb +13 -0
  24. data/spec/dummy/bin/bundle +3 -0
  25. data/spec/dummy/bin/rails +4 -0
  26. data/spec/dummy/bin/rake +4 -0
  27. data/spec/dummy/bin/setup +29 -0
  28. data/spec/dummy/config.ru +4 -0
  29. data/spec/dummy/config/application.rb +18 -0
  30. data/spec/dummy/config/boot.rb +5 -0
  31. data/spec/dummy/config/database.yml +25 -0
  32. data/spec/dummy/config/environment.rb +5 -0
  33. data/spec/dummy/config/environments/development.rb +38 -0
  34. data/spec/dummy/config/environments/production.rb +79 -0
  35. data/spec/dummy/config/environments/test.rb +37 -0
  36. data/spec/dummy/config/initializers/assets.rb +11 -0
  37. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  38. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  39. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  40. data/spec/dummy/config/initializers/inflections.rb +16 -0
  41. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  42. data/spec/dummy/config/initializers/session_store.rb +3 -0
  43. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  44. data/spec/dummy/config/locales/en.yml +23 -0
  45. data/spec/dummy/config/routes.rb +3 -0
  46. data/spec/dummy/config/secrets.yml +22 -0
  47. data/spec/dummy/db/development.sqlite3 +0 -0
  48. data/spec/dummy/db/migrate/20150709221900_create_users.rb +11 -0
  49. data/spec/dummy/db/production.sqlite3 +0 -0
  50. data/spec/dummy/db/schema.rb +34 -0
  51. data/spec/dummy/db/test.sqlite3 +0 -0
  52. data/spec/dummy/log/development.log +16 -0
  53. data/spec/dummy/log/test.log +8350 -0
  54. data/spec/dummy/public/404.html +67 -0
  55. data/spec/dummy/public/422.html +67 -0
  56. data/spec/dummy/public/500.html +66 -0
  57. data/spec/dummy/public/favicon.ico +0 -0
  58. data/spec/factories/logins.rb +11 -0
  59. data/spec/models/login_spec.rb +69 -0
  60. data/spec/requests/authenticated_spec.rb +47 -0
  61. data/spec/requests/oauth2_spec.rb +187 -0
  62. data/spec/services/facebook_authenticator_spec.rb +54 -0
  63. data/spec/spec_helper.rb +19 -0
  64. data/spec/support/factory_girl.rb +3 -0
  65. metadata +233 -0
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/404.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The page you were looking for doesn't exist.</h1>
62
+ <p>You may have mistyped the address or the page may have moved.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/422.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>The change you wanted was rejected.</h1>
62
+ <p>Maybe you tried to change something you didn't have access to.</p>
63
+ </div>
64
+ <p>If you are the application owner check the logs for more information.</p>
65
+ </div>
66
+ </body>
67
+ </html>
@@ -0,0 +1,66 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <style>
7
+ body {
8
+ background-color: #EFEFEF;
9
+ color: #2E2F30;
10
+ text-align: center;
11
+ font-family: arial, sans-serif;
12
+ margin: 0;
13
+ }
14
+
15
+ div.dialog {
16
+ width: 95%;
17
+ max-width: 33em;
18
+ margin: 4em auto 0;
19
+ }
20
+
21
+ div.dialog > div {
22
+ border: 1px solid #CCC;
23
+ border-right-color: #999;
24
+ border-left-color: #999;
25
+ border-bottom-color: #BBB;
26
+ border-top: #B00100 solid 4px;
27
+ border-top-left-radius: 9px;
28
+ border-top-right-radius: 9px;
29
+ background-color: white;
30
+ padding: 7px 12% 0;
31
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 100%;
36
+ color: #730E15;
37
+ line-height: 1.5em;
38
+ }
39
+
40
+ div.dialog > p {
41
+ margin: 0 0 1em;
42
+ padding: 1em;
43
+ background-color: #F7F7F7;
44
+ border: 1px solid #CCC;
45
+ border-right-color: #999;
46
+ border-left-color: #999;
47
+ border-bottom-color: #999;
48
+ border-bottom-left-radius: 4px;
49
+ border-bottom-right-radius: 4px;
50
+ border-top-color: #DADADA;
51
+ color: #666;
52
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
53
+ }
54
+ </style>
55
+ </head>
56
+
57
+ <body>
58
+ <!-- This file lives in public/500.html -->
59
+ <div class="dialog">
60
+ <div>
61
+ <h1>We're sorry, but something went wrong.</h1>
62
+ </div>
63
+ <p>If you are the application owner check the logs for more information.</p>
64
+ </div>
65
+ </body>
66
+ </html>
File without changes
@@ -0,0 +1,11 @@
1
+ FactoryGirl.define do
2
+ factory :login do
3
+ email { Faker::Internet.email }
4
+ password { Faker::Lorem.word }
5
+
6
+ trait :facebook do
7
+ facebook_uid { Faker::Number.number }
8
+ password nil
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Login do
4
+ it { is_expected.to validate_presence_of(:email) }
5
+ it { is_expected.to allow_value('test@example.com').for(:email) }
6
+ it { is_expected.to_not allow_value('test_example.com').for(:email) }
7
+
8
+ it 'validates presence of either password or Facebook UID' do
9
+ login = described_class.new(email: 'test@example.com', oauth2_token: 'token')
10
+
11
+ expect(login).to_not be_valid
12
+ end
13
+
14
+ it "doesn't validate presence of password when Facebook UID is present" do
15
+ login = described_class.new(email: 'test@example.com', oauth2_token: 'token', facebook_uid: '123')
16
+
17
+ expect(login).to be_valid
18
+ end
19
+
20
+ it "doesn't validate presence of Facebook UID when password is present" do
21
+ login = described_class.new(email: 'test@example.com', oauth2_token: 'token', password: '123')
22
+
23
+ expect(login).to be_valid
24
+ end
25
+
26
+ describe '#refresh_oauth2_token!' do
27
+ subject { described_class.new(oauth2_token: 'oldtoken') }
28
+
29
+ before do
30
+ allow(subject).to receive(:save!)
31
+ end
32
+
33
+ it 'force-resets the oauth2 token' do
34
+ expect { subject.refresh_oauth2_token! }.to change(subject, :oauth2_token)
35
+ end
36
+
37
+ it 'saves the model' do
38
+ expect(subject).to receive(:save!)
39
+
40
+ subject.refresh_oauth2_token!
41
+ end
42
+ end
43
+
44
+ describe '#consume_single_use_oauth2_token!' do
45
+ subject { described_class.new(single_use_oauth2_token: 'token') }
46
+
47
+ before do
48
+ allow(subject).to receive(:save!)
49
+ end
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
61
+ end
62
+
63
+ context 'when the supplied token is invalid' do
64
+ it 'raises an InvalidSingleUseOAuth2Token' do
65
+ expect { subject.consume_single_use_oauth2_token!('invalid token') }.to raise_error(Login::InvalidSingleUseOAuth2Token)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Authenticated route' do
4
+ let!(:login) { create(:login) }
5
+ let(:headers) do
6
+ {
7
+ 'Authorization': "Bearer #{login.oauth2_token}"
8
+ }
9
+ end
10
+
11
+ subject { get '/authenticated', {}, headers }
12
+
13
+ it 'assigns login found to @current_login' do
14
+ subject
15
+
16
+ assigns[:current_login] = login
17
+ end
18
+
19
+ it '200' do
20
+ subject
21
+
22
+ expect(response.status).to eq 200
23
+ end
24
+
25
+ it 'lets the action get rendered' do
26
+ subject
27
+
28
+ expect(response.body).to eql 'zuper content'
29
+ end
30
+
31
+ context 'no token' do
32
+
33
+ subject { get '/authenticated' }
34
+
35
+ it '401' do
36
+ subject
37
+
38
+ expect(response.status).to eq 401
39
+ end
40
+
41
+ it 'responds with an empty body' do
42
+ subject
43
+
44
+ expect(response.body).to be_empty
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,187 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Oauth2 API' do
4
+
5
+ let!(:login) { create(:login) }
6
+
7
+ describe 'POST /token' do
8
+ let(:params) { { grant_type: 'password', username: login.email, password: login.password } }
9
+
10
+ subject { post '/token', params }
11
+
12
+ context 'for grant_type "password"' do
13
+ context 'with valid login credentials' do
14
+ it 'succeeds' do
15
+ subject
16
+
17
+ expect(response).to have_http_status(200)
18
+ end
19
+
20
+ it 'responds with an access token' do
21
+ subject
22
+
23
+ expect(response.body).to be_json_eql({ access_token: login.oauth2_token }.to_json)
24
+ end
25
+ end
26
+
27
+ context 'with invalid login credentials' do
28
+ let(:params) { { grant_type: 'password', username: 'bad@email.com', password: 'badpassword' } }
29
+
30
+ it 'responds with status 400' do
31
+ subject
32
+
33
+ expect(response).to have_http_status(400)
34
+ end
35
+
36
+ it 'responds with an invalid grant error' do
37
+ subject
38
+
39
+ expect(response.body).to be_json_eql({ error: 'invalid_grant' }.to_json)
40
+ end
41
+ end
42
+ end
43
+
44
+ context 'for grant_type "facebook_auth_code"' do
45
+ let(:secret) { described_class::FB_APP_SECRET }
46
+ let(:params) { { grant_type: 'facebook_auth_code', auth_code: 'fb auth code' } }
47
+ let(:facebook_email) { login.email }
48
+ let(:facebook_data) do
49
+ {
50
+ id: '1238190321',
51
+ email: facebook_email
52
+ }
53
+ end
54
+
55
+ before do
56
+ stub_request(:get, %r{https://graph.facebook.com/v2.3/oauth/access_token}).to_return(body: '{ "access_token": "access_token" }')
57
+ stub_request(:get, %r{https://graph.facebook.com/v2.3/me}).to_return(body: JSON.generate(facebook_data), headers: { 'Content-Type' => 'application/json' })
58
+ end
59
+
60
+ context 'when a login with the posted Facebook email exists' do
61
+ it 'connects the login to the Facebook account' do
62
+ subject
63
+
64
+ expect(login.reload.facebook_uid).to eq(facebook_data[:id])
65
+ end
66
+
67
+ it 'succeeds' do
68
+ subject
69
+
70
+ expect(response).to have_http_status(200)
71
+ end
72
+
73
+ it 'responds with an oauth2 token' do
74
+ subject
75
+
76
+ expect(response.body).to be_json_eql({ access_token: login.oauth2_token }.to_json)
77
+ end
78
+ end
79
+
80
+ context 'when no login with the posted Facebook email exists' do
81
+ let(:facebook_email) { Faker::Internet.email }
82
+
83
+ it 'succeeds' do
84
+ subject
85
+
86
+ expect(response).to have_http_status(200)
87
+ end
88
+
89
+ it 'creates a login with it' do
90
+ expect { subject }.to change { Login.where(email: facebook_email).count }.by(1)
91
+ end
92
+
93
+ it 'responds with an oauth2 token' do
94
+ subject
95
+ login = Login.find_by(email: facebook_email)
96
+
97
+ expect(response.body).to be_json_eql({ access_token: login.oauth2_token }.to_json)
98
+ end
99
+ end
100
+
101
+ context 'when no facebook code is sent' do
102
+ let(:params) { { grant_type: 'facebook_auth_code' } }
103
+
104
+ it 'responds with status 400' do
105
+ subject
106
+
107
+ expect(response).to have_http_status(400)
108
+ end
109
+
110
+ it 'responds with a no authorization code error' do
111
+ subject
112
+
113
+ expect(response.body).to be_json_eql({ error: 'no_authorization_code' }.to_json)
114
+ end
115
+ end
116
+
117
+ context 'when Facebook responds with an error' do
118
+ before do
119
+ stub_request(:get, %r{https://graph.facebook.com/v2.3/oauth/access_token}).to_return(status: 422)
120
+ end
121
+
122
+ it 'responds with status 500' do
123
+ subject
124
+
125
+ expect(response).to have_http_status(500)
126
+ end
127
+
128
+ it 'responds with an empty response body' do
129
+ subject
130
+
131
+ expect(response.body).to eql('')
132
+ end
133
+ end
134
+ end
135
+
136
+ context 'for an unknown grant type' do
137
+ let(:params) { { grant_type: 'UNKNOWN' } }
138
+
139
+ it 'responds with status 400' do
140
+ subject
141
+
142
+ expect(response).to have_http_status(400)
143
+ end
144
+
145
+ it 'responds with an invalid grant error' do
146
+ subject
147
+
148
+ expect(response.body).to be_json_eql({ error: 'unsupported_grant_type' }.to_json)
149
+ end
150
+ end
151
+ end
152
+
153
+ describe 'POST #destroy' do
154
+ let(:params) { { token_type_hint: 'access_token', token: login.oauth2_token } }
155
+
156
+ subject { post '/revoke', params }
157
+
158
+ it 'succeeds' do
159
+ subject
160
+
161
+ expect(response).to have_http_status(200)
162
+ end
163
+
164
+ it 'resets login token' do
165
+ expect { subject }.to change { login.reload.oauth2_token }
166
+
167
+ subject
168
+ end
169
+
170
+ context 'for an unknown (or stale) token' do
171
+ let(:params) { { token_type_hint: 'access_token', token: 'badtoken' } }
172
+
173
+ it 'succeeds' do
174
+ subject
175
+
176
+ expect(response).to have_http_status(200)
177
+ end
178
+
179
+ it "doesn't reset any logins' token" do
180
+
181
+ expect_any_instance_of(LoginNotFound).to receive(:refresh_oauth2_token!)
182
+
183
+ subject
184
+ end
185
+ end
186
+ end
187
+ end