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 (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.

VERSION

Same as gem version number.

Attributes

options[R]

Options specific for this handle. @see Options

Public Class Methods

access_grant(identity, client_id, scope = nil, expires_in = 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_in 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_in = nil)
  client = get_client(client_id) or fail "No such client"
  AccessGrant.create(identity, client, scope || client.scope, nil, expires_in).code
end
create_indexes(&block) click to toggle source

@private

# File lib/rack/oauth2/models.rb, line 27
def create_indexes(&block)
  if block
    @create_indexes ||= []
    @create_indexes << block
  elsif @create_indexes
    @create_indexes.each do |block|
      block.call
    end
    @create_indexes = nil
  end
end
database() click to toggle source

A Mongo::DB object.

# File lib/rack/oauth2/models.rb, line 40
def database
  @database ||= Server.options.database
  raise "No database Configured. You must configure it using Server.options.database = Mongo::Connection.new()[db_name]" unless @database
  raise "You set Server.database to #{@database.class}, should be a Mongo::DB object" unless Mongo::DB === @database
  @database
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 24
def get_auth_request(authorization)
  AuthRequest.find(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 32
def get_client(client_id)
  return client_id if Client === client_id
  Client.find(client_id)
end
get_issuer(identifier) click to toggle source

Returns an Issuer from it’s identifier.

@param [String] identifier the Issuer’s identifier @return [Issuer]

# File lib/rack/oauth2/server.rb, line 153
def get_issuer(identifier)
  Issuer.from_identifier(identifier)
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 119
def list_access_tokens(identity)
  AccessToken.from_identity(identity)
end
new(app, options = nil, &authenticator) click to toggle source
# File lib/rack/oauth2/server.rb, line 217
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
new_instance(klass, fields) click to toggle source

Create new instance of the klass and populate its attributes.

# File lib/rack/oauth2/models.rb, line 12
def new_instance(klass, fields)
  return unless fields
  instance = klass.new
  fields.each do |name, value|
    instance.instance_variable_set :"@#{name}", value
  end
  instance
end
options() click to toggle source

Global options. This is what we set during configuration (e.g. Rails’ config/application), and options all handlers inherit by default.

# File lib/rack/oauth2/server.rb, line 211
def self.options
  @options
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
register_issuer(args) click to toggle source

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"
# File lib/rack/oauth2/server.rb, line 141
def register_issuer(args)
  if args[:identifier] && (issuer = get_issuer(args[:identifier]))
    issuer.update(args)
  else
    Issuer.create(args)
  end
end
secure_random() click to toggle source

Long, random and hexy.

# File lib/rack/oauth2/models.rb, line 22
def secure_random
  OpenSSL::Random.random_bytes(32).unpack("H*")[0]
end
token_for(identity, client_id, scope = nil, expires_in = 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 @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

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

Public Instance Methods

call(env) click to toggle source
# File lib/rack/oauth2/server.rb, line 232
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 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 368
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
bad_request(message) click to toggle source
# File lib/rack/oauth2/server.rb, line 495
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 469
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
process_jwt_assertion(assertion) click to toggle source

Processes a JWT assertion

# File lib/rack/oauth2/server.rb, line 507
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
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 491
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 308
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
respond_with_access_token(request, logger) click to toggle source
  1. Obtaining an Access Token

# File lib/rack/oauth2/server.rb, line 402
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
unauthorized(request, error = nil) click to toggle source

Returns WWW-Authenticate header.

# File lib/rack/oauth2/server.rb, line 500
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, "Content-Type"=>"application/json" }, [error && error.message || ""]]
end