class CF::UAA::TokenIssuer

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 {tools.ietf.org/html/rfc6749}

Public Class Methods

new(target, client_id, client_secret = nil, options = {}) click to toggle source

@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.
# File lib/uaa/token_issuer.rb, line 108
def initialize(target, client_id, client_secret = nil, options = {})
  @target, @client_id, @client_secret = target, client_id, client_secret
  @token_target = options[:token_target] || target
  @key_style = options[:symbolize_keys] ? :sym : nil
  self.skip_ssl_validation = options[:skip_ssl_validation]
  self.ssl_ca_file = options[:ssl_ca_file]
  self.ssl_cert_store = options[:ssl_cert_store]
  self.http_proxy = options[:http_proxy]
  self.https_proxy = options[:https_proxy]
end

Public Instance Methods

authcode_grant(authcode_uri, callback_query) click to toggle source

Uses the instance client credentials in addition to callback_query to get a token via the authorization code grant. @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 tools.ietf.org/html/rfc6749#section-4.1 @return [TokenInfo]

# File lib/uaa/token_issuer.rb, line 223
def authcode_grant(authcode_uri, callback_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(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'])
end
authcode_uri(redirect_uri, scope = nil) click to toggle source

Constructs a uri that the client is to return to the browser to direct 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

# File lib/uaa/token_issuer.rb, line 209
def authcode_uri(redirect_uri, scope = nil)
  @target + authorize_path_args('code', redirect_uri, scope)
end
autologin_uri(redirect_uri, credentials, scope = nil) click to toggle source

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 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)

# File lib/uaa/token_issuer.rb, line 194
def autologin_uri(redirect_uri, credentials, scope = nil)
  headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8,
      'authorization' => Http.basic_auth(@client_id, @client_secret) }
  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,
      random_state, :code => reply['code'])
end
client_credentials_grant(scope = nil) click to toggle source

Uses the instance client credentials to get a token with a client credentials grant. See tools.ietf.org/html/rfc6749#section-4.4 @return [TokenInfo]

# File lib/uaa/token_issuer.rb, line 270
def client_credentials_grant(scope = nil)
  request_token(:grant_type => 'client_credentials', :scope => scope)
end
implicit_grant(implicit_uri, callback_fragment) click to toggle source

Gets a token via an implicit grant. @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 tools.ietf.org/html/rfc6749#section-4.2 @return [TokenInfo]

# File lib/uaa/token_issuer.rb, line 176
def implicit_grant(implicit_uri, callback_fragment)
  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'])
end
implicit_grant_with_creds(credentials, scope = nil) click to toggle source

Gets an access token in a single call to the UAA with the user 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]

# File lib/uaa/token_issuer.rb, line 135
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}"
  response_type = "token"
  response_type = "#{response_type} id_token" if scope && (scope.include? "openid")
  uri = authorize_path_args(response_type, 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' => FORM_UTF8, 'accept' => JSON_UTF8 }
  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
  parse_implicit_params(fragment, state)
rescue URI::Error => e
  raise BadResponse, "bad location header in reply: #{e.message}"
end
implicit_uri(redirect_uri, scope = nil) click to toggle source

Constructs a uri that the client is to return to the browser to direct the user to the authorization server to get an authcode. @param [String] redirect_uri (see authcode_uri) @return [String]

# File lib/uaa/token_issuer.rb, line 159
def implicit_uri(redirect_uri, scope = nil)
  response_type = "token"
  response_type = "#{response_type} id_token" if scope && (scope.include? "openid")
  @target + authorize_path_args(response_type, redirect_uri, scope)
end
owner_password_credentials_grant(credentials) click to toggle source

Gets an access token with the user credentials used for authentication via the owner password grant. See {tools.ietf.org/html/rfc6749#section-4.3}. @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]

# File lib/uaa/token_issuer.rb, line 262
def owner_password_credentials_grant(credentials)
  credentials[:grant_type] = 'password'
  request_token(credentials)
end
owner_password_grant(username, password, scope = nil) click to toggle source

Uses the instance client credentials in addition to the username and password to get a token via the owner password grant. See {tools.ietf.org/html/rfc6749#section-4.3}. @return [TokenInfo]

# File lib/uaa/token_issuer.rb, line 243
def owner_password_grant(username, password, scope = nil)
  request_token(:grant_type => 'password', :username => username,
      :password => password, :scope => scope)
end
passcode_grant(passcode, scope = nil) click to toggle source

Uses a one-time passcode obtained from the UAA to get a token. @return [TokenInfo]

# File lib/uaa/token_issuer.rb, line 251
def passcode_grant(passcode, scope = nil)
  request_token(:grant_type => 'password', :passcode => passcode, :scope => scope)
end
prompts() click to toggle source

Allows an app to discover what credentials are required for {#implicit_grant_with_creds}. @return [Hash] of credential names with type and suggested prompt value,

e.g. !{"username":["text","Email"],"password":["password","Password"]}
# File lib/uaa/token_issuer.rb, line 123
def 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
refresh_token_grant(refresh_token, scope = nil) click to toggle source

Uses the instance client credentials and the given refresh_token to get a new access token. See tools.ietf.org/html/rfc6749#section-6 @return [TokenInfo] which may include a new refresh token as well as an access token.

# File lib/uaa/token_issuer.rb, line 277
def refresh_token_grant(refresh_token, scope = nil)
  request_token(:grant_type => 'refresh_token', :refresh_token => refresh_token, :scope => scope)
end

Private Instance Methods

authorize_path_args(response_type, redirect_uri, scope, state = random_state, args = {}) click to toggle source
# File lib/uaa/token_issuer.rb, line 83
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] = state
  "/oauth/authorize?#{Util.encode_form(params)}"
end
jkey(k) click to toggle source
# File lib/uaa/token_issuer.rb, line 91
def jkey(k) @key_style ? k : k.to_s end
parse_implicit_params(encoded_params, state) click to toggle source
# File lib/uaa/token_issuer.rb, line 58
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
random_state() click to toggle source
# File lib/uaa/token_issuer.rb, line 56
def random_state; SecureRandom.hex end
request_token(params) click to toggle source

returns a CF::UAA::TokenInfo object which includes the access token and metadata.

# File lib/uaa/token_issuer.rb, line 71
def request_token(params)
  if scope = Util.arglist(params.delete(:scope))
    params[:scope] = Util.strlist(scope)
  end
  headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8,
      '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