rack-oauth2-server 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,4 +1,30 @@
1
- 2010-11-07 version 1.3.1
1
+ 2010-11-09 version 1.4.0
2
+
3
+ If authorization handle is passed as request parameter (the recommended way),
4
+ then you can call oauth.grant! with a single argument and oauth.deny! with no
5
+ arguments.
6
+
7
+ You can now call oauth.deny! at any point during the authorization flow, e.g.
8
+ automatically deny all requests based on scope and client.
9
+
10
+ To deny access, return status code 403 (was, incorrectly 401). Or just use
11
+ oauth.deny!.
12
+
13
+ Web console gets template_url setting you can use to map access token identity
14
+ into a URL in your application. The substitution variable is "{id}".
15
+
16
+ Added error page when authorization attempt fails (instead of endless
17
+ redirect).
18
+
19
+ Fixed mounting of Web console on Rails. If it failed you before, try again.
20
+
21
+ Fixed documentation for configuration under Rails, clarify that all the
22
+ interesting stuff happens in after_initialize.
23
+
24
+ Fixed error responses for response_type=token to use fragment identifier.
25
+
26
+
27
+ 2010-11-08 version 1.3.1
2
28
 
3
29
  Added command line tool, helps you get started and setup:
4
30
  $ oauth2-server setup --db my_db
data/README.rdoc CHANGED
@@ -30,14 +30,15 @@ when required, but you do need to configure it from within
30
30
  example:
31
31
 
32
32
  Rails::Initializer.run do |config|
33
- config.oauth.database = Mongo::Connection.new["my_db"]
34
- config.oauth.scopes = %w{read write}
35
- config.oauth.authenticator = lambda do |username, password|
36
- user = User.find(username)
37
- user if user && user.authenticated?(password)
38
- end
39
-
40
33
  . . .
34
+ config.after_initialize do
35
+ config.oauth.database = Mongo::Connection.new["my_db"]
36
+ config.oauth.scopes = %w{read write}
37
+ config.oauth.authenticator = lambda do |username, password|
38
+ user = User.find(username)
39
+ user if user && user.authenticated?(password)
40
+ end
41
+ end
41
42
  end
42
43
 
43
44
  For Sinatra and Padrino, first require +rack/oauth2/sinatra+ and register
@@ -110,8 +111,8 @@ user back to the client application with an authorization code or access token.
110
111
 
111
112
  To signal that the user denied the authorization requests your application sets
112
113
  the response header oauth.authorization as before, and returns the status code
113
- 401 (Unauthorized). Rack::OAuth2::Server will then redirect the user back to
114
- the client application with a suitable error code.
114
+ 403 (Forbidden). Rack::OAuth2::Server will then redirect the user back to the
115
+ client application with a suitable error code.
115
116
 
116
117
  In Rails, the entire flow would look something like this:
117
118
 
@@ -125,11 +126,11 @@ In Rails, the entire flow would look something like this:
125
126
  end
126
127
 
127
128
  def grant
128
- head oauth.grant!(params[:authorization], current_user.id)
129
+ head oauth.grant!(current_user.id)
129
130
  end
130
131
 
131
132
  def deny
132
- head oauth.deny!(params[:authorization])
133
+ head oauth.deny!
133
134
  end
134
135
  end
135
136
 
@@ -149,11 +150,11 @@ In Sinatra/Padrino, it would look something like this:
149
150
  end
150
151
 
151
152
  post "/oauth/grant" do
152
- oauth.grant! params[:authorization], "Superman"
153
+ oauth.grant! "Superman"
153
154
  end
154
155
 
155
156
  post "/oauth/deny" do
156
- oauth.deny! params[:authorization]
157
+ oauth.deny!
157
158
  end
158
159
 
159
160
  The view would look something like this:
@@ -374,9 +375,7 @@ the console, by granting them access to the +oauth-admin+ scope. For example:
374
375
 
375
376
  def authorize
376
377
  # Only admins allowed to authorize the scope oauth-admin
377
- if oauth.scope.include?("oauth-admin") && !current_user.admin?
378
- oauth.deny! oauth.authorization
379
- end
378
+ head oauth.deny! if oauth.scope.include?("oauth-admin") && !current_user.admin?
380
379
  end
381
380
 
382
381
  Make sure you do that, or you'll allow anyone access to the OAuth Web console.
@@ -386,9 +385,11 @@ client ID/secret. For example, for Rails add this to +config/environment.rb+:
386
385
 
387
386
  Rails::Initializer.run do |config|
388
387
  . . .
