require "rack" require "rack/oauth2/models" require "rack/oauth2/server/errors" require "rack/oauth2/server/utils" require "rack/oauth2/server/helper" require "iconv" require "json" module Rack module OAuth2 # Implements an OAuth 2 Authorization Server, based on http://tools.ietf.org/html/draft-ietf-oauth-v2-10 class Server # Same as gem version number. VERSION = IO.read(::File.expand_path("../../../VERSION", ::File.dirname(__FILE__))).strip class << self # Return AuthRequest from authorization request handle. # # @param [String] authorization Authorization handle (e.g. from # oauth.authorization) # @return [AuthReqeust] def get_auth_request(authorization) AuthRequest.find(authorization) end # Returns Client from client identifier. # # @param [String] client_id Client identifier (e.g. from oauth.client.id) # @return [Client] def get_client(client_id) return client_id if Client === client_id Client.find(client_id) end # Registers and returns a new Client. Can also be used to update # existing client registration, by passing identifier (and secret) of # existing client record. That way, your setup script can create a new # client application and run repeatedly without fail. # # @param [Hash] args Arguments for registering client application # @option args [String] :id Client identifier. Use this to update # existing client registration (in combination wih secret) # @option args [String] :secret Client secret. Use this to update # existing client registration. # @option args [String] :display_name Name to show when authorizing # access (e.g. "My Awesome Application") # @option args [String] link Link to client application's Web site # @option args [String] image_url URL of image to show alongside display # name. # @option args [String] redirect_uri Redirect URL: authorization # requests for this client will always redirect back to this URL. # @option args [Array] scope Scope that client application can request # (list of names). # @option args [Array] notes Free form text, for internal use. # # @example Registering new client application # Server.register :display_name=>"My Application", # :link=>"http://example.com", :scope=>%w{read write}, # :redirect_uri=>"http://example.com/oauth/callback" # @example Migration using configuration file # config = YAML.load_file(Rails.root + "config/oauth.yml") # Server.register config["id"], config["secret"], # :display_name=>"My Application", :link=>"http://example.com", # :scope=>config["scope"], # :redirect_uri=>"http://example.com/oauth/callback" def register(args) if args[:id] && args[:secret] && (client = get_client(args[:id])) fail "Client secret does not match" unless client.secret == args[:secret] client.update args else Client.create(args) end end # Creates and returns a new access grant. Actually, returns only the # authorization code which you can turn into an access token by # making a request to /oauth/access_token. # # @param [String,Integer] identity User ID, account ID, etc # @param [String] client_id Client identifier # @param [Array, nil] scope Array of string, nil if you want 'em all # @param [Integer, nil] expires_in How many seconds before access grant # expires (default to 5 minutes) # @return [String] Access grant authorization code def access_grant(identity, client_id, scope = nil, expires_in = nil) client = get_client(client_id) or fail "No such client" AccessGrant.create(identity, client, scope || client.scope, nil, expires_in).code end # Returns AccessToken from token. # # @param [String] token Access token (e.g. from oauth.access_token) # @return [AccessToken] def get_access_token(token) AccessToken.from_token(token) end # Returns AccessToken for the specified identity, client application and # scope. You can use this method to request existing access token, new # token generated if one does not already exists. # # @param [String,Integer] identity Identity, e.g. user ID, account ID # @param [String] client_id Client application identifier # @param [Array, nil] scope Array of names, nil if you want 'em all # @param [Integer, nil] expires How many seconds before access token # expires, defaults to never. If zero or nil, token never expires. # @return [String] Access token def token_for(identity, client_id, scope = nil, expires_in = nil) client = get_client(client_id) or fail "No such client" AccessToken.get_token_for(identity, client, scope || client.scope, expires_in).token end # Returns all AccessTokens for an identity. # # @param [String] identity Identity, e.g. user ID, account ID # @return [Array] def list_access_tokens(identity) AccessToken.from_identity(identity) end # Registers and returns a new Issuer. Can also be used to update # existing Issuer, by passing the identifier of an existing Issuer record. # That way, your setup script can create a new client application and run # repeatedly without fail. # # @param [Hash] args Arguments for registering Issuer # @option args [String] :identifier Issuer identifier. Use this to update # an existing Issuer # @option args [String] :hmac_secret The HMAC secret for this Issuer # @option args [String] :public_key The RSA public key (in PEM format) for this Issuer # @option args [Array] :notes Free form text, for internal use. # # @example Registering new Issuer # Server.register_issuer :hmac_secret=>"foo", :notes=>"Company A" # @example Migration using configuration file # config = YAML.load_file(Rails.root + "config/oauth.yml") # Server.register_issuer config["id"], # :hmac_secret=>"bar", :notes=>"Company A" def register_issuer(args) if args[:identifier] && (issuer = get_issuer(args[:identifier])) issuer.update(args) else Issuer.create(args) end end # Returns an Issuer from it's identifier. # # @param [String] identifier the Issuer's identifier # @return [Issuer] def get_issuer(identifier) Issuer.from_identifier(identifier) 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 (this is a global option). # - :expires_in -- Number of seconds an auth token will live. If nil or # zero, access token never expires. # - :host -- Only check requests sent to this host. # - :path -- Only check requests for resources under this path. # - :param_authentication -- If true, supports authentication using # query/form parameters. # - :realm -- Authorization realm that will show up in 401 responses. # Defaults to use the request host name. # - :logger -- The logger to use. Under Rails, defaults to use the Rails # logger. Will use Rack::Logger if available. # - :collection_prefix -- Prefix to use for MongoDB collections created by rack-oauth2-server. Defaults to "oauth2". # # Authenticator is a block that receives either two or four parameters. # The first two are username and password. The other two are the client # identifier and scope. It authenticated, it returns an identity, # otherwise it can return nil or false. For example: # oauth.authenticator = lambda do |username, password| # user = User.find_by_username(username) # user if user && user.authenticated?(password) # end # # Assertion handler is a hash of blocks keyed by assertion_type. Blocks receive # three parameters: the client, the assertion, and the scope. If authenticated, # it returns an identity. Otherwise it can return nil or false. For example: # oauth.assertion_handler['facebook.com'] = lambda do |client, assertion, scope| # facebook = URI.parse('https://graph.facebook.com/me?access_token=' + assertion) # response = Net::HTTP.get_response(facebook) # # user_data = JSON.parse(response.body) # user = User.from_facebook_data(user_data) # end # Assertion handlers are optional; if one is not present for a given assertion # type, no error will result. # Options = Struct.new(:access_token_path, :authenticator, :assertion_handler, :authorization_types, :authorize_path, :database, :host, :param_authentication, :path, :realm, :expires_in,:logger, :collection_prefix) # Global options. This is what we set during configuration (e.g. Rails' # config/application), and options all handlers inherit by default. def self.options @options end @options = Options.new def initialize(app, options = nil, &authenticator) @app = app @options = options || Server.options @options.authenticator ||= authenticator @options.assertion_handler ||= {} @options.access_token_path ||= "/oauth/access_token" @options.authorize_path ||= "/oauth/authorize" @options.authorization_types ||= %w{code token} @options.param_authentication ||= false @options.collection_prefix ||= "oauth2" end # Options specific for this handle. @see Options attr_reader :options def call(env) request = OAuthRequest.new(env) return @app.call(env) if options.host && options.host != request.host return @app.call(env) if options.path && request.path.index(options.path) != 0 logger = options.logger || env["rack.logger"] # 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? elsif options.param_authentication && !request.GET["oauth_verifier"] # Ignore OAuth 1.0 callbacks # 5.1.2. URI Query Parameter # 5.1.3. Form-Encoded Body Parameter token = request.GET["oauth_token"] || request.POST["oauth_token"] token ||= request.GET['access_token'] || request.POST['access_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.to_i request.env["oauth.access_token"] = token request.env["oauth.identity"] = access_token.identity access_token.access! logger.info "RO2S: Authorized #{access_token.identity}" if logger rescue OAuthError=>error # 5.2. The WWW-Authenticate Response Header Field logger.info "RO2S: HTTP authorization failed #{error.code}" if logger return unauthorized(request, error) rescue =>ex logger.info "RO2S: 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 = Utils.normalize_scope(response[1].delete("oauth.no_scope")) challenge = 'OAuth realm="%s", error="insufficient_scope", scope="%s"' % [(options.realm || request.host), scope.join(" ")] response[1]["WWW-Authenticate"] = challenge return response else return response end else response = @app.call(env) if response[1] && response[1].delete("oauth.no_access") logger.debug "RO2S: Unauthorized request" if logger # 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 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) state = request.GET["state"] begin if request.GET["authorization"] auth_request = self.class.get_auth_request(request.GET["authorization"]) rescue nil if !auth_request || auth_request.revoked logger.error "RO2S: Invalid authorization request #{auth_request}" if logger return bad_request("Invalid authorization request") end response_type = auth_request.response_type # Needed for error handling client = self.class.get_client(auth_request.client_id) # Pass back to application, watch for 403 (deny!) logger.info "RO2S: Client #{client.display_name} requested #{auth_request.response_type} with scope #{auth_request.scope.join(" ")}" if logger request.env["oauth.authorization"] = auth_request.id.to_s response = @app.call(request.env) raise AccessDeniedError if response[0] == 403 return response else # 3. Obtaining End-User Authorization begin redirect_uri = Utils.parse_redirect_uri(request.GET["redirect_uri"]) rescue InvalidRequestError=>error logger.error "RO2S: Authorization request with invalid redirect_uri: #{request.GET["redirect_uri"]} #{error.message}" if logger return bad_request(error.message) end # 3. Obtaining End-User Authorization response_type = request.GET["response_type"].to_s # Need this first, for error handling client = get_client(request, :dont_authenticate => true) raise RedirectUriMismatchError unless client.redirect_uri.nil? || client.redirect_uri == redirect_uri.to_s raise UnsupportedResponseTypeError unless options.authorization_types.include?(response_type) requested_scope = Utils.normalize_scope(request.GET["scope"]) allowed_scope = client.scope raise InvalidScopeError unless (requested_scope - allowed_scope).empty? # Create object to track authorization request and let application # handle the rest. auth_request = AuthRequest.create(client, requested_scope, redirect_uri.to_s, response_type, state) uri = URI.parse(request.url) uri.query = "authorization=#{auth_request.id.to_s}" return redirect_to(uri, 303) end rescue OAuthError=>error logger.error "RO2S: Authorization request error #{error.code}: #{error.message}" if logger params = { :error=>error.code, :error_description=>error.message, :state=>state } if response_type == "token" redirect_uri.fragment = Rack::Utils.build_query(params) else # response type is code, or invalid params = Rack::Utils.parse_query(redirect_uri.query).merge(params) redirect_uri.query = Rack::Utils.build_query(params) end 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 == 403 auth_request.deny! else auth_request.grant! headers["oauth.identity"], options.expires_in end # 3.1. Authorization Response if auth_request.response_type == "code" if auth_request.grant_code logger.info "RO2S: Client #{auth_request.client_id} granted access code #{auth_request.grant_code}" if logger params = { :code=>auth_request.grant_code, :scope=>auth_request.scope.join(" "), :state=>auth_request.state } else logger.info "RO2S: Client #{auth_request.client_id} denied authorization" if logger params = { :error=>:access_denied, :state=>auth_request.state } end params = Rack::Utils.parse_query(redirect_uri.query).merge(params) redirect_uri.query = Rack::Utils.build_query(params) else # response type if token if auth_request.access_token logger.info "RO2S: 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.join(" "), :state=>auth_request.state } else logger.info "RO2S: Client #{auth_request.client_id} denied authorization" if logger params = { :error=>:access_denied, :state=>auth_request.state } end redirect_uri.fragment = Rack::Utils.build_query(params) end return redirect_to(redirect_uri) 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 "none" # 4.1 "none" access grant type (i.e. two-legged OAuth flow) requested_scope = request.POST["scope"] ? Utils.normalize_scope(request.POST["scope"]) : client.scope access_token = AccessToken.create_token_for(client, requested_scope, nil, options.expires_in) when "authorization_code" # 4.1.1. Authorization Code grant = AccessGrant.from_code(request.POST["code"]) raise InvalidGrantError, "Wrong client" unless grant && client.id == grant.client_id unless client.redirect_uri.nil? || client.redirect_uri.to_s.empty? raise InvalidGrantError, "Wrong redirect URI" unless grant.redirect_uri == Utils.parse_redirect_uri(request.POST["redirect_uri"]).to_s end raise InvalidGrantError, "This access grant expired" if grant.expires_at && grant.expires_at <= Time.now.to_i access_token = grant.authorize!(options.expires_in) when "password" raise UnsupportedGrantType unless options.authenticator # 4.1.2. Resource Owner Password Credentials username, password = request.POST.values_at("username", "password") raise InvalidGrantError, "Missing username/password" unless username && password requested_scope = request.POST["scope"] ? Utils.normalize_scope(request.POST["scope"]) : client.scope allowed_scope = client.scope raise InvalidScopeError unless (requested_scope - allowed_scope).empty? args = [username, password] args << client.id << requested_scope unless options.authenticator.arity == 2 identity = options.authenticator.call(*args) raise InvalidGrantError, "Username/password do not match" unless identity access_token = AccessToken.get_token_for(identity, client, requested_scope, options.expires_in) when "assertion" # 4.1.3. Assertion requested_scope = request.POST["scope"] ? Utils.normalize_scope(request.POST["scope"]) : client.scope assertion_type, assertion = request.POST.values_at("assertion_type", "assertion") raise InvalidGrantError, "Missing assertion_type/assertion" unless assertion_type && assertion # TODO: Add other supported assertion types (i.e. SAML) here if assertion_type == "urn:ietf:params:oauth:grant-type:jwt-bearer" identity = process_jwt_assertion(assertion) access_token = AccessToken.get_token_for(identity, client, requested_scope, options.expires_in) elsif options.assertion_handler[assertion_type] args = [client, assertion, requested_scope] identity = options.assertion_handler[assertion_type].call(*args) raise InvalidGrantError, "Unknown assertion for #{assertion_type}" unless identity access_token = AccessToken.get_token_for(identity, client, requested_scope, options.expires_in) else raise InvalidGrantError, "Unsupported assertion_type" if assertion_type != "urn:ietf:params:oauth:grant-type:jwt-bearer" end else raise UnsupportedGrantType end logger.info "RO2S: 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.join(" ") return [200, { "Content-Type"=>"application/json", "Cache-Control"=>"no-store" }, [response.to_json]] # 4.3. Error Response rescue OAuthError=>error logger.error "RO2S: 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, options={}) # 2.1 Client Password Credentials if request.basic? client_id, client_secret = request.credentials elsif request.post? 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 if !client unless options[:dont_authenticate] raise InvalidClientError unless client.secret == client_secret end raise InvalidClientError if client.revoked return client rescue BSON::InvalidObjectId raise InvalidClientError end # Rack redirect response. # The argument is typically a URI object, and the status should be a 302 or 303. def redirect_to(uri, status = 302) return [status, { "Content-Type"=>"text/plain", "Location"=>uri.to_s }, ["You are being redirected"]] 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 }, [error && error.message || ""]] end # Processes a JWT assertion def process_jwt_assertion(assertion) begin require 'jwt' require 'json' require 'openssl' require 'time' # JWT.decode only returns the claims. Gotta get the header ourselves header = JSON.parse(JWT.base64url_decode(assertion.split('.')[0])) algorithm = header['alg'] payload = JWT.decode(assertion, nil, false) raise InvalidGrantError, "missing issuer claim" if !payload.has_key?('iss') issuer_identifier = payload['iss'] issuer = Issuer.from_identifier(issuer_identifier) raise InvalidGrantError, 'Invalid issuer' if issuer.nil? if algorithm =~ /^HS/ validated_payload = JWT.decode(assertion, issuer.hmac_secret, true) elsif algorithm =~ /^RS/ validated_payload = JWT.decode(assertion, OpenSSL::PKey::RSA.new(issuer.public_key), true) end raise InvalidGrantError, "missing principal claim" if !validated_payload.has_key?('prn') raise InvalidGrantError, "missing audience claim" if !validated_payload.has_key?('aud') raise InvalidGrantError, "missing expiration claim" if !validated_payload.has_key?('exp') expires = validated_payload['exp'].to_i # add a 10 minute fudge factor for clock skew between servers skewed_expires_time = expires + (10 * 60) now = Time.now.utc.to_i raise InvalidGrantError, "expired claims" if skewed_expires_time <= now principal = validated_payload['prn'] principal rescue JWT::DecodeError => de raise InvalidGrantError, de.message rescue JSON::ParserError => pe raise InvalidGrantError, "Invalid segment encoding" end 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