rack-oauth2-server 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGELOG +14 -0
  2. data/Gemfile +3 -0
  3. data/README.rdoc +26 -7
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -0
  6. data/lib/rack/oauth2/admin/css/screen.css +233 -0
  7. data/lib/rack/oauth2/admin/images/loading.gif +0 -0
  8. data/lib/rack/oauth2/admin/js/application.js +154 -0
  9. data/lib/rack/oauth2/admin/js/jquery.js +166 -0
  10. data/lib/rack/oauth2/admin/js/jquery.tmpl.js +414 -0
  11. data/lib/rack/oauth2/admin/js/sammy.js +5 -0
  12. data/lib/rack/oauth2/admin/js/sammy.json.js +5 -0
  13. data/lib/rack/oauth2/admin/js/sammy.storage.js +5 -0
  14. data/lib/rack/oauth2/admin/js/sammy.title.js +5 -0
  15. data/lib/rack/oauth2/admin/js/sammy.tmpl.js +5 -0
  16. data/lib/rack/oauth2/admin/js/underscore.js +722 -0
  17. data/lib/rack/oauth2/admin/views/client.tmpl +48 -0
  18. data/lib/rack/oauth2/admin/views/clients.tmpl +36 -0
  19. data/lib/rack/oauth2/admin/views/edit.tmpl +57 -0
  20. data/lib/rack/oauth2/admin/views/index.html +26 -0
  21. data/lib/rack/oauth2/models/access_grant.rb +6 -4
  22. data/lib/rack/oauth2/models/access_token.rb +36 -4
  23. data/lib/rack/oauth2/models/auth_request.rb +4 -3
  24. data/lib/rack/oauth2/models/client.rb +15 -2
  25. data/lib/rack/oauth2/server.rb +71 -58
  26. data/lib/rack/oauth2/server/admin.rb +216 -0
  27. data/lib/rack/oauth2/server/helper.rb +4 -4
  28. data/lib/rack/oauth2/sinatra.rb +2 -2
  29. data/rack-oauth2-server.gemspec +2 -3
  30. data/test/admin/api_test.rb +196 -0
  31. data/test/admin_test_.rb +49 -0
  32. data/test/{access_grant_test.rb → oauth/access_grant_test.rb} +1 -1
  33. data/test/{access_token_test.rb → oauth/access_token_test.rb} +83 -12
  34. data/test/{authorization_test.rb → oauth/authorization_test.rb} +1 -1
  35. data/test/rails/config/environment.rb +2 -0
  36. data/test/rails/log/test.log +72938 -0
  37. data/test/setup.rb +17 -1
  38. data/test/sinatra/my_app.rb +1 -1
  39. metadata +27 -9
  40. data/lib/rack/oauth2/server/version.rb +0 -9
