Skip to content

Commit

Permalink
feature: authorization_code grant with public client usage (#90)
Browse files Browse the repository at this point in the history
* feature: authoriation_code grant with public client usage

* add PKCE to authorization code
* optional allow to omit client_secret
* add client_auth_method to class to distinguish between basic and post - later private_key_jwt
* add example ruby script

* add default as it was before

* test update

* add extra option for pkce and set it to false

* review
removed client_secret_post for now
The methods itself are useful therefore add it later with extra PR

renamed the test

PKCE in cf-uaa-lib is active if
a) you provide a secret for the calculation
b) you set use_pkce=true in initialization of the lib

By default PKCE is off.

* less code, less logic.

Tests not touched
  • Loading branch information
strehle authored Aug 29, 2023
1 parent 37ca0b5 commit 90e422b
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 4 deletions.
42 changes: 42 additions & 0 deletions examples/authorization_grant_public_pkce.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env ruby

# Start a develop UAA with default profile or add client with allowpublic=true
# uaac client add login -s loginsecret \
# --authorized_grant_types authorization_code,refresh_token \
# --scope "openid" \
# --authorities uaa.none \
# --allowpublic true \
# --redirect_uri=http://localhost:7000/callback

require 'uaa'
require 'cgi'

url = ENV["UAA_URL"] || 'http://localhost:8080/uaa'
client = "login"
secret = nil

def show(title, object)
puts "#{title}: #{object.inspect}"
puts
end

uaa_options = { skip_ssl_validation: true, use_pkce:true, client_auth_method: 'none'}
uaa_options[:ssl_ca_file] = ENV["UAA_CA_CERT_FILE"] if ENV["UAA_CA_CERT_FILE"]
show "uaa_options", uaa_options

uaa_info = CF::UAA::Info.new(url, uaa_options)
show "UAA server info", uaa_info.server

token_issuer = CF::UAA::TokenIssuer.new(url, client, secret, uaa_options)
auth_uri = token_issuer.authcode_uri("http://localhost:7000/callback", nil)
show "UAA authorization URL", auth_uri

puts "Enter Callback URL: "
callback_url = gets
show "Perform Token Request with: ", callback_url

token = token_issuer.authcode_grant(auth_uri, URI.parse(callback_url).query.to_s)
show "User authorization grant", token

token_info = CF::UAA::TokenCoder.decode(token.info["access_token"], nil, nil, false) #token signature not verified
show "Decoded access token", token_info
45 changes: 41 additions & 4 deletions lib/uaa/token_issuer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#++

require 'securerandom'
require "digest"
require 'uaa/http'
require 'cgi'

Expand Down Expand Up @@ -53,6 +54,7 @@ class TokenIssuer
include Http

private
@client_auth_method = 'client_secret_basic'

def random_state; SecureRandom.hex end

Expand All @@ -74,8 +76,15 @@ def request_token(params)
params[:scope] = Util.strlist(scope)
end
headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8}
if @basic_auth
headers['authorization'] = Http.basic_auth(@client_id, @client_secret)
if @client_auth_method == 'client_secret_basic' && @client_secret && @client_id
if @basic_auth
headers['authorization'] = Http.basic_auth(@client_id, @client_secret)
else
headers['X-CF-ENCODED-CREDENTIALS'] = 'true'
headers['authorization'] = Http.basic_auth(CGI.escape(@client_id), CGI.escape(@client_secret))
end
elsif @client_id && params[:code_verifier]
params[:client_id] = @client_id
else
headers['X-CF-ENCODED-CREDENTIALS'] = 'true'
headers['authorization'] = Http.basic_auth(CGI.escape(@client_id || ''), CGI.escape(@client_secret || ''))
Expand All @@ -91,6 +100,10 @@ def authorize_path_args(response_type, redirect_uri, scope, state = random_state
redirect_uri: redirect_uri, state: state)
params[:scope] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
params[:nonce] = state
if not @code_verifier.nil?
params[:code_challenge] = get_challenge
params[:code_challenge_method] = 'S256'
end
"/oauth/authorize?#{Util.encode_form(params)}"
end

