oauth2-provider-jonrowe 0.1.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 (45) hide show
  1. data/README.rdoc +314 -0
  2. data/example/README.rdoc +11 -0
  3. data/example/application.rb +151 -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/home.erb +25 -0
  14. data/example/views/layout.erb +25 -0
  15. data/example/views/login.erb +20 -0
  16. data/example/views/new_client.erb +25 -0
  17. data/example/views/new_user.erb +22 -0
  18. data/example/views/show_client.erb +15 -0
  19. data/lib/oauth2/model.rb +17 -0
  20. data/lib/oauth2/model/authorization.rb +113 -0
  21. data/lib/oauth2/model/client.rb +55 -0
  22. data/lib/oauth2/model/client_owner.rb +13 -0
  23. data/lib/oauth2/model/hashing.rb +27 -0
  24. data/lib/oauth2/model/resource_owner.rb +26 -0
  25. data/lib/oauth2/model/schema.rb +42 -0
  26. data/lib/oauth2/provider.rb +117 -0
  27. data/lib/oauth2/provider/access_token.rb +66 -0
  28. data/lib/oauth2/provider/authorization.rb +168 -0
  29. data/lib/oauth2/provider/error.rb +29 -0
  30. data/lib/oauth2/provider/exchange.rb +212 -0
  31. data/lib/oauth2/router.rb +60 -0
  32. data/spec/factories.rb +27 -0
  33. data/spec/oauth2/model/authorization_spec.rb +216 -0
  34. data/spec/oauth2/model/client_spec.rb +55 -0
  35. data/spec/oauth2/model/resource_owner_spec.rb +55 -0
  36. data/spec/oauth2/provider/access_token_spec.rb +125 -0
  37. data/spec/oauth2/provider/authorization_spec.rb +323 -0
  38. data/spec/oauth2/provider/exchange_spec.rb +330 -0
  39. data/spec/oauth2/provider_spec.rb +531 -0
  40. data/spec/request_helpers.rb +46 -0
  41. data/spec/spec_helper.rb +44 -0
  42. data/spec/test_app/helper.rb +33 -0
  43. data/spec/test_app/provider/application.rb +61 -0
  44. data/spec/test_app/provider/views/authorize.erb +19 -0
  45. metadata +220 -0
