descope 1.0.6 → 1.0.7
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/ci.yaml +2 -2
- data/.github/workflows/publish-gem.yaml +10 -3
- data/.gitignore +2 -0
- data/.ruby-version +1 -1
- data/Gemfile +7 -7
- data/Gemfile.lock +68 -55
- data/README.md +159 -51
- data/examples/ruby-on-rails-api/descope/Gemfile +8 -8
- data/examples/ruby-on-rails-api/descope/Gemfile.lock +1 -1
- data/examples/ruby-on-rails-api/descope/package-lock.json +187 -131
- data/examples/ruby-on-rails-api/descope/package.json +1 -1
- data/examples/ruby-on-rails-api/descope/yarn.lock +182 -84
- data/lib/descope/api/v1/auth/enchantedlink.rb +3 -1
- data/lib/descope/api/v1/auth/magiclink.rb +3 -1
- data/lib/descope/api/v1/auth/otp.rb +3 -1
- data/lib/descope/api/v1/auth/password.rb +6 -2
- data/lib/descope/api/v1/auth/totp.rb +3 -1
- data/lib/descope/api/v1/auth.rb +47 -12
- data/lib/descope/api/v1/management/common.rb +20 -5
- data/lib/descope/api/v1/management/sso_application.rb +236 -0
- data/lib/descope/api/v1/management/sso_settings.rb +2 -24
- data/lib/descope/api/v1/management/user.rb +151 -13
- data/lib/descope/api/v1/management.rb +2 -0
- data/lib/descope/api/v1/session.rb +37 -4
- data/lib/descope/mixins/common.rb +1 -0
- data/lib/descope/mixins/http.rb +60 -9
- data/lib/descope/mixins/initializer.rb +2 -1
- data/lib/descope/mixins/logging.rb +12 -4
- data/lib/descope/version.rb +1 -1
- data/spec/descope/api/v1/auth_spec.rb +29 -0
- data/spec/descope/api/v1/auth_token_extraction_spec.rb +126 -0
- data/spec/descope/api/v1/session_refresh_spec.rb +98 -0
- data/spec/factories/user.rb +1 -1
- data/spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb +1 -1
- data/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb +1 -1
- data/spec/integration/lib.descope/api/v1/auth/otp_spec.rb +1 -1
- data/spec/integration/lib.descope/api/v1/auth/session_spec.rb +49 -0
- data/spec/integration/lib.descope/api/v1/auth/totp_spec.rb +1 -1
- data/spec/integration/lib.descope/api/v1/management/access_key_spec.rb +3 -0
- data/spec/integration/lib.descope/api/v1/management/audit_spec.rb +5 -3
- data/spec/integration/lib.descope/api/v1/management/authz_spec.rb +2 -0
- data/spec/integration/lib.descope/api/v1/management/flow_spec.rb +3 -1
- data/spec/integration/lib.descope/api/v1/management/permissions_spec.rb +4 -2
- data/spec/integration/lib.descope/api/v1/management/project_spec.rb +2 -0
- data/spec/integration/lib.descope/api/v1/management/roles_spec.rb +2 -0
- data/spec/integration/lib.descope/api/v1/management/user_spec.rb +55 -6
- data/spec/lib.descope/api/v1/auth/enchantedlink_spec.rb +11 -2
- data/spec/lib.descope/api/v1/auth/password_spec.rb +10 -1
- data/spec/lib.descope/api/v1/auth_spec.rb +167 -5
- data/spec/lib.descope/api/v1/cookie_domain_fix_integration_spec.rb +245 -0
- data/spec/lib.descope/api/v1/management/sso_application_spec.rb +217 -0
- data/spec/lib.descope/api/v1/management/sso_settings_spec.rb +2 -2
- data/spec/lib.descope/api/v1/management/user_spec.rb +134 -46
- data/spec/lib.descope/api/v1/session_spec.rb +119 -6
- data/spec/lib.descope/mixins/http_spec.rb +218 -0
- data/spec/support/client_config.rb +0 -1
- data/spec/support/utils.rb +6 -0
- metadata +13 -8
|
@@ -4,17 +4,21 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
describe Descope::Api::V1::Management::User do
|
|
6
6
|
before(:all) do
|
|
7
|
+
raise 'DESCOPE_MANAGEMENT_KEY is not set' if ENV['DESCOPE_MANAGEMENT_KEY'].nil?
|
|
8
|
+
|
|
9
|
+
@client = DescopeClient.new(Configuration.config)
|
|
10
|
+
|
|
7
11
|
@password = SpecUtils.generate_password
|
|
8
12
|
@new_password = SpecUtils.generate_password
|
|
9
13
|
@user = build(:user)
|
|
10
|
-
|
|
14
|
+
|
|
11
15
|
include Descope::Mixins::Common::DeliveryMethod
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
after(:all) do
|
|
15
19
|
all_users = @client.search_all_users
|
|
16
20
|
all_users['users'].each do |user|
|
|
17
|
-
if user['middleName'] ==
|
|
21
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
18
22
|
puts "Deleting ruby spec test user #{user['loginIds'][0]}"
|
|
19
23
|
@client.delete_user(user['loginIds'][0])
|
|
20
24
|
end
|
|
@@ -58,6 +62,41 @@ describe Descope::Api::V1::Management::User do
|
|
|
58
62
|
expect(updated_user['first_name']).to eq(created_user[updated_first_name])
|
|
59
63
|
end
|
|
60
64
|
|
|
65
|
+
it 'should patch a user' do
|
|
66
|
+
user = build(:user)
|
|
67
|
+
role_name = 'some-new-role'
|
|
68
|
+
|
|
69
|
+
# ensure no roles exist with that name
|
|
70
|
+
all_roles = @client.load_all_roles
|
|
71
|
+
all_roles['roles'].each do |role|
|
|
72
|
+
@client.delete_role(name: role['name']) if role['name'] == role_name
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@client.create_role(name: role_name)
|
|
76
|
+
@client.create_user(**user)['user']
|
|
77
|
+
updated_first_name = 'new name'
|
|
78
|
+
updated_given_name = 'new given name'
|
|
79
|
+
update_phone_number = "+1#{Faker::Number.number(digits: 10)}"
|
|
80
|
+
updated_role_names = [role_name]
|
|
81
|
+
updated_middle_name = 'new middle name'
|
|
82
|
+
updated_user = @client.patch_user(
|
|
83
|
+
**user,
|
|
84
|
+
name: updated_first_name,
|
|
85
|
+
given_name: updated_given_name,
|
|
86
|
+
phone: update_phone_number,
|
|
87
|
+
role_names: updated_role_names,
|
|
88
|
+
middle_name: updated_middle_name
|
|
89
|
+
)['user']
|
|
90
|
+
|
|
91
|
+
puts "updated_user #{updated_user}"
|
|
92
|
+
|
|
93
|
+
expect(updated_user['name']).to eq(updated_first_name)
|
|
94
|
+
expect(updated_user['givenName']).to eq(updated_given_name)
|
|
95
|
+
expect(updated_user['phone']).to eq(update_phone_number)
|
|
96
|
+
expect(updated_user['roleNames']).to eq(updated_role_names)
|
|
97
|
+
expect(updated_user['middleName']).to eq(updated_middle_name)
|
|
98
|
+
end
|
|
99
|
+
|
|
61
100
|
it 'should delete a user' do
|
|
62
101
|
user = build(:user)
|
|
63
102
|
created_user = @client.create_user(**user)['user']
|
|
@@ -77,18 +116,28 @@ describe Descope::Api::V1::Management::User do
|
|
|
77
116
|
users = FactoryBot.build_list(:user, 5)
|
|
78
117
|
@client.create_batch_users(users)
|
|
79
118
|
all_users = @client.search_all_users
|
|
80
|
-
sdk_users = all_users['users'].select { |user| user['middleName'] ==
|
|
119
|
+
sdk_users = all_users['users'].select { |user| user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User" }
|
|
81
120
|
expect(sdk_users.length).to be >= 5
|
|
82
121
|
end
|
|
83
122
|
|
|
84
123
|
it 'should create a test user' do
|
|
85
124
|
@client.delete_all_test_users
|
|
125
|
+
# ensure no roles exist with that name
|
|
126
|
+
role_name = 'some-new-role'
|
|
127
|
+
all_roles = @client.load_all_roles
|
|
128
|
+
all_roles['roles'].each do |role|
|
|
129
|
+
@client.delete_role(name: role['name']) if role['name'] == role_name
|
|
130
|
+
end
|
|
86
131
|
sleep 5
|
|
132
|
+
|
|
87
133
|
user_args = build(:user)
|
|
88
134
|
test_user = @client.create_test_user(**user_args)['user']
|
|
89
|
-
|
|
135
|
+
@client.create_role(name: role_name)
|
|
136
|
+
@client.user_add_roles(login_id: test_user['loginIds'][0], role_names: [role_name])
|
|
137
|
+
test_users = @client.search_all_test_users(role_names: [role_name])['users']
|
|
90
138
|
expect(test_users.length).to be >= 1
|
|
91
139
|
expect(test_users[0]['loginIds'][0]).to eq(test_user['loginIds'][0])
|
|
140
|
+
expect(test_users[0]['roleNames']).to eq([role_name])
|
|
92
141
|
end
|
|
93
142
|
|
|
94
143
|
it 'should update user status' do
|
|
@@ -191,8 +240,8 @@ describe Descope::Api::V1::Management::User do
|
|
|
191
240
|
new_password = SpecUtils.generate_password
|
|
192
241
|
@client.set_password(login_id: user['loginIds'][0], password: new_password)
|
|
193
242
|
@client.password_sign_in(login_id: user['loginIds'][0], password:)
|
|
194
|
-
rescue Descope::
|
|
195
|
-
expect(e.message).to match(/"
|
|
243
|
+
rescue Descope::Unauthorized => e
|
|
244
|
+
expect(e.message).to match(/"Invalid signin credentials"/)
|
|
196
245
|
end
|
|
197
246
|
end
|
|
198
247
|
|
|
@@ -51,7 +51,7 @@ describe Descope::Api::V1::EnchantedLink do
|
|
|
51
51
|
|
|
52
52
|
it 'is expected to validate refresh token and not raise an error with refresh token and valid login options' do
|
|
53
53
|
expect do
|
|
54
|
-
@instance.send(:validate_refresh_token_provided, { mfa: true, stepup: true },
|
|
54
|
+
@instance.send(:validate_refresh_token_provided, { mfa: true, stepup: true }, 'some-token')
|
|
55
55
|
end.not_to raise_error
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -148,7 +148,16 @@ describe Descope::Api::V1::EnchantedLink do
|
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
it 'is expected to get session by pending ref with enchanted link' do
|
|
151
|
-
jwt_response = {
|
|
151
|
+
jwt_response = {
|
|
152
|
+
'sessionJwt' => 'fake_session_jwt',
|
|
153
|
+
'refreshJwt' => 'fake_refresh_jwt',
|
|
154
|
+
'cookies' => {
|
|
155
|
+
'refresh_token' => 'fake_refresh_cookie'
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
allow(@instance).to receive(:post).with(
|
|
159
|
+
GET_SESSION_ENCHANTEDLINK_AUTH_PATH, { pendingRef: 'pendingRef' }
|
|
160
|
+
).and_return(jwt_response)
|
|
152
161
|
allow(@instance).to receive(:generate_jwt_response).and_return(jwt_response)
|
|
153
162
|
|
|
154
163
|
expect do
|
|
@@ -43,9 +43,18 @@ describe Descope::Api::V1::Password do
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
it 'is expected to sign in with password' do
|
|
46
|
+
response_body = {
|
|
47
|
+
'sessionJwt' => 'fake_session_jwt',
|
|
48
|
+
'refreshJwt' => 'fake_refresh_jwt',
|
|
49
|
+
'cookies' => {
|
|
50
|
+
'refresh_token' => 'fake_refresh_cookie'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
expect(@instance).to receive(:post).with(
|
|
47
55
|
SIGN_IN_PASSWORD_PATH, { loginId: 'test', password: 's3cr3t', ssoAppId: nil }
|
|
48
|
-
)
|
|
56
|
+
).and_return(response_body)
|
|
57
|
+
|
|
49
58
|
# stub the jwt_get_unverified_header method to return the kid of the public key created above
|
|
50
59
|
allow(@instance).to receive(:generate_jwt_response).and_return({})
|
|
51
60
|
expect { @instance.password_sign_in(login_id: 'test', password: 's3cr3t') }.not_to raise_error
|
|
@@ -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 = {
|
|
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 = {
|
|
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(:
|
|
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 = {
|
|
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(:
|
|
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
|