Skip to content

Commit 90e422b

Browse files
authored
feature: authorization_code grant with public client usage (#90)
* 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
1 parent 37ca0b5 commit 90e422b

3 files changed

Lines changed: 118 additions & 4 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env ruby
2+
3+
# Start a develop UAA with default profile or add client with allowpublic=true
4+
# uaac client add login -s loginsecret \
5+
# --authorized_grant_types authorization_code,refresh_token \
6+
# --scope "openid" \
7+
# --authorities uaa.none \
8+
# --allowpublic true \
9+
# --redirect_uri=http://localhost:7000/callback
10+
11+
require 'uaa'
12+
require 'cgi'
13+
14+
url = ENV["UAA_URL"] || 'http://localhost:8080/uaa'
15+
client = "login"
16+
secret = nil
17+
18+
def show(title, object)
19+
puts "#{title}: #{object.inspect}"
20+
puts
21+
end
22+
23+
uaa_options = { skip_ssl_validation: true, use_pkce:true, client_auth_method: 'none'}
24+
uaa_options[:ssl_ca_file] = ENV["UAA_CA_CERT_FILE"] if ENV["UAA_CA_CERT_FILE"]
25+
show "uaa_options", uaa_options
26+
27+
uaa_info = CF::UAA::Info.new(url, uaa_options)
28+
show "UAA server info", uaa_info.server
29+
30+
token_issuer = CF::UAA::TokenIssuer.new(url, client, secret, uaa_options)
31+
auth_uri = token_issuer.authcode_uri("http://localhost:7000/callback", nil)
32+
show "UAA authorization URL", auth_uri
33+
34+
puts "Enter Callback URL: "
35+
callback_url = gets
36+
show "Perform Token Request with: ", callback_url
37+
38+
token = token_issuer.authcode_grant(auth_uri, URI.parse(callback_url).query.to_s)
39+
show "User authorization grant", token
40+
41+
token_info = CF::UAA::TokenCoder.decode(token.info["access_token"], nil, nil, false) #token signature not verified
42+
show "Decoded access token", token_info

lib/uaa/token_issuer.rb

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#++
1313

1414
require 'securerandom'
15+
require "digest"
1516
require 'uaa/http'
1617
require 'cgi'
1718

@@ -53,6 +54,7 @@ class TokenIssuer
5354
include Http
5455

5556
private
57+
@client_auth_method = 'client_secret_basic'
5658

5759
def random_state; SecureRandom.hex end
5860

@@ -74,8 +76,15 @@ def request_token(params)
7476
params[:scope] = Util.strlist(scope)
7577
end
7678
headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8}
77-
if @basic_auth
78-
headers['authorization'] = Http.basic_auth(@client_id, @client_secret)
79+
if @client_auth_method == 'client_secret_basic' && @client_secret && @client_id
80+
if @basic_auth
81+
headers['authorization'] = Http.basic_auth(@client_id, @client_secret)
82+
else
83+
headers['X-CF-ENCODED-CREDENTIALS'] = 'true'
84+
headers['authorization'] = Http.basic_auth(CGI.escape(@client_id), CGI.escape(@client_secret))
85+
end
86+
elsif @client_id && params[:code_verifier]
87+
params[:client_id] = @client_id
7988
else
8089
headers['X-CF-ENCODED-CREDENTIALS'] = 'true'
8190
headers['authorization'] = Http.basic_auth(CGI.escape(@client_id || ''), CGI.escape(@client_secret || ''))
@@ -91,6 +100,10 @@ def authorize_path_args(response_type, redirect_uri, scope, state = random_state
91100
redirect_uri: redirect_uri, state: state)
92101
params[:scope] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
93102
params[:nonce] = state
103+
if not @code_verifier.nil?
104+
params[:code_challenge] = get_challenge
105+
params[:code_challenge_method] = 'S256'
106+
end
94107
"/oauth/authorize?#{Util.encode_form(params)}"
95108
end
96109

@@ -116,6 +129,11 @@ def initialize(target, client_id, client_secret = nil, options = {})
116129
@token_target = options[:token_target] || target
117130
@key_style = options[:symbolize_keys] ? :sym : nil
118131
@basic_auth = options[:basic_auth] == true ? true : false
132+
@client_auth_method = options[:client_auth_method] || 'client_secret_basic'
133+
@code_verifier = options[:code_verifier] || nil
134+
if @code_verifier.nil? && options[:use_pkce] && options[:use_pkce] == true
135+
@code_verifier = get_verifier
136+
end
119137
initialize_http_options(options)
120138
end
121139

@@ -235,8 +253,27 @@ def authcode_grant(authcode_uri, callback_query)
235253
rescue URI::InvalidURIError, ArgumentError, BadResponse
236254
raise BadResponse, "received invalid response from target #{@target}"
237255
end
238-
request_token(grant_type: 'authorization_code', code: authcode,
239-
redirect_uri: ac_params['redirect_uri'])
256+
if not @code_verifier.nil?
257+
request_token(grant_type: 'authorization_code', code: authcode,
258+
redirect_uri: ac_params['redirect_uri'], code_verifier: @code_verifier)
259+
else
260+
request_token(grant_type: 'authorization_code', code: authcode,
261+
redirect_uri: ac_params['redirect_uri'])
262+
end
263+
end
264+
265+
# Generates a random verifier for PKCE usage
266+
def get_verifier
267+
if not @code_verifier.nil?
268+
@verifier = @code_verifier
269+
else
270+
@verifier ||= SecureRandom.base64(96).tr("+/", "-_").tr("=", "")
271+
end
272+
end
273+
274+
# Calculates the challenge from code_verifier
275+
def get_challenge
276+
@challenge ||= Digest::SHA256.base64digest(get_verifier).tr("+/", "-_").tr("=", "")
240277
end
241278

242279
# Uses the instance client credentials in addition to the +username+

spec/token_issuer_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ module CF::UAA
264264
end
265265

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

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

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

304311
end
305312

313+
context 'pkce with own code verifier' do
314+
let(:options) { {basic_auth: false, code_verifier: 'umoq1e_4XMYXvfHlaO9mSlSI17OKfxnwfR5ZD-oYreFxyn8yQZ-ZHPZfUZ4n3WjY_tkOB_MAisSy4ddqsa6aoTU5ZOcX4ps3de933PczYlC8pZpKL8EQWaDZOnpOyB2W'} }
315+
316+
it 'calculate code_challenge on existing verifier' do
317+
redir_uri = 'http://call.back/uri_path'
318+
uri_parts = subject.authcode_uri(redir_uri, 'openid').split('?')
319+
code_challenge = subject.get_challenge
320+
code_verifier = subject.get_verifier
321+
params = Util.decode_form(uri_parts[1])
322+
params['code_challenge'].should == code_challenge
323+
params['code_challenge_method'].should == 'S256'
324+
code_verifier.should == options[:code_verifier]
325+
code_challenge.should == 'TAnM2AKGgiQKOC16cRpMdF_55qwmz3B333cq6T18z0s'
326+
end
327+
end
328+
329+
context 'no pkce active as this is the default' do
330+
#let(:options) { {use_pkce: false} }
331+
# by default PKCE is off
332+
it 'no code pkce generation with an authorization code' do
333+
redir_uri = 'http://call.back/uri_path'
334+
uri_parts = subject.authcode_uri(redir_uri, 'openid').split('?')
335+
params = Util.decode_form(uri_parts[1])
336+
params['code_challenge'].should_not
337+
params['code_challenge_method'].should_not
338+
end
339+
end
340+
306341
end
307342

308343
end

0 commit comments

Comments
 (0)