rack-oauth2-server 1.0.beta

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