lib/uaa/token_issuer.rb in cf-uaa-lib-1.3.1 vs lib/uaa/token_issuer.rb in cf-uaa-lib-1.3.2
- old
+ new
@@ -14,81 +14,130 @@
require 'securerandom'
require 'uaa/http'
module CF::UAA
-# The Token class is returned by various TokenIssuer methods. It holds access
+# The TokenInfo class is returned by various TokenIssuer methods. It holds access
# and refresh tokens as well as token meta-data such as token type and
-# expiration time. See Token#info for contents.
-class Token
+# expiration time. See {TokenInfo#info} for contents.
+class TokenInfo
- # Returns a hash of information about the current token. The info hash MUST include
+ # Information about the current token. The info hash MUST include
# access_token, token_type and scope (if granted scope differs from requested
# scope). It should include expires_in. It may include refresh_token, scope,
# and other values from the auth server.
+ # @return [Hash]
attr_reader :info
- def initialize(info) # :nodoc:
- @info = info
+ # Normally instantiated by {TokenIssuer}.
+ def initialize(info) @info = info end
+
+ # Constructs a string for use in an authorization header from the contents of
+ # the TokenInfo.
+ # @return [String] Typically a string such as "bearer xxxx.xxxx.xxxx".
+ def auth_header
+ "#{@info[:token_type] || @info['token_type']} #{@info[:access_token] || @info['access_token']}"
end
- # Returns a string for use in an authorization header that is constructed
- # from contents of the Token. Typically a string such as "bearer xxxx.xxxx.xxxx".
- def auth_header; "#{info['token_type']} #{info['access_token']}" end
end
# Client Apps that want to get access to resource servers on behalf of their
# users need to get tokens via authcode and implicit flows,
# request scopes, etc., but they don't need to process tokens. This
# class is for these use cases.
#
# In general most of this class is an implementation of the client pieces of
-# the OAuth2 protocol. See http://tools.ietf.org/html/rfc6749
+# the OAuth2 protocol. See {http://tools.ietf.org/html/rfc6749}
class TokenIssuer
include Http
- # parameters:
- # [+target+] The base URL of a UAA's oauth authorize endpoint. For example
- # the target would be \https://login.cloudfoundry.com if the
- # endpoint is \https://login.cloudfoundry.com/oauth/authorize.
- # The target would be \http://localhost:8080/uaa if the endpoint
- # is \http://localhost:8080/uaa/oauth/authorize.
- # [+client_id+] The oauth2 client id. See http://tools.ietf.org/html/rfc6749#section-2.2
- # [+client_secret+] needed to authenticate the client for all grant types
- # except implicit.
- # [+token_target+] The base URL of the oauth token endpoint. If not specified,
- # +target+ is used.
- def initialize(target, client_id, client_secret = nil, token_target = nil)
+ private
+
+ def random_state; SecureRandom.hex end
+
+ def parse_implicit_params(encoded_params, state)
+ params = Util.decode_form(encoded_params)
+ raise BadResponse, "mismatched state" unless state && params.delete('state') == state
+ raise TargetError.new(params), "error response from #{@target}" if params['error']
+ raise BadResponse, "no type and token" unless params['token_type'] && params['access_token']
+ exp = params['expires_in'].to_i
+ params['expires_in'] = exp if exp.to_s == params['expires_in']
+ TokenInfo.new(Util.hash_keys!(params, @key_style))
+ rescue URI::InvalidURIError, ArgumentError
+ raise BadResponse, "received invalid response from target #{@target}"
+ end
+
+ # returns a CF::UAA::TokenInfo object which includes the access token and metadata.
+ def request_token(params)
+ if scope = Util.arglist(params.delete(:scope))
+ params[:scope] = Util.strlist(scope)
+ end
+ headers = {'content-type' => 'application/x-www-form-urlencoded',
+ 'accept' => 'application/json',
+ 'authorization' => Http.basic_auth(@client_id, @client_secret) }
+ reply = json_parse_reply(@key_style, *request(@token_target, :post,
+ '/oauth/token', Util.encode_form(params), headers))
+ raise BadResponse unless reply[jkey :token_type] && reply[jkey :access_token]
+ TokenInfo.new(reply)
+ end
+
+ def authorize_path_args(response_type, redirect_uri, scope, state = random_state, args = {})
+ params = args.merge(:client_id => @client_id, :response_type => response_type,
+ :redirect_uri => redirect_uri, :state => state)
+ params[:scope] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
+ params[:nonce], params[:response_type] = state, "#{response_type} id_token" if scope && scope.include?('openid')
+ "/oauth/authorize?#{Util.encode_form(params)}"
+ end
+
+ def jkey(k) @key_style ? k : k.to_s end
+
+ public
+
+ # @param [String] target The base URL of a UAA's oauth authorize endpoint.
+ # For example the target would be {https://login.cloudfoundry.com} if the
+ # endpoint is {https://login.cloudfoundry.com/oauth/authorize}.
+ # The target would be {http://localhost:8080/uaa} if the endpoint
+ # is {http://localhost:8080/uaa/oauth/authorize}.
+ # @param [String] client_id The oauth2 client id, see
+ # {http://tools.ietf.org/html/rfc6749#section-2.2}
+ # @param [String] client_secret Needed to authenticate the client for all
+ # grant types except implicit.
+ # @param [Hash] options can be
+ # * +:token_target+, the base URL of the oauth token endpoint -- if
+ # not specified, +target+ is used.
+ # * +:symbolize_keys+, if true, returned hash keys are symbols.
+ def initialize(target, client_id, client_secret = nil, options = {})
@target, @client_id, @client_secret = target, client_id, client_secret
- @token_target = token_target || target
+ @token_target = options[:token_target] || target
+ @key_style = options[:symbolize_keys] ? :sym : nil
end
# Allows an app to discover what credentials are required for
- # #implicit_grant_with_creds. Returns a hash of credential names with type
- # and suggested prompt value, e.g.
- # {"username":["text","Email"],"password":["password","Password"]}
+ # {#implicit_grant_with_creds}.
+ # @return [Hash] of credential names with type and suggested prompt value,
+ # e.g. !{"username":["text","Email"],"password":["password","Password"]}
def prompts
- reply = json_get @target, '/login'
- return reply['prompts'] if reply && reply['prompts']
+ reply = json_get(@target, '/login')
+ return reply[jkey :prompts] if reply && reply[jkey :prompts]
raise BadResponse, "No prompts in response from target #{@target}"
end
# Gets an access token in a single call to the UAA with the user
- # credentials used for authentication. The +credentials+ should
- # be an object such as a hash that can be converted to a json
- # representation of the credential name/value pairs
- # corresponding to the keys retrieved by #prompts.
- # Returns a Token.
+ # credentials used for authentication.
+ # @param credentials should be an object such as a hash that can be converted
+ # to a json representation of the credential name/value pairs corresponding to
+ # the keys retrieved by {#prompts}.
+ # @return [TokenInfo]
def implicit_grant_with_creds(credentials, scope = nil)
# this manufactured redirect_uri is a convention here, not part of OAuth2
redir_uri = "https://uaa.cloudfoundry.com/redirect/#{@client_id}"
- uri = authorize_path_args("token", redir_uri, scope, state = SecureRandom.uuid)
+ uri = authorize_path_args("token", redir_uri, scope, state = random_state)
# the accept header is only here so the uaa will issue error replies in json to aid debugging
headers = {'content-type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json' }
- body = URI.encode_www_form(credentials.merge('source' => 'credentials'))
+ body = Util.encode_form(credentials.merge(:source => 'credentials'))
status, body, headers = request(@target, :post, uri, body, headers)
raise BadResponse, "status #{status}" unless status == 302
req_uri, reply_uri = URI.parse(redir_uri), URI.parse(headers['location'])
fragment, reply_uri.fragment = reply_uri.fragment, nil
raise BadResponse, "bad location header" unless req_uri == reply_uri
@@ -96,144 +145,112 @@
rescue URI::Error => e
raise BadResponse, "bad location header in reply: #{e.message}"
end
# Constructs a uri that the client is to return to the browser to direct
- # the user to the authorization server to get an authcode. The +redirect_uri+
- # is embedded in the returned uri so the authorization server can redirect
- # the user back to the client app.
+ # the user to the authorization server to get an authcode.
+ # @param [String] redirect_uri (see #authcode_uri)
+ # @return [String]
def implicit_uri(redirect_uri, scope = nil)
@target + authorize_path_args("token", redirect_uri, scope)
end
# Gets a token via an implicit grant.
- # [+authcode_uri+] must be from a previous call to #implicit_uri and contains
- # state used to validate the contents of the reply from the
- # Authorization Server.
- # [+callback_fragment+] must be the fragment portion of the URL received by
- # user's browser after the Authorization Server
- # redirects back to the +redirect_uri+ that was given to
- # #implicit_uri. How the application get's the contents
- # of the fragment is application specific -- usually
- # some javascript in the page at the +redirect_uri+.
- #
- # See http://tools.ietf.org/html/rfc6749#section-4.2 .
- #
- # Returns a Token.
+ # @param [String] implicit_uri must be from a previous call to
+ # {#implicit_uri}, contains state used to validate the contents of the
+ # reply from the server.
+ # @param [String] callback_fragment must be the fragment portion of the URL
+ # received by the user's browser after the server redirects back to the
+ # +redirect_uri+ that was given to {#implicit_uri}. How the application
+ # gets the contents of the fragment is application specific -- usually
+ # some javascript in the page at the +redirect_uri+.
+ # @see http://tools.ietf.org/html/rfc6749#section-4.2
+ # @return [TokenInfo]
def implicit_grant(implicit_uri, callback_fragment)
- in_params = Util.decode_form_to_hash(URI.parse(implicit_uri).query)
+ in_params = Util.decode_form(URI.parse(implicit_uri).query)
unless in_params['state'] && in_params['redirect_uri']
raise ArgumentError, "redirect must happen before implicit grant"
end
- parse_implicit_params callback_fragment, in_params['state']
+ parse_implicit_params(callback_fragment, in_params['state'])
end
# A UAA extension to OAuth2 that allows a client to pre-authenticate a
# user at the start of an authorization code flow. By passing in the
- # user's credentials (see #prompts) the Authorization Server can establish
- # a session with the user's browser without reprompting for authentication.
- # This is useful for user account management apps so that they can create
- # a user account, or reset a password for the user, without requiring the
- # user to type in their credentials again.
+ # user's credentials the server can establish a session with the user's
+ # browser without reprompting for authentication. This is useful for
+ # user account management apps so that they can create a user account,
+ # or reset a password for the user, without requiring the user to type
+ # in their credentials again.
+ # @param [String] credentials (see #implicit_grant_with_creds)
+ # @param [String] redirect_uri (see #authcode_uri)
+ # @return (see #authcode_uri)
def autologin_uri(redirect_uri, credentials, scope = nil)
- headers = {'content_type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json',
+ headers = {'content-type' => 'application/x-www-form-urlencoded',
+ 'accept' => 'application/json',
'authorization' => Http.basic_auth(@client_id, @client_secret) }
- body = URI.encode_www_form(credentials)
- reply = json_parse_reply(*request(@target, :post, "/autologin", body, headers))
+ body = Util.encode_form(credentials)
+ reply = json_parse_reply(nil, *request(@target, :post, "/autologin", body, headers))
raise BadResponse, "no autologin code in reply" unless reply['code']
- @target + authorize_path_args('code', redirect_uri, scope, SecureRandom.uuid, code: reply[:code])
+ @target + authorize_path_args('code', redirect_uri, scope,
+ random_state, :code => reply['code'])
end
# Constructs a uri that the client is to return to the browser to direct
- # the user to the authorization server to get an authcode. The redirect_uri
- # is embedded in the returned uri so the authorization server can redirect
- # the user back to the client app.
+ # the user to the authorization server to get an authcode.
+ # @param [String] redirect_uri is embedded in the returned uri so the server
+ # can redirect the user back to the caller's endpoint.
+ # @return [String] uri which
def authcode_uri(redirect_uri, scope = nil)
@target + authorize_path_args('code', redirect_uri, scope)
end
# Uses the instance client credentials in addition to +callback_query+
# to get a token via the authorization code grant.
- # [+authcode_uri+] must be from a previous call to #authcode_uri and contains
- # state used to validate the contents of the reply from the
- # Authorization Server.
- # [callback_query] must be the query portion of the URL received by the
- # client after the user's browser is redirected back from
- # the Authorization server. It contains the authorization
- # code.
- #
- # See http://tools.ietf.org/html/rfc6749#section-4.1 .
- #
- # Returns a Token.
+ # @param [String] authcode_uri must be from a previous call to {#authcode_uri}
+ # and contains state used to validate the contents of the reply from the
+ # server.
+ # @param [String] callback_query must be the query portion of the URL
+ # received by the client after the user's browser is redirected back from
+ # the server. It contains the authorization code.
+ # @see http://tools.ietf.org/html/rfc6749#section-4.1
+ # @return [TokenInfo]
def authcode_grant(authcode_uri, callback_query)
- ac_params = Util.decode_form_to_hash(URI.parse(authcode_uri).query)
+ ac_params = Util.decode_form(URI.parse(authcode_uri).query)
unless ac_params['state'] && ac_params['redirect_uri']
raise ArgumentError, "authcode redirect must happen before authcode grant"
end
begin
- params = Util.decode_form_to_hash(callback_query)
+ params = Util.decode_form(callback_query)
authcode = params['code']
raise BadResponse unless params['state'] == ac_params['state'] && authcode
rescue URI::InvalidURIError, ArgumentError, BadResponse
raise BadResponse, "received invalid response from target #{@target}"
end
- request_token('grant_type' => 'authorization_code', 'code' => authcode, 'redirect_uri' => ac_params['redirect_uri'])
+ request_token(:grant_type => 'authorization_code', :code => authcode,
+ :redirect_uri => ac_params['redirect_uri'])
end
# Uses the instance client credentials in addition to the +username+
# and +password+ to get a token via the owner password grant.
- # See http://tools.ietf.org/html/rfc6749#section-4.3 .
- # Returns a Token.
+ # See {http://tools.ietf.org/html/rfc6749#section-4.3}.
+ # @return [TokenInfo]
def owner_password_grant(username, password, scope = nil)
- request_token('grant_type' => 'password', 'username' => username, 'password' => password, 'scope' => scope)
+ request_token(:grant_type => 'password', :username => username,
+ :password => password, :scope => scope)
end
# Uses the instance client credentials to get a token with a client
# credentials grant. See http://tools.ietf.org/html/rfc6749#section-4.4
- # Returns a Token.
+ # @return [TokenInfo]
def client_credentials_grant(scope = nil)
- request_token('grant_type' => 'client_credentials', 'scope' => scope)
+ request_token(:grant_type => 'client_credentials', :scope => scope)
end
# Uses the instance client credentials and the given +refresh_token+ to get
# a new access token. See http://tools.ietf.org/html/rfc6749#section-6
- # Returns a Token, which may include a new refresh token as well as an access token.
+ # @return [TokenInfo] which may include a new refresh token as well as an access token.
def refresh_token_grant(refresh_token, scope = nil)
- request_token('grant_type' => 'refresh_token', 'refresh_token' => refresh_token, 'scope' => scope)
- end
-
- private
-
- def parse_implicit_params(encoded_params, state)
- params = Util.decode_form_to_hash(encoded_params)
- raise BadResponse, "mismatched state" unless state && params.delete('state') == state
- raise TargetError.new(params), "error response from #{@target}" if params['error']
- raise BadResponse, "no type and token" unless params['token_type'] && params['access_token']
- exp = params['expires_in'].to_i
- params['expires_in'] = exp if exp.to_s == params['expires_in']
- Token.new params
- rescue URI::InvalidURIError, ArgumentError
- raise BadResponse, "received invalid response from target #{@target}"
- end
-
- # returns a CF::UAA::Token object which includes the access token and metadata.
- def request_token(params)
- if scope = Util.arglist(params.delete('scope'))
- params['scope'] = Util.strlist(scope)
- end
- headers = {'content-type' => 'application/x-www-form-urlencoded', 'accept' => 'application/json',
- 'authorization' => Http.basic_auth(@client_id, @client_secret) }
- body = URI.encode_www_form(params)
- reply = json_parse_reply(*request(@token_target, :post, '/oauth/token', body, headers))
- raise BadResponse unless reply['token_type'] && reply['access_token']
- Token.new reply
- end
-
- def authorize_path_args(response_type, redirect_uri, scope, state = SecureRandom.uuid, args = {})
- params = args.merge('client_id' => @client_id, 'response_type' => response_type, 'redirect_uri' => redirect_uri, 'state' => state)
- params['scope'] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
- params['nonce'], params['response_type'] = state, "#{response_type} id_token" if scope && scope.include?('openid')
- "/oauth/authorize?#{URI.encode_www_form(params)}"
+ request_token(:grant_type => 'refresh_token', :refresh_token => refresh_token, :scope => scope)
end
end
end