@@ -0,0 +1,216 @@
1
+ require "sinatra/base"
2
+ require "json"
3
+ require "rack/oauth2/sinatra"
4
+
5
+ module Rack
6
+ module OAuth2
7
+ class Server
8
+ class Admin < ::Sinatra::Base
9
+
10
+ class << self
11
+
12
+ # Rack module that mounts the specified class on the specified path,
13
+ # and passes all other request to the application.
14
+ class Mount
15
+ class << self
16
+ def mount(klass, path)
17
+ @klass = klass
18
+ @path = path
19
+ @match = /^#{Regexp.escape(path)}\/(.*)$/
20
+ end
21
+
22
+ attr_reader :klass, :path, :match
23
+ end
24
+
25
+ def initialize(app)
26
+ @pass = app
27
+ @admin = self.class.klass.new
28
+ end
29
+
30
+ def call(env)
31
+ path = env["PATH_INFO"].to_s
32
+ script_name = env['SCRIPT_NAME']
33
+ if path =~ self.class.match && rest = $1
34
+ env.merge! "SCRIPT_NAME"=>(script_name + self.class.path), "PATH_INFO"=>"/#{rest}"
35
+ return @admin.call(env)
36
+ else
37
+ return @pass.call(env)
38
+ end
39
+ end
40
+ end
41
+
42
+ # Returns Rack handle that mounts Admin on the specified path, and
43
+ # forwards all other requests back to the application.
44
+ #
45
+ # @param [String, nil] path The path to mount on, defaults to
46
+ # /oauth/admin
47
+ # @return [Object] Rack module
48
+ #
49
+ # @example To include admin console in Rails 2.x app
50
+ # config.middleware.use Rack::OAuth2::Server::Admin.mount
51
+ def mount(path = "/oauth/admin")
52
+ mount = Class.new(Mount)
53
+ mount.mount Admin, "/oauth/admin"
54
+ mount
55
+ end
56
+
57
+ end
58
+
59
+ # Need client ID to get access token to access this console.
60
+ set :client_id, nil
61
+ # Need client secret to get access token to access this console.
62
+ set :client_secret, nil
63
+ # Use this URL to authorize access to this console. If not set, goes to
64
+ # /oauth/authorize.
65
+ set :authorize, nil
66
+
67
+ # Number of tokens to return in each page.
68
+ set :tokens_per_page, 100
69
+ set :public, ::File.dirname(__FILE__) + "/../admin"
70
+ mime_type :js, "text/javascript"
71
+ mime_type :tmpl, "text/x-jquery-template"
72
+
73
+
74
+ helpers Rack::OAuth2::Sinatra::Helpers
75
+ extend Rack::OAuth2::Sinatra
76
+ use Rack::OAuth2::Server
77
+
78
+ # Force HTTPS except for development environment.
79
+ before do
80
+ redirect request.url.sub(/^http:/, "https:") unless request.scheme == "https"
81
+ end unless development?
82
+
83
+
84
+
85
+ # -- Static content --
86
+
87
+ # It's a single-page app, this is that single page.
88
+ get "/" do
89
+ send_file settings.public + "/views/index.html"
90
+ end
91
+
92
+ # Service JavaScript, CSS and jQuery templates from the gem.
93
+ %w{js css views}.each do |path|
94
+ get "/#{path}/:name" do
95
+ send_file settings.public + "/#{path}/" + params[:name]
96
+ end
97
+ end
98
+
99
+
100
+ # -- Getting an access token --
101
+
102
+ # To get an OAuth token, you need client ID and secret, two values we
103
+ # didn't pass on to the JavaScript code, so it has no way to request
104
+ # authorization directly. Instead, it redirects to this URL which in turn
105
+ # redirects to the authorization endpoint. This redirect does accept the
106
+ # state parameter, which will be returned after authorization.
107
+ get "/authorize" do
108
+ redirect_uri = "#{request.scheme}://#{request.host}:#{request.port}#{request.script_name}"
109
+ query = { :client_id=>settings.client_id, :client_secret=>settings.client_secret, :state=>params[:state],
110
+ :response_type=>"token", :scope=>"oauth-admin", :redirect_uri=>redirect_uri }
111
+ auth_url = settings.authorize || "#{request.scheme}://#{request.host}:#{request.port}/oauth/authorize"
112
+ redirect "#{auth_url}?#{Rack::Utils.build_query(query)}"
113
+ end
114
+
115
+
116
+ # -- API --
117
+
118
+ oauth_required "/api/clients", "/api/client/:id", "/api/client/:id/revoke", "/api/token/:token/revoke", :scope=>"oauth-admin"
119
+
120
+ get "/api/clients" do
121
+ content_type "application/json"
122
+ json = { :list=>Server::Client.all.map { |client| client_as_json(client) },
123
+ :tokens=>{ :total=>Server::AccessToken.count, :week=>Server::AccessToken.count(:days=>7),
124
+ :revoked=>Server::AccessToken.count(:days=>7, :revoked=>true) } }
125
+ json.to_json
126
+ end
127
+
128
+ post "/api/clients" do
129
+ begin
130
+ client = Server::Client.create(validate_params(params))
131
+ redirect "#{request.script_name}/api/client/#{client.id}"
132
+ rescue
133
+ halt 400, $!.message
134
+ end
135
+ end
136
+
137
+ get "/api/client/:id" do
138
+ content_type "application/json"
139
+ client = Server::Client.find(params[:id])
140
+ json = client_as_json(client, true)
141
+
142
+ page = [params[:page].to_i, 1].max
143
+ offset = (page - 1) * settings.tokens_per_page
144
+ total = Server::AccessToken.count(:client_id=>client.id)
145
+ tokens = Server::AccessToken.for_client(params[:id], offset, settings.tokens_per_page)
146
+ json[:tokens] = { :list=>tokens.map { |token| token_as_json(token) } }
147
+ json[:tokens][:total] = total
148
+ json[:tokens][:page] = page
149
+ json[:tokens][:next] = "#{request.script_name}/client/#{params[:id]}?page=#{page + 1}" if total > page * settings.tokens_per_page
150
+ json[:tokens][:previous] = "#{request.script_name}/client/#{params[:id]}?page=#{page - 1}" if page > 1
151
+ json[:tokens][:total] = Server::AccessToken.count(:client_id=>client.id)
152
+ json[:tokens][:week] = Server::AccessToken.count(:client_id=>client.id, :days=>7)
153
+ json[:tokens][:revoked] = Server::AccessToken.count(:client_id=>client.id, :days=>7, :revoked=>true)
154
+
155
+ json.to_json
156
+ end
157
+
158
+ put "/api/client/:id" do
159
+ client = Server::Client.find(params[:id])
160
+ begin
161
+ client.update validate_params(params)
162
+ redirect "#{request.script_name}/api/client/#{client.id}"
163
+ rescue
164
+ halt 400, $!.message
165
+ end
166
+ end
167
+
168
+ post "/api/client/:id/revoke" do
169
+ client = Server::Client.find(params[:id])
170
+ client.revoke!
171
+ 200
172
+ end
173
+
174
+ post "/api/token/:token/revoke" do
175
+ token = Server::AccessToken.from_token(params[:token])
176
+ token.revoke!
177
+ 200
178
+ end
179
+
180
+ helpers do
181
+ def validate_params(params)
182
+ display_name = params[:displayName].to_s.strip
183
+ halt 400, "Missing display name" if display_name.empty?
184
+ link = URI.parse(params[:link].to_s.strip).normalize rescue nil
185
+ halt 400, "Link is not a URL (must be http://....)" unless link
186
+ halt 400, "Link must be an absolute URL with HTTP/S scheme" unless link.absolute? && %{http https}.include?(link.scheme)
187
+ redirect_uri = URI.parse(params[:redirectUri].to_s.strip).normalize rescue nil
188
+ halt 400, "Redirect URL is not a URL (must be http://....)" unless redirect_uri
189
+ halt 400, "Redirect URL must be an absolute URL with HTTP/S scheme" unless
190
+ redirect_uri.absolute? && %{http https}.include?(redirect_uri.scheme)
191
+ if image_url = URI.parse(params[:imageUrl].to_s.strip).normalize rescue nil
192
+ halt 400, "Image URL must be an absolute URL with HTTP/S scheme" unless
193
+ image_url.absolute? && %{http https}.include?(image_url.scheme)
194
+ end
195
+ { :display_name=>display_name, :link=>link.to_s, :image_url=>image_url.to_s, :redirect_uri=>redirect_uri.to_s }
196
+ end
197
+
198
+ def client_as_json(client, with_stats = false)
199
+ { "id"=>client.id.to_s, "secret"=>client.secret, :redirectUri=>client.redirect_uri,
200
+ :displayName=>client.display_name, :link=>client.link, :imageUrl=>client.image_url,
201
+ :url=>"#{request.script_name}/api/client/#{client.id}",
202
+ :revoke=>"#{request.script_name}/api/client/#{client.id}/revoke",
203
+ :created=>client.created_at, :revoked=>client.revoked }
204
+ end
205
+
206
+ def token_as_json(token)
207
+ { :token=>token.token, :identity=>token.identity, :scope=>token.scope, :created=>token.created_at,
208
+ :expired=>token.expires_at, :revoked=>token.revoked,
209
+ :revoke=>"#{request.script_name}/api/token/#{token.token}/revoke" }
210
+ end
211
+ end
212
+
213
+ end
214
+ end
215
+ end
216
+ end
@@ -66,7 +66,7 @@ module Rack
66
66
  #
