descope 1.0.6 → 1.1.0

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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +51 -12
  3. data/.github/workflows/publish-gem.yaml +6 -26
  4. data/.github/workflows/release-please.yaml +36 -0
  5. data/.gitignore +5 -2
  6. data/.release-please-manifest.json +1 -1
  7. data/.ruby-version +1 -1
  8. data/CHANGELOG.md +21 -0
  9. data/Gemfile +8 -7
  10. data/Gemfile.lock +70 -56
  11. data/README.md +170 -51
  12. data/examples/ruby-on-rails-api/descope/Gemfile +8 -8
  13. data/examples/ruby-on-rails-api/descope/Gemfile.lock +1 -1
  14. data/examples/ruby-on-rails-api/descope/package-lock.json +203 -141
  15. data/examples/ruby-on-rails-api/descope/package.json +1 -1
  16. data/examples/ruby-on-rails-api/descope/yarn.lock +185 -87
  17. data/lib/descope/api/v1/auth/enchantedlink.rb +3 -1
  18. data/lib/descope/api/v1/auth/magiclink.rb +3 -1
  19. data/lib/descope/api/v1/auth/otp.rb +3 -1
  20. data/lib/descope/api/v1/auth/password.rb +6 -2
  21. data/lib/descope/api/v1/auth/totp.rb +3 -1
  22. data/lib/descope/api/v1/auth.rb +47 -12
  23. data/lib/descope/api/v1/management/common.rb +20 -5
  24. data/lib/descope/api/v1/management/sso_application.rb +236 -0
  25. data/lib/descope/api/v1/management/sso_settings.rb +2 -24
  26. data/lib/descope/api/v1/management/user.rb +151 -13
  27. data/lib/descope/api/v1/management.rb +2 -0
  28. data/lib/descope/api/v1/session.rb +37 -4
  29. data/lib/descope/mixins/common.rb +1 -0
  30. data/lib/descope/mixins/http.rb +60 -9
  31. data/lib/descope/mixins/initializer.rb +5 -2
  32. data/lib/descope/mixins/logging.rb +12 -4
  33. data/lib/descope/version.rb +1 -1
  34. data/spec/descope/api/v1/auth_spec.rb +29 -0
  35. data/spec/descope/api/v1/auth_token_extraction_spec.rb +126 -0
  36. data/spec/descope/api/v1/session_refresh_spec.rb +98 -0
  37. data/spec/factories/user.rb +1 -1
  38. data/spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb +20 -22
  39. data/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb +6 -2
  40. data/spec/integration/lib.descope/api/v1/auth/otp_spec.rb +6 -2
  41. data/spec/integration/lib.descope/api/v1/auth/session_spec.rb +68 -0
  42. data/spec/integration/lib.descope/api/v1/auth/totp_spec.rb +6 -2
  43. data/spec/integration/lib.descope/api/v1/management/access_key_spec.rb +12 -1
  44. data/spec/integration/lib.descope/api/v1/management/audit_spec.rb +5 -3
  45. data/spec/integration/lib.descope/api/v1/management/authz_spec.rb +28 -5
  46. data/spec/integration/lib.descope/api/v1/management/flow_spec.rb +3 -1
  47. data/spec/integration/lib.descope/api/v1/management/permissions_spec.rb +22 -2
  48. data/spec/integration/lib.descope/api/v1/management/project_spec.rb +18 -2
  49. data/spec/integration/lib.descope/api/v1/management/roles_spec.rb +116 -36
  50. data/spec/integration/lib.descope/api/v1/management/user_spec.rb +74 -8
  51. data/spec/lib.descope/api/v1/auth/enchantedlink_spec.rb +11 -2
  52. data/spec/lib.descope/api/v1/auth/password_spec.rb +10 -1
  53. data/spec/lib.descope/api/v1/auth_spec.rb +167 -5
  54. data/spec/lib.descope/api/v1/cookie_domain_fix_integration_spec.rb +245 -0
  55. data/spec/lib.descope/api/v1/management/sso_application_spec.rb +217 -0
  56. data/spec/lib.descope/api/v1/management/sso_settings_spec.rb +2 -2
  57. data/spec/lib.descope/api/v1/management/user_spec.rb +134 -46
  58. data/spec/lib.descope/api/v1/session_spec.rb +119 -6
  59. data/spec/lib.descope/mixins/http_spec.rb +229 -0
  60. data/spec/support/client_config.rb +0 -1
  61. data/spec/support/utils.rb +21 -0
  62. metadata +14 -8
