tpitale-rack-oauth2-server 2.2.1

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 (64) hide show
  1. data/CHANGELOG +202 -0
  2. data/Gemfile +16 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +604 -0
  5. data/Rakefile +90 -0
  6. data/VERSION +1 -0
  7. data/bin/oauth2-server +206 -0
  8. data/lib/rack-oauth2-server.rb +4 -0
  9. data/lib/rack/oauth2/admin/css/screen.css +347 -0
  10. data/lib/rack/oauth2/admin/images/loading.gif +0 -0
  11. data/lib/rack/oauth2/admin/images/oauth-2.png +0 -0
  12. data/lib/rack/oauth2/admin/js/application.coffee +220 -0
  13. data/lib/rack/oauth2/admin/js/jquery.js +166 -0
  14. data/lib/rack/oauth2/admin/js/jquery.tmpl.js +414 -0
  15. data/lib/rack/oauth2/admin/js/protovis-r3.2.js +277 -0
  16. data/lib/rack/oauth2/admin/js/sammy.js +5 -0
  17. data/lib/rack/oauth2/admin/js/sammy.json.js +5 -0
  18. data/lib/rack/oauth2/admin/js/sammy.oauth2.js +142 -0
  19. data/lib/rack/oauth2/admin/js/sammy.storage.js +5 -0
  20. data/lib/rack/oauth2/admin/js/sammy.title.js +5 -0
  21. data/lib/rack/oauth2/admin/js/sammy.tmpl.js +5 -0
  22. data/lib/rack/oauth2/admin/js/underscore.js +722 -0
  23. data/lib/rack/oauth2/admin/views/client.tmpl +58 -0
  24. data/lib/rack/oauth2/admin/views/clients.tmpl +52 -0
  25. data/lib/rack/oauth2/admin/views/edit.tmpl +80 -0
  26. data/lib/rack/oauth2/admin/views/index.html +39 -0
  27. data/lib/rack/oauth2/admin/views/no_access.tmpl +4 -0
  28. data/lib/rack/oauth2/models.rb +27 -0
  29. data/lib/rack/oauth2/models/access_grant.rb +54 -0
  30. data/lib/rack/oauth2/models/access_token.rb +129 -0
  31. data/lib/rack/oauth2/models/auth_request.rb +61 -0
  32. data/lib/rack/oauth2/models/client.rb +93 -0
  33. data/lib/rack/oauth2/rails.rb +105 -0
  34. data/lib/rack/oauth2/server.rb +458 -0
  35. data/lib/rack/oauth2/server/admin.rb +250 -0
  36. data/lib/rack/oauth2/server/errors.rb +104 -0
  37. data/lib/rack/oauth2/server/helper.rb +147 -0
  38. data/lib/rack/oauth2/server/practice.rb +79 -0
  39. data/lib/rack/oauth2/server/railtie.rb +24 -0
  40. data/lib/rack/oauth2/server/utils.rb +30 -0
  41. data/lib/rack/oauth2/sinatra.rb +71 -0
  42. data/rack-oauth2-server.gemspec +24 -0
  43. data/rails/init.rb +11 -0
  44. data/test/admin/api_test.rb +228 -0
  45. data/test/admin/ui_test.rb +38 -0
  46. data/test/oauth/access_grant_test.rb +276 -0
  47. data/test/oauth/access_token_test.rb +311 -0
  48. data/test/oauth/authorization_test.rb +298 -0
  49. data/test/oauth/server_methods_test.rb +292 -0
  50. data/test/rails2/app/controllers/api_controller.rb +40 -0
  51. data/test/rails2/app/controllers/application_controller.rb +2 -0
  52. data/test/rails2/app/controllers/oauth_controller.rb +17 -0
  53. data/test/rails2/config/environment.rb +19 -0
  54. data/test/rails2/config/environments/test.rb +0 -0
  55. data/test/rails2/config/routes.rb +13 -0
  56. data/test/rails3/app/controllers/api_controller.rb +40 -0
  57. data/test/rails3/app/controllers/application_controller.rb +2 -0
  58. data/test/rails3/app/controllers/oauth_controller.rb +17 -0
  59. data/test/rails3/config/application.rb +19 -0
  60. data/test/rails3/config/environment.rb +2 -0
  61. data/test/rails3/config/routes.rb +12 -0
  62. data/test/setup.rb +120 -0
  63. data/test/sinatra/my_app.rb +69 -0
  64. metadata +145 -0