67
67
  # @return 401
68
68
  def no_access!
69
- @response["oauth.no_access"] = true
69
+ @response["oauth.no_access"] = "true"
70
70
  @response.status = 401
71
71
  end
72
72
 
@@ -77,7 +77,7 @@ module Rack
77
77
  # @param [String] scope The missing scope, e.g. "read"
78
78
  # @return 403
79
79
  def no_scope!(scope)
80
- @response["oauth.no_scope"] = scope
80
+ @response["oauth.no_scope"] = scope.to_s
81
81
  @response.status = 403
82
82
  end
83
83
 
@@ -106,7 +106,7 @@ module Rack
106
106
  # @param [String] identity Identity string
107
107
  # @return 200
108
108
  def grant!(authorization, identity)
109
- @response["oauth.authorization"] = authorization
109
+ @response["oauth.authorization"] = authorization.to_s
110
110
  @response["oauth.identity"] = identity.to_s
111
111
  @response.status = 200
112
112
  end
@@ -118,7 +118,7 @@ module Rack
118
118
  # @param [String] authorization Authorization handle
119
119
  # @return 401
120
120
  def deny!(authorization)
121
- @response["oauth.authorization"] = authorization
121
+ @response["oauth.authorization"] = authorization.to_s
122
122
  @response.status = 401
