cf-uaa-lib 4.0.2 → 4.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/gem-push.yml +1 -1
- data/.github/workflows/ruby.yml +1 -1
- data/examples/authorization_grant_public_pkce.rb +42 -0
- data/lib/uaa/scim.rb +22 -0
- data/lib/uaa/token_issuer.rb +41 -4
- data/lib/uaa/version.rb +1 -1
- data/spec/scim_spec.rb +24 -0
- data/spec/token_issuer_spec.rb +35 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec18b7af2736542f939dab136eb1f7d2b728fa394a6c845423eb1e5c1a9c5b59
|
4
|
+
data.tar.gz: 1aaa66c33d5070d78b22e6e8f6ee4d31940182bd23918e8b8e4ff5203c271e68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b4b3af6298e27f3941596509fc3cfcd90434d173d83ecc3d70094ca366ce550fdf9e78c54aaa29753d831cfb1cfae06cf077fb57c5bce8acd8109f4c2e35ae31
|
7
|
+
data.tar.gz: daaf82f350b34392b3cf2fef2748a9403ede4e311e8856e0ce6a4df48d1d70a294f506f15292e5f17762a1c327e46800a29acf48d5d13266f705d28f97be8535
|
data/.github/workflows/ruby.yml
CHANGED
@@ -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
|
data/lib/uaa/scim.rb
CHANGED
@@ -369,6 +369,28 @@ class Scim
|
|
369
369
|
"#{type_info(:client, :path)}/#{Addressable::URI.encode(client_id)}/secret", req, headers))
|
370
370
|
end
|
371
371
|
|
372
|
+
# Change client jwt trust configuration.
|
373
|
+
# * For a client to change its jwt client trust, the token in @auth_header must contain
|
374
|
+
# "client.trust" scope.
|
375
|
+
# * For an admin to set a client secret, the token in @auth_header must contain
|
376
|
+
# "uaa.admin" scope.
|
377
|
+
# @see https://docs.cloudfoundry.org/api/uaa/index.html#change-client-jwt
|
378
|
+
# @param [String] client_id the {Scim} +id+ attribute of the client
|
379
|
+
# @param [String] jwks_uri the URI to token endpoint
|
380
|
+
# @param [String] jwks the JSON Web Key Set
|
381
|
+
# @param [String] kid If changeMode is DELETE provide the id of key
|
382
|
+
# @param [String] changeMode Change mode, possible is ADD, UPDATE, DELETE
|
383
|
+
# @return [Hash] success message from server
|
384
|
+
def change_clientjwt(client_id, jwks_uri = nil, jwks = nil, kid = nil, changeMode = nil)
|
385
|
+
req = {"client_id" => client_id }
|
386
|
+
req["jwks_uri"] = jwks_uri if jwks_uri
|
387
|
+
req["jwks"] = jwks if jwks
|
388
|
+
req["kid"] = kid if kid
|
389
|
+
req["changeMode"] = changeMode if changeMode
|
390
|
+
json_parse_reply(@key_style, *json_put(@target,
|
391
|
+
"#{type_info(:client, :path)}/#{Addressable::URI.encode(client_id)}/clientjwt", req, headers))
|
392
|
+
end
|
393
|
+
|
372
394
|
def unlock_user(user_id)
|
373
395
|
req = {"locked" => false}
|
374
396
|
json_parse_reply(@key_style, *json_patch(@target,
|
data/lib/uaa/token_issuer.rb
CHANGED
@@ -12,6 +12,7 @@
|
|
12
12
|
#++
|
13
13
|
|
14
14
|
require 'securerandom'
|
15
|
+
require "digest"
|
15
16
|
require 'uaa/http'
|
16
17
|
require 'cgi'
|
17
18
|
|
@@ -53,6 +54,7 @@ class TokenIssuer
|
|
53
54
|
include Http
|
54
55
|
|
55
56
|
private
|
57
|
+
@client_auth_method = 'client_secret_basic'
|
56
58
|
|
57
59
|
def random_state; SecureRandom.hex end
|
58
60
|
|
@@ -74,8 +76,15 @@ class TokenIssuer
|
|
74
76
|
params[:scope] = Util.strlist(scope)
|
75
77
|
end
|
76
78
|
headers = {'content-type' => FORM_UTF8, 'accept' => JSON_UTF8}
|
77
|
-
if @
|
78
|
-
|
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
|
79
88
|
else
|
80
89
|
headers['X-CF-ENCODED-CREDENTIALS'] = 'true'
|
81
90
|
headers['authorization'] = Http.basic_auth(CGI.escape(@client_id || ''), CGI.escape(@client_secret || ''))
|
@@ -91,6 +100,10 @@ class TokenIssuer
|
|
91
100
|
redirect_uri: redirect_uri, state: state)
|
92
101
|
params[:scope] = scope = Util.strlist(scope) if scope = Util.arglist(scope)
|
93
102
|
params[:nonce] = state
|
103
|
+
if not @code_verifier.nil?
|
104
|
+
params[:code_challenge] = get_challenge
|
105
|
+
params[:code_challenge_method] = 'S256'
|
106
|
+
end
|
94
107
|
"/oauth/authorize?#{Util.encode_form(params)}"
|
95
108
|
end
|
96
109
|
|
@@ -116,6 +129,11 @@ class TokenIssuer
|
|
116
129
|
@token_target = options[:token_target] || target
|
117
130
|
@key_style = options[:symbolize_keys] ? :sym : nil
|
118
131
|
@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
|
119
137
|
initialize_http_options(options)
|
120
138
|
end
|
121
139
|
|
@@ -235,8 +253,27 @@ class TokenIssuer
|
|
235
253
|
rescue URI::InvalidURIError, ArgumentError, BadResponse
|
236
254
|
raise BadResponse, "received invalid response from target #{@target}"
|
237
255
|
end
|
238
|
-
|
239
|
-
|
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("=", "")
|
240
277
|
end
|
241
278
|
|
242
279
|
# Uses the instance client credentials in addition to the +username+
|
data/lib/uaa/version.rb
CHANGED
data/spec/scim_spec.rb
CHANGED
@@ -160,6 +160,30 @@ describe Scim do
|
|
160
160
|
result['id'].should == 'id12345'
|
161
161
|
end
|
162
162
|
|
163
|
+
it "add a client's jwt trust using jwks_uri" do
|
164
|
+
subject.set_request_handler do |url, method, body, headers|
|
165
|
+
url.should == "#{@target}/oauth/clients/id12345/clientjwt"
|
166
|
+
method.should == :put
|
167
|
+
check_headers(headers, :json, :json, nil)
|
168
|
+
body.should include('"jwks_uri":"http://localhost:8080/uaa/token_keys"')
|
169
|
+
[200, '{"id":"id12345"}', {'content-type' => 'application/json'}]
|
170
|
+
end
|
171
|
+
result = subject.change_clientjwt('id12345', 'http://localhost:8080/uaa/token_keys')
|
172
|
+
result['id'].should == 'id12345'
|
173
|
+
end
|
174
|
+
|
175
|
+
it "add a client's jwt trust using jwks" do
|
176
|
+
subject.set_request_handler do |url, method, body, headers|
|
177
|
+
url.should == "#{@target}/oauth/clients/id12345/clientjwt"
|
178
|
+
method.should == :put
|
179
|
+
check_headers(headers, :json, :json, nil)
|
180
|
+
body.should include('"jwks":"keys"')
|
181
|
+
[200, '{"id":"id12345"}', {'content-type' => 'application/json'}]
|
182
|
+
end
|
183
|
+
result = subject.change_clientjwt('id12345', nil, 'keys')
|
184
|
+
result['id'].should == 'id12345'
|
185
|
+
end
|
186
|
+
|
163
187
|
it 'unlocks a user' do
|
164
188
|
subject.set_request_handler do |url, method, body, headers|
|
165
189
|
url.should == "#{@target}/Users/id12345/status"
|
data/spec/token_issuer_spec.rb
CHANGED
@@ -264,6 +264,7 @@ describe TokenIssuer do
|
|
264
264
|
end
|
265
265
|
|
266
266
|
context 'with auth code grant' do
|
267
|
+
let(:options) { {use_pkce: true} }
|
267
268
|
|
268
269
|
it 'gets the authcode uri to be sent to the user agent for an authcode' do
|
269
270
|
redir_uri = 'http://call.back/uri_path'
|
@@ -275,6 +276,8 @@ describe TokenIssuer do
|
|
275
276
|
params['scope'].should == 'openid'
|
276
277
|
params['redirect_uri'].should == redir_uri
|
277
278
|
params['state'].should_not be_nil
|
279
|
+
params['code_challenge'].should =~ /^[0-9A-Za-z_-]{43}$/i
|
280
|
+
params['code_challenge_method'].should == 'S256'
|
278
281
|
end
|
279
282
|
|
280
283
|
it 'gets an access token with an authorization code' do
|
@@ -292,6 +295,10 @@ describe TokenIssuer do
|
|
292
295
|
cburi = 'http://call.back/uri_path'
|
293
296
|
redir_uri = subject.authcode_uri(cburi)
|
294
297
|
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'
|
295
302
|
reply_query = "state=#{state}&code=kz8%2F5gQZ2pc%3D"
|
296
303
|
token = subject.authcode_grant(redir_uri, reply_query)
|
297
304
|
token.should be_an_instance_of TokenInfo
|
@@ -303,6 +310,34 @@ describe TokenIssuer do
|
|
303
310
|
|
304
311
|
end
|
305
312
|
|
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
|
+
|
306
341
|
end
|
307
342
|
|
308
343
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cf-uaa-lib
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.0.
|
4
|
+
version: 4.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dave Syer
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date: 2023-
|
15
|
+
date: 2023-10-17 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: multi_json
|
@@ -237,6 +237,7 @@ files:
|
|
237
237
|
- README.md
|
238
238
|
- Rakefile
|
239
239
|
- cf-uaa-lib.gemspec
|
240
|
+
- examples/authorization_grant_public_pkce.rb
|
240
241
|
- examples/password_grant_and_decode_token.rb
|
241
242
|
- lib/uaa.rb
|
242
243
|
- lib/uaa/http.rb
|