@@ -0,0 +1,250 @@
1
+ require "sinatra/base"
2
+ require "json"
3
+ require "rack/oauth2/server"
4
+ require "rack/oauth2/sinatra"
5
+
6
+ module Rack
7
+ module OAuth2
8
+ class Server
9
+ class Admin < ::Sinatra::Base
10
+
11
+ class << self
12
+
13
+ # Rack module that mounts the specified class on the specified path,
14
+ # and passes all other request to the application.
15
+ class Mount
16
+ class << self
17
+ def mount(klass, path)
18
+ @klass = klass
19
+ @path = path
20
+ @match = /^#{Regexp.escape(path)}(\/.*|$)?/
21
+ end
22
+
23
+ attr_reader :klass, :path, :match
24
+ end
25
+
26
+ def initialize(app)
27
+ @pass = app
28
+ @admin = self.class.klass.new
29
+ end
30
+
31
+ def call(env)
32
+ path = env["PATH_INFO"].to_s
33
+ script_name = env['SCRIPT_NAME']
34
+ if path =~ self.class.match && rest = $1
35
+ env.merge! "SCRIPT_NAME"=>(script_name + self.class.path), "PATH_INFO"=>rest
36
+ return @admin.call(env)
37
+ else
38
+ return @pass.call(env)
39
+ end
40
+ end
41
+ end
42
+
43
+ # Returns Rack handle that mounts Admin on the specified path, and
44
+ # forwards all other requests back to the application.
45
+ #
46
+ # @param [String, nil] path The path to mount on, defaults to
47
+ # /oauth/admin
48
+ # @return [Object] Rack module
49
+ #
50
+ # @example To include Web admin in Rails 2.x app:
51
+ # config.middleware.use Rack::OAuth2::Server::Admin.mount
52
+ def mount(path = "/oauth/admin")
53
+ mount = Class.new(Mount)
54
+ mount.mount Admin, "/oauth/admin"
55
+ mount
56
+ end
57
+
58
+ end
59
+
60
+
61
+ # Client application identified, require to authenticate.
62
+ set :client_id, nil
63
+ # Client application secret, required to authenticate.
64
+ set :client_secret, nil
65
+ # Endpoint for requesing authorization, defaults to /oauth/admin.
66
+ set :authorize, nil
67
+ # Will map an access token identity into a URL in your application,
68
+ # using the substitution value "{id}", e.g.
69
+ # "http://example.com/users/#{id}")
70
+ set :template_url, nil
71
+ # Forces all requests to use HTTPS (true by default except in
72
+ # development mode).
73
+ set :force_ssl, !development?
74
+ # Common scope shown and added by default to new clients.
75
+ set :scope, []
76
+
77
+
78
+ set :logger, ::Rails.logger if defined?(::Rails)
79
+ # Number of tokens to return in each page.
80
+ set :tokens_per_page, 100
81
+ set :public, ::File.dirname(__FILE__) + "/../admin"
82
+ set :method_override, true
83
+ mime_type :js, "text/javascript"
84
+ mime_type :tmpl, "text/x-jquery-template"
85
+
86
+ register Rack::OAuth2::Sinatra
87
+
88
+ # Force HTTPS except for development environment.
89
+ before do
90
+ redirect request.url.sub(/^http:/, "https:") if settings.force_ssl && request.scheme != "https"
91
+ end
92
+
93
+
94
+ # -- Static content --
95
+
96
+ # It's a single-page app, this is that single page.
97
+ get "/" do
98
+ send_file settings.public + "/views/index.html"
99
+ end
100
+
101
+ # Service JavaScript, CSS and jQuery templates from the gem.
102
+ %w{js css views}.each do |path|
103
+ get "/#{path}/:name" do
104
+ send_file settings.public + "/#{path}/" + params[:name]
105
+ end
106
+ end
107
+
108
+
109
+ # -- Getting an access token --
110
+
111
+ # To get an OAuth token, you need client ID and secret, two values we
112
+ # didn't pass on to the JavaScript code, so it has no way to request
113
+ # authorization directly. Instead, it redirects to this URL which in turn
114
+ # redirects to the authorization endpoint. This redirect does accept the
115
+ # state parameter, which will be returned after authorization.
116
+ get "/authorize" do
117
+ redirect_uri = "#{request.scheme}://#{request.host}:#{request.port}#{request.script_name}"
118
+ query = { :client_id=>settings.client_id, :client_secret=>settings.client_secret, :state=>params[:state],
119
+ :response_type=>"token", :scope=>"oauth-admin", :redirect_uri=>redirect_uri }
120
+ auth_url = settings.authorize || "#{request.scheme}://#{request.host}:#{request.port}/oauth/authorize"
121
+ redirect "#{auth_url}?#{Rack::Utils.build_query(query)}"
122
+ end
123
+
124
+
125
+ # -- API --
126
+
127
+ oauth_required "/api/clients", "/api/client/:id", "/api/client/:id/revoke", "/api/token/:token/revoke", :scope=>"oauth-admin"
128
+
129
+ get "/api/clients" do
130
+ content_type "application/json"
131
+ json = { :list=>Server::Client.all.map { |client| client_as_json(client) },
132
+ :scope=>Server::Utils.normalize_scope(settings.scope),
133
+ :history=>"#{request.script_name}/api/clients/history",
134
+ :tokens=>{ :total=>Server::AccessToken.count, :week=>Server::AccessToken.count(:days=>7),
135
+ :revoked=>Server::AccessToken.count(:days=>7, :revoked=>true) } }
136
+ json.to_json
137
+ end
138
+
139
+ # get "/api/clients/history" do
140
+ # content_type "application/json"
141
+ # { :data=>Server::AccessToken.historical }.to_json
142
+ # end
143
+
144
+ post "/api/clients" do
145
+ begin
146
+ client = Server::Client.create(validate_params(params))
147
+ redirect "#{request.script_name}/api/client/#{client.id}"
148
+ rescue
149
+ halt 400, $!.message
150
+ end
151
+ end
152
+
153
+ get "/api/client/:id" do
154
+ content_type "application/json"
155
+ client = Server::Client.find(params[:id])
156
+ json = client_as_json(client, true)
157
+
158
+ page = [params[:page].to_i, 1].max
159
+ offset = (page - 1) * settings.tokens_per_page
160
+ total = Server::AccessToken.count(:client_id=>client.id)
161
+ tokens = Server::AccessToken.for_client(params[:id], offset, settings.tokens_per_page)
162
+ json[:tokens] = { :list=>tokens.map { |token| token_as_json(token) } }
163
+ json[:tokens][:total] = total
164
+ json[:tokens][:page] = page
165
+ json[:tokens][:next] = "#{request.script_name}/client/#{params[:id]}?page=#{page + 1}" if total > page * settings.tokens_per_page
166
+ json[:tokens][:previous] = "#{request.script_name}/client/#{params[:id]}?page=#{page - 1}" if page > 1
167
+ json[:tokens][:total] = Server::AccessToken.count(:client_id=>client.id)
168
+ json[:tokens][:week] = Server::AccessToken.count(:client_id=>client.id, :days=>7)
169
+ json[:tokens][:revoked] = Server::AccessToken.count(:client_id=>client.id, :days=>7, :revoked=>true)
170
+
171
+ json.to_json
172
+ end
173
+
174
+ # get "/api/client/:id/history" do
175
+ # content_type "application/json"
176
+ # client = Server::Client.find(params[:id])
177
+ # { :data=>Server::AccessToken.historical(:client_id=>client.id) }.to_json
178
+ # end
179
+
180
+ put "/api/client/:id" do
181
+ client = Server::Client.find(params[:id])
182
+ begin
183
+ client.update validate_params(params)
184
+ redirect "#{request.script_name}/api/client/#{client.id}"
185
+ rescue
186
+ halt 400, $!.message
187
+ end
188
+ end
189
+
190
+ delete "/api/client/:id" do
191
+ Server::Client.delete(params[:id])
192
+ 200
193
+ end
194
+
195
+ post "/api/client/:id/revoke" do
196
+ client = Server::Client.find(params[:id])
197
+ client.revoke!
198
+ 200
199
+ end
200
+
201
+ post "/api/token/:token/revoke" do
202
+ token = Server::AccessToken.from_token(params[:token])
203
+ token.revoke!
204
+ 200
205
+ end
206
+
207
+ helpers do
208
+ def validate_params(params)
209
+ display_name = params[:displayName].to_s.strip
210
+ halt 400, "Missing display name" if display_name.empty?
211
+ link = URI.parse(params[:link].to_s.strip).normalize rescue nil
212
+ halt 400, "Link is not a URL (must be http://....)" unless link
213
+ halt 400, "Link must be an absolute URL with HTTP/S scheme" unless link.absolute? && %{http https}.include?(link.scheme)
214
+ redirect_uri = URI.parse(params[:redirectUri].to_s.strip).normalize rescue nil
215
+ halt 400, "Redirect URL is not a URL (must be http://....)" unless redirect_uri
216
+ halt 400, "Redirect URL must be an absolute URL with HTTP/S scheme" unless
217
+ redirect_uri.absolute? && %{http https}.include?(redirect_uri.scheme)
218
+ unless params[:imageUrl].nil? || params[:imageUrl].to_s.empty?
219
+ image_url = URI.parse(params[:imageUrl].to_s.strip).normalize rescue nil
220
+ halt 400, "Image URL must be an absolute URL with HTTP/S scheme" unless
221
+ image_url.absolute? && %{http https}.include?(image_url.scheme)
222
+ end
223
+ scope = Server::Utils.normalize_scope(params[:scope])
224
+ { :display_name=>display_name, :link=>link.to_s, :image_url=>image_url.to_s,
225
+ :redirect_uri=>redirect_uri.to_s, :scope=>scope, :notes=>params[:notes] }
226
+ end
227
+
228
+ def client_as_json(client, with_stats = false)
229
+ { "id"=>client.id.to_s, "secret"=>client.secret, :redirectUri=>client.redirect_uri,
230
+ :displayName=>client.display_name, :link=>client.link, :imageUrl=>client.image_url,
231
+ :notes=>client.notes, :scope=>client.scope,
232
+ :url=>"#{request.script_name}/api/client/#{client.id}",
233
+ :revoke=>"#{request.script_name}/api/client/#{client.id}/revoke",
234
+ :history=>"#{request.script_name}/api/client/#{client.id}/history",
235
+ :created=>client.created_at, :revoked=>client.revoked }
236
+ end
237
+
238
+ def token_as_json(token)
239
+ { :token=>token.token, :identity=>token.identity, :scope=>token.scope, :created=>token.created_at,
240
+ :expired=>token.expires_at, :revoked=>token.revoked,
241
+ :link=>settings.template_url && settings.template_url.gsub("{id}", token.identity),
242
+ :last_access=>token.last_access,
243
+ :revoke=>"#{request.script_name}/api/token/#{token.token}/revoke" }
244
+ end
245
+ end
246
+
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,104 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ # Base class for all OAuth errors. These map to error codes in the spec.
6
+ class OAuthError < StandardError
7
+
8
+ def initialize(code, message)
9
+ super message
10
+ @code = code.to_sym
11
+ end
12
+
13
+ # The OAuth error code.
14
+ attr_reader :code
15
+ end
16
+
17
+ # The end-user or authorization server denied the request.
18
+ class AccessDeniedError < OAuthError
19
+ def initialize
20
+ super :access_denied, "You are now allowed to access this resource."
21
+ end
22
+ end
23
+
24
+ # Access token expired, client expected to request new one using refresh
25
+ # token.
26
+ class ExpiredTokenError < OAuthError
27
+ def initialize
28
+ super :expired_token, "The access token has expired."
29
+ end
30
+ end
31
+
32
+ # The client identifier provided is invalid, the client failed to
33
+ # authenticate, the client did not include its credentials, provided
34
+ # multiple client credentials, or used unsupported credentials type.
35
+ class InvalidClientError < OAuthError
36
+ def initialize
37
+ super :invalid_client, "Client ID and client secret do not match."
38
+ end
39
+ end
40
+
41
+ # The provided access grant is invalid, expired, or revoked (e.g. invalid
42
+ # assertion, expired authorization token, bad end-user password credentials,
43
+ # or mismatching authorization code and redirection URI).
44
+ class InvalidGrantError < OAuthError
45
+ def initialize(message = nil)
46
+ super :invalid_grant, message || "This access grant is no longer valid."
47
+ end
48
+ end
49
+
50
+ # Invalid_request, the request is missing a required parameter, includes an
51
+ # unsupported parameter or parameter value, repeats the same parameter, uses
52
+ # more than one method for including an access token, or is otherwise
53
+ # malformed.
54
+ class InvalidRequestError < OAuthError
55
+ def initialize(message)
56
+ super :invalid_request, message || "The request has the wrong parameters."
57
+ end
58
+ end
59
+
60
+ # The requested scope is invalid, unknown, or malformed.
61
+ class InvalidScopeError < OAuthError
62
+ def initialize
63
+ super :invalid_scope, "The requested scope is not supported."
64
+ end
65
+ end
66
+
67
+ # Access token expired, client cannot refresh and needs new authorization.
68
+ class InvalidTokenError < OAuthError
69
+ def initialize
70
+ super :invalid_token, "The access token is no longer valid."
71
+ end
72
+ end
73
+
74
+ # The redirection URI provided does not match a pre-registered value.
75
+ class RedirectUriMismatchError < OAuthError
76
+ def initialize
77
+ super :redirect_uri_mismatch, "Must use the same redirect URI you registered with us."
78
+ end
79
+ end
80
+
81
+ # The authenticated client is not authorized to use the access grant type provided.
82
+ class UnauthorizedClientError < OAuthError
83
+ def initialize
84
+ super :unauthorized_client, "You are not allowed to access this resource."
85
+ end
86
+ end
87
+
88
+ # This access grant type is not supported by this server.
89
+ class UnsupportedGrantType < OAuthError
90
+ def initialize
91
+ super :unsupported_grant_type, "This access grant type is not supported by this server."
92
+ end
93
+ end
94
+
95
+ # The requested response type is not supported by the authorization server.
96
+ class UnsupportedResponseTypeError < OAuthError
97
+ def initialize
98
+ super :unsupported_response_type, "The requested response type is not supported."
99
+ end
100
+ end
101
+
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,147 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ # Helper methods that provide access to the OAuth state during the
6
+ # authorization flow, and from authenticated requests. For example:
7
+ #
8
+ # def show
9
+ # logger.info "#{oauth.client.display_name} accessing #{oauth.scope}"
10
+ # end
11
+ class Helper
12
+
13
+ def initialize(request, response)
14
+ @request, @response = request, response
15
+ end
16
+
17
+ # Returns the access token. Only applies if client authenticated.
18
+ #
19
+ # @return [String, nil] Access token, if authenticated
20
+ def access_token
21
+ @access_token ||= @request.env["oauth.access_token"]
22
+ end
23
+
24
+ # True if client authenticated.
25
+ #
26
+ # @return [true, false] True if authenticated
27
+ def authenticated?
28
+ !!access_token
29
+ end
30
+
31
+ # Returns the authenticated identity. Only applies if client
32
+ # authenticated.
33
+ #
34
+ # @return [String, nil] Identity, if authenticated
35
+ def identity
36
+ @identity ||= @request.env["oauth.identity"]
37
+ end
38
+
39
+ # Returns the Client object associated with this request. Available if
40
+ # client authenticated, or while processing authorization request.
41
+ #
42
+ # @return [Client, nil] Client if authenticated, or while authorizing
43
+ def client
44
+ if access_token
45
+ @client ||= Server.get_access_token(access_token).client
46
+ elsif authorization
47
+ @client ||= Server.get_auth_request(authorization).client
48
+ end
49
+ end
50
+
51
+ # Returns scope associated with this request. Available if client
52
+ # authenticated, or while processing authorization request.
53
+ #
54
+ # @return [Array<String>, nil] Scope names, e.g ["read, "write"]
55
+ def scope
56
+ if access_token
57
+ @scope ||= Server.get_access_token(access_token).scope
58
+ elsif authorization
59
+ @scope ||= Server.get_auth_request(authorization).scope
60
+ end
61
+ end
62
+
63
+ # Rejects the request and returns 401 (Unauthorized). You can just
64
+ # return 401, but this also sets the WWW-Authenticate header the right
65
+ # value.
66
+ #
67
+ # @return 401
68
+ def no_access!
69
+ @response["oauth.no_access"] = "true"
70
+ @response.status = 401
71
+ end
72
+
73
+ # Rejects the request and returns 403 (Forbidden). You can just
74
+ # return 403, but this also sets the WWW-Authenticate header the right
75
+ # value. Indicates which scope the client needs to make this request.
76
+ #
77
+ # @param [String] scope The missing scope, e.g. "read"
78
+ # @return 403
79
+ def no_scope!(scope)
80
+ @response["oauth.no_scope"] = scope.to_s
81
+ @response.status = 403
82
+ end
83
+
84
+ # Returns the authorization request handle. Available when starting an
85
+ # authorization request (i.e. /oauth/authorize).
86
+ #
87
+ # @return [String] Authorization handle
88
+ def authorization
89
+ @request_id ||= @request.env["oauth.authorization"] || @request.params["authorization"]
90
+ end
91
+
92
+ # Sets the authorization request handle. Use this during the
93
+ # authorization flow.
94
+ #
95
+ # @param [String] authorization handle
96
+ def authorization=(authorization)
97
+ @scope, @client = nil
98
+ @request_id = authorization
99
+ end
100
+
101
+ # Grant authorization request. Call this at the end of the authorization
102
+ # flow to signal that the user has authorized the client to access the
103
+ # specified identity. Don't render anything else. Argument required if
104
+ # authorization handle is not passed in the request parameter
105
+ # +authorization+.
106
+ #
107
+ # @param [String, nil] authorization Authorization handle
108
+ # @param [String] identity Identity string
109
+ # @return 200
110
+ def grant!(auth, identity = nil)
111
+ auth, identity = authorization, auth unless identity
112
+ @response["oauth.authorization"] = auth.to_s
113
+ @response["oauth.identity"] = identity.to_s
114
+ @response.status = 200
115
+ end
116
+
117
+ # Deny authorization request. Call this at the end of the authorization
118
+ # flow to signal that the user has not authorized the client. Don't
119
+ # render anything else. Argument required if authorization handle is not
120
+ # passed in the request parameter +authorization+.
121
+ #
122
+ # @param [String, nil] auth Authorization handle
123
+ # @return 401
124
+ def deny!(auth = nil)
125
+ auth ||= authorization
126
+ @response["oauth.authorization"] = auth.to_s
127
+ @response.status = 403
128
+ end
129
+
130
+ # Returns all access tokens associated with this identity.
131
+ #
132
+ # @param [String] identity Identity string
133
+ # @return [Array<AccessToken>]
134
+ def list_access_tokens(identity)
135
+ Rack::OAuth2::Server.list_access_tokens(identity)
136
+ end
137
+
138
+ def inspect
139
+ authorization ? "Authorization request for #{Utils.normalize_scope(scope).join(",")} on behalf of #{client.display_name}" :
140
+ authenticated? ? "Authenticated as #{identity}" : nil
141
+ end
142
+
143
+ end
144
+
145
+ end
146
+ end
147
+ end