123
123
  end
124
124
 
@@ -42,10 +42,10 @@ module Rack
42
42
  before path do
43
43
  if oauth.authenticated?
44
44
  if scope && !oauth.scope.include?(scope)
45
- oauth.no_scope! scope
45
+ halt oauth.no_scope! scope
46
46
  end
47
47
  else
48
- oauth.no_access!
48
+ halt oauth.no_access!
49
49
  end
50
50
  end
51
51
  end
@@ -1,9 +1,8 @@
1
1
  $: << File.dirname(__FILE__) + "/lib"
2
- require "rack/oauth2/server/version"
3
2
 
4
3
  Gem::Specification.new do |spec|
5
4
  spec.name = "rack-oauth2-server"
6
- spec.version = Rack::OAuth2::Server::VERSION
5
+ spec.version = IO.read("VERSION")
7
6
  spec.author = "Assaf Arkin"
8
7
  spec.email = "assaf@labnotes.org"
9
8
  spec.homepage = "http://github.com/assaf/#{spec.name}"
@@ -11,7 +10,7 @@ Gem::Specification.new do |spec|
11
10
  spec.description = "Because you don't allow strangers into your app, and OAuth 2.0 is the new awesome."
12
11
  spec.post_install_message = ""
13
12
 
14
- spec.files = Dir["{bin,lib,rails,test}/**/*", "CHANGELOG", "MIT-LICENSE", "README.rdoc", "Rakefile", "Gemfile", "*.gemspec"]
13
+ spec.files = Dir["{bin,lib,rails,test}/**/*", "CHANGELOG", "VERSION", "MIT-LICENSE", "README.rdoc", "Rakefile", "Gemfile", "*.gemspec"]
15
14
 
16
15
  spec.has_rdoc = true
17
16
  spec.extra_rdoc_files = "README.rdoc", "CHANGELOG"
