#--
# Cloud Foundry
# Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved.
#
# This product is licensed to you under the Apache License, Version 2.0 (the "License").
# You may not use this product except in compliance with the License.
#
# This product includes a number of subcomponents with
# separate copyright notices and license terms. Your use of these
# subcomponents is subject to the terms and conditions of the
# subcomponent's license, as noted in the LICENSE file.
#++
require 'uaa/cli/common'
require 'launchy'
require 'uaa'
require 'uaa/stub/server'
module CF::UAA
class TokenCatcher < Stub::Base
def process_grant(data)
server.logger.debug "processing grant for path #{request.path}"
secret = server.info.delete(:client_secret)
do_authcode = server.info.delete(:do_authcode)
ti = TokenIssuer.new(Config.target, server.info.delete(:client_id), secret,
{ token_target: Config.target_value(:token_target),
basic_auth: Config.target_value(:basic_auth),
use_pkce: true,
code_verifier: server.info.delete(:code_verifier),
skip_ssl_validation: Config.target_value(:skip_ssl_validation)})
tkn = do_authcode ? ti.authcode_grant(server.info.delete(:uri), data) :
ti.implicit_grant(server.info.delete(:uri), data)
server.info.update(token_info: tkn.info)
reply.text "you are now logged in and can close this window"
rescue TargetError => e
reply.text "#{e.message}:\r\n#{Util.json_pretty(e.info)}\r\n#{e.backtrace}"
rescue Exception => e
reply.text "#{e.message}\r\n#{e.backtrace}"
ensure
server.logger.debug "reply: #{reply.body}"
end
route :get, '/favicon.ico' do
reply.headers['content-type'] = "image/vnd.microsoft.icon"
reply.body = File.read File.expand_path(File.join(__FILE__, '..', 'favicon.ico'))
end
route :get, %r{^/authcode\?(.*)$} do process_grant match[1] end
route :post, '/callback' do process_grant request.body end
route :get, '/callback' do
server.logger.debug "caught redirect back from UAA after authentication"
reply.headers['content-type'] = "text/html"
reply.body = <<-HTML.gsub(/^ +/, '')
HTML
end
end
class TokenCli < CommonCli
topic "Tokens", "token", "login"
def say_success(grant)
say "\nSuccessfully fetched token via #{grant} grant.\nTarget: #{Config.target}\nContext: #{Config.context}, from client #{Config[:client_id]}\n\n"
end
def set_context(token_info)
return gripe "attempt to get token failed\n" unless token_info && token_info["access_token"]
contents = TokenCoder.decode(token_info["access_token"], verify: false)
new_context = contents["user_name"] || contents["client_id"] || "bad_token"
Config.delete(Config.target, new_context)
Config.context = new_context
did_save = true
(did_save &= Config.add_opts(user_id: contents["user_id"])) if contents["user_id"]
(did_save &= Config.add_opts(client_id: contents["client_id"])) if contents["client_id"]
jti = token_info.delete("jti") if token_info.has_key? "jti"
did_save &= Config.add_opts token_info
(did_save &= Config.add_opts(scope: contents["scope"])) if contents["scope"]
(did_save &= Config.add_opts(jti: jti)) if jti
did_save
end
def issuer_request(client_id, secret = nil, code_verifier = nil)
update_target_info
yield TokenIssuer.new(Config.target.to_s, client_id, secret,
{ token_target: Config.target_value(:token_endpoint),
basic_auth: Config.target_value(:basic_auth),
use_pkce: true,
code_verifier: code_verifier,
skip_ssl_validation: Config.target_value(:skip_ssl_validation),
ssl_ca_file: Config.target_value(:ca_cert) })
rescue Exception => e
complain e
end
define_option :client, "--client ", "-c"
define_option :scope, "--scope "
desc "token get [credentials...]",
"Gets a token by posting user credentials with an implicit grant request",
:client, :scope do |*args|
client_name = opts[:client] || "cf"
reply = issuer_request(client_name, "") { |ti|
prompts = ti.prompts
creds = {}
prompts.each do |k, v|
if arg = args.shift
creds[k] = arg
elsif v[0] == "text"
creds[k] = ask(v[1])
elsif v[0] == "password"
creds[k] = ask_pwd v[1]
else
raise "Unknown prompt type \"#{v[0]}\" received from #{Context.target}"
end
end
ti.implicit_grant_with_creds(creds, opts[:scope]).info
}
say_success "implicit (with posted credentials)" if set_context(reply)
end
define_option :secret, "--secret ", "-s", "client secret"
desc "token client get [id]",
"Gets a token with client credentials grant", :secret, :scope do |id|
reply = issuer_request(clientid(id), clientsecret) { |ti|
ti.client_credentials_grant(opts[:scope]).info
}
say_success "client credentials" if set_context(reply)
end
define_option :password, "-p", "--password ", "user password"
desc "token owner get [client] [user]", "Gets a token with a resource owner password grant",
:secret, :password, :scope do |client, user|
reply = issuer_request(clientid(client), clientsecret) { |ti|
ti.owner_password_grant(user = username(user), userpwd, opts[:scope]).info
}
say_success "owner password" if set_context(reply)
end
define_option :passcode, "--passcode "
desc "token sso get [client]", "Gets a token based on a one time passcode after successful SSO via browser",
:secret,:passcode,:scope do |client|
reply = issuer_request(clientid(client), clientsecret) { |ti|
ti.passcode_grant(passcode, opts[:scope]).info
}
say_success "owner passcode" if set_context(reply)
end
desc "token refresh [refreshtoken]", "Gets a new access token from a refresh token", :client, :secret, :scope do |rtok|
rtok ||= Config.value(:refresh_token)
reply = issuer_request(clientid, clientsecret) { |ti| ti.refresh_token_grant(rtok, opts[:scope]).info }
say_success "refresh" if set_context(reply)
end
CF_TOKEN_FILE = File.join ENV["HOME"], ".cf_token"
CF_TARGET_FILE = File.join ENV["HOME"], ".cf_target"
def use_browser(client_id, secret = nil, grant = nil)
do_authcode = (not grant.nil?) && (grant == 'authcode')
code_verifier = SecureRandom.base64(96).tr("+/", "-_").tr("=", "")
catcher = Stub::Server.new(TokenCatcher,
logger: Util.default_logger(debug? ? :debug : trace? ? :trace : :info),
info: {client_id: client_id, client_secret: secret, code_verifier: code_verifier, do_authcode: do_authcode},
port: opts[:port]).run_on_thread
uri = issuer_request(client_id, secret, code_verifier) { |ti|
do_authcode ? ti.authcode_uri("#{catcher.url}/authcode", opts[:scope]) :
ti.implicit_uri("#{catcher.url}/callback", opts[:scope])
}
return unless catcher.info[:uri] = uri
say " and launching browser with #{uri}"
Launchy.open(uri, debug: false, dry_run: false)
print "waiting for token "
while catcher.info[:uri] || !catcher.info[:token_info]
sleep 5
print "."
end
say_success(do_authcode ? "authorization code" : "implicit") if set_context(catcher.info[:token_info])
return unless opts[:cf]
begin
cf_target = File.open(CF_TARGET_FILE, 'r') { |f| f.read.strip }
tok_json = File.open(CF_TOKEN_FILE, 'r') { |f| f.read } if File.exist?(CF_TOKEN_FILE)
cf_tokens = Util.json_parse(tok_json, :none) || {}
cf_tokens[cf_target] = auth_header
File.open(CF_TOKEN_FILE, 'w') { |f| f.write(cf_tokens.to_json) }
rescue Exception => e
gripe "\nUnable to save token to cf token file"
complain e
end
end
define_option :port, "--port ", "pin internal server to specific port"
define_option :cf, "--[no-]cf", "save token in the ~/.cf_tokens file"
desc "token authcode get", "Gets a token using the authcode flow with browser",
:client, :secret, :scope, :cf, :port do use_browser(clientid, opts[:secret], 'authcode') end
desc "token implicit get", "Gets a token using the implicit flow with browser",
:client, :scope, :cf, :port do use_browser opts[:client] || "cf" end
define_option :key, "--key ", "Token validation key"
desc "token decode [token] [tokentype]", "Show token contents as parsed locally or by the UAA. " +
"Decodes locally unless --client and --secret are given. Validates locally if --key given or server's signing key has been retrieved",
:key, :client, :secret do |token, ttype|
ttype = "bearer" if token && !ttype
token ||= Config.value(:access_token)
ttype ||= Config.value(:token_type)
return say "no token to decode" unless token && ttype
handle_request do
if opts[:client] && opts[:secret]
pp @cli_class.uaa_info_client.decode_token(opts[:client], opts[:secret], token, ttype)
else
seckey = opts[:key] || (Config.target_value(:signing_key) if Config.target_value(:signing_alg) !~ /rsa$/i)
pubkey = opts[:key] || (Config.target_value(:signing_key) if Config.target_value(:signing_alg) =~ /rsa$/i)
info = TokenCoder.decode(token, skey: seckey, pkey: pubkey, verify: !!(seckey || pubkey))
say seckey || pubkey ? "\nValid token signature\n\n": "\nNote: no key given to validate token signature\n\n"
pp info
end
end
end
define_option :all, "--[no-]all", "remove all contexts"
desc "token delete [contexts...]",
"Delete current or specified context tokens and settings", :all do |*args|
begin
return Config.delete if opts[:all]
return args.each { |arg| Config.delete(Config.target, arg.to_i.to_s == arg ? arg.to_i : arg) } unless args.empty?
return Config.delete(Config.target, Config.context) if Config.context
say "no target set, no contexts given -- nothing to delete"
rescue Exception => e
complain e
end
end
end
end