rack-oauth2-server 1.0.beta

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.
@@ -0,0 +1,312 @@
1
+ require "rack/oauth2/models"
2
+ require "rack/oauth2/server/errors"
3
+ require "rack/oauth2/server/utils"
4
+ require "rack/oauth2/server/helper"
5
+ require "rack/oauth2/server/version"
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
+ class << self
15
+ # Return AuthRequest from authorization request handle.
16
+ def get_auth_request(authorization)
17
+ AuthRequest.find(authorization)
18
+ end
19
+
20
+ # Returns Client from client identifier.
21
+ def get_client(client_id)
22
+ Client.find(client_id)
23
+ end
24
+
25
+ # Returns AccessToken from token.
26
+ def get_access_token(token)
27
+ AccessToken.from_token(token)
28
+ end
29
+
30
+ # Returns all AccessTokens for a resource.
31
+ def list_access_tokens(resource)
32
+ AccessToken.from_resource(resource)
33
+ end
34
+ end
35
+
36
+ def initialize(app, options = {}, &authenticator)
37
+ @app = app
38
+ @options = options
39
+ @options[:authenticator] ||= authenticator
40
+ @options[:access_token_path] ||= "/oauth/access_token"
41
+ @options[:authorize_path] ||= "/oauth/authorize"
42
+ @options[:authorization_types] ||= %w{code token}
43
+ end
44
+
45
+ # Options are:
46
+ # - :access_token_path -- Path for requesting access token. By convention
47
+ # defaults to /oauth/access_token.
48
+ # - :authenticator -- For username/password authorization. A block that
49
+ # receives the credentials and returns resource string (e.g. user ID) or
50
+ # nil.
51
+ # - :authorization_types -- Array of supported authorization types.
52
+ # Defaults to ["code", "token"], and you can change it to just one of
53
+ # these names.
54
+ # - :authorize_path -- Path for requesting end-user authorization. By
55
+ # convention defaults to /oauth/authorize.
56
+ # - :database -- Mongo::DB instance.
57
+ # - :realm -- Authorization realm that will show up in 401 responses.
58
+ # Defaults to use the request host name.
59
+ # - :scopes -- Array listing all supported scopes, e.g. %w{read write}.
60
+ # - :logger -- The logger to use. Under Rails, defaults to use the Rails
61
+ # logger. Will use Rack::Logger if available.
62
+ attr_reader :options
63
+
64
+ def call(env)
65
+ # Use options[:database] if specified.
66
+ org_database, Server.database = Server.database, options[:database] || Server.database
67
+ logger = options[:logger] || env["rack.logger"]
68
+ request = OAuthRequest.new(env)
69
+
70
+ # 3. Obtaining End-User Authorization
71
+ # Flow starts here.
72
+ return request_authorization(request, logger) if request.path == options[:authorize_path]
73
+ # 4. Obtaining an Access Token
74
+ return respond_with_access_token(request, logger) if request.path == options[:access_token_path]
75
+
76
+ # 5. Accessing a Protected Resource
77
+ if request.authorization
78
+ # 5.1.1. The Authorization Request Header Field
79
+ token = request.credentials if request.oauth?
80
+ else
81
+ # 5.1.2. URI Query Parameter
82
+ # 5.1.3. Form-Encoded Body Parameter
83
+ token = request.GET["oauth_token"] || request.POST["oauth_token"]
84
+ end
85
+
86
+ if token
87
+ begin
88
+ access_token = AccessToken.from_token(token)
89
+ raise InvalidTokenError if access_token.nil? || access_token.revoked
90
+ raise ExpiredTokenError if access_token.expires_at && access_token.expires_at <= Time.now.utc
91
+ request.env["oauth.access_token"] = token
92
+ request.env["oauth.resource"] = access_token.resource
93
+ logger.info "Authorized #{access_token.resource}" if logger
94
+ rescue Error=>error
95
+ # 5.2. The WWW-Authenticate Response Header Field
96
+ logger.info "HTTP authorization failed #{error.code}" if logger
97
+ return unauthorized(request, error)
98
+ rescue =>ex
99
+ logger.info "HTTP authorization failed #{ex.message}" if logger
100
+ return unauthorized(request)
101
+ end
102
+
103
+ # We expect application to use 403 if request has insufficient scope,
104
+ # and return appropriate WWW-Authenticate header.
105
+ response = @app.call(env)
106
+ if response[0] == 403
107
+ scope = response[1]["oauth.no_scope"] || ""
108
+ scope = scope.join(" ") if scope.respond_to?(:join)
109
+ challenge = 'OAuth realm="%s", error="insufficient_scope", scope="%s"' % [(options[:realm] || request.host), scope]
110
+ return [403, { "WWW-Authenticate"=>challenge }, []]
111
+ else
112
+ return response
113
+ end
114
+ else
115
+ response = @app.call(env)
116
+ if response[1] && response[1]["oauth.no_access"]
117
+ # OAuth access required.
118
+ return unauthorized(request)
119
+ elsif response[1] && response[1]["oauth.authorization"]
120
+ # 3. Obtaining End-User Authorization
121
+ # Flow ends here.
122
+ return authorization_response(response, logger)
123
+ else
124
+ return response
125
+ end
126
+ end
127
+ ensure
128
+ Server.database = org_database
129
+ end
130
+
131
+ protected
132
+
133
+ # Get here for authorization request. Check the request parameters and
134
+ # redirect with an error if we find any issue. Otherwise, create a new
135
+ # authorization request, set in oauth.request and pass control to the
136
+ # application.
137
+ def request_authorization(request, logger)
138
+ # 3. Obtaining End-User Authorization
139
+ begin
140
+ redirect_uri = Utils.parse_redirect_uri(request.GET["redirect_uri"])
141
+ rescue InvalidRequestError=>error
142
+ logger.error "Authorization request with invalid redirect_uri: #{request.GET["redirect_uri"]} #{error.message}" if logger
143
+ return bad_request(error.message)
144
+ end
145
+ state = request.GET["state"]
146
+
147
+ begin
148
+ # 3. Obtaining End-User Authorization
149
+ client = get_client(request)
150
+ raise RedirectUriMismatchError unless client.redirect_uri.nil? || client.redirect_uri == redirect_uri.to_s
151
+ requested_scope = request.GET["scope"].to_s.split.uniq.join(" ")
152
+ response_type = request.GET["response_type"].to_s
153
+ raise UnsupportedResponseTypeError unless options[:authorization_types].include?(response_type)
154
+ if scopes = options[:scopes]
155
+ allowed_scope = scopes.respond_to?(:all?) ? scopes : scopes.split
156
+ raise InvalidScopeError unless requested_scope.split.all? { |v| allowed_scope.include?(v) }
157
+ end
158
+ # Create object to track authorization request and let application
159
+ # handle the rest.
160
+ auth_request = AuthRequest.create(client.id, requested_scope, redirect_uri.to_s, response_type, state)
161
+ request.env["oauth.authorization"] = auth_request.id.to_s
162
+ logger.info "Request #{auth_request.id}: Client #{client.display_name} requested #{response_type} with scope #{requested_scope}" if logger
163
+ return @app.call(request.env)
164
+ rescue Error=>error
165
+ logger.error "Authorization request error: #{error.code} #{error.message}" if logger
166
+ params = Rack::Utils.parse_query(redirect_uri.query).merge(:error=>error.code, :error_description=>error.message, :state=>state)
167
+ redirect_uri.query = Rack::Utils.build_query(params)
168
+ return redirect_to(redirect_uri)
169
+ end
170
+ end
171
+
172
+ # Get here on completion of the authorization. Authorization response in
173
+ # oauth.response either grants or denies authroization. In either case, we
174
+ # redirect back with the proper response.
175
+ def authorization_response(response, logger)
176
+ status, headers, body = response
177
+ auth_request = self.class.get_auth_request(headers["oauth.authorization"])
178
+ redirect_uri = URI.parse(auth_request.redirect_uri)
179
+ if status == 401
180
+ auth_request.deny!
181
+ else
182
+ auth_request.grant! headers["oauth.resource"]
183
+ end
184
+ # 3.1. Authorization Response
185
+ if auth_request.response_type == "code" && auth_request.grant_code
186
+ logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access code #{auth_request.grant_code}" if logger
187
+ params = { :code=>auth_request.grant_code, :scope=>auth_request.scope, :state=>auth_request.state }
188
+ params = Rack::Utils.parse_query(redirect_uri.query).merge(params)
189
+ redirect_uri.query = Rack::Utils.build_query(params)
190
+ return redirect_to(redirect_uri)
191
+ elsif auth_request.response_type == "token" && auth_request.access_token
192
+ logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access token #{auth_request.access_token}" if logger
193
+ params = { :access_token=>auth_request.access_token, :scope=>auth_request.scope, :state=>auth_request.state }
194
+ redirect_uri.fragment = Rack::Utils.build_query(params)
195
+ return redirect_to(redirect_uri)
196
+ else
197
+ logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} denied authorization" if logger
198
+ params = Rack::Utils.parse_query(redirect_uri.query).merge(:error=>:access_denied, :state=>auth_request.state)
199
+ redirect_uri.query = Rack::Utils.build_query(params)
200
+ return redirect_to(redirect_uri)
201
+ end
202
+ end
203
+
204
+ # 4. Obtaining an Access Token
205
+ def respond_with_access_token(request, logger)
206
+ return [405, { "Content-Type"=>"application/json" }, ["POST only"]] unless request.post?
207
+ # 4.2. Access Token Response
208
+ begin
209
+ client = get_client(request)
210
+ case request.POST["grant_type"]
211
+ when "authorization_code"
212
+ # 4.1.1. Authorization Code
213
+ grant = AccessGrant.from_code(request.POST["code"])
214
+ raise InvalidGrantError unless grant && client.id == grant.client_id
215
+ raise InvalidGrantError unless grant.redirect_uri.nil? || grant.redirect_uri == Utils.parse_redirect_uri(request.POST["redirect_uri"]).to_s
216
+ access_token = grant.authorize!
217
+ when "password"
218
+ raise UnsupportedGrantType unless options[:authenticator]
219
+ # 4.1.2. Resource Owner Password Credentials
220
+ username, password = request.POST.values_at("username", "password")
221
+ requested_scope = request.POST["scope"].to_s.split.uniq.join(" ")
222
+ raise InvalidGrantError unless username && password
223
+ resource = options[:authenticator].call(username, password)
224
+ raise InvalidGrantError unless resource
225
+ if scopes = options[:scopes]
226
+ allowed_scope = scopes.respond_to?(:all?) ? scopes : scopes.split
227
+ raise InvalidScopeError unless requested_scope.split.all? { |v| allowed_scope.include?(v) }
228
+ end
229
+ access_token = AccessToken.get_token_for(resource, requested_scope.to_s, client.id)
230
+ else raise UnsupportedGrantType
231
+ end
232
+ logger.info "Access token #{access_token.token} granted to client #{client.display_name}, resource #{access_token.resource}" if logger
233
+ response = { :access_token=>access_token.token }
234
+ response[:scope] = access_token.scope unless access_token.scope.empty?
235
+ return [200, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" }, response.to_json]
236
+ # 4.3. Error Response
237
+ rescue Error=>error
238
+ logger.error "Access token request error: #{error.code} #{error.message}" if logger
239
+ return unauthorized(request, error) if InvalidClientError === error && request.basic?
240
+ return [400, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" },
241
+ { :error=>error.code, :error_description=>error.message }.to_json]
242
+ end
243
+ end
244
+
245
+ # Returns client from request based on credentials. Raises
246
+ # InvalidClientError if client doesn't exist or secret doesn't match.
247
+ def get_client(request)
248
+ # 2.1 Client Password Credentials
249
+ if request.basic?
250
+ client_id, client_secret = request.credentials
251
+ elsif request.form_data?
252
+ client_id, client_secret = request.POST.values_at("client_id", "client_secret")
253
+ else
254
+ client_id, client_secret = request.GET.values_at("client_id", "client_secret")
255
+ end
256
+ client = self.class.get_client(client_id)
257
+ raise InvalidClientError unless client && client.secret == client_secret
258
+ raise InvalidClientError if client.revoked
259
+ return client
260
+ rescue BSON::InvalidObjectId
261
+ raise InvalidClientError
262
+ end
263
+
264
+ # Rack redirect response. The argument is typically a URI object.
265
+ def redirect_to(uri)
266
+ return [302, { "Location"=>uri.to_s }, []]
267
+ end
268
+
269
+ def bad_request(message)
270
+ return [400, { "Content-Type"=>"text/plain" }, [message]]
271
+ end
272
+
273
+ # Returns WWW-Authenticate header.
274
+ def unauthorized(request, error = nil)
275
+ challenge = 'OAuth realm="%s"' % (options[:realm] || request.host)
276
+ challenge << ', error="%s", error_description="%s"' % [error.code, error.message] if error
277
+ return [401, { "WWW-Authenticate"=>challenge }, []]
278
+ end
279
+
280
+ # Wraps Rack::Request to expose Basic and OAuth authentication
281
+ # credentials.
282
+ class OAuthRequest < Rack::Request
283
+
284
+ AUTHORIZATION_KEYS = %w{HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION}
285
+
286
+ # Returns authorization header.
287
+ def authorization
288
+ @authorization ||= AUTHORIZATION_KEYS.inject(nil) { |auth, key| auth || @env[key] }
289
+ end
290
+
291
+ # True if authentication scheme is OAuth.
292
+ def oauth?
293
+ authorization[/^oauth/i] if authorization
294
+ end
295
+
296
+ # True if authentication scheme is Basic.
297
+ def basic?
298
+ authorization[/^basic/i] if authorization
299
+ end
300
+
301
+ # If Basic auth, returns username/password, if OAuth, returns access
302
+ # token.
303
+ def credentials
304
+ basic? ? authorization.gsub(/\n/, "").split[1].unpack("m*").first.split(/:/, 2) :
305
+ oauth? ? authorization.gsub(/\n/, "").split[1] : nil
306
+ end
307
+ end
308
+
309
+ end
310
+
311
+ end
312
+ end
@@ -0,0 +1,97 @@
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 Error < 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
+ # Access token expired, client expected to request new one using refresh
18
+ # token.
19
+ class ExpiredTokenError < Error
20
+ def initialize
21
+ super :expired_token, "The access token has expired."
22
+ end
23
+ end
24
+
25
+ # The client identifier provided is invalid, the client failed to
26
+ # authenticate, the client did not include its credentials, provided
27
+ # multiple client credentials, or used unsupported credentials type.
28
+ class InvalidClientError < Error
29
+ def initialize
30
+ super :invalid_client, "Client ID and client secret do not match."
31
+ end
32
+ end
33
+
34
+ # The provided access grant is invalid, expired, or revoked (e.g. invalid
35
+ # assertion, expired authorization token, bad end-user password credentials,
36
+ # or mismatching authorization code and redirection URI).
37
+ class InvalidGrantError < Error
38
+ def initialize
39
+ super :invalid_grant, "This access grant is no longer valid."
40
+ end
41
+ end
42
+
43
+ # Invalid_request, the request is missing a required parameter, includes an
44
+ # unsupported parameter or parameter value, repeats the same parameter, uses
45
+ # more than one method for including an access token, or is otherwise
46
+ # malformed.
47
+ class InvalidRequestError < Error
48
+ def initialize(message)
49
+ super :invalid_request, message || "The request has the wrong parameters."
50
+ end
51
+ end
52
+
53
+ # The requested scope is invalid, unknown, or malformed.
54
+ class InvalidScopeError < Error
55
+ def initialize
56
+ super :invalid_scope, "The requested scope is not supported."
57
+ end
58
+ end
59
+
60
+ # Access token expired, client cannot refresh and needs new authorization.
61
+ class InvalidTokenError < Error
62
+ def initialize
63
+ super :invalid_token, "The access token is no longer valid."
64
+ end
65
+ end
66
+
67
+ # The redirection URI provided does not match a pre-registered value.
68
+ class RedirectUriMismatchError < Error
69
+ def initialize
70
+ super :redirect_uri_mismatch, "Must use the same redirect URI you registered with us."
71
+ end
72
+ end
73
+
74
+ # The authenticated client is not authorized to use the access grant type provided.
75
+ class UnauthorizedClientError < Error
76
+ def initialize
77
+ super :unauthorized_client, "You are not allowed to access this resource."
78
+ end
79
+ end
80
+
81
+ # This access grant type is not supported by this server.
82
+ class UnsupportedGrantType < Error
83
+ def initialize
84
+ super :unsupported_grant_type, "This access grant type is not supported by this server."
85
+ end
86
+ end
87
+
88
+ # The requested response type is not supported by the authorization server.
89
+ class UnsupportedResponseTypeError < Error
90
+ def initialize
91
+ super :unsupported_response_type, "The requested response type is not supported."
92
+ end
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,142 @@
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 resource. Only applies if client
32
+ # authenticated.
33
+ #
34
+ # @return [String, nil] Resource, if authenticated
35
+ def resource
36
+ @resource ||= @request.env["oauth.resource"]
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_client(Server.get_access_token(access_token).client_id)
46
+ elsif authorization
47
+ @client ||= Server.get_client(Server.get_auth_request(authorization).client_id)
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.split
58
+ elsif authorization
59
+ @scope ||= Server.get_auth_request(authorization).scope.split
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
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"]
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 resource. Don't render anything else.
104
+ #
105
+ # @param [String] authorization Authorization handle
106
+ # @param [String] resource Resource string
107
+ # @return 200
108
+ def grant!(authorization, resource)
109
+ @response["oauth.authorization"] = authorization
110
+ @response["oauth.resource"] = resource.to_s
111
+ @response.status = 200
112
+ end
113
+
114
+ # Deny authorization request. Call this at the end of the authorization
115
+ # flow to signal that the user has not authorized the client. Don't
116
+ # render anything else.
117
+ #
118
+ # @param [String] authorization Authorization handle
119
+ # @return 401
120
+ def deny!(authorization)
121
+ @response["oauth.authorization"] = authorization
122
+ @response.status = 401
123
+ end
124
+
125
+ # Returns all access tokens associated with this resource.
126
+ #
127
+ # @param [String] resource Resource string
128
+ # @return [Array<AccessToken>]
129
+ def list_access_tokens(resource)
130
+ Rack::OAuth2::Server.list_access_tokens(resource)
131
+ end
132
+
133
+ def inspect
134
+ authorization ? "Authorization request for #{scope.join(",")} on behalf of #{client.display_name}" :
135
+ authenticated? ? "Authenticated as #{resource}" : nil
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+ end
142
+ end