songkick-oauth2-provider 0.10.0

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 (47) hide show
  1. data/README.rdoc +394 -0
  2. data/example/README.rdoc +11 -0
  3. data/example/application.rb +159 -0
  4. data/example/config.ru +3 -0
  5. data/example/environment.rb +11 -0
  6. data/example/models/connection.rb +9 -0
  7. data/example/models/note.rb +4 -0
  8. data/example/models/user.rb +6 -0
  9. data/example/public/style.css +78 -0
  10. data/example/schema.rb +27 -0
  11. data/example/views/authorize.erb +28 -0
  12. data/example/views/create_user.erb +3 -0
  13. data/example/views/error.erb +6 -0
  14. data/example/views/home.erb +25 -0
  15. data/example/views/layout.erb +25 -0
  16. data/example/views/login.erb +20 -0
  17. data/example/views/new_client.erb +25 -0
  18. data/example/views/new_user.erb +22 -0
  19. data/example/views/show_client.erb +15 -0
  20. data/lib/songkick/oauth2/model.rb +20 -0
  21. data/lib/songkick/oauth2/model/authorization.rb +126 -0
  22. data/lib/songkick/oauth2/model/client.rb +61 -0
  23. data/lib/songkick/oauth2/model/client_owner.rb +15 -0
  24. data/lib/songkick/oauth2/model/hashing.rb +29 -0
  25. data/lib/songkick/oauth2/model/resource_owner.rb +54 -0
  26. data/lib/songkick/oauth2/provider.rb +122 -0
  27. data/lib/songkick/oauth2/provider/access_token.rb +68 -0
  28. data/lib/songkick/oauth2/provider/authorization.rb +190 -0
  29. data/lib/songkick/oauth2/provider/error.rb +22 -0
  30. data/lib/songkick/oauth2/provider/exchange.rb +227 -0
  31. data/lib/songkick/oauth2/router.rb +79 -0
  32. data/lib/songkick/oauth2/schema.rb +17 -0
  33. data/lib/songkick/oauth2/schema/20120828112156_songkick_oauth2_schema_original_schema.rb +36 -0
  34. data/spec/factories.rb +27 -0
  35. data/spec/request_helpers.rb +52 -0
  36. data/spec/songkick/oauth2/model/authorization_spec.rb +216 -0
  37. data/spec/songkick/oauth2/model/client_spec.rb +55 -0
  38. data/spec/songkick/oauth2/model/resource_owner_spec.rb +88 -0
  39. data/spec/songkick/oauth2/provider/access_token_spec.rb +125 -0
  40. data/spec/songkick/oauth2/provider/authorization_spec.rb +346 -0
  41. data/spec/songkick/oauth2/provider/exchange_spec.rb +353 -0
  42. data/spec/songkick/oauth2/provider_spec.rb +545 -0
  43. data/spec/spec_helper.rb +62 -0
  44. data/spec/test_app/helper.rb +33 -0
  45. data/spec/test_app/provider/application.rb +68 -0
  46. data/spec/test_app/provider/views/authorize.erb +19 -0
  47. metadata +273 -0
