lib/rack/oauth2/server.rb in rack-oauth2-server-2.5.1 vs lib/rack/oauth2/server.rb in rack-oauth2-server-2.6.0
- old
+ new
@@ -1,10 +1,11 @@
require "rack"
require "rack/oauth2/models"
require "rack/oauth2/server/errors"
require "rack/oauth2/server/utils"
require "rack/oauth2/server/helper"
+require "iconv"
module Rack
module OAuth2
@@ -117,12 +118,46 @@
# @return [Array<AccessToken>]
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
@@ -379,10 +414,21 @@
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
+ raise InvalidGrantError, "Unsupported assertion_type" if assertion_type != "urn:ietf:params:oauth:grant-type:jwt-bearer"
+ 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)
+ 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 }
@@ -433,9 +479,51 @@
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