require "rack/oauth2/models" require "rack/oauth2/server/errors" require "rack/oauth2/server/utils" require "rack/oauth2/server/helper" require "rack/oauth2/server/version" module Rack module OAuth2 # Implements an OAuth 2 Authorization Server, based on http://tools.ietf.org/html/draft-ietf-oauth-v2-10 class Server class << self # Return AuthRequest from authorization request handle. def get_auth_request(authorization) AuthRequest.find(authorization) end # Returns Client from client identifier. def get_client(client_id) Client.find(client_id) end # Returns AccessToken from token. def get_access_token(token) AccessToken.from_token(token) end # Returns all AccessTokens for an identity. def list_access_tokens(identity) AccessToken.from_identity(identity) end end # Options are: # - :access_token_path -- Path for requesting access token. By convention # defaults to /oauth/access_token. # - :authenticator -- For username/password authorization. A block that # receives the credentials and returns identity string (e.g. user ID) or # nil. # - :authorization_types -- Array of supported authorization types. # Defaults to ["code", "token"], and you can change it to just one of # these names. # - :authorize_path -- Path for requesting end-user authorization. By # convention defaults to /oauth/authorize. # - :database -- Mongo::DB instance. # - :realm -- Authorization realm that will show up in 401 responses. # Defaults to use the request host name. # - :scopes -- Array listing all supported scopes, e.g. %w{read write}. # - :logger -- The logger to use. Under Rails, defaults to use the Rails # logger. Will use Rack::Logger if available. Options = Struct.new(:access_token_path, :authenticator, :authorization_types, :authorize_path, :database, :realm, :scopes, :logger) def initialize(app, options = Options.new, &authenticator) @app = app @options = options @options.authenticator ||= authenticator @options.access_token_path ||= "/oauth/access_token" @options.authorize_path ||= "/oauth/authorize" @options.authorization_types ||= %w{code token} end # @see Options attr_reader :options def call(env) # Use options.database if specified. org_database, Server.database = Server.database, options.database || Server.database logger = options.logger || env["rack.logger"] request = OAuthRequest.new(env) # 3. Obtaining End-User Authorization # Flow starts here. return request_authorization(request, logger) if request.path == options.authorize_path # 4. Obtaining an Access Token return respond_with_access_token(request, logger) if request.path == options.access_token_path # 5. Accessing a Protected Resource if request.authorization # 5.1.1. The Authorization Request Header Field token = request.credentials if request.oauth? else # 5.1.2. URI Query Parameter # 5.1.3. Form-Encoded Body Parameter token = request.GET["oauth_token"] || request.POST["oauth_token"] end if token begin access_token = AccessToken.from_token(token) raise InvalidTokenError if access_token.nil? || access_token.revoked raise ExpiredTokenError if access_token.expires_at && access_token.expires_at <= Time.now.utc request.env["oauth.access_token"] = token request.env["oauth.identity"] = access_token.identity logger.info "Authorized #{access_token.identity}" if logger rescue Error=>error # 5.2. The WWW-Authenticate Response Header Field logger.info "HTTP authorization failed #{error.code}" if logger return unauthorized(request, error) rescue =>ex logger.info "HTTP authorization failed #{ex.message}" if logger return unauthorized(request) end # We expect application to use 403 if request has insufficient scope, # and return appropriate WWW-Authenticate header. response = @app.call(env) if response[0] == 403 scope = response[1]["oauth.no_scope"] || "" scope = scope.join(" ") if scope.respond_to?(:join) challenge = 'OAuth realm="%s", error="insufficient_scope", scope="%s"' % [(options.realm || request.host), scope] return [403, { "WWW-Authenticate"=>challenge }, []] else return response end else response = @app.call(env) if response[1] && response[1]["oauth.no_access"] # OAuth access required. return unauthorized(request) elsif response[1] && response[1]["oauth.authorization"] # 3. Obtaining End-User Authorization # Flow ends here. return authorization_response(response, logger) else return response end end ensure Server.database = org_database end protected # Get here for authorization request. Check the request parameters and # redirect with an error if we find any issue. Otherwise, create a new # authorization request, set in oauth.request and pass control to the # application. def request_authorization(request, logger) # 3. Obtaining End-User Authorization begin redirect_uri = Utils.parse_redirect_uri(request.GET["redirect_uri"]) rescue InvalidRequestError=>error logger.error "Authorization request with invalid redirect_uri: #{request.GET["redirect_uri"]} #{error.message}" if logger return bad_request(error.message) end state = request.GET["state"] begin # 3. Obtaining End-User Authorization client = get_client(request) raise RedirectUriMismatchError unless client.redirect_uri.nil? || client.redirect_uri == redirect_uri.to_s requested_scope = request.GET["scope"].to_s.split.uniq.join(" ") response_type = request.GET["response_type"].to_s raise UnsupportedResponseTypeError unless options.authorization_types.include?(response_type) if scopes = options.scopes allowed_scope = scopes.respond_to?(:all?) ? scopes : scopes.split raise InvalidScopeError unless requested_scope.split.all? { |v| allowed_scope.include?(v) } end # Create object to track authorization request and let application # handle the rest. auth_request = AuthRequest.create(client.id, requested_scope, redirect_uri.to_s, response_type, state) request.env["oauth.authorization"] = auth_request.id.to_s logger.info "Request #{auth_request.id}: Client #{client.display_name} requested #{response_type} with scope #{requested_scope}" if logger return @app.call(request.env) rescue Error=>error logger.error "Authorization request error: #{error.code} #{error.message}" if logger params = Rack::Utils.parse_query(redirect_uri.query).merge(:error=>error.code, :error_description=>error.message, :state=>state) redirect_uri.query = Rack::Utils.build_query(params) return redirect_to(redirect_uri) end end # Get here on completion of the authorization. Authorization response in # oauth.response either grants or denies authroization. In either case, we # redirect back with the proper response. def authorization_response(response, logger) status, headers, body = response auth_request = self.class.get_auth_request(headers["oauth.authorization"]) redirect_uri = URI.parse(auth_request.redirect_uri) if status == 401 auth_request.deny! else auth_request.grant! headers["oauth.identity"] end # 3.1. Authorization Response if auth_request.response_type == "code" && auth_request.grant_code logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access code #{auth_request.grant_code}" if logger params = { :code=>auth_request.grant_code, :scope=>auth_request.scope, :state=>auth_request.state } params = Rack::Utils.parse_query(redirect_uri.query).merge(params) redirect_uri.query = Rack::Utils.build_query(params) return redirect_to(redirect_uri) elsif auth_request.response_type == "token" && auth_request.access_token logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} granted access token #{auth_request.access_token}" if logger params = { :access_token=>auth_request.access_token, :scope=>auth_request.scope, :state=>auth_request.state } redirect_uri.fragment = Rack::Utils.build_query(params) return redirect_to(redirect_uri) else logger.info "Request #{auth_request.id}: Client #{auth_request.client_id} denied authorization" if logger params = Rack::Utils.parse_query(redirect_uri.query).merge(:error=>:access_denied, :state=>auth_request.state) redirect_uri.query = Rack::Utils.build_query(params) return redirect_to(redirect_uri) end end # 4. Obtaining an Access Token def respond_with_access_token(request, logger) return [405, { "Content-Type"=>"application/json" }, ["POST only"]] unless request.post? # 4.2. Access Token Response begin client = get_client(request) case request.POST["grant_type"] when "authorization_code" # 4.1.1. Authorization Code grant = AccessGrant.from_code(request.POST["code"]) raise InvalidGrantError unless grant && client.id == grant.client_id raise InvalidGrantError unless grant.redirect_uri.nil? || grant.redirect_uri == Utils.parse_redirect_uri(request.POST["redirect_uri"]).to_s access_token = grant.authorize! when "password" raise UnsupportedGrantType unless options.authenticator # 4.1.2. Resource Owner Password Credentials username, password = request.POST.values_at("username", "password") requested_scope = request.POST["scope"].to_s.split.uniq.join(" ") raise InvalidGrantError unless username && password identity = options.authenticator.call(username, password) raise InvalidGrantError unless identity if scopes = options.scopes allowed_scope = scopes.respond_to?(:all?) ? scopes : scopes.split raise InvalidScopeError unless requested_scope.split.all? { |v| allowed_scope.include?(v) } end access_token = AccessToken.get_token_for(identity, requested_scope.to_s, client.id) else raise UnsupportedGrantType end logger.info "Access token #{access_token.token} granted to client #{client.display_name}, identity #{access_token.identity}" if logger response = { :access_token=>access_token.token } response[:scope] = access_token.scope unless access_token.scope.empty? return [200, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" }, response.to_json] # 4.3. Error Response rescue Error=>error logger.error "Access token request error: #{error.code} #{error.message}" if logger return unauthorized(request, error) if InvalidClientError === error && request.basic? return [400, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" }, { :error=>error.code, :error_description=>error.message }.to_json] end end # Returns client from request based on credentials. Raises # InvalidClientError if client doesn't exist or secret doesn't match. def get_client(request) # 2.1 Client Password Credentials if request.basic? client_id, client_secret = request.credentials elsif request.form_data? client_id, client_secret = request.POST.values_at("client_id", "client_secret") else client_id, client_secret = request.GET.values_at("client_id", "client_secret") end client = self.class.get_client(client_id) raise InvalidClientError unless client && client.secret == client_secret raise InvalidClientError if client.revoked return client rescue BSON::InvalidObjectId raise InvalidClientError end # Rack redirect response. The argument is typically a URI object. def redirect_to(uri) return [302, { "Location"=>uri.to_s }, []] end def bad_request(message) return [400, { "Content-Type"=>"text/plain" }, [message]] end # Returns WWW-Authenticate header. def unauthorized(request, error = nil) challenge = 'OAuth realm="%s"' % (options.realm || request.host) challenge << ', error="%s", error_description="%s"' % [error.code, error.message] if error return [401, { "WWW-Authenticate"=>challenge }, []] end # Wraps Rack::Request to expose Basic and OAuth authentication # credentials. class OAuthRequest < Rack::Request AUTHORIZATION_KEYS = %w{HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION} # Returns authorization header. def authorization @authorization ||= AUTHORIZATION_KEYS.inject(nil) { |auth, key| auth || @env[key] } end # True if authentication scheme is OAuth. def oauth? authorization[/^oauth/i] if authorization end # True if authentication scheme is Basic. def basic? authorization[/^basic/i] if authorization end # If Basic auth, returns username/password, if OAuth, returns access # token. def credentials basic? ? authorization.gsub(/\n/, "").split[1].unpack("m*").first.split(/:/, 2) : oauth? ? authorization.gsub(/\n/, "").split[1] : nil end end end end end