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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b74dffd99890350b0ed57b02a6a79343e47967be405bf6fc48e0630f534e6d3
4
- data.tar.gz: 229fb7742d1503f18bb6a9097741c11b4de378e68e9ea66f198645031d92d381
3
+ metadata.gz: ec18b7af2736542f939dab136eb1f7d2b728fa394a6c845423eb1e5c1a9c5b59
4
+ data.tar.gz: 1aaa66c33d5070d78b22e6e8f6ee4d31940182bd23918e8b8e4ff5203c271e68
5
5
  SHA512:
6
- metadata.gz: 0d2b41fbb96a3fbf94aa5e27ea7f5f408bd7c23c7555736a01750970b5a52672ae9318376a7615141ab2a777def229d587a431f887be67247747bc230b18a2fd
7
- data.tar.gz: 5ccb01cb63301c8abf33a11a1fbb3bba8e3ccc40d200e8b7ae12106cb8a6320c4ba5be912858cbfa25dadde377e2b859aaad8c60dcd27673d046801b40be4564
6
+ metadata.gz: b4b3af6298e27f3941596509fc3cfcd90434d173d83ecc3d70094ca366ce550fdf9e78c54aaa29753d831cfb1cfae06cf077fb57c5bce8acd8109f4c2e35ae31
7
+ data.tar.gz: daaf82f350b34392b3cf2fef2748a9403ede4e311e8856e0ce6a4df48d1d70a294f506f15292e5f17762a1c327e46800a29acf48d5d13266f705d28f97be8535
@@ -11,7 +11,7 @@ jobs:
11
11
  packages: write
12
12
 
13
13
  steps:
14
- - uses: actions/checkout@v3
14
+ - uses: actions/checkout@v4
15
15
  - name: Set up Ruby 2.6
16
16
  uses: ruby/setup-ruby@v1
17
17
  with:
@@ -15,7 +15,7 @@ jobs:
15
15
  ruby-version: ['2.5', '2.7', '3.0', '3.1', '3.2']
16
16
 
17
17
  steps:
18
- - uses: actions/checkout@v3
18
+ - uses: actions/checkout@v4
19
19
  - name: Set up Ruby
20
20
  uses: ruby/setup-ruby@v1
21
21
  with:
@@ -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,
@@ -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 @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
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
- 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("=", "")
240
277
  end
241
278
 
242
279
  # Uses the instance client credentials in addition to the +username+
data/lib/uaa/version.rb CHANGED
@@ -14,6 +14,6 @@
14
14
  # Cloud Foundry namespace
15
15
  module CF
16
16
  module UAA
17
- VERSION = '4.0.2'
17
+ VERSION = '4.0.4'
18
18
  end
19
19
  end
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"
@@ -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.2
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-05-10 00:00:00.000000000 Z
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