@@ -0,0 +1,29 @@
1
+ module OAuth2
2
+ class Provider
3
+
4
+ class Error
5
+ def initialize(message = nil)
6
+ @message = message
7
+ end
8
+
9
+ def redirect?
10
+ false
11
+ end
12
+
13
+ def response_body
14
+ message = 'Bad request' + (@message ? ": #{@message}" : '')
15
+ JSON.unparse(ERROR => INVALID_REQUEST, ERROR_DESCRIPTION => message)
16
+ end
17
+
18
+ def response_headers
19
+ Exchange::RESPONSE_HEADERS
20
+ end
21
+
22
+ def response_status
23
+ 400
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+
@@ -0,0 +1,212 @@
1
+ module OAuth2
2
+ class Provider
3
+
4
+ class Exchange
5
+ attr_reader :client, :error, :error_description
6
+
7
+ REQUIRED_PARAMS = [CLIENT_ID, CLIENT_SECRET, GRANT_TYPE]
8
+ VALID_GRANT_TYPES = [AUTHORIZATION_CODE, PASSWORD, ASSERTION, REFRESH_TOKEN]
9
+
10
+ REQUIRED_PASSWORD_PARAMS = [USERNAME, PASSWORD]
11
+ REQUIRED_ASSERTION_PARAMS = [ASSERTION_TYPE, ASSERTION]
12
+
13
+ RESPONSE_HEADERS = {
14
+ 'Cache-Control' => 'no-store',
15
+ 'Content-Type' => 'application/json'
16
+ }
17
+
18
+ def initialize(resource_owner, params)
19
+ @params = params
20
+ @scope = params[SCOPE]
21
+ @grant_type = @params[GRANT_TYPE]
22
+ validate!
23
+ end
24
+
25
+ def owner
26
+ @authorization && @authorization.owner
27
+ end
28
+
29
+ def scopes
30
+ @scope ? @scope.split(/\s+/).delete_if { |s| s.empty? } : []
31
+ end
32
+
33
+ def redirect?
34
+ false
35
+ end
36
+
37
+ def response_body
38
+ return jsonize(ERROR, ERROR_DESCRIPTION) unless valid?
39
+ update_authorization
40
+
41
+ response = {}
42
+ %w[access_token refresh_token scope].each do |key|
43
+ value = @authorization.__send__(key)
44
+ response[key] = value if value
45
+ end
46
+
47
+ JSON.unparse(response)
48
+ end
49
+
50
+ def response_headers
51
+ RESPONSE_HEADERS
52
+ end
53
+
54
+ def response_status
55
+ valid? ? 200 : 400
56
+ end
57
+
58
+ def update_authorization
59
+ return if not valid? or @already_updated
60
+ @authorization.exchange!
61
+ @already_updated = true
62
+ end
63
+
64
+ def valid?
65
+ @error.nil?
66
+ end
67
+
68
+ private
69
+
70
+ def jsonize(*ivars)
71
+ hash = {}
72
+ ivars.each { |key| hash[key] = instance_variable_get("@#{key}") }
73
+ JSON.unparse(hash)
74
+ end
75
+
76
+ def validate!
77
+ validate_required_params
78
+
79
+ return if @error
80
+ validate_client
81
+
82
+ unless VALID_GRANT_TYPES.include?(@grant_type)
83
+ @error = UNSUPPORTED_GRANT_TYPE
84
+ @error_description = "The grant type #{@grant_type} is not recognized"
85
+ end
86
+ return if @error
87
+
88
+ __send__("validate_#{@grant_type}")
89
+ validate_scope
90
+ end
91
+
92
+ def validate_required_params
93
+ REQUIRED_PARAMS.each do |param|
94
+ next if @params.has_key?(param)
95
+ @error = INVALID_REQUEST
96
+ @error_description = "Missing required parameter #{param}"
97
+ end
98
+ end
99
+
100
+ def validate_client
101
+ @client = Model::Client.find_by_client_id(@params[CLIENT_ID])
102
+ unless @client
103
+ @error = INVALID_CLIENT
104
+ @error_description = "Unknown client ID #{@params[CLIENT_ID]}"
105
+ end
106
+
107
+ if @client and not @client.valid_client_secret? @params[CLIENT_SECRET]
108
+ @error = INVALID_CLIENT
109
+ @error_description = 'Parameter client_secret does not match'
110
+ end
111
+ end
112
+
113
+ def validate_scope
114
+ if @authorization and not @authorization.in_scope?(scopes)
115
+ @error = INVALID_SCOPE
116
+ @error_description = 'The request scope was never granted by the user'
117
+ end
118
+ end
119
+
120
+ def validate_authorization_code
121
+ unless @params[CODE]
122
+ @error = INVALID_REQUEST
123
+ @error_description = "Missing required parameter code"
124
+ end
125
+
126
+ if @client.redirect_uri and @client.redirect_uri != @params[REDIRECT_URI]
127
+ @error = REDIRECT_MISMATCH
128
+ @error_description = "Parameter redirect_uri does not match registered URI"
129
+ end
130
+
131
+ unless @params.has_key?(REDIRECT_URI)
132
+ @error = INVALID_REQUEST
133
+ @error_description = "Missing required parameter redirect_uri"
134
+ end
135
+
136
+ return if @error
137
+
138
+ @authorization = @client.authorizations.find_by_code(@params[CODE])
139
+ validate_authorization
140
+ end
141
+
142
+ def validate_password
143
+ REQUIRED_PASSWORD_PARAMS.each do |param|
144
+ next if @params.has_key?(param)
145
+ @error = INVALID_REQUEST
146
+ @error_description = "Missing required parameter #{param}"
147
+ end
148
+
149
+ return if @error
150
+
151
+ @authorization = Provider.handle_password(@client, @params[USERNAME], @params[PASSWORD])
152
+ return validate_authorization if @authorization
153
+
154
+ @error = INVALID_GRANT
155
+ @error_description = 'The access grant you supplied is invalid'
156
+ end
157
+
158
+ def validate_assertion
159
+ REQUIRED_ASSERTION_PARAMS.each do |param|
160
+ next if @params.has_key?(param)
161
+ @error = INVALID_REQUEST
162
+ @error_description = "Missing required parameter #{param}"
163
+ end
164
+
165
+ if @params[ASSERTION_TYPE]
166
+ uri = URI.parse(@params[ASSERTION_TYPE]) rescue nil
167
+ unless uri and uri.absolute?
168
+ @error = INVALID_REQUEST
169
+ @error_description = 'Parameter assertion_type must be an absolute URI'
170
+ end
171
+ end
172
+
173
+ return if @error
174
+
175
+ assertion = Assertion.new(@params)
176
+ @authorization = Provider.handle_assertion(@client, assertion)
177
+ return validate_authorization if @authorization
178
+
179
+ @error = UNAUTHORIZED_CLIENT
180
+ @error_description = 'Client cannot use the given assertion type'
181
+ end
182
+
183
+ def validate_refresh_token
184
+ refresh_token_hash = OAuth2.hashify(@params[REFRESH_TOKEN])
185
+ @authorization = @client.authorizations.find_by_refresh_token_hash(refresh_token_hash)
186
+ validate_authorization
187
+ end
188
+
189
+ def validate_authorization
190
+ unless @authorization
191
+ @error = INVALID_GRANT
192
+ @error_description = 'The access grant you supplied is invalid'
193
+ end
194
+
195
+ if @authorization and @authorization.expired?
196
+ @error = INVALID_GRANT
197
+ @error_description = 'The access grant you supplied is invalid'
198
+ end
199
+ end
200
+ end
201
+
202
+ class Assertion
203
+ attr_reader :type, :value
204
+ def initialize(params)
205
+ @type = params[ASSERTION_TYPE]
206
+ @value = params[ASSERTION]
207
+ end
208
+ end
209
+
210
+ end
211
+ end
212
+
@@ -0,0 +1,60 @@
1
+ require 'base64'
2
+
3
+ module OAuth2
4
+ class Router
5
+
6
+ def self.auth_params(request, params = nil)
7
+ return {} unless basic = request.env['HTTP_AUTHORIZATION']
8
+ parts = basic.split(/\s+/)
9
+ username, password = Base64.decode64(parts.last).split(':')
10
+ {CLIENT_ID => username, CLIENT_SECRET => password}
11
+ end
12
+
13
+ def self.transport_error(request)
14
+ uri = URI.parse(request.url)
15
+
16
+ if Provider.enforce_ssl and not uri.is_a?(URI::HTTPS)
17
+ return Provider::Error.new("must make requests using HTTPS")
18
+ end
19
+ end
20
+
21
+ def self.parse(resource_owner, request, params = nil)
22
+ if error = transport_error(request)
23
+ return error
24
+ end
25
+
26
+ params ||= request.params
27
+ auth = auth_params(request, params)
28
+
29
+ if auth[CLIENT_ID] and auth[CLIENT_ID] != params[CLIENT_ID]
30
+ return Provider::Error.new("#{CLIENT_ID} from Basic Auth and request body do not match")
31
+ end
32
+
33
+ params = params.merge(auth)
34
+
35
+ if params[GRANT_TYPE]
36
+ request.post? ?
37
+ Provider::Exchange.new(resource_owner, params) :
38
+ Provider::Error.new("should be a POST request")
39
+ else
40
+ Provider::Authorization.new(resource_owner, params)
41
+ end
42
+ end
43
+
44
+ def self.access_token(resource_owner, scopes, request, params = nil)
45
+ params ||= request.params
46
+ header = request.env['HTTP_AUTHORIZATION']
47
+
48
+ access_token = header && header =~ /^OAuth\s+/ ?
49
+ header.gsub(/^OAuth\s+/, '') :
50
+ params[OAUTH_TOKEN]
51
+
52
+ Provider::AccessToken.new(resource_owner,
53
+ scopes,
54
+ access_token,
55
+ transport_error(request))
56
+ end
57
+
58
+ end
59
+ end
60
+
@@ -0,0 +1,27 @@
1
+ require 'factory_girl'
2
+
3
+ Factory.sequence :client_name do |n|
4
+ "Client ##{n}"
5
+ end
6
+
7
+ Factory.sequence :user_name do |n|
8
+ "User ##{n}"
9
+ end
10
+
11
+ Factory.define :owner, :class => TestApp::User do |u|
12
+ u.name { Factory.next :user_name }
13
+ end
14
+
15
+ Factory.define :client, :class => OAuth2::Model::Client do |c|
16
+ c.client_id { OAuth2.random_string }
17
+ c.client_secret { OAuth2.random_string }
18
+ c.name { Factory.next :client_name }
19
+ c.redirect_uri 'https://client.example.com/cb'
20
+ end
21
+
22
+ Factory.define :authorization, :class => OAuth2::Model::Authorization do |ac|
23
+ ac.client Factory(:client)
24
+ ac.code { OAuth2.random_string }
25
+ ac.expires_at nil
26
+ end
27
+
@@ -0,0 +1,216 @@
1
+ require 'spec_helper'
2
+
3
+ describe 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
+ OAuth2::Model::Authorization.new(: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
+ OAuth2::Model::Authorization.create(
30
+ :owner => user,
31
+ :client => impostor,
32
+ :access_token => 'existing_access_token')
33
+
34
+ OAuth2::Model::Authorization.create(
35
+ :owner => owner,
36
+ :client => client,
37
+ :code => 'existing_code')
38
+
39
+ OAuth2::Model::Authorization.create(
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
+ 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 { OAuth2.stub(:random_string).and_return('existing_code', 'new_code') }
84
+
85
+ it "returns the first code the client has not used" do
86
+ 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
+ OAuth2::Model::Authorization.create_code(impostor).should == 'existing_code'
91
+ end
92
+ end
93
+
94
+ describe ".create_access_token" do
95
+ before { 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
+ OAuth2::Model::Authorization.create_access_token.should == 'new_access_token'
99
+ end
100
+ end
101
+
102
+ describe ".create_refresh_token" do
103
+ before { 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
+ 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
+ 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
+ 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
+