Expand All @@ -116,6 +129,11 @@ def initialize(target, client_id, client_secret = nil, options = {})
@token_target = options[:token_target] || target
@key_style = options[:symbolize_keys] ? :sym : nil
@basic_auth = options[:basic_auth] == true ? true : false
@client_auth_method = options[:client_auth_method] || 'client_secret_basic'
@code_verifier = options[:code_verifier] || nil
if @code_verifier.nil? && options[:use_pkce] && options[:use_pkce] == true
@code_verifier = get_verifier
end
initialize_http_options(options)
end

Expand Down Expand Up @@ -235,8 +253,27 @@ def authcode_grant(authcode_uri, callback_query)
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'])
if not @code_verifier.nil?
request_token(grant_type: 'authorization_code', code: authcode,
redirect_uri: ac_params['redirect_uri'], code_verifier: @code_verifier)
else
request_token(grant_type: 'authorization_code', code: authcode,
redirect_uri: ac_params['redirect_uri'])
end
end

# Generates a random verifier for PKCE usage
def get_verifier
if not @code_verifier.nil?
@verifier = @code_verifier
else
@verifier ||= SecureRandom.base64(96).tr("+/", "-_").tr("=", "")
end
end

# Calculates the challenge from code_verifier
def get_challenge
@challenge ||= Digest::SHA256.base64digest(get_verifier).tr("+/", "-_").tr("=", "")
end

# Uses the instance client credentials in addition to the +username+
Expand Down
35 changes: 35 additions & 0 deletions spec/token_issuer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ module CF::UAA
end

context 'with auth code grant' do
let(:options) { {use_pkce: true} }

it 'gets the authcode uri to be sent to the user agent for an authcode' do
redir_uri = 'http://call.back/uri_path'
Expand All @@ -275,6 +276,8 @@ module CF::UAA
params['scope'].should == 'openid'
params['redirect_uri'].should == redir_uri
params['state'].should_not be_nil
params['code_challenge'].should =~ /^[0-9A-Za-z_-]{43}$/i
params['code_challenge_method'].should == 'S256'
end

it 'gets an access token with an authorization code' do
Expand All @@ -292,6 +295,10 @@ module CF::UAA
cburi = 'http://call.back/uri_path'
redir_uri = subject.authcode_uri(cburi)
state = /state=([^&]+)/.match(redir_uri)[1]
challenge = /code_challenge=([^&]+)/.match(redir_uri)[1]
challenge.should =~ /^[0-9A-Za-z_-]{43}$/i
challenge_method = /code_challenge_method=([^&]+)/.match(redir_uri)[1]
challenge_method.should == 'S256'
reply_query = "state=#{state}&code=kz8%2F5gQZ2pc%3D"
token = subject.authcode_grant(redir_uri, reply_query)
token.should be_an_instance_of TokenInfo
Expand All @@ -303,6 +310,34 @@ module CF::UAA

end

context 'pkce with own code verifier' do
let(:options) { {basic_auth: false, code_verifier: 'umoq1e_4XMYXvfHlaO9mSlSI17OKfxnwfR5ZD-oYreFxyn8yQZ-ZHPZfUZ4n3WjY_tkOB_MAisSy4ddqsa6aoTU5ZOcX4ps3de933PczYlC8pZpKL8EQWaDZOnpOyB2W'} }

it 'calculate code_challenge on existing verifier' do
redir_uri = 'http://call.back/uri_path'
uri_parts = subject.authcode_uri(redir_uri, 'openid').split('?')
code_challenge = subject.get_challenge
code_verifier = subject.get_verifier
params = Util.decode_form(uri_parts[1])
params['code_challenge'].should == code_challenge
params['code_challenge_method'].should == 'S256'
code_verifier.should == options[:code_verifier]
code_challenge.should == 'TAnM2AKGgiQKOC16cRpMdF_55qwmz3B333cq6T18z0s'
end
end

context 'no pkce active as this is the default' do
#let(:options) { {use_pkce: false} }
# by default PKCE is off
it 'no code pkce generation with an authorization code' do
redir_uri = 'http://call.back/uri_path'
uri_parts = subject.authcode_uri(redir_uri, 'openid').split('?')
params = Util.decode_form(uri_parts[1])
params['code_challenge'].should_not
params['code_challenge_method'].should_not
end
end

end

end

0 comments on commit 90e422b

Please sign in to comment.