oauth2-provider-jonrowe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+