@@ -249,7 +249,13 @@ describe Descope::Api::V1::Auth do
249
249
  end
250
250
 
251
251
  it 'is expected to select tenant' do
252
- jwt_response = { 'fake': 'response' }
252
+ jwt_response = {
253
+ 'sessionJwt' => 'fake_session_jwt',
254
+ 'refreshJwt' => 'fake_refresh_jwt',
255
+ 'cookies' => {
256
+ 'refresh_token' => 'fake_refresh_cookie'
257
+ }
258
+ }
253
259
 
254
260
  expect(@instance).to receive(:post).with(
255
261
  SELECT_TENANT_PATH, { tenantId: 'tenant123' }, {}, 'refresh-token'
@@ -390,20 +396,26 @@ describe Descope::Api::V1::Auth do
390
396
  end
391
397
 
392
398
  it 'is expected to successfully exchange access key without login_options' do
393
- jwt_response = { 'fake': 'response' }
399
+ jwt_response = {
400
+ 'sessionJwt' => 'fake_session_jwt',
401
+ 'refreshJwt' => 'fake_refresh_jwt'
402
+ }
394
403
  access_key = 'abc'
395
404
 
396
405
  expect(@instance).to receive(:post).with(
397
406
  EXCHANGE_AUTH_ACCESS_KEY_PATH, { loginOptions: {}, audience: 'IT' }, {}, access_key
398
407
  ).and_return(jwt_response)
399
408
 
400
- allow(@instance).to receive(:generate_jwt_response).and_return(jwt_response)
409
+ allow(@instance).to receive(:generate_auth_info).and_return(jwt_response)
401
410
 
402
411
  expect { @instance.exchange_access_key(access_key:, audience: 'IT') }.not_to raise_error
403
412
  end
404
413
 
405
414
  it 'is expected to successfully exchange access key with login_options' do
406
- jwt_response = { 'fake': 'response' }
415
+ jwt_response = {
416
+ 'sessionJwt' => 'fake_session_jwt',
417
+ 'refreshJwt' => 'fake_refresh_jwt'
418
+ }
407
419
  access_key = 'abc'
408
420
 
409
421
  expect(@instance).to receive(:post).with(
@@ -413,9 +425,159 @@ describe Descope::Api::V1::Auth do
413
425
  access_key
414
426
  ).and_return(jwt_response)
415
427
 
416
- allow(@instance).to receive(:generate_jwt_response).and_return(jwt_response)
428
+ allow(@instance).to receive(:generate_auth_info).and_return(jwt_response)
417
429
 
418
430
  expect { @instance.exchange_access_key(access_key:, login_options: { customClaims: { k1: 'v1' } }, audience: 'IT') }.not_to raise_error
419
431
  end
420
432
  end
433
+
434
+ describe '#generate_auth_info cookie handling enhancements' do
435
+ let(:audience) { nil }
436
+ let(:session_jwt) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS5kZXNjb3BlLmNvbSJ9.session_sig' }
437
+ let(:refresh_jwt) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS5kZXNjb3BlLmNvbSJ9.refresh_sig' }
438
+
439
+ let(:mock_token_validation) do
440
+ {
441
+ 'iss' => 'https://api.descope.com/P2abcde12345',
442
+ 'sub' => 'U2abcde12345',
443
+ 'permissions' => ['read', 'write'],
444
+ 'roles' => ['admin'],
445
+ 'tenants' => { 'tenant1' => { 'permissions' => ['read'] } }
446
+ }
447
+ end
448
+
449
+ before do
450
+ allow(@instance).to receive(:validate_token).and_return(mock_token_validation)
451
+ end
452
+
453
+ context 'when session token is in cookies (custom domain scenario)' do
454
+ let(:response_body) do
455
+ {
456
+ 'userId' => 'test123',
457
+ 'cookieExpiration' => 1640704758,
458
+ 'cookieDomain' => 'dev.lulukuku.com',
459
+ 'cookies' => {
460
+ 'DS' => session_jwt, # Session token in cookies
461
+ 'DSR' => refresh_jwt # Refresh token in cookies
462
+ }
463
+ }
464
+ end
465
+
466
+ it 'extracts session token from cookies when not in response body' do
467
+ result = @instance.send(:generate_auth_info, response_body, nil, true, audience)
468
+
469
+ expect(result['sessionToken']).to eq(mock_token_validation)
470
+ expect(result['refreshSessionToken']).to eq(mock_token_validation)
471
+ end
472
+
473
+ it 'validates session token from cookies' do
474
+ expect(@instance).to receive(:validate_token).with(session_jwt, audience).and_return(mock_token_validation)
475
+ expect(@instance).to receive(:validate_token).with(refresh_jwt, audience).and_return(mock_token_validation)
476
+
477
+ @instance.send(:generate_auth_info, response_body, nil, true, audience)
478
+ end
479
+
480
+ it 'includes permissions and roles from cookie tokens' do
481
+ result = @instance.send(:generate_auth_info, response_body, nil, true, audience)
482
+
483
+ expect(result['permissions']).to eq(['read', 'write'])
484
+ expect(result['roles']).to eq(['admin'])
485
+ expect(result['tenants']).to eq({ 'tenant1' => { 'permissions' => ['read'] } })
486
+ end
487
+ end
488
+
489
+ context 'when session token is in response body and refresh token in cookies' do
490
+ let(:response_body) do
491
+ {
492
+ 'sessionJwt' => session_jwt, # Session token in response body
493
+ 'userId' => 'test123',
494
+ 'cookies' => {
495
+ 'DSR' => refresh_jwt # Only refresh token in cookies
496
+ }
497
+ }
498
+ end
499
+
500
+ it 'uses session token from response body and refresh token from cookies' do
501
+ expect(@instance).to receive(:validate_token).with(session_jwt, audience).and_return(mock_token_validation)
502
+ expect(@instance).to receive(:validate_token).with(refresh_jwt, audience).and_return(mock_token_validation)
503
+
504
+ result = @instance.send(:generate_auth_info, response_body, nil, true, audience)
505
+
506
+ expect(result['sessionToken']).to eq(mock_token_validation)
507
+ expect(result['refreshSessionToken']).to eq(mock_token_validation)
508
+ end
509
+ end
510
+
511
+ context 'when refresh token is passed as parameter' do
512
+ let(:response_body) do
513
+ {
514
+ 'userId' => 'test123',
515
+ 'cookies' => {
516
+ 'DS' => session_jwt # Only session token in cookies
517
+ }
518
+ }
519
+ end
520
+
521
+ it 'uses passed refresh token when not in response body or cookies' do
522
+ expect(@instance).to receive(:validate_token).with(session_jwt, audience).and_return(mock_token_validation)
523
+ expect(@instance).to receive(:validate_token).with(refresh_jwt, audience).and_return(mock_token_validation)
524
+
525
+ result = @instance.send(:generate_auth_info, response_body, refresh_jwt, true, audience)
526
+
527
+ expect(result['sessionToken']).to eq(mock_token_validation)
528
+ expect(result['refreshSessionToken']).to eq(mock_token_validation)
529
+ end
530
+ end
531
+
532
+ context 'error handling for missing tokens' do
533
+ let(:response_body) do
534
+ {
535
+ 'userId' => 'test123',
536
+ 'cookieExpiration' => 1640704758,
537
+ 'cookies' => {} # No tokens anywhere
538
+ }
539
+ end
540
+
541
+ it 'raises helpful error when no refresh token is found' do
542
+ expect {
543
+ @instance.send(:generate_auth_info, response_body, nil, true, audience)
544
+ }.to raise_error(Descope::AuthException, /Could not find refreshJwt in response body \/ cookies \/ passed in refresh_token/)
545
+ end
546
+ end
547
+
548
+ context 'backward compatibility' do
549
+ let(:traditional_response_body) do
550
+ {
551
+ 'sessionJwt' => session_jwt,
552
+ 'refreshJwt' => refresh_jwt,
553
+ 'userId' => 'test123'
554
+ }
555
+ end
556
+
557
+ it 'continues to work with traditional response body tokens' do
558
+ expect(@instance).to receive(:validate_token).with(session_jwt, audience).and_return(mock_token_validation)
559
+ expect(@instance).to receive(:validate_token).with(refresh_jwt, audience).and_return(mock_token_validation)
560
+
561
+ result = @instance.send(:generate_auth_info, traditional_response_body, nil, true, audience)
562
+
563
+ expect(result['sessionToken']).to eq(mock_token_validation)
564
+ expect(result['refreshSessionToken']).to eq(mock_token_validation)
565
+ end
566
+
567
+ it 'works with same-domain cookies (existing RestClient behavior)' do
568
+ response_with_restclient_cookies = {
569
+ 'userId' => 'test123',
570
+ 'cookies' => {
571
+ 'DSR' => refresh_jwt
572
+ }
573
+ }
574
+
575
+ expect(@instance).to receive(:validate_token).with(refresh_jwt, audience).and_return(mock_token_validation)
576
+
577
+ result = @instance.send(:generate_auth_info, response_with_restclient_cookies, nil, false, audience)
578
+
579
+ expect(result['refreshSessionToken']).to eq(mock_token_validation)
580
+ end
581
+ end
582
+ end
421
583
  end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Cookie Domain Fix Integration' do
6
+ before(:all) do
7
+ dummy_instance = DummyClass.new
8
+ dummy_instance.extend(Descope::Api::V1::Session)
9
+ dummy_instance.extend(Descope::Api::V1::Auth)
10
+ dummy_instance.extend(Descope::Mixins::HTTP)
11
+ dummy_instance.extend(Descope::Mixins::Common::EndpointsV1)
12
+ @instance = dummy_instance
13
+ end
14
+
15
+ describe 'refresh_session with custom domain cookies' do
16
+ let(:refresh_token) { 'test_refresh_token' }
17
+ let(:audience) { nil }
18
+
19
+ let(:session_jwt) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS5kZXNjb3BlLmNvbS9QMmFiY2RlMTIzNDUiLCJzdWIiOiJVMmFiY2RlMTIzNDUifQ.session_signature' }
20
+ let(:refresh_jwt) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS5kZXNjb3BlLmNvbS9QMmFiY2RlMTIzNDUiLCJzdWIiOiJVMmFiY2RlMTIzNDUifQ.refresh_signature' }
21
+
22
+ context 'when Descope is configured for cookie-only tokens with custom domain' do
23
+ let(:api_response_body) do
24
+ # Response body without sessionJwt/refreshJwt (cookie-only configuration)
25
+ {
26
+ 'userId' => 'test123',
27
+ 'cookieExpiration' => 1640704758,
28
+ 'cookieDomain' => 'dev.lulukuku.com',
29
+ 'cookiePath' => '/'
30
+ }
31
+ end
32
+
33
+ let(:set_cookie_headers) do
34
+ [
35
+ "DS=#{session_jwt}; Path=/; Domain=dev.lulukuku.com; HttpOnly; Secure; SameSite=None",
36
+ "DSR=#{refresh_jwt}; Path=/; Domain=dev.lulukuku.com; HttpOnly; Secure; SameSite=None; Max-Age=2592000"
37
+ ]
38
+ end
39
+
40
+ let(:mock_response) do
41
+ double('response').tap do |response|
42
+ allow(response).to receive(:code).and_return(200)
43
+ allow(response).to receive(:body).and_return(api_response_body.to_json)
44
+ allow(response).to receive(:cookies).and_return({}) # RestClient filters out custom domain cookies
45
+ allow(response).to receive(:headers).and_return({ 'set-cookie' => set_cookie_headers })
46
+ end
47
+ end
48
+
49
+ before do
50
+ allow(@instance).to receive(:validate_refresh_token_not_nil).and_return(true)
51
+ allow(@instance).to receive(:validate_token).and_return({
52
+ 'iss' => 'https://api.descope.com/P2abcde12345',
53
+ 'sub' => 'U2abcde12345',
54
+ 'permissions' => [],
55
+ 'roles' => [],
56
+ 'tenants' => {}
57
+ })
58
+ allow(@instance).to receive(:call).and_return(mock_response)
59
+ end
60
+
61
+ it 'successfully extracts tokens from Set-Cookie headers' do
62
+ result = @instance.refresh_session(refresh_token: refresh_token, audience: audience)
63
+ expect(result).to be_a(Hash)
64
+ expect(result['sessionToken']).to be_a(Hash)
65
+ expect(result['sessionToken']['iss']).to eq('https://api.descope.com/P2abcde12345')
66
+ expect(result['sessionToken']['sub']).to eq('U2abcde12345')
67
+
68
+ expect(result['refreshSessionToken']).to be_a(Hash)
69
+ expect(result['refreshSessionToken']['iss']).to eq('https://api.descope.com/P2abcde12345')
70
+ expect(result['refreshSessionToken']['sub']).to eq('U2abcde12345')
71
+
72
+ expect(result['cookieData'][:domain]).to eq('dev.lulukuku.com')
73
+ end
74
+
75
+ it 'validates the extracted session token' do
76
+ expect(@instance).to receive(:validate_token).with(session_jwt, audience).and_return({
77
+ 'iss' => 'https://api.descope.com/P2abcde12345',
78
+ 'sub' => 'U2abcde12345'
79
+ })
80
+
81
+ @instance.refresh_session(refresh_token: refresh_token, audience: audience)
82
+ end
83
+
84
+ it 'validates the extracted refresh token' do
85
+ expect(@instance).to receive(:validate_token).with(refresh_jwt, audience).and_return({
86
+ 'iss' => 'https://api.descope.com/P2abcde12345',
87
+ 'sub' => 'U2abcde12345'
88
+ })
89
+
90
+ @instance.refresh_session(refresh_token: refresh_token, audience: audience)
91
+ end
92
+
93
+ it 'includes cookie metadata in response' do
94
+ result = @instance.refresh_session(refresh_token: refresh_token, audience: audience)
95
+ expect(result['cookieData'][:domain]).to eq('dev.lulukuku.com')
96
+ end
97
+ end
98
+
99
+ context 'when only refresh token is in cookies (partial custom domain)' do
100
+ let(:api_response_body) do
101
+ {
102
+ 'sessionJwt' => session_jwt, # Session token in response body
103
+ 'userId' => 'test123',
104
+ 'cookieExpiration' => 1640704758,
105
+ 'cookieDomain' => 'dev.lulukuku.com'
106
+ }
107
+ end
108
+
109
+ let(:set_cookie_headers) do
110
+ [
111
+ "DSR=#{refresh_jwt}; Path=/; Domain=dev.lulukuku.com; HttpOnly; Secure; Max-Age=2592000"
112
+ ]
113
+ end
114
+
115
+ let(:mock_response) do
116
+ double('response').tap do |response|
117
+ allow(response).to receive(:code).and_return(200)
118
+ allow(response).to receive(:body).and_return(api_response_body.to_json)
119
+ allow(response).to receive(:cookies).and_return({})
120
+ allow(response).to receive(:headers).and_return({ 'set-cookie' => set_cookie_headers })
121
+ end
122
+ end
123
+
124
+ before do
125
+ allow(@instance).to receive(:validate_refresh_token_not_nil).and_return(true)
126
+ allow(@instance).to receive(:validate_token).and_return({
127
+ 'iss' => 'https://api.descope.com/P2abcde12345',
128
+ 'sub' => 'U2abcde12345',
129
+ 'permissions' => [],
130
+ 'roles' => [],
131
+ 'tenants' => {}
132
+ })
133
+ allow(@instance).to receive(:call).and_return(mock_response)
134
+ end
135
+
136
+ it 'handles mixed token sources (response body + custom domain cookies)' do
137
+ result = @instance.refresh_session(refresh_token: refresh_token, audience: audience)
138
+
139
+ expect(result).to be_a(Hash)
140
+ expect(result['sessionToken']).to_not be_nil
141
+ expect(result['refreshSessionToken']).to_not be_nil
142
+ end
143
+ end
144
+
145
+ context 'error handling for custom domain configurations' do
146
+ let(:api_response_body) do
147
+ {
148
+ 'userId' => 'test123',
149
+ 'cookieExpiration' => 1640704758,
150
+ 'cookieDomain' => 'dev.lulukuku.com'
151
+ }
152
+ end
153
+
154
+ let(:mock_response_no_cookies) do
155
+ double('response').tap do |response|
156
+ allow(response).to receive(:code).and_return(200)
157
+ allow(response).to receive(:body).and_return(api_response_body.to_json)
158
+ allow(response).to receive(:cookies).and_return({})
159
+ allow(response).to receive(:headers).and_return({}) # No Set-Cookie headers
160
+ end
161
+ end
162
+
163
+ before do
164
+ allow(@instance).to receive(:validate_refresh_token_not_nil).and_return(true)
165
+ allow(@instance).to receive(:validate_token).and_return({
166
+ 'iss' => 'https://api.descope.com/P2abcde12345',
167
+ 'sub' => 'U2abcde12345'
168
+ })
169
+ allow(@instance).to receive(:call).and_return(mock_response_no_cookies)
170
+ end
171
+
172
+ it 'provides helpful error message when no tokens are found' do
173
+ result = @instance.refresh_session(refresh_token: refresh_token, audience: audience)
174
+ expect(result).to be_a(Hash)
175
+ expect(result['sessionToken']).to be_nil
176
+ expect(result['refreshSessionToken']).to_not be_nil
177
+ end
178
+ end
179
+ end
180
+
181
+ describe 'validate_and_refresh_session with custom domain cookies' do
182
+ let(:session_token) { 'expired_session_token' }
183
+ let(:refresh_token) { 'valid_refresh_token' }
184
+ let(:audience) { nil }
185
+
186
+ context 'when session is expired and refresh uses custom domain cookies' do
187
+ let(:refresh_jwt) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2FwaS5kZXNjb3BlLmNvbSJ9.signature' }
188
+
189
+ let(:api_response_body) do
190
+ {
191
+ 'userId' => 'test123',
192
+ 'cookieExpiration' => 1640704758,
193
+ 'cookieDomain' => 'dev.lulukuku.com'
194
+ }
195
+ end
196
+
197
+ let(:set_cookie_headers) do
198
+ [
199
+ "DS=new_session_jwt; Path=/; Domain=dev.lulukuku.com; HttpOnly; Secure",
200
+ "DSR=#{refresh_jwt}; Path=/; Domain=dev.lulukuku.com; HttpOnly; Secure; Max-Age=2592000"
201
+ ]
202
+ end
203
+
204
+ let(:mock_response) do
205
+ double('response').tap do |response|
206
+ allow(response).to receive(:code).and_return(200)
207
+ allow(response).to receive(:body).and_return(api_response_body.to_json)
208
+ allow(response).to receive(:cookies).and_return({})
209
+ allow(response).to receive(:headers).and_return({ 'set-cookie' => set_cookie_headers })
210
+ end
211
+ end
212
+
213
+ before do
214
+ # Mock session validation to fail (expired token)
215
+ allow(@instance).to receive(:validate_session).and_raise(Descope::AuthException.new('Token expired'))
216
+
217
+ # Mock refresh_session to work with custom domain cookies
218
+ allow(@instance).to receive(:validate_refresh_token_not_nil).and_return(true)
219
+ allow(@instance).to receive(:validate_token).and_return({
220
+ 'iss' => 'https://api.descope.com/P2abcde12345',
221
+ 'sub' => 'U2abcde12345',
222
+ 'permissions' => [],
223
+ 'roles' => [],
224
+ 'tenants' => {}
225
+ })
226
+ allow(@instance).to receive(:call).and_return(mock_response)
227
+ end
228
+
229
+ it 'falls back to refresh_session when validate_session fails' do
230
+ expect(@instance).to receive(:refresh_session).with(
231
+ refresh_token: refresh_token,
232
+ audience: audience
233
+ ).and_call_original
234
+
235
+ result = @instance.validate_and_refresh_session(
236
+ session_token: session_token,
237
+ refresh_token: refresh_token,
238
+ audience: audience
239
+ )
240
+
241
+ expect(result).to be_a(Hash)
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Descope::Api::V1::Management::SSOApplication do
6
+ before(:all) do
7
+ dummy_instance = DummyClass.new
8
+ dummy_instance.extend(Descope::Api::V1::Management::SSOApplication)
9
+ @instance = dummy_instance
10
+ end
11
+
12
+ context('.create_sso_oidc_application') do
13
+ it 'should respond to .create_saml_application' do
14
+ expect(@instance).to respond_to :create_saml_application
15
+ end
16
+
17
+ it 'is expected to create SAML application' do
18
+ expect(@instance).to receive(:post).with(
19
+ SSO_APPLICATION_OIDC_CREATE_PATH, {
20
+ id: 'tenant1',
21
+ name: 'test',
22
+ description: 'awesome tenant',
23
+ enabled: true,
24
+ logo: 'https://logo.com',
25
+ loginPageUrl: 'https://dummy.com/login'
26
+ }
27
+ )
28
+ expect do
29
+ @instance.create_sso_oidc_app(
30
+ id: 'tenant1',
31
+ name: 'test',
32
+ description: 'awesome tenant',
33
+ enabled: true,
34
+ logo: 'https://logo.com',
35
+ login_page_url: 'https://dummy.com/login'
36
+ )
37
+ end.not_to raise_error
38
+ end
39
+ end
40
+
41
+ context('.create_saml_application') do
42
+ it 'should respond to .create_saml_application' do
43
+ expect(@instance).to respond_to :create_saml_application
44
+ end
45
+
46
+ it 'is expected to create SAML application' do
47
+ expect(@instance).to receive(:post).with(
48
+ SSO_APPLICATION_SAML_CREATE_PATH, {
49
+ name: 'test',
50
+ description: 'awesome tenant',
51
+ id: 'tenant1',
52
+ loginPageUrl: 'https://dummy.com/login',
53
+ logo: 'https://logo.com',
54
+ enabled: true,
55
+ useMetadataInfo: true,
56
+ metadataUrl: 'https://dummy.com/metadata',
57
+ entityId: 'ent1234',
58
+ acsUrl: 'https://dummy.com/acs',
59
+ certificate: 'something',
60
+ attributeMapping: [
61
+ {
62
+ 'abc': '123'
63
+ }
64
+ ],
65
+ groupsMapping: [
66
+ {
67
+ 'abc': '123'
68
+ }
69
+ ],
70
+ acsAllowedCallbacks: true,
71
+ subjectNameIdType: 'test',
72
+ subjectNameIdFormat: 'test',
73
+ defaultRelayState: 'test',
74
+ forceAuthentication: true,
75
+ logoutRedirectUrl: 'https://dummy.com/logout'
76
+ }
77
+ )
78
+ expect do
79
+ @instance.create_saml_application(
80
+ name: 'test',
81
+ login_page_url: 'https://dummy.com/login',
82
+ id: 'tenant1',
83
+ description: 'awesome tenant',
84
+ logo: 'https://logo.com',
85
+ enabled: true,
86
+ use_metadata_info: true,
87
+ metadata_url: 'https://dummy.com/metadata',
88
+ entity_id: 'ent1234',
89
+ acs_url: 'https://dummy.com/acs',
90
+ certificate: 'something',
91
+ attribute_mapping: [
92
+ {
93
+ 'abc': '123'
94
+ }
95
+ ],
96
+ groups_mapping: [
97
+ {
98
+ 'abc': '123'
99
+ }
100
+ ],
101
+ acs_allowed_callbacks: true,
102
+ subject_name_id_type: 'test',
103
+ subject_name_id_format: 'test',
104
+ default_relay_state: 'test',
105
+ force_authentication: true,
106
+ logout_redirect_url: 'https://dummy.com/logout'
107
+ )
108
+ end.not_to raise_error
109
+ end
110
+
111
+ it 'is expected to raise error if metadata_url is empty' do
112
+ expect do
113
+ @instance.create_saml_application(
114
+ name: 'test',
115
+ login_page_url: 'https://dummy.com/login',
116
+ id: 'tenant1',
117
+ description: 'awesome tenant',
118
+ logo: 'https://logo.com',
119
+ enabled: true,
120
+ use_metadata_info: true,
121
+ entity_id: 'ent1234',
122
+ acs_url: 'https://dummy.com/acs',
123
+ certificate: 'something',
124
+ attribute_mapping: [
125
+ {
126
+ 'abc': '123'
127
+ }
128
+ ],
129
+ groups_mapping: [
130
+ {
131
+ 'abc': '123'
132
+ }
133
+ ],
134
+ acs_allowed_callbacks: true,
135
+ subject_name_id_type: 'test',
136
+ subject_name_id_format: 'test',
137
+ default_relay_state: 'test',
138
+ force_authentication: true,
139
+ logout_redirect_url: 'https://dummy.com/logout'
140
+ )
141
+ end.to raise_error(Descope::ArgumentException, 'metadata_url argument must be set')
142
+ end
143
+ end
144
+
145
+ it 'is expected to raise error if entity_id acs_url and certificate arguments are missing' do
146
+ expect do
147
+ @instance.create_saml_application(
148
+ name: 'test',
149
+ login_page_url: 'https://dummy.com/login',
150
+ id: 'tenant1',
151
+ description: 'awesome tenant',
152
+ logo: 'https://logo.com',
153
+ enabled: true,
154
+ attribute_mapping: [
155
+ {
156
+ 'abc': '123'
157
+ }
158
+ ],
159
+ groups_mapping: [
160
+ {
161
+ 'abc': '123'
162
+ }
163
+ ],
164
+ acs_allowed_callbacks: true,
165
+ subject_name_id_type: 'test',
166
+ subject_name_id_format: 'test',
167
+ default_relay_state: 'test',
168
+ force_authentication: true,
169
+ logout_redirect_url: 'https://dummy.com/logout'
170
+ )
171
+ end.to raise_error(Descope::ArgumentException, 'entity_id, acs_url, certificate arguments must be set')
172
+ end
173
+
174
+ it 'is expected to update sso oidc application' do
175
+ expect(@instance).to receive(:post).with(
176
+ SSO_APPLICATION_OIDC_UPDATE_PATH, {
177
+ id: 'tenant1',
178
+ name: 'test',
179
+ description: 'awesome tenant',
180
+ enabled: true,
181
+ logo: 'https://logo.com',
182
+ loginPageUrl: 'https://dummy.com/login'
183
+ }
184
+ )
185
+ expect do
186
+ @instance.update_sso_oidc_app(
187
+ id: 'tenant1',
188
+ name: 'test',
189
+ description: 'awesome tenant',
190
+ enabled: true,
191
+ logo: 'https://logo.com',
192
+ login_page_url: 'https://dummy.com/login'
193
+ )
194
+ end.not_to raise_error
195
+ end
196
+
197
+ it 'is expected to delete sso app' do
198
+ expect(@instance).to receive(:delete).with(
199
+ SSO_APPLICATION_DELETE_PATH, { id: 'tenant1' }
200
+ )
201
+ expect { @instance.delete_sso_app('tenant1') }.not_to raise_error
202
+ end
203
+
204
+ it 'is expected to load sso app' do
205
+ expect(@instance).to receive(:get).with(
206
+ SSO_APPLICATION_LOAD_PATH, { id: 'tenant1' }
207
+ )
208
+ expect { @instance.load_sso_app('tenant1') }.not_to raise_error
209
+ end
210
+
211
+ it 'is expected to load all sso apps' do
212
+ expect(@instance).to receive(:get).with(
213
+ SSO_APPLICATION_LOAD_ALL_PATH, {}
214
+ )
215
+ expect { @instance.load_all_sso_apps }.not_to raise_error
216
+ end
217
+ end
@@ -87,7 +87,7 @@ describe Descope::Api::V1::Management::SSOSettings do
87
87
 
88
88
  it 'is expected to configure SSO settings' do
89
89
  expect(@instance).to receive(:post).with(
90
- SSO_SAML_PATH, {
90
+ SSO_SETTINGS_PATH, {
91
91
  tenantId: '123',
92
92
  settings: {
93
93
  name: 'test',
@@ -132,7 +132,7 @@ describe Descope::Api::V1::Management::SSOSettings do
132
132
 
133
133
  it 'is expected to configure SAML metadata' do
134
134
  expect(@instance).to receive(:post).with(
135
- SSO_SAML_METADATA_PATH, {
135
+ SSO_METADATA_PATH, {
136
136
  tenantId: '123',
137
137
  settings: {
138
138
  name: 'test',