389
- config.middleware.use Rack::OAuth2::Server::Admin.mount
390
- Rack::OAuth2::Server::Admin.set :client_id, "4dca20453e4859cb000007"
391
- Rack::OAuth2::Server::Admin.set :client_secret, "981fa734e110496fcf667cbf52fbaf03"
388
+ config.after_initialize do
389
+ config.middleware.use Rack::OAuth2::Server::Admin.mount
390
+ Rack::OAuth2::Server::Admin.set :client_id, "4dca20453e4859cb000007"
391
+ Rack::OAuth2::Server::Admin.set :client_secret, "981fa734e110496fcf667cbf52fbaf03"
392
+ end
392
393
  end
393
394
 
394
395
  For Sinatra, Padrino and other Rack-based applications, you'll want to mount
@@ -404,6 +405,11 @@ like so (e.g. in +config.ru+):
404
405
  Next, open your browser to http://example.com/oauth/admin, or wherever you
405
406
  mounted the console.
406
407
 
408
+ Another option, +template_url+ allows you to link access token identities to
409
+ URLs in your application, using the substitution variable "{id}". For example:
410
+
411
+ Rack::OAuth2::Server::Admin.set :template_url, "https://example.com/accounts/{id}"
412
+
407
413
  The OAuth Web console is a single-page client application that operates by
408
414
  accessing the OAuth API. The API is mounted at /oauth/admin/api (basically /api
409
415
  relative to the console), you can access it yourself if you have an access
data/Rakefile CHANGED
@@ -39,14 +39,14 @@ Rake::TestTask.new do |task|
39
39
  end
40
40
 
41
41
  namespace :test do
42
- task :all=>["test:sinatra", "test:rails2"]
42
+ task :all=>["test:sinatra", "test:rails"]
43
43
  desc "Run all tests against Sinatra"
44
44
  task :sinatra do
45
45
  sh "rake test FRAMEWORK=sinatra"
46
46
  end
47
47
  desc "Run all tests against Rails 2.3.x"
48
- task :rails2 do
49
- sh "rake test FRAMEWORK=rails2"
48
+ task :rails do
49
+ sh "rake test FRAMEWORK=rails"
50
50
  end
51
51
  end
52
52
  task :default=>"test:all"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.1
1
+ 1.4.0
data/bin/oauth2-server CHANGED
@@ -62,18 +62,18 @@ when "setup"
62
62
  Make sure you ONLY authorize administrators to use the oauth-admin scope.
63
63
  For example:
64
64
 
65
- def authorize
65
+ before_filter do
66
66
  # Only admins allowed to authorize the scope oauth-admin
67
- if oauth.scope.include?("oauth-admin") && !current_user.admin?
68
- oauth.deny! oauth.authorization
69
- end
67
+ head oauth.deny! if oauth.scope.include?("oauth-admin") && !current_user.admin?
70
68
  end
71
69
 
72
70
  Rails 2.x, add the following to config/environment.rb:
73
71
 
74
- config.middleware.use Rack::OAuth2::Server::Admin.mount "#{uri.path}"
75
- Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}"
76
- Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}"
72
+ config.after_initialize do
73
+ config.middleware.use Rack::OAuth2::Server::Admin.mount "#{uri.path}"
74
+ Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}"
75
+ Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}"
76
+ end
77
77
 
78
78
  Sinatra, Padrino and other Rack applications, mount the console:
79
79
 
@@ -107,8 +107,10 @@ button:active { background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(0
107
107
  }
108
108
 
109
109
  #main {
110
+ margin: 0;
110
111
  padding: 0 2em 4em 2em;
111
112
  background: #fff;
113
+ border: 1px solid #fff;
112
114
  }
113
115
  #footer {
114
116
  background: #eee;
@@ -189,7 +191,7 @@ table.clients td.secrets dd:after {
189
191
  }
190
192
 
191
193
  table.tokens td.token {
192
- width: 32em;
194
+ width: 36em;
193
195
  }
194
196
  table.tokens td.scope {
195
197
  }
@@ -264,6 +266,14 @@ table.tokens td.scope {
264
266
  margin-left: 60px;
265
267
  }
266
268
 
269
+ .no-access {
270
+ margin: 0;
271
+ padding: 0;
272
+ }
273
+ .no-access h1 {
274
+ color: red;
275
+ }
276
+
267
277
  .loading {
268
278
  background: url("../images/loading.gif") no-repeat 50% 50%;
269
279
  }