@@ -0,0 +1,196 @@
1
+ require "test/setup"
2
+
3
+ class AdminApiTest < Test::Unit::TestCase
4
+ module Helpers
5
+ def should_fail_authentication
6
+ should "respond with status 401 (Unauthorized)" do
7
+ assert_equal 401, last_response.status
8
+ end
9
+ end
10
+
11
+ def should_forbid_access
12
+ should "respond with status 403 (Forbidden)" do
13
+ assert_equal 403, last_response.status
14
+ end
15
+ end
16
+ end
17
+ extend Helpers
18
+
19
+
20
+ def setup
21
+ super
22
+ end
23
+
24
+ def without_scope
25
+ token = Rack::OAuth2::Server::AccessToken.get_token_for("Superman", "nobody", client.id)
26
+ header "Authorization", "OAuth #{token.token}"
27
+ end
28
+
29
+ def with_scope
30
+ token = Rack::OAuth2::Server::AccessToken.get_token_for("Superman", "oauth-admin", client.id)
31
+ header "Authorization", "OAuth #{token.token}"
32
+ end
33
+
34
+ def json
35
+ JSON.parse(last_response.body)
36
+ end
37
+
38
+
39
+ # -- /oauth/admin/api/clients
40
+
41
+ context "all clients" do
42
+ context "without authentication" do
43
+ setup { get "/oauth/admin/api/clients" }
44
+ should_fail_authentication
45
+ end
46
+
47
+ context "without scope" do
48
+ setup { without_scope ; get "/oauth/admin/api/clients" }
49
+ should_forbid_access
50
+ end
51
+
52
+ context "with scope" do
53
+ setup { with_scope ; get "/oauth/admin/api/clients" }
54
+ should "return OK" do
55
+ assert_equal 200, last_response.status
56
+ end
57
+ should "return JSON document" do
58
+ assert_equal "application/json;charset=utf-8", last_response.content_type
59
+ end
60
+ should "return list of clients" do
61
+ assert Array === json["list"]
62
+ end
63
+ end
64
+
65
+ context "client" do
66
+ setup do
67
+ with_scope
68
+ get "/oauth/admin/api/clients"
69
+ @first = json["list"].first
70
+ end
71
+
72
+ should "provide client identifier" do
73
+ assert_equal client.id.to_s, @first["id"]
74
+ end
75
+ should "provide client secret" do
76
+ assert_equal client.secret, @first["secret"]
77
+ end
78
+ should "provide redirect URI" do
79
+ assert_equal client.redirect_uri, @first["redirectUri"]
80
+ end
81
+ should "provide display name" do
82
+ assert_equal client.display_name, @first["displayName"]
83
+ end
84
+ should "provide site URL" do
85
+ assert_equal client.link, @first["link"]
86
+ end
87
+ should "provide image URL" do
88
+ assert_equal client.image_url, @first["imageUrl"]
89
+ end
90
+ should "provide created timestamp" do
91
+ assert_equal client.created_at.to_i, @first["created"]
92
+ end
93
+ should "provide link to client resource"do
94
+ assert_equal ["/oauth/admin/api/client", client.id].join("/"), @first["url"]
95
+ end
96
+ should "provide link to revoke resource"do
97
+ assert_equal ["/oauth/admin/api/client", client.id, "revoke"].join("/"), @first["revoke"]
98
+ end
99
+ should "tell if not revoked" do
100
+ assert @first["revoked"].nil?
101
+ end
102
+ end
103
+
104
+ context "revoked client" do
105
+ setup do
106
+ client.revoke!
107
+ with_scope
108
+ get "/oauth/admin/api/clients"
109
+ @first = json["list"].first
110
+ end
111
+
112
+ should "provide revoked timestamp" do
113
+ assert_equal client.revoked.to_i, @first["revoked"]
114
+ end
115
+ end
116
+
117
+ context "tokens" do
118
+ setup do
119
+ tokens = []
120
+ 1.upto(10).map do |days|
121
+ Timecop.travel -days*86400 do
122
+ tokens << Rack::OAuth2::Server::AccessToken.get_token_for("Superman", days.to_s, client.id)
123
+ end
124
+ end
125
+ # Revoke one token today (within past 7 days), one 10 days ago (beyond)
126
+ tokens.first.revoke!
127
+ tokens.last.revoke!
128
+ with_scope ; get "/oauth/admin/api/clients"
129
+ end
130
+
131
+ should "return total number of tokens" do
132
+ assert_equal 11, json["tokens"]["total"]
133
+ end
134
+ should "return number of tokens created past week" do
135
+ assert_equal 7, json["tokens"]["week"]
136
+ end
137
+ should "return number of revoked token past week" do
138
+ assert_equal 1, json["tokens"]["revoked"]
139
+ end
140
+ end
141
+ end
142
+
143
+
144
+ # -- /oauth/admin/api/client/:id
145
+
146
+ context "single client" do
147
+ context "without authentication" do
148
+ setup { get "/oauth/admin/api/client/#{client.id}" }
149
+ should_fail_authentication
150
+ end
151
+
152
+ context "without scope" do
153
+ setup { without_scope ; get "/oauth/admin/api/client/#{client.id}" }
154
+ should_forbid_access
155
+ end
156
+
157
+ context "with scope" do
158
+ setup { with_scope ; get "/oauth/admin/api/client/#{client.id}" }
159
+
160
+ should "return OK" do
161
+ assert_equal 200, last_response.status
162
+ end
163
+ should "return JSON document" do
164
+ assert_equal "application/json;charset=utf-8", last_response.content_type
165
+ end
166
+ should "provide client identifier" do
167
+ assert_equal client.id.to_s, json["id"]
168
+ end
169
+ should "provide client secret" do
170
+ assert_equal client.secret, json["secret"]
171
+ end
172
+ should "provide redirect URI" do
173
+ assert_equal client.redirect_uri, json["redirectUri"]
174
+ end
175
+ should "provide display name" do
176
+ assert_equal client.display_name, json["displayName"]
177
+ end
178
+ should "provide site URL" do
179
+ assert_equal client.link, json["link"]
180
+ end
181
+ should "provide image URL" do
182
+ assert_equal client.image_url, json["imageUrl"]
183
+ end
184
+ should "provide created timestamp" do
185
+ assert_equal client.created_at.to_i, json["created"]
186
+ end
187
+ should "provide link to client resource"do
188
+ assert_equal ["/oauth/admin/api/client", client.id].join("/"), json["url"]
189
+ end
190
+ should "provide link to revoke resource"do
191
+ assert_equal ["/oauth/admin/api/client", client.id, "revoke"].join("/"), json["revoke"]
192
+ end
193
+ end
194
+ end
195
+
196
+ end