rack-oauth2-server 1.2.2 → 1.3.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 (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