class Rack::OAuth2::Server

Implements an OAuth 2 Authorization Server, based on tools.ietf.org/html/draft-ietf-oauth-v2-10

Constants

Options

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.

  • :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.

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
VERSION

Same as gem version number.

Attributes

options[R]

@see Options

Public Class Methods

access_grant(identity, client_id, scope = nil, expires = nil) click to toggle source

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 How many seconds before access grant expires (default to 5 minutes) @return [String] Access grant authorization code

# File lib/rack/oauth2/server.rb, line 87
def access_grant(identity, client_id, scope = nil, expires = nil)
  client = get_client(client_id) or fail "No such client"
  AccessGrant.create(identity, client, scope || client.scope, nil, expires).code
end
get_access_token(token) click to toggle source

Returns AccessToken from token.

@param [String] token Access token (e.g. from oauth.access_token) @return [AccessToken]

# File lib/rack/oauth2/server.rb, line 96
def get_access_token(token)
  AccessToken.from_token(token)
end
get_auth_request(authorization) click to toggle source

Return AuthRequest from authorization request handle.

@param [String] authorization Authorization handle (e.g. from oauth.authorization) @return [AuthReqeust]

# File lib/rack/oauth2/server.rb, line 23
def get_auth_request(authorization)
  AuthRequest.find_by_code(authorization)
end
get_client(client_id) click to toggle source

Returns Client from client identifier.

@param [String] client_id Client identifier (e.g. from oauth.client.id) @return [Client]

# File lib/rack/oauth2/server.rb, line 31
def get_client(client_id)
  p client_id
  Client.find_by_code(client_id)
end
list_access_tokens(identity) click to toggle source

Returns all AccessTokens for an identity.

@param [String] identity Identity, e.g. user ID, account ID @return [Array<AccessToken>]

# File lib/rack/oauth2/server.rb, line 117
def list_access_tokens(identity)
  AccessToken.from_identity(identity)
end
new(app, options = Options.new, &authenticator) click to toggle source
# File lib/rack/oauth2/server.rb, line 155
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}
  @options.param_authentication ||= false
end
register(args) click to toggle source

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"
# File lib/rack/oauth2/server.rb, line 68
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
secure_random() click to toggle source

Long, random and hexy.

# File lib/rack/oauth2/models.rb, line 15
def self.secure_random
  OpenSSL::Random.random_bytes(32).unpack("H*")[0]
end
token_for(identity, client_id, scope = nil) click to toggle source

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 @return [String] Access token

# File lib/rack/oauth2/server.rb, line 108
def token_for(identity, client_id, scope = nil)
  client = get_client(client_id) or fail "No such client"
  AccessToken.get_token_for(identity, client, scope || client.scope).token
end

Public Instance Methods

call(env) click to toggle source
# File lib/rack/oauth2/server.rb, line 168
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

  begin
    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]["oauth.no_scope"])
        challenge = 'OAuth realm="%s", error="insufficient_scope", scope="%s"' % [(options.realm || request.host), scope]
        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
end

Protected Instance Methods

authorization_response(response, logger) click to toggle source

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.

# File lib/rack/oauth2/server.rb, line 306
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"]
  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, :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, :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
bad_request(message) click to toggle source
# File lib/rack/oauth2/server.rb, line 415
def bad_request(message)
  return [400, { "Content-Type"=>"text/plain" }, [message]]
end
get_client(request, options={}) click to toggle source

Returns client from request based on credentials. Raises InvalidClientError if client doesn't exist or secret doesn't match.

# File lib/rack/oauth2/server.rb, line 391
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
end
redirect_to(uri, status = 302) click to toggle source

Rack redirect response. The argument is typically a URI object, and the status should be a 302 or 303.

# File lib/rack/oauth2/server.rb, line 411
def redirect_to(uri, status = 302)
  return [status, { "Content-Type"=>"text/plain", "Location"=>uri.to_s }, ["You are being redirected"]]
end
request_authorization(request, logger) click to toggle source

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.

# File lib/rack/oauth2/server.rb, line 246
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 = auth_request.client
      # 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}" if logger
      request.env["oauth.authorization"] = auth_request.code
      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 = Utils.normalize_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.code}"
      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
respond_with_access_token(request, logger) click to toggle source
  1. Obtaining an Access Token

# File lib/rack/oauth2/server.rb, line 340
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"]) : Utils.normalize_scope(client.scope)
      access_token = AccessToken.create_token_for(client, requested_scope)
    when "authorization_code"
      # 4.1.1.  Authorization Code
      grant = AccessGrant.from_code(request.POST["code"])
      p grant
      raise InvalidGrantError, "Wrong client" unless grant && client == grant.client
      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!
    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"]) : Utils.normalize_scope(client.scope)
      allowed_scope = Utils.normalize_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)
    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
    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
unauthorized(request, error = nil) click to toggle source

Returns WWW-Authenticate header.

# File lib/rack/oauth2/server.rb, line 420
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