@@ -0,0 +1,216 @@
1
+ require 'spec_helper'
2
+
3
+ describe Songkick::OAuth2::Model::Authorization do
4
+ let(:client) { Factory :client }
5
+ let(:impostor) { Factory :client }
6
+ let(:owner) { Factory :owner }
7
+ let(:user) { Factory :owner }
8
+
9
+ let(:authorization) do
10
+ create_authorization(:owner => owner, :client => client)
11
+ end
12
+
13
+ it "is vaid" do
14
+ authorization.should be_valid
15
+ end
16
+
17
+ it "is not valid without a client" do
18
+ authorization.client = nil
19
+ authorization.should_not be_valid
20
+ end
21
+
22
+ it "is not valid without an owner" do
23
+ authorization.owner = nil
24
+ authorization.should_not be_valid
25
+ end
26
+
27
+ describe "when there are existing authorizations" do
28
+ before do
29
+ create_authorization(
30
+ :owner => user,
31
+ :client => impostor,
32
+ :access_token => 'existing_access_token')
33
+
34
+ create_authorization(
35
+ :owner => owner,
36
+ :client => client,
37
+ :code => 'existing_code')
38
+
39
+ create_authorization(
40
+ :owner => owner,
41
+ :client => client,
42
+ :refresh_token => 'existing_refresh_token')
43
+ end
44
+
45
+ it "is valid if its access_token is unique" do
46
+ authorization.should be_valid
47
+ end
48
+
49
+ it "is valid if both access_tokens are nil" do
50
+ Songkick::OAuth2::Model::Authorization.first.update_attribute(:access_token, nil)
51
+ authorization.access_token = nil
52
+ authorization.should be_valid
53
+ end
54
+
55
+ it "is not valid if its access_token is not unique" do
56
+ authorization.access_token = 'existing_access_token'
57
+ authorization.should_not be_valid
58
+ end
59
+
60
+ it "is valid if it has a unique code for its client" do
61
+ authorization.client = impostor
62
+ authorization.code = 'existing_code'
63
+ authorization.should be_valid
64
+ end
65
+
66
+ it "is not valid if it does not have a unique client and code" do
67
+ authorization.code = 'existing_code'
68
+ authorization.should_not be_valid
69
+ end
70
+
71
+ it "is valid if it has a unique refresh_token for its client" do
72
+ authorization.client = impostor
73
+ authorization.refresh_token = 'existing_refresh_token'
74
+ authorization.should be_valid
75
+ end
76
+
77
+ it "is not valid if it does not have a unique client and refresh_token" do
78
+ authorization.refresh_token = 'existing_refresh_token'
79
+ authorization.should_not be_valid
80
+ end
81
+
82
+ describe ".create_code" do
83
+ before { Songkick::OAuth2.stub(:random_string).and_return('existing_code', 'new_code') }
84
+
85
+ it "returns the first code the client has not used" do
86
+ Songkick::OAuth2::Model::Authorization.create_code(client).should == 'new_code'
87
+ end
88
+
89
+ it "returns the first code another client has not used" do
90
+ Songkick::OAuth2::Model::Authorization.create_code(impostor).should == 'existing_code'
91
+ end
92
+ end
93
+
94
+ describe ".create_access_token" do
95
+ before { Songkick::OAuth2.stub(:random_string).and_return('existing_access_token', 'new_access_token') }
96
+
97
+ it "returns the first unused token it can find" do
98
+ Songkick::OAuth2::Model::Authorization.create_access_token.should == 'new_access_token'
99
+ end
100
+ end
101
+
102
+ describe ".create_refresh_token" do
103
+ before { Songkick::OAuth2.stub(:random_string).and_return('existing_refresh_token', 'new_refresh_token') }
104
+
105
+ it "returns the first refresh_token the client has not used" do
106
+ Songkick::OAuth2::Model::Authorization.create_refresh_token(client).should == 'new_refresh_token'
107
+ end
108
+
109
+ it "returns the first refresh_token another client has not used" do
110
+ Songkick::OAuth2::Model::Authorization.create_refresh_token(impostor).should == 'existing_refresh_token'
111
+ end
112
+ end
113
+ end
114
+
115
+ describe "#exchange!" do
116
+ it "saves the record" do
117
+ authorization.should_receive(:save!)
118
+ authorization.exchange!
119
+ end
120
+
121
+ it "uses its helpers to find unique tokens" do
122
+ Songkick::OAuth2::Model::Authorization.should_receive(:create_access_token).and_return('access_token')
123
+ authorization.exchange!
124
+ authorization.access_token.should == 'access_token'
125
+ end
126
+
127
+ it "updates the tokens correctly" do
128
+ authorization.exchange!
129
+ authorization.should be_valid
130
+ authorization.code.should be_nil
131
+ authorization.refresh_token.should be_nil
132
+ end
133
+ end
134
+
135
+ describe "#expired?" do
136
+ it "returns false when not expiry is set" do
137
+ authorization.should_not be_expired
138
+ end
139
+
140
+ it "returns false when expiry is in the future" do
141
+ authorization.expires_at = 2.days.from_now
142
+ authorization.should_not be_expired
143
+ end
144
+
145
+ it "returns true when expiry is in the past" do
146
+ authorization.expires_at = 2.days.ago
147
+ authorization.should be_expired
148
+ end
149
+ end
150
+
151
+ describe "#grants_access?" do
152
+ it "returns true given the right user" do
153
+ authorization.grants_access?(owner).should be_true
154
+ end
155
+
156
+ it "returns false given the wrong user" do
157
+ authorization.grants_access?(user).should be_false
158
+ end
159
+
160
+ describe "when the authorization is expired" do
161
+ before { authorization.expires_at = 2.days.ago }
162
+
163
+ it "returns false in all cases" do
164
+ authorization.grants_access?(owner).should be_false
165
+ authorization.grants_access?(user).should be_false
166
+ end
167
+ end
168
+ end
169
+
170
+ describe "with a scope" do
171
+ before { authorization.scope = 'foo bar' }
172
+
173
+ describe "#in_scope?" do
174
+ it "returns true for authorized scopes" do
175
+ authorization.should be_in_scope('foo')
176
+ authorization.should be_in_scope('bar')
177
+ end
178
+
179
+ it "returns false for unauthorized scopes" do
180
+ authorization.should_not be_in_scope('qux')
181
+ authorization.should_not be_in_scope('fo')
182
+ end
183
+ end
184
+
185
+ describe "#grants_access?" do
186
+ it "returns true given the right user and all authorization scopes" do
187
+ authorization.grants_access?(owner, 'foo', 'bar').should be_true
188
+ end
189
+
190
+ it "returns true given the right user and some authorization scopes" do
191
+ authorization.grants_access?(owner, 'bar').should be_true
192
+ end
193
+
194
+ it "returns false given the right user and some unauthorization scopes" do
195
+ authorization.grants_access?(owner, 'foo', 'bar', 'qux').should be_false
196
+ end
197
+
198
+ it "returns false given an unauthorized scope" do
199
+ authorization.grants_access?(owner, 'qux').should be_false
200
+ end
201
+
202
+ it "returns true given the right user" do
203
+ authorization.grants_access?(owner).should be_true
204
+ end
205
+
206
+ it "returns false given the wrong user" do
207
+ authorization.grants_access?(user).should be_false
208
+ end
209
+
210
+ it "returns false given the wrong user and an authorized scope" do
211
+ authorization.grants_access?(user, 'foo').should be_false
212
+ end
213
+ end
214
+ end
215
+ end
216
+
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Songkick::OAuth2::Model::Client do
4
+ before do
5
+ @client = Songkick::OAuth2::Model::Client.create(:name => 'App', :redirect_uri => 'http://example.com/cb')
6
+ @owner = Factory(:owner)
7
+ Factory(:authorization, :client => @client, :owner => @owner)
8
+ end
9
+
10
+ it "is valid" do
11
+ @client.should be_valid
12
+ end
13
+
14
+ it "is invalid without a name" do
15
+ @client.name = nil
16
+ @client.should_not be_valid
17
+ end
18
+
19
+ it "is invalid without a redirect_uri" do
20
+ @client.redirect_uri = nil
21
+ @client.should_not be_valid
22
+ end
23
+
24
+ it "is invalid with a non-URI redirect_uri" do
25
+ @client.redirect_uri = 'foo'
26
+ @client.should_not be_valid
27
+ end
28
+
29
+ # http://en.wikipedia.org/wiki/HTTP_response_splitting
30
+ it "is invalid if the URI contains HTTP line breaks" do
31
+ @client.redirect_uri = "http://example.com/c\r\nb"
32
+ @client.should_not be_valid
33
+ end
34
+
35
+ it "cannot mass-assign client_id" do
36
+ @client.update_attributes(:client_id => 'foo')
37
+ @client.client_id.should_not == 'foo'
38
+ end
39
+
40
+ it "cannot mass-assign client_secret" do
41
+ @client.update_attributes(:client_secret => 'foo')
42
+ @client.client_secret.should_not == 'foo'
43
+ end
44
+
45
+ it "has client_id and client_secret filled in" do
46
+ @client.client_id.should_not be_nil
47
+ @client.client_secret.should_not be_nil
48
+ end
49
+
50
+ it "destroys its authorizations on destroy" do
51
+ @client.destroy
52
+ Songkick::OAuth2::Model::Authorization.count.should be_zero
53
+ end
54
+ end
55
+
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+
3
+ describe Songkick::OAuth2::Model::ResourceOwner do
4
+ before do
5
+ @owner = Factory(:owner)
6
+ @client = Factory(:client)
7
+ end
8
+
9
+ describe "#grant_access!" do
10
+ it "raises an error when passed an invalid client argument" do
11
+ lambda{ @owner.grant_access!('client') }.should raise_error(ArgumentError)
12
+ end
13
+
14
+ it "creates an authorization between the owner and the client" do
15
+ authorization = Songkick::OAuth2::Model::Authorization.new
16
+ Songkick::OAuth2::Model::Authorization.should_receive(:new).and_return(authorization)
17
+ @owner.grant_access!(@client)
18
+ end
19
+
20
+ # This is hacky, but mocking ActiveRecord turns out to get messy
21
+ it "creates an Authorization" do
22
+ Songkick::OAuth2::Model::Authorization.count.should == 0
23
+ @owner.grant_access!(@client)
24
+ Songkick::OAuth2::Model::Authorization.count.should == 1
25
+ end
26
+
27
+ it "returns the authorization" do
28
+ @owner.grant_access!(@client).should be_kind_of(Songkick::OAuth2::Model::Authorization)
29
+ end
30
+
31
+ # This method must return the same owner object, since the assertion
32
+ # handler may modify it -- either by changing its attributes or by extending
33
+ # it with new methods. These changes must be returned to the app calling the
34
+ # Provider interface.
35
+ it "sets the receiver as the authorization's owner" do
36
+ authorization = @owner.grant_access!(@client)
37
+ authorization.owner.should be_equal(@owner)
38
+ end
39
+
40
+ it "sets the duration of the authorization" do
41
+ authorization = @owner.grant_access!(@client, :duration => 5.hours)
42
+ authorization.expires_at.to_i.should == (Time.now + 5.hours.to_i).to_i
43
+ end
44
+ end
45
+
46
+ describe "when there is an existing authorization" do
47
+ before do
48
+ @authorization = Factory(:authorization, :owner => @owner, :client => @client)
49
+ end
50
+
51
+ it "does not create a new one" do
52
+ Songkick::OAuth2::Model::Authorization.should_not_receive(:new)
53
+ @owner.grant_access!(@client)
54
+ end
55
+
56
+ it "updates the authorization with scopes" do
57
+ @owner.grant_access!(@client, :scopes => ['foo', 'bar'])
58
+ @authorization.reload
59
+ @authorization.scopes.should == Set.new(['foo', 'bar'])
60
+ end
61
+
62
+ describe "with scopes" do
63
+ before do
64
+ @authorization.update_attribute(:scope, 'foo bar')
65
+ end
66
+
67
+ it "merges the new scopes with the existing ones" do
68
+ @owner.grant_access!(@client, :scopes => ['qux'])
69
+ @authorization.reload
70
+ @authorization.scopes.should == Set.new(['foo', 'bar', 'qux'])
71
+ end
72
+
73
+ it "does not add duplicate scopes to the list" do
74
+ @owner.grant_access!(@client, :scopes => ['qux'])
75
+ @owner.grant_access!(@client, :scopes => ['qux'])
76
+ @authorization.reload
77
+ @authorization.scopes.should == Set.new(['foo', 'bar', 'qux'])
78
+ end
79
+ end
80
+ end
81
+
82
+ it "destroys its authorizations on destroy" do
83
+ Factory(:authorization, :owner => @owner, :client => @client)
84
+ @owner.destroy
85
+ Songkick::OAuth2::Model::Authorization.count.should be_zero
86
+ end
87
+ end
88
+
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ describe Songkick::OAuth2::Provider::AccessToken do
4
+ before do
5
+ @alice = TestApp::User['Alice']
6
+ @bob = TestApp::User['Bob']
7
+
8
+ Factory(:authorization,
9
+ :owner => @alice,
10
+ :scope => 'profile',
11
+ :access_token => 'sesame')
12
+
13
+ @authorization = Factory(:authorization,
14
+ :owner => @bob,
15
+ :scope => 'profile',
16
+ :access_token => 'magic-key')
17
+
18
+ Songkick::OAuth2::Provider.realm = 'Demo App'
19
+ end
20
+
21
+ let :token do
22
+ Songkick::OAuth2::Provider::AccessToken.new(@bob, ['profile'], 'magic-key')
23
+ end
24
+
25
+ shared_examples_for "valid token" do
26
+ it "is valid" do
27
+ token.should be_valid
28
+ end
29
+ it "does not add headers" do
30
+ token.response_headers.should == {}
31
+ end
32
+ it "has an OK status code" do
33
+ token.response_status.should == 200
34
+ end
35
+ it "returns the owner who granted the authorization" do
36
+ token.owner.should == @bob
37
+ end
38
+ end
39
+
40
+ shared_examples_for "invalid token" do
41
+ it "is not valid" do
42
+ token.should_not be_valid
43
+ end
44
+ it "does not return the owner" do
45
+ token.owner.should be_nil
46
+ end
47
+ end
48
+
49
+ describe "with the right user, scope and token" do
50
+ it_should_behave_like "valid token"
51
+ end
52
+
53
+ describe "with no user" do
54
+ let :token do
55
+ Songkick::OAuth2::Provider::AccessToken.new(nil, ['profile'], 'magic-key')
56
+ end
57
+ it_should_behave_like "valid token"
58
+ end
59
+
60
+ describe "with less scope than was granted" do
61
+ let :token do
62
+ Songkick::OAuth2::Provider::AccessToken.new(@bob, [], 'magic-key')
63
+ end
64
+ it_should_behave_like "valid token"
65
+ end
66
+
67
+ describe "when the authorization has expired" do
68
+ before { @authorization.update_attribute(:expires_at, 1.hour.ago) }
69
+ it_should_behave_like "invalid token"
70
+
71
+ it "returns an error response" do
72
+ token.response_headers['WWW-Authenticate'].should == "OAuth realm='Demo App', error='expired_token'"
73
+ token.response_status.should == 401
74
+ end
75
+ end
76
+
77
+ describe "with a non-existent token" do
78
+ let :token do
79
+ Songkick::OAuth2::Provider::AccessToken.new(@bob, ['profile'], 'is-the-password-books')
80
+ end
81
+ it_should_behave_like "invalid token"
82
+
83
+ it "returns an error response" do
84
+ token.response_headers['WWW-Authenticate'].should == "OAuth realm='Demo App', error='invalid_token'"
85
+ token.response_status.should == 401
86
+ end
87
+ end
88
+
89
+ describe "with a token for the wrong user" do
90
+ let :token do
91
+ Songkick::OAuth2::Provider::AccessToken.new(@bob, ['profile'], 'sesame')
92
+ end
93
+ it_should_behave_like "invalid token"
94
+
95
+ it "returns an error response" do
96
+ token.response_headers['WWW-Authenticate'].should == "OAuth realm='Demo App', error='insufficient_scope'"
97
+ token.response_status.should == 403
98
+ end
99
+ end
100
+
101
+ describe "with a token for an ungranted scope" do
102
+ let :token do
103
+ Songkick::OAuth2::Provider::AccessToken.new(@bob, ['offline_access'], 'magic-key')
104
+ end
105
+ it_should_behave_like "invalid token"
106
+
107
+ it "returns an error response" do
108
+ token.response_headers['WWW-Authenticate'].should == "OAuth realm='Demo App', error='insufficient_scope'"
109
+ token.response_status.should == 403
110
+ end
111
+ end
112
+
113
+ describe "with no token string" do
114
+ let :token do
115
+ Songkick::OAuth2::Provider::AccessToken.new(@bob, ['profile'], nil)
116
+ end
117
+ it_should_behave_like "invalid token"
118
+
119
+ it "returns an error response" do
120
+ token.response_headers['WWW-Authenticate'].should == "OAuth realm='Demo App'"
121
+ token.response_status.should == 401
122
+ end
123
+ end
124
+ end
125
+