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,61 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ # Authorization request. Represents request on behalf of client to access
6
+ # particular scope. Use this to keep state from incoming authorization
7
+ # request to grant/deny redirect.
8
+ class AuthRequest < ActiveRecord::Base
9
+ belongs_to :client, :class_name => 'Rack::OAuth2::Server::Client'
10
+
11
+ # Find AuthRequest from identifier.
12
+ # def find(request_id)
13
+ # id = BSON::ObjectId(request_id.to_s)
14
+ # Server.new_instance self, collection.find_one(id)
15
+ # rescue BSON::InvalidObjectId
16
+ # end
17
+
18
+ # Create a new authorization request. This holds state, so in addition
19
+ # to client ID and scope, we need to know the URL to redirect back to
20
+ # and any state value to pass back in that redirect.
21
+ def self.create(client, scope, redirect_uri, response_type, state)
22
+ scope = Utils.normalize_scope(scope) & Utils.normalize_scope(client.scope) # Only allowed scope
23
+
24
+ attributes = {
25
+ :code => Server.secure_random,
26
+ :client_id => client.id,
27
+ :scope => scope,
28
+ :redirect_uri => (client.redirect_uri || redirect_uri),
29
+ :response_type => response_type,
30
+ :state => state
31
+ }
32
+
33
+ super(attributes)
34
+ end
35
+
36
+ # Grant access to the specified identity.
37
+ def grant!(identity)
38
+ raise ArgumentError, "Must supply a identity" unless identity
39
+ return if revoked
40
+
41
+ if response_type == "code" # Requested authorization code
42
+ access_grant = AccessGrant.create(identity, client, scope, redirect_uri)
43
+ update_attributes(:grant_code => access_grant.code, :authorized_at => Time.now)
44
+ else # Requested access token
45
+ access_token = AccessToken.get_token_for(identity, client, scope)
46
+ update_attributes(:access_token => access_token.token, :authorized_at => Time.now)
47
+ end
48
+ end
49
+
50
+ # Deny access.
51
+ # this seems broken … ?
52
+ def deny!
53
+ # self.authorized_at = Time.now.to_i
54
+ # self.class.collection.update({ :_id=>id }, { :$set=>{ :authorized_at=>authorized_at } })
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,93 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ class Client < ActiveRecord::Base
6
+ has_many :auth_requests, :dependent => :destroy
7
+ has_many :access_grants, :dependent => :destroy
8
+ has_many :access_tokens, :dependent => :destroy
9
+
10
+ validates_presence_of :display_name
11
+ validates_presence_of :link
12
+ validates_presence_of :code
13
+ validates_presence_of :secret
14
+
15
+ validates_uniqueness_of :display_name
16
+ validates_uniqueness_of :link
17
+ validates_uniqueness_of :code
18
+ validates_uniqueness_of :secret
19
+
20
+ before_validation_on_create :assign_code_and_secret
21
+
22
+ # Create a new client. Client provides the following properties:
23
+ # # :display_name -- Name to show (e.g. UberClient)
24
+ # # :link -- Link to client Web site (e.g. http://uberclient.dot)
25
+ # # :image_url -- URL of image to show alongside display name
26
+ # # :redirect_uri -- Registered redirect URI.
27
+ # # :scope -- List of names the client is allowed to request.
28
+ # # :notes -- Free form text.
29
+ #
30
+ # This method does not validate any of these fields, in fact, you're
31
+ # not required to set them, use them, or use them as suggested. Using
32
+ # them as suggested would result in better user experience. Don't ask
33
+ # how we learned that.
34
+ # def self.create(args)
35
+ # unless args[:redirect_uri].blank?
36
+ # redirect_uri = Server::Utils.parse_redirect_uri(args.delete(:redirect_uri)).to_s
37
+ # end
38
+ #
39
+ # scope = Server::Utils.normalize_scope(args[:scope])
40
+ # args.merge!({:redirect_uri => redirect_uri})
41
+ #
42
+ # if args[:id] && args[:secret]
43
+ # args[:code] = args.delete(:id)
44
+ # super(args)
45
+ # else
46
+ # args[:secret] = Server.secure_random
47
+ # super(args)
48
+ # end
49
+ # end
50
+
51
+ def assign_code_and_secret
52
+ self.code = Server.secure_random[0,20]
53
+ self.secret = Server.secure_random
54
+ end
55
+
56
+ def redirect_url=(url)
57
+ self[:redirect_uri] = Server::Utils.parse_redirect_uri(url).to_s
58
+ end
59
+
60
+ # Lookup client by ID, display name or URL.
61
+ def self.lookup(field)
62
+ find_by_id(field) || find_by_code(field) || find_by_display_name(field) || find_by_link(field)
63
+ end
64
+
65
+ # # Counts how many access tokens were granted.
66
+ # attr_reader :tokens_granted
67
+ # # Counts how many access tokens were revoked.
68
+ # attr_reader :tokens_revoked
69
+
70
+ # Revoke all authorization requests, access grants and access tokens for
71
+ # this client. Ward off the evil.
72
+ def revoke!
73
+ revoked_at = Time.now
74
+ update_attribute(:revoked, revoked_at)
75
+ # can we use the association here
76
+ AuthRequest.update_all(:revoked=>revoked_at, :client_id=>id)
77
+ AccessGrant.update_all(:revoked=>revoked_at, :client_id=>id)
78
+ AccessToken.update_all(:revoked=>revoked_at, :client_id=>id)
79
+ end
80
+
81
+ # def update(args)
82
+ # redirect_url = Server::Utils.parse_redirect_uri(args[:redirect_uri]).to_s unless args[:redirect_uri].blank?
83
+ # args.merge!({
84
+ # :redirect_url => redirect_url,
85
+ # :scope => Server::Utils.normalize_scope(args.delete(:scope))
86
+ # })
87
+ #
88
+ # update_attributes(args)
89
+ # end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,105 @@
1
+ require "rack/oauth2/server"
2
+
3
+ module Rack
4
+ module OAuth2
5
+
6
+ # Rails support.
7
+ #
8
+ # Adds oauth instance method that returns Rack::OAuth2::Helper, see there for
9
+ # more details.
10
+ #
11
+ # Adds oauth_required filter method. Use this filter with actions that require
12
+ # authentication, and with actions that require client to have a specific
13
+ # access scope.
14
+ #
15
+ # Adds oauth setting you can use to configure the module (e.g. setting
16
+ # available scope, see example).
17
+ #
18
+ # @example config/environment.rb
19
+ # require "rack/oauth2/rails"
20
+ #
21
+ # Rails::Initializer.run do |config|
22
+ # config.oauth[:scope] = %w{read write}
23
+ # config.oauth[:authenticator] = lambda do |username, password|
24
+ # User.authenticated username, password
25
+ # end
26
+ # . . .
27
+ # end
28
+ #
29
+ # @example app/controllers/my_controller.rb
30
+ # class MyController < ApplicationController
31
+ #
32
+ # oauth_required :only=>:show
33
+ # oauth_required :only=>:update, :scope=>"write"
34
+ #
35
+ # . . .
36
+ #
37
+ # protected
38
+ # def current_user
39
+ # @current_user ||= User.find(oauth.identity) if oauth.authenticated?
40
+ # end
41
+ # end
42
+ #
43
+ # @see Helpers
44
+ # @see Filters
45
+ # @see Configuration
46
+ module Rails
47
+
48
+ # Helper methods available to controller instance and views.
49
+ module Helpers
50
+ # Returns the OAuth helper.
51
+ #
52
+ # @return [Server::Helper]
53
+ def oauth
54
+ @oauth ||= Rack::OAuth2::Server::Helper.new(request, response)
55
+ end
56
+
57
+ # Filter that denies access if the request is not authenticated. If you
58
+ # do not specify a scope, the class method oauth_required will use this
59
+ # filter; you can set the filter in a parent class and skip it in child
60
+ # classes that need special handling.
61
+ def oauth_required
62
+ head oauth.no_access! unless oauth.authenticated?
63
+ end
64
+ end
65
+
66
+ # Filter methods available in controller.
67
+ module Filters
68
+
69
+ # Adds before filter to require authentication on all the listed paths.
70
+ # Use the :scope option if client must also have access to that scope.
71
+ #
72
+ # @param [Hash] options Accepts before_filter options like :only and
73
+ # :except, and the :scope option.
74
+ def oauth_required(options = {})
75
+ if scope = options.delete(:scope)
76
+ before_filter options do |controller|
77
+ if controller.oauth.authenticated?
78
+ if !controller.oauth.scope.include?(scope)
79
+ controller.send :head, controller.oauth.no_scope!(scope)
80
+ end
81
+ else
82
+ controller.send :head, controller.oauth.no_access!
83
+ end
84
+ end
85
+ else
86
+ before_filter :oauth_required, options
87
+ end
88
+ end
89
+ end
90
+
91
+ # Configuration methods available in config/environment.rb.
92
+ module Configuration
93
+
94
+ # Rack module settings.
95
+ #
96
+ # @return [Hash] Settings
97
+ def oauth
98
+ @oauth ||= Server::Options.new
99
+ end
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,458 @@
1
+ require "rack"
2
+ require "rack/oauth2/models"
3
+ require "rack/oauth2/server/errors"
4
+ require "rack/oauth2/server/utils"
5
+ require "rack/oauth2/server/helper"
6
+
7
+
8
+ module Rack
9
+ module OAuth2
10
+
11
+ # Implements an OAuth 2 Authorization Server, based on http://tools.ietf.org/html/draft-ietf-oauth-v2-10
12
+ class Server
13
+
14
+ # Same as gem version number.
15
+ VERSION = IO.read(::File.expand_path("../../../VERSION", ::File.dirname(__FILE__))).strip
16
+
17
+ class << self
18
+ # Return AuthRequest from authorization request handle.
19
+ #
20
+ # @param [String] authorization Authorization handle (e.g. from
21
+ # oauth.authorization)
22
+ # @return [AuthReqeust]
23
+ def get_auth_request(authorization)
24
+ AuthRequest.find_by_code(authorization)
25
+ end
26
+
27
+ # Returns Client from client identifier.
28
+ #
29
+ # @param [String] client_id Client identifier (e.g. from oauth.client.id)
30
+ # @return [Client]
31
+ def get_client(client_id)
32
+ p client_id
33
+ Client.find_by_code(client_id)
34
+ end
35
+
36
+ # Registers and returns a new Client. Can also be used to update
37
+ # existing client registration, by passing identifier (and secret) of
38
+ # existing client record. That way, your setup script can create a new
39
+ # client application and run repeatedly without fail.
40
+ #
41
+ # @param [Hash] args Arguments for registering client application
42
+ # @option args [String] :id Client identifier. Use this to update
43
+ # existing client registration (in combination wih secret)
44
+ # @option args [String] :secret Client secret. Use this to update
45
+ # existing client registration.
46
+ # @option args [String] :display_name Name to show when authorizing
47
+ # access (e.g. "My Awesome Application")
48
+ # @option args [String] link Link to client application's Web site
49
+ # @option args [String] image_url URL of image to show alongside display
50
+ # name.
51
+ # @option args [String] redirect_uri Redirect URL: authorization
52
+ # requests for this client will always redirect back to this URL.
53
+ # @option args [Array] scope Scope that client application can request
54
+ # (list of names).
55
+ # @option args [Array] notes Free form text, for internal use.
56
+ #
57
+ # @example Registering new client application
58
+ # Server.register :display_name=>"My Application",
59
+ # :link=>"http://example.com", :scope=>%w{read write},
60
+ # :redirect_uri=>"http://example.com/oauth/callback"
61
+ # @example Migration using configuration file
62
+ # config = YAML.load_file(Rails.root + "config/oauth.yml")
63
+ # Server.register config["id"], config["secret"],
64
+ # :display_name=>"My Application", :link=>"http://example.com",
65
+ # :scope=>config["scope"],
66
+ # :redirect_uri=>"http://example.com/oauth/callback"
67
+
68
+ def register(args)
69
+ if args[:id] && args[:secret] && (client = get_client(args[:id]))
70
+ fail "Client secret does not match" unless client.secret == args[:secret]
71
+ client.update args
72
+ else
73
+ Client.create(args)
74
+ end
75
+ end
76
+
77
+ # Creates and returns a new access grant. Actually, returns only the
78
+ # authorization code which you can turn into an access token by
79
+ # making a request to /oauth/access_token.
80
+ #
81
+ # @param [String,Integer] identity User ID, account ID, etc
82
+ # @param [String] client_id Client identifier
83
+ # @param [Array, nil] scope Array of string, nil if you want 'em all
84
+ # @param [Integer, nil] expires How many seconds before access grant
85
+ # expires (default to 5 minutes)
86
+ # @return [String] Access grant authorization code
87
+ def access_grant(identity, client_id, scope = nil, expires = nil)
88
+ client = get_client(client_id) or fail "No such client"
89
+ AccessGrant.create(identity, client, scope || client.scope, nil, expires).code
90
+ end
91
+
92
+ # Returns AccessToken from token.
93
+ #
94
+ # @param [String] token Access token (e.g. from oauth.access_token)
95
+ # @return [AccessToken]
96
+ def get_access_token(token)
97
+ AccessToken.from_token(token)
98
+ end
99
+
100
+ # Returns AccessToken for the specified identity, client application and
101
+ # scope. You can use this method to request existing access token, new
102
+ # token generated if one does not already exists.
103
+ #
104
+ # @param [String,Integer] identity Identity, e.g. user ID, account ID
105
+ # @param [String] client_id Client application identifier
106
+ # @param [Array, nil] scope Array of names, nil if you want 'em all
107
+ # @return [String] Access token
108
+ def token_for(identity, client_id, scope = nil)
109
+ client = get_client(client_id) or fail "No such client"
110
+ AccessToken.get_token_for(identity, client, scope || client.scope).token
111
+ end
112
+
113
+ # Returns all AccessTokens for an identity.
114
+ #
115
+ # @param [String] identity Identity, e.g. user ID, account ID
116
+ # @return [Array<AccessToken>]
117
+ def list_access_tokens(identity)
118
+ AccessToken.from_identity(identity)
119
+ end
120
+
121
+ end
122
+
123
+ # Options are:
124
+ # - :access_token_path -- Path for requesting access token. By convention
125
+ # defaults to /oauth/access_token.
126
+ # - :authenticator -- For username/password authorization. A block that
127
+ # receives the credentials and returns identity string (e.g. user ID) or
128
+ # nil.
129
+ # - :authorization_types -- Array of supported authorization types.
130
+ # Defaults to ["code", "token"], and you can change it to just one of
131
+ # these names.
132
+ # - :authorize_path -- Path for requesting end-user authorization. By
133
+ # convention defaults to /oauth/authorize.
134
+ # - :database -- Mongo::DB instance.
135
+ # - :host -- Only check requests sent to this host.
136
+ # - :path -- Only check requests for resources under this path.
137
+ # - :param_authentication -- If true, supports authentication using
138
+ # query/form parameters.
139
+ # - :realm -- Authorization realm that will show up in 401 responses.
140
+ # Defaults to use the request host name.
141
+ # - :logger -- The logger to use. Under Rails, defaults to use the Rails
142
+ # logger. Will use Rack::Logger if available.
143
+ #
144
+ # Authenticator is a block that receives either two or four parameters.
145
+ # The first two are username and password. The other two are the client
146
+ # identifier and scope. It authenticated, it returns an identity,
147
+ # otherwise it can return nil or false. For example:
148
+ # oauth.authenticator = lambda do |username, password|
149
+ # user = User.find_by_username(username)
150
+ # user if user && user.authenticated?(password)
151
+ # end
152
+ Options = Struct.new(:access_token_path, :authenticator, :authorization_types,
153
+ :authorize_path, :database, :host, :param_authentication, :path, :realm, :logger)
154
+
155
+ def initialize(app, options = Options.new, &authenticator)
156
+ @app = app
157
+ @options = options
158
+ @options.authenticator ||= authenticator
159
+ @options.access_token_path ||= "/oauth/access_token"
160
+ @options.authorize_path ||= "/oauth/authorize"
161
+ @options.authorization_types ||= %w{code token}
162
+ @options.param_authentication ||= false
163
+ end
164
+
165
+ # @see Options
166
+ attr_reader :options
167
+
168
+ def call(env)
169
+ request = OAuthRequest.new(env)
170
+ return @app.call(env) if options.host && options.host != request.host
171
+ return @app.call(env) if options.path && request.path.index(options.path) != 0
172
+
173
+ begin
174
+ logger = options.logger || env["rack.logger"]
175
+
176
+ # 3. Obtaining End-User Authorization
177
+ # Flow starts here.
178
+ return request_authorization(request, logger) if request.path == options.authorize_path
179
+ # 4. Obtaining an Access Token
180
+ return respond_with_access_token(request, logger) if request.path == options.access_token_path
181
+
182
+ # 5. Accessing a Protected Resource
183
+ if request.authorization
184
+ # 5.1.1. The Authorization Request Header Field
185
+ token = request.credentials if request.oauth?
186
+ elsif options.param_authentication && !request.GET["oauth_verifier"] # Ignore OAuth 1.0 callbacks
187
+ # 5.1.2. URI Query Parameter
188
+ # 5.1.3. Form-Encoded Body Parameter
189
+ token = request.GET["oauth_token"] || request.POST["oauth_token"]
190
+ token ||= request.GET['access_token'] || request.POST['access_token']
191
+ end
192
+
193
+ if token
194
+ begin
195
+ access_token = AccessToken.from_token(token)
196
+ raise InvalidTokenError if access_token.nil? || access_token.revoked
197
+ raise ExpiredTokenError if access_token.expires_at && access_token.expires_at <= Time.now.to_i
198
+ request.env["oauth.access_token"] = token
199
+
200
+ request.env["oauth.identity"] = access_token.identity
201
+ access_token.access!
202
+ logger.info "RO2S: Authorized #{access_token.identity}" if logger
203
+ rescue OAuthError=>error
204
+ # 5.2. The WWW-Authenticate Response Header Field
205
+ logger.info "RO2S: HTTP authorization failed #{error.code}" if logger
206
+ return unauthorized(request, error)
207
+ rescue =>ex
208
+ logger.info "RO2S: HTTP authorization failed #{ex.message}" if logger
209
+ return unauthorized(request)
210
+ end
211
+
212
+ # We expect application to use 403 if request has insufficient scope,
213
+ # and return appropriate WWW-Authenticate header.
214
+ response = @app.call(env)
215
+ if response[0] == 403
216
+ scope = Utils.normalize_scope(response[1]["oauth.no_scope"])
217
+ challenge = 'OAuth realm="%s", error="insufficient_scope", scope="%s"' % [(options.realm || request.host), scope]
218
+ response[1]["WWW-Authenticate"] = challenge
219
+ return response
220
+ else
221
+ return response
222
+ end
223
+ else
224
+ response = @app.call(env)
225
+ if response[1] && response[1].delete("oauth.no_access")
226
+ logger.debug "RO2S: Unauthorized request" if logger
227
+ # OAuth access required.
228
+ return unauthorized(request)
229
+ elsif response[1] && response[1]["oauth.authorization"]
230
+ # 3. Obtaining End-User Authorization
231
+ # Flow ends here.
232
+ return authorization_response(response, logger)
233
+ else
234
+ return response
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ protected
241
+
242
+ # Get here for authorization request. Check the request parameters and
243
+ # redirect with an error if we find any issue. Otherwise, create a new
244
+ # authorization request, set in oauth.request and pass control to the
245
+ # application.
246
+ def request_authorization(request, logger)
247
+ state = request.GET["state"]
248
+ begin
249
+
250
+ if request.GET["authorization"]
251
+ auth_request = self.class.get_auth_request(request.GET["authorization"]) rescue nil
252
+ if !auth_request || auth_request.revoked
253
+ logger.error "RO2S: Invalid authorization request #{auth_request}" if logger
254
+ return bad_request("Invalid authorization request")
255
+ end
256
+ response_type = auth_request.response_type # Needed for error handling
257
+ client = auth_request.client
258
+ # Pass back to application, watch for 403 (deny!)
259
+ logger.info "RO2S: Client #{client.display_name} requested #{auth_request.response_type} with scope #{auth_request.scope}" if logger
260
+ request.env["oauth.authorization"] = auth_request.code
261
+ response = @app.call(request.env)
262
+ raise AccessDeniedError if response[0] == 403
263
+ return response
264
+
265
+ else
266
+
267
+ # 3. Obtaining End-User Authorization
268
+ begin
269
+ redirect_uri = Utils.parse_redirect_uri(request.GET["redirect_uri"])
270
+ rescue InvalidRequestError=>error
271
+ logger.error "RO2S: Authorization request with invalid redirect_uri: #{request.GET["redirect_uri"]} #{error.message}" if logger
272
+ return bad_request(error.message)
273
+ end
274
+
275
+ # 3. Obtaining End-User Authorization
276
+ response_type = request.GET["response_type"].to_s # Need this first, for error handling
277
+ client = get_client(request, :dont_authenticate => true)
278
+ raise RedirectUriMismatchError unless client.redirect_uri.nil? || client.redirect_uri == redirect_uri.to_s
279
+ raise UnsupportedResponseTypeError unless options.authorization_types.include?(response_type)
280
+ requested_scope = Utils.normalize_scope(request.GET["scope"])
281
+ allowed_scope = Utils.normalize_scope(client.scope)
282
+ raise InvalidScopeError unless (requested_scope - allowed_scope).empty?
283
+ # Create object to track authorization request and let application
284
+ # handle the rest.
285
+ auth_request = AuthRequest.create(client, requested_scope, redirect_uri.to_s, response_type, state)
286
+ uri = URI.parse(request.url)
287
+ uri.query = "authorization=#{auth_request.code}"
288
+ return redirect_to(uri, 303)
289
+ end
290
+ rescue OAuthError=>error
291
+ logger.error "RO2S: Authorization request error #{error.code}: #{error.message}" if logger
292
+ params = { :error=>error.code, :error_description=>error.message, :state=>state }
293
+ if response_type == "token"
294
+ redirect_uri.fragment = Rack::Utils.build_query(params)
295
+ else # response type is code, or invalid
296
+ params = Rack::Utils.parse_query(redirect_uri.query).merge(params)
297
+ redirect_uri.query = Rack::Utils.build_query(params)
298
+ end
299
+ return redirect_to(redirect_uri)
300
+ end
301
+ end
302
+
303
+ # Get here on completion of the authorization. Authorization response in
304
+ # oauth.response either grants or denies authroization. In either case, we
305
+ # redirect back with the proper response.
306
+ def authorization_response(response, logger)
307
+ status, headers, body = response
308
+ auth_request = self.class.get_auth_request(headers["oauth.authorization"])
309
+ redirect_uri = URI.parse(auth_request.redirect_uri)
310
+ if status == 403
311
+ auth_request.deny!
312
+ else
313
+ auth_request.grant! headers["oauth.identity"]
314
+ end
315
+ # 3.1. Authorization Response
316
+ if auth_request.response_type == "code"
317
+ if auth_request.grant_code
318
+ logger.info "RO2S: Client #{auth_request.client_id} granted access code #{auth_request.grant_code}" if logger
319
+ params = { :code=>auth_request.grant_code, :scope=>auth_request.scope, :state=>auth_request.state }
320
+ else
321
+ logger.info "RO2S: Client #{auth_request.client_id} denied authorization" if logger
322
+ params = { :error=>:access_denied, :state=>auth_request.state }
323
+ end
324
+ params = Rack::Utils.parse_query(redirect_uri.query).merge(params)
325
+ redirect_uri.query = Rack::Utils.build_query(params)
326
+ else # response type if token
327
+ if auth_request.access_token
328
+ logger.info "RO2S: Client #{auth_request.client_id} granted access token #{auth_request.access_token}" if logger
329
+ params = { :access_token=>auth_request.access_token, :scope=>auth_request.scope, :state=>auth_request.state }
330
+ else
331
+ logger.info "RO2S: Client #{auth_request.client_id} denied authorization" if logger
332
+ params = { :error=>:access_denied, :state=>auth_request.state }
333
+ end
334
+ redirect_uri.fragment = Rack::Utils.build_query(params)
335
+ end
336
+ return redirect_to(redirect_uri)
337
+ end
338
+
339
+ # 4. Obtaining an Access Token
340
+ def respond_with_access_token(request, logger)
341
+ return [405, { "Content-Type"=>"application/json" }, ["POST only"]] unless request.post?
342
+ # 4.2. Access Token Response
343
+ begin
344
+ client = get_client(request)
345
+ case request.POST["grant_type"]
346
+ when "none"
347
+ # 4.1 "none" access grant type (i.e. two-legged OAuth flow)
348
+ requested_scope = request.POST["scope"] ? Utils.normalize_scope(request.POST["scope"]) : Utils.normalize_scope(client.scope)
349
+ access_token = AccessToken.create_token_for(client, requested_scope)
350
+ when "authorization_code"
351
+ # 4.1.1. Authorization Code
352
+ grant = AccessGrant.from_code(request.POST["code"])
353
+ p grant
354
+ raise InvalidGrantError, "Wrong client" unless grant && client == grant.client
355
+ unless client.redirect_uri.nil? || client.redirect_uri.to_s.empty?
356
+ raise InvalidGrantError, "Wrong redirect URI" unless grant.redirect_uri == Utils.parse_redirect_uri(request.POST["redirect_uri"]).to_s
357
+ end
358
+ raise InvalidGrantError, "This access grant expired" if grant.expires_at && grant.expires_at <= Time.now.to_i
359
+ access_token = grant.authorize!
360
+ when "password"
361
+ raise UnsupportedGrantType unless options.authenticator
362
+ # 4.1.2. Resource Owner Password Credentials
363
+ username, password = request.POST.values_at("username", "password")
364
+ raise InvalidGrantError, "Missing username/password" unless username && password
365
+ requested_scope = request.POST["scope"] ? Utils.normalize_scope(request.POST["scope"]) : Utils.normalize_scope(client.scope)
366
+ allowed_scope = Utils.normalize_scope(client.scope)
367
+ raise InvalidScopeError unless (requested_scope - allowed_scope).empty?
368
+ args = [username, password]
369
+ args << client.id << requested_scope unless options.authenticator.arity == 2
370
+ identity = options.authenticator.call(*args)
371
+ raise InvalidGrantError, "Username/password do not match" unless identity
372
+ access_token = AccessToken.get_token_for(identity, client, requested_scope)
373
+ else
374
+ raise UnsupportedGrantType
375
+ end
376
+ logger.info "RO2S: Access token #{access_token.token} granted to client #{client.display_name}, identity #{access_token.identity}" if logger
377
+ response = { :access_token=>access_token.token }
378
+ response[:scope] = access_token.scope
379
+ return [200, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" }, [response.to_json]]
380
+ # 4.3. Error Response
381
+ rescue OAuthError=>error
382
+ logger.error "RO2S: Access token request error #{error.code}: #{error.message}" if logger
383
+ return unauthorized(request, error) if InvalidClientError === error && request.basic?
384
+ return [400, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" },
385
+ [{ :error=>error.code, :error_description=>error.message }.to_json]]
386
+ end
387
+ end
388
+
389
+ # Returns client from request based on credentials. Raises
390
+ # InvalidClientError if client doesn't exist or secret doesn't match.
391
+ def get_client(request, options={})
392
+ # 2.1 Client Password Credentials
393
+ if request.basic?
394
+ client_id, client_secret = request.credentials
395
+ elsif request.post?
396
+ client_id, client_secret = request.POST.values_at("client_id", "client_secret")
397
+ else
398
+ client_id, client_secret = request.GET.values_at("client_id", "client_secret")
399
+ end
400
+ client = self.class.get_client(client_id)
401
+ raise InvalidClientError if !client
402
+ unless options[:dont_authenticate]
403
+ raise InvalidClientError unless client.secret == client_secret
404
+ end
405
+ raise InvalidClientError if client.revoked
406
+ return client
407
+ end
408
+
409
+ # Rack redirect response.
410
+ # The argument is typically a URI object, and the status should be a 302 or 303.
411
+ def redirect_to(uri, status = 302)
412
+ return [status, { "Content-Type"=>"text/plain", "Location"=>uri.to_s }, ["You are being redirected"]]
413
+ end
414
+
415
+ def bad_request(message)
416
+ return [400, { "Content-Type"=>"text/plain" }, [message]]
417
+ end
418
+
419
+ # Returns WWW-Authenticate header.
420
+ def unauthorized(request, error = nil)
421
+ challenge = 'OAuth realm="%s"' % (options.realm || request.host)
422
+ challenge << ', error="%s", error_description="%s"' % [error.code, error.message] if error
423
+ return [401, { "WWW-Authenticate"=>challenge }, [error && error.message || ""]]
424
+ end
425
+
426
+ # Wraps Rack::Request to expose Basic and OAuth authentication
427
+ # credentials.
428
+ class OAuthRequest < Rack::Request
429
+
430
+ AUTHORIZATION_KEYS = %w{HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION}
431
+
432
+ # Returns authorization header.
433
+ def authorization
434
+ @authorization ||= AUTHORIZATION_KEYS.inject(nil) { |auth, key| auth || @env[key] }
435
+ end
436
+
437
+ # True if authentication scheme is OAuth.
438
+ def oauth?
439
+ authorization[/^oauth/i] if authorization
440
+ end
441
+
442
+ # True if authentication scheme is Basic.
443
+ def basic?
444
+ authorization[/^basic/i] if authorization
445
+ end
446
+
447
+ # If Basic auth, returns username/password, if OAuth, returns access
448
+ # token.
449
+ def credentials
450
+ basic? ? authorization.gsub(/\n/, "").split[1].unpack("m*").first.split(/:/, 2) :
451
+ oauth? ? authorization.gsub(/\n/, "").split[1] : nil
452
+ end
453
+ end
454
+
455
+ end
456
+
457
+ end
458
+ end