@@ -6,28 +6,39 @@ Sammy("#main", function(app) {
6
6
 
7
7
  // Use OAuth access token in all API requests.
8
8
  $(document).ajaxSend(function(e, xhr) {
9
- xhr.setRequestHeader("Authorization", "OAuth " + app.session("oauth.token"));
9
+ if (app.session("oauth.token"))
10
+ xhr.setRequestHeader("Authorization", "OAuth " + app.session("oauth.token"));
10
11
  });
11
12
  // For all request (except callback), if we don't have an OAuth access token,
12
13
  // ask for one by requesting authorization.
13
- this.before({ except: { path: /^#(access_token=|[^\\].*&access_token=)/ } }, function(context) {
14
+ this.before({ except: { path: /^#\w+=.+/ } }, function(context) {
14
15
  if (!app.session("oauth.token"))
15
16
  context.redirect(document.location.pathname + "/authorize?state=" + escape(context.path));
16
17
  })
18
+ function hashParams(hash) {
19
+ var pairs = hash.substring(1).split("&"), params = {};
20
+ for (var i in pairs) {
21
+ var splat = pairs[i].split("=");
22
+ params[splat[0]] = splat[1];
23
+ }
24
+ return params;
25
+ }
17
26
  // We recognize the OAuth authorization callback based on one of its
18
27
  // parameters. Crude but works here.
19
- this.get(/^#(access_token=|[^\\].*&access_token=)/, function(context) {
28
+ this.get(/^#(access_token=|[^\\].*\&access_token=)/, function(context) {
20
29
  // Instead of a hash we get query parameters, so turn those into an object.
21
- var params = context.path.substring(1).split("&"), args = {};
22
- for (var i in params) {
23
- var splat = params[i].split("=");
24
- args[splat[0]] = splat[1];
25
- }
26
- app.session("oauth.token", args.access_token);
30
+ var params = hashParams(context.path);
31
+ app.session("oauth.token", params.access_token);
27
32
  // When the filter redirected the original request, it passed the original
28
33
  // request's URL in the state parameter, which we get back after
29
34
  // authorization.
30
- context.redirect(args.state.length == 0 ? "#/" : unescape(args.state));
35
+ context.redirect(params.state.length == 0 ? "#/" : unescape(params.state));
36
+ });
37
+ // Authorization error/rejected.
38
+ this.get(/^#(error=|[^\\].*\&error=)/, function(context) {
39
+ var params = hashParams(context.path);
40
+ var error = params.error_description || "You were denied access";
41
+ context.partial("admin/views/no_access.tmpl", { error: error.replace(/\+/g, " ") });
31
42
  });
32
43
 
33
44
 
@@ -28,7 +28,7 @@
28
28
  {{each tokens.list}}
29
29
  <tr>
30
30
  <td class="token">${token}</td>
31
- <td class="identity">${identity}</td>
31
+ <td class="identity">{{if link}}<a href="${link}">${identity}</a>{{else}}${identity}{{/if}}</td>
32
32
  <td class="scope">${scope}</td>
33
33
  <td class="created">{{html $.shortdate(created)}}</td>
34
34
  <td class="revoke">
@@ -0,0 +1,4 @@
1
+ <div class="no-access">
2
+ <h1>${error}</h1>
3
+ <p>You can try to <a href="#/">authenticate again</a></p>
4
+ </div>
@@ -2,6 +2,7 @@ require "rack/oauth2/models"
2
2
  require "rack/oauth2/server/errors"
3
3
  require "rack/oauth2/server/utils"
4
4
  require "rack/oauth2/server/helper"
5
+ require "rack/oauth2/server/admin"
5
6
 
6
7
 
7
8
  module Rack
@@ -107,7 +108,7 @@ module Rack
107
108
  request.env["oauth.access_token"] = token
108
109
  request.env["oauth.identity"] = access_token.identity
109
110
  logger.info "Authorized #{access_token.identity}" if logger
110
- rescue Error=>error
111
+ rescue OAuthError=>error
111
112
  # 5.2. The WWW-Authenticate Response Header Field
112
113
  logger.info "HTTP authorization failed #{error.code}" if logger
113
114
  return unauthorized(request, error)
@@ -154,30 +155,32 @@ module Rack
154
155
  # application.
155
156
  def request_authorization(request, logger)
156
157
  state = request.GET["state"]
157
- if request.GET["authorization"]
158
- auth_request = self.class.get_auth_request(request.GET["authorization"]) rescue nil
159
- if !auth_request || auth_request.revoked
160
- logger.error "Invalid authorization request #{auth_request}" if logger
161
- return bad_request("Invalid authorization request")
162
- end
163
- client = self.class.get_client(auth_request.client_id)
158
+ begin
164
159
 
165
- else
160
+ if request.GET["authorization"]
161
+ auth_request = self.class.get_auth_request(request.GET["authorization"]) rescue nil
162
+ if !auth_request || auth_request.revoked
163
+ logger.error "Invalid authorization request #{auth_request}" if logger
164
+ return bad_request("Invalid authorization request")
165
+ end
166
+ response_type = auth_request.response_type # Needed for error handling
167
+ client = self.class.get_client(auth_request.client_id)
166
168
 
167
- # 3. Obtaining End-User Authorization
168
- begin
169
- redirect_uri = Utils.parse_redirect_uri(request.GET["redirect_uri"])
170
- rescue InvalidRequestError=>error
171
- logger.error "Authorization request with invalid redirect_uri: #{request.GET["redirect_uri"]} #{error.message}" if logger
172
- return bad_request(error.message)
173
- end
169
+ else
170
+
171
+ # 3. Obtaining End-User Authorization
172
+ begin
173
+ redirect_uri = Utils.parse_redirect_uri(request.GET["redirect_uri"])
174
+ rescue InvalidRequestError=>error
175
+ logger.error "Authorization request with invalid redirect_uri: #{request.GET["redirect_uri"]} #{error.message}" if logger
176
+ return bad_request(error.message)
177
+ end
174
178
 
175
- begin
176
179
  # 3. Obtaining End-User Authorization
180
+ response_type = request.GET["response_type"].to_s # Need this first, for error handling
177
181
  client = get_client(request)
178
182
  raise RedirectUriMismatchError unless client.redirect_uri.nil? || client.redirect_uri == redirect_uri.to_s
179
183
  requested_scope = request.GET["scope"].to_s.split.uniq.join(" ")
180
- response_type = request.GET["response_type"].to_s
181
184
  raise UnsupportedResponseTypeError unless options.authorization_types.include?(response_type)
182
185
  if scopes = options.scopes
183
186
  allowed_scope = scopes.respond_to?(:all?) ? scopes : scopes.split
@@ -186,17 +189,24 @@ module Rack
186
189
  # Create object to track authorization request and let application
187
190
  # handle the rest.
188
191
  auth_request = AuthRequest.create(client.id, requested_scope, redirect_uri.to_s, response_type, state)
189
- rescue Error=>error
190
- logger.error "Authorization request error: #{error.code} #{error.message}" if logger
191
- params = Rack::Utils.parse_query(redirect_uri.query).merge(:error=>error.code, :error_description=>error.message, :state=>state)
192
+ request.env["oauth.authorization"] = auth_request.id.to_s
193
+ end
194
+ # Pass back to application, watch for 403 (deny!)
195
+ logger.info "Request #{auth_request.id}: Client #{client.display_name} requested #{auth_request.response_type} with scope #{auth_request.scope}" if logger
196
+ response = @app.call(request.env)
197
+ raise AccessDeniedError if response[0] == 403
198
+ return response
199
+ rescue OAuthError=>error
200
+ logger.error "Authorization request error: #{error.code} #{error.message}" if logger
201
+ params = { :error=>error.code, :error_description=>error.message, :state=>state }
202
+ if response_type == "token"
203
+ redirect_uri.fragment = Rack::Utils.build_query(params)
204
+ else # response type is code, or invalid
205
+ params = Rack::Utils.parse_query(redirect_uri.query).merge(params)
192
206
  redirect_uri.query = Rack::Utils.build_query(params)
193
- return redirect_to(redirect_uri)
194
207
  end
208
+ return redirect_to(redirect_uri)
195
209
  end
196
-
197
- request.env["oauth.authorization"] = auth_request.id.to_s
198
- logger.info "Request #{auth_request.id}: Client #{client.display_name} requested #{auth_request.response_type} with scope #{auth_request.scope}" if logger
199
- return @app.call(request.env)
200
210
  end
201
211
 
202
212
  # Get here on completion of the authorization. Authorization response in
@@ -206,29 +216,33 @@ module Rack
206
216
  status, headers, body = response
207
217
  auth_request = self.class.get_auth_request(headers["oauth.authorization"])
208
218
  redirect_uri = URI.parse(auth_request.redirect_uri)
209
- if status == 401
219
+ if status == 403
210
220
  auth_request.deny!
211
221
  else
212
222
  auth_request.grant! headers["oauth.identity"]
213
223
  end
214
224
  # 3.1. Authorization Response
215
- if auth_request.response_type == "code" && auth_request.grant_code
216
- logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access code #{auth_request.grant_code}" if logger
217
- params = { :code=>auth_request.grant_code, :scope=>auth_request.scope, :state=>auth_request.state }
225
+ if auth_request.response_type == "code"
226
+ if auth_request.grant_code
227
+ logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access code #{auth_request.grant_code}" if logger
228
+ params = { :code=>auth_request.grant_code, :scope=>auth_request.scope, :state=>auth_request.state }
229
+ else
230
+ logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} denied authorization" if logger
231
+ params = { :error=>:access_denied, :state=>auth_request.state }
232
+ end
218
233
  params = Rack::Utils.parse_query(redirect_uri.query).merge(params)
219
234
  redirect_uri.query = Rack::Utils.build_query(params)
220
- return redirect_to(redirect_uri)
221
- elsif auth_request.response_type == "token" && auth_request.access_token
222
- logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access token #{auth_request.access_token}" if logger
223
- params = { :access_token=>auth_request.access_token, :scope=>auth_request.scope, :state=>auth_request.state }
235
+ else # response type if token
236
+ if auth_request.access_token
237
+ logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access token #{auth_request.access_token}" if logger
238
+ params = { :access_token=>auth_request.access_token, :scope=>auth_request.scope, :state=>auth_request.state }
239
+ else
240
+ logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} denied authorization" if logger
241
+ params = { :error=>:access_denied, :state=>auth_request.state }
242
+ end
224
243
  redirect_uri.fragment = Rack::Utils.build_query(params)
225
- return redirect_to(redirect_uri)
226
- else
227
- logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} denied authorization" if logger
228
- params = Rack::Utils.parse_query(redirect_uri.query).merge(:error=>:access_denied, :state=>auth_request.state)
229
- redirect_uri.query = Rack::Utils.build_query(params)
230
- return redirect_to(redirect_uri)
231
244
  end
245
+ return redirect_to(redirect_uri)
232
246
  end
233
247
 
234
248
  # 4. Obtaining an Access Token
@@ -264,7 +278,7 @@ module Rack
264
278
  response[:scope] = access_token.scope unless access_token.scope.empty?
265
279
  return [200, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" }, response.to_json]
266
280
  # 4.3. Error Response
267
- rescue Error=>error
281
+ rescue OAuthError=>error
268
282
  logger.error "Access token request error: #{error.code} #{error.message}" if logger
269
283
  return unauthorized(request, error) if InvalidClientError === error && request.basic?
270
284
  return [400, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" },
@@ -16,7 +16,7 @@ module Rack
16
16
  def mount(klass, path)
17
17
  @klass = klass
18
18
  @path = path
19
- @match = /^#{Regexp.escape(path)}\/(.*)$/
19
+ @match = /^#{Regexp.escape(path)}(\/.*|$)?/
20
20
  end
21
21
 
22
22
  attr_reader :klass, :path, :match
@@ -31,7 +31,7 @@ module Rack
31
31
  path = env["PATH_INFO"].to_s
32
32
  script_name = env['SCRIPT_NAME']
33
33
  if path =~ self.class.match && rest = $1
34
- env.merge! "SCRIPT_NAME"=>(script_name + self.class.path), "PATH_INFO"=>"/#{rest}"
34
+ env.merge! "SCRIPT_NAME"=>(script_name + self.class.path), "PATH_INFO"=>rest
35
35
  return @admin.call(env)
36
36
  else
37
37
  return @pass.call(env)
@@ -63,6 +63,9 @@ module Rack
63
63
  # Use this URL to authorize access to this console. If not set, goes to
64
64
  # /oauth/authorize.
65
65
  set :authorize, nil
66
+ # Map access token identity to URL on your application, by replacing
67
+ # "{id}" with the token identity (e.g. "http://example.com/user/{id}")
68
+ set :template_url, nil
66
69
 
67
70
  # Number of tokens to return in each page.
68
71
  set :tokens_per_page, 100
@@ -213,6 +216,7 @@ module Rack
213
216
  def token_as_json(token)
214
217
  { :token=>token.token, :identity=>token.identity, :scope=>token.scope, :created=>token.created_at,
215
218
  :expired=>token.expires_at, :revoked=>token.revoked,
219
+ :link=>settings.template_url && settings.template_url.gsub("{id}", token.identity),
216
220
  :revoke=>"#{request.script_name}/api/token/#{token.token}/revoke" }
217
221
  end
218
222
  end