tpitale-rack-oauth2-server 2.2.1

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