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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +51 -12
- data/.github/workflows/publish-gem.yaml +6 -26
- data/.github/workflows/release-please.yaml +36 -0
- data/.gitignore +5 -2
- data/.release-please-manifest.json +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +21 -0
- data/Gemfile +8 -7
- data/Gemfile.lock +70 -56
- data/README.md +170 -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 +203 -141
- data/examples/ruby-on-rails-api/descope/package.json +1 -1
- data/examples/ruby-on-rails-api/descope/yarn.lock +185 -87
- 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 +5 -2
- 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 +20 -22
- data/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb +6 -2
- data/spec/integration/lib.descope/api/v1/auth/otp_spec.rb +6 -2
- data/spec/integration/lib.descope/api/v1/auth/session_spec.rb +68 -0
- data/spec/integration/lib.descope/api/v1/auth/totp_spec.rb +6 -2
- data/spec/integration/lib.descope/api/v1/management/access_key_spec.rb +12 -1
- data/spec/integration/lib.descope/api/v1/management/audit_spec.rb +5 -3
- data/spec/integration/lib.descope/api/v1/management/authz_spec.rb +28 -5
- data/spec/integration/lib.descope/api/v1/management/flow_spec.rb +3 -1
- data/spec/integration/lib.descope/api/v1/management/permissions_spec.rb +22 -2
- data/spec/integration/lib.descope/api/v1/management/project_spec.rb +18 -2
- data/spec/integration/lib.descope/api/v1/management/roles_spec.rb +116 -36
- data/spec/integration/lib.descope/api/v1/management/user_spec.rb +74 -8
- 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 +229 -0
- data/spec/support/client_config.rb +0 -1
- data/spec/support/utils.rb +21 -0
- metadata +14 -8
data/lib/descope/mixins/http.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require
|
|
2
|
+
require 'descope/mixins/common'
|
|
3
|
+
require 'addressable/uri'
|
|
3
4
|
require 'retryable'
|
|
4
5
|
require_relative '../exception'
|
|
5
6
|
|
|
@@ -7,6 +8,7 @@ module Descope
|
|
|
7
8
|
module Mixins
|
|
8
9
|
# HTTP-related methods
|
|
9
10
|
module HTTP
|
|
11
|
+
include Descope::Mixins::Common
|
|
10
12
|
attr_accessor :headers, :base_uri, :timeout, :retry_count
|
|
11
13
|
|
|
12
14
|
DEFAULT_RETRIES = 3
|
|
@@ -44,13 +46,61 @@ module Descope
|
|
|
44
46
|
}
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
def safe_parse_json(body)
|
|
49
|
+
def safe_parse_json(body, cookies: {}, headers: {})
|
|
48
50
|
@logger.debug "response => #{JSON.parse(body.to_s)}"
|
|
49
|
-
JSON.parse(body.to_s)
|
|
51
|
+
res = JSON.parse(body.to_s)
|
|
52
|
+
|
|
53
|
+
# Handle DS and DSR cookies in response.
|
|
54
|
+
# First check RestClient's cookies (works for same-domain cookies)
|
|
55
|
+
extracted_cookies = {}
|
|
56
|
+
if cookies.key?(SESSION_COOKIE_NAME)
|
|
57
|
+
extracted_cookies[SESSION_COOKIE_NAME] = cookies[SESSION_COOKIE_NAME]
|
|
58
|
+
end
|
|
59
|
+
if cookies.key?(REFRESH_SESSION_COOKIE_NAME)
|
|
60
|
+
extracted_cookies[REFRESH_SESSION_COOKIE_NAME] = cookies[REFRESH_SESSION_COOKIE_NAME]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# If no cookies found via RestClient, parse Set-Cookie headers directly
|
|
64
|
+
# This handles custom domain cookies that RestClient filters out
|
|
65
|
+
if extracted_cookies.empty? && headers.respond_to?(:[])
|
|
66
|
+
set_cookie_headers = headers[:set_cookie] || headers['set-cookie'] || headers['Set-Cookie'] || []
|
|
67
|
+
set_cookie_headers = [set_cookie_headers] unless set_cookie_headers.is_a?(Array)
|
|
68
|
+
|
|
69
|
+
set_cookie_headers.each do |cookie_header|
|
|
70
|
+
next unless cookie_header.is_a?(String)
|
|
71
|
+
|
|
72
|
+
# Parse DS cookie (session token)
|
|
73
|
+
if cookie_header.include?("#{SESSION_COOKIE_NAME}=")
|
|
74
|
+
cookie_value = parse_cookie_value(cookie_header, SESSION_COOKIE_NAME)
|
|
75
|
+
extracted_cookies[SESSION_COOKIE_NAME] = cookie_value if cookie_value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Parse DSR cookie (refresh token)
|
|
79
|
+
if cookie_header.include?("#{REFRESH_SESSION_COOKIE_NAME}=")
|
|
80
|
+
cookie_value = parse_cookie_value(cookie_header, REFRESH_SESSION_COOKIE_NAME)
|
|
81
|
+
extracted_cookies[REFRESH_SESSION_COOKIE_NAME] = cookie_value if cookie_value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Add extracted cookies to response if any were found
|
|
87
|
+
unless extracted_cookies.empty?
|
|
88
|
+
res['cookies'] = extracted_cookies
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
res
|
|
50
92
|
rescue JSON::ParserError
|
|
51
93
|
body
|
|
52
94
|
end
|
|
53
95
|
|
|
96
|
+
def parse_cookie_value(cookie_header, cookie_name)
|
|
97
|
+
# Extract cookie value from Set-Cookie header
|
|
98
|
+
# Format: "cookieName=cookieValue; attribute1=value1; attribute2=value2"
|
|
99
|
+
# Only match valid cookie value characters (RFC 6265: exclude whitespace, semicolon, comma)
|
|
100
|
+
match = cookie_header.match(/#{Regexp.escape(cookie_name)}=([^;]+)/)
|
|
101
|
+
match ? match[1].strip : nil
|
|
102
|
+
end
|
|
103
|
+
|
|
54
104
|
def encode_uri(uri)
|
|
55
105
|
encoded_uri = base_uri ? Addressable::URI.parse(uri).normalize : Addressable::URI.escape(uri)
|
|
56
106
|
@logger.debug "will call #{url(encoded_uri)}"
|
|
@@ -94,11 +144,12 @@ module Descope
|
|
|
94
144
|
call(method, encode_uri(uri), timeout, @headers, body.to_json)
|
|
95
145
|
end
|
|
96
146
|
|
|
97
|
-
raise Descope::Unsupported.new(
|
|
147
|
+
raise Descope::Unsupported.new('No response from server', code: 400) unless result.respond_to?(:code)
|
|
98
148
|
|
|
99
149
|
@logger.info("API Request: [#{method}] #{uri} - Response Code: #{result.code}")
|
|
150
|
+
|
|
100
151
|
case result.code
|
|
101
|
-
when 200...226 then safe_parse_json(result.body)
|
|
152
|
+
when 200...226 then safe_parse_json(result.body, cookies: result.cookies, headers: result.headers)
|
|
102
153
|
when 400 then raise Descope::BadRequest.new(result.body, code: result.code, headers: result.headers)
|
|
103
154
|
when 401 then raise Descope::Unauthorized.new(result.body, code: result.code, headers: result.headers)
|
|
104
155
|
when 403 then raise Descope::AccessDenied.new(result.body, code: result.code, headers: result.headers)
|
|
@@ -113,10 +164,10 @@ module Descope
|
|
|
113
164
|
|
|
114
165
|
def call(method, url, timeout, headers, body = nil)
|
|
115
166
|
RestClient::Request.execute(
|
|
116
|
-
method
|
|
117
|
-
url
|
|
118
|
-
timeout
|
|
119
|
-
headers
|
|
167
|
+
method: method,
|
|
168
|
+
url: url,
|
|
169
|
+
timeout: timeout,
|
|
170
|
+
headers: headers,
|
|
120
171
|
payload: body
|
|
121
172
|
)
|
|
122
173
|
rescue RestClient::Exception => e
|
|
@@ -13,10 +13,13 @@ module Descope
|
|
|
13
13
|
@base_uri = base_url(options)
|
|
14
14
|
@headers = client_headers
|
|
15
15
|
@project_id = options[:project_id] || ENV['DESCOPE_PROJECT_ID'] || ''
|
|
16
|
+
@headers['x-descope-project-id'] = @project_id
|
|
16
17
|
@public_key = options[:public_key] || ENV['DESCOPE_PUBLIC_KEY']
|
|
17
18
|
@mlock = Mutex.new
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
@logger = options[:logger] || begin
|
|
20
|
+
log_level = options[:log_level] || ENV['DESCOPE_LOG_LEVEL'] || 'info'
|
|
21
|
+
Descope::Mixins::Logging.logger_for(self.class.name, log_level, @project_id)
|
|
22
|
+
end
|
|
20
23
|
|
|
21
24
|
@logger.debug("Initializing Descope API with project_id: #{@project_id} and base_uri: #{@base_uri}")
|
|
22
25
|
|
|
@@ -7,21 +7,29 @@ module Descope
|
|
|
7
7
|
|
|
8
8
|
def logger
|
|
9
9
|
# This is the magical bit that gets mixed into the other modules
|
|
10
|
-
@logger ||= Logging.logger_for(self.class.name, 'info')
|
|
10
|
+
@logger ||= Logging.logger_for(self.class.name, 'info', @project_id)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# Use a hash class-ivar to cache a unique Logger per class:
|
|
14
14
|
@loggers = {}
|
|
15
15
|
|
|
16
16
|
class << self
|
|
17
|
-
def logger_for(classname, level)
|
|
18
|
-
|
|
17
|
+
def logger_for(classname, level, project_id = nil)
|
|
18
|
+
key = "#{classname}-#{project_id}"
|
|
19
|
+
@loggers[key] ||= configure_logger_for(classname, level, project_id)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
|
-
def configure_logger_for(classname, level = 'info')
|
|
22
|
+
def configure_logger_for(classname, level = 'info', project_id = nil)
|
|
22
23
|
logger = Logger.new(STDOUT)
|
|
23
24
|
logger.level = Object.const_get("Logger::#{level.upcase}")
|
|
24
25
|
logger.progname = classname
|
|
26
|
+
|
|
27
|
+
# Adding Custom Formatter for Project ID
|
|
28
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
|
29
|
+
project_info = project_id ? "PRID: #{project_id}" : ""
|
|
30
|
+
"[#{datetime}] #{severity} #{project_info} #{progname}: #{msg}\n"
|
|
31
|
+
end
|
|
32
|
+
|
|
25
33
|
logger
|
|
26
34
|
end
|
|
27
35
|
end
|
data/lib/descope/version.rb
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Descope::Api::V1::Auth do
|
|
6
|
+
let(:client) { Class.new { include Descope::Api::V1::Auth }.new }
|
|
7
|
+
let(:valid_token) { 'valid_token' }
|
|
8
|
+
let(:refresh_token) { 'refresh_token' }
|
|
9
|
+
|
|
10
|
+
describe 'token extraction with empty strings' do
|
|
11
|
+
it 'handles empty string tokens correctly' do
|
|
12
|
+
response_body = {
|
|
13
|
+
'sessionJwt' => '',
|
|
14
|
+
'refreshJwt' => '',
|
|
15
|
+
'cookies' => {
|
|
16
|
+
'DS' => valid_token,
|
|
17
|
+
'DSR' => refresh_token
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
|
|
22
|
+
|
|
23
|
+
result = client.generate_jwt_response(response_body: response_body)
|
|
24
|
+
|
|
25
|
+
expect(result).to have_key('sessionToken')
|
|
26
|
+
expect(result).to have_key('refreshSessionToken')
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Descope::Api::V1::Auth do
|
|
6
|
+
let(:client) { Descope::Client.new(project_id: 'test_project_id', management_key: 'test_key') }
|
|
7
|
+
let(:valid_session_token) { 'valid.session.token' }
|
|
8
|
+
let(:valid_refresh_token) { 'valid.refresh.token' }
|
|
9
|
+
|
|
10
|
+
describe '#generate_auth_info (private method)' do
|
|
11
|
+
context 'session token extraction' do
|
|
12
|
+
it 'extracts session token from sessionJwt field' do
|
|
13
|
+
response_body = {
|
|
14
|
+
'sessionJwt' => valid_session_token,
|
|
15
|
+
'refreshJwt' => valid_refresh_token,
|
|
16
|
+
'cookies' => {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
|
|
20
|
+
|
|
21
|
+
result = client.send(:generate_auth_info, response_body, nil, true, nil)
|
|
22
|
+
|
|
23
|
+
expect(result).to have_key('sessionToken')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'extracts session token from cookies with DS name' do
|
|
27
|
+
response_body = {
|
|
28
|
+
'sessionJwt' => '',
|
|
29
|
+
'refreshJwt' => valid_refresh_token,
|
|
30
|
+
'cookies' => {
|
|
31
|
+
'DS' => valid_session_token
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
|
|
36
|
+
|
|
37
|
+
result = client.send(:generate_auth_info, response_body, nil, true, nil)
|
|
38
|
+
|
|
39
|
+
expect(result).to have_key('sessionToken')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'extracts session token from cookies with SESSION_COOKIE_NAME' do
|
|
43
|
+
stub_const('Descope::Api::V1::Auth::SESSION_COOKIE_NAME', 'CustomSession')
|
|
44
|
+
|
|
45
|
+
response_body = {
|
|
46
|
+
'sessionJwt' => '',
|
|
47
|
+
'refreshJwt' => valid_refresh_token,
|
|
48
|
+
'cookies' => {
|
|
49
|
+
'CustomSession' => valid_session_token
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
|
|
54
|
+
|
|
55
|
+
result = client.send(:generate_auth_info, response_body, nil, true, nil)
|
|
56
|
+
|
|
57
|
+
expect(result).to have_key('sessionToken')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context 'refresh token extraction' do
|
|
62
|
+
it 'extracts refresh token from refreshJwt field' do
|
|
63
|
+
response_body = {
|
|
64
|
+
'sessionJwt' => valid_session_token,
|
|
65
|
+
'refreshJwt' => valid_refresh_token,
|
|
66
|
+
'cookies' => {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
|
|
70
|
+
|
|
71
|
+
result = client.send(:generate_auth_info, response_body, nil, true, nil)
|
|
72
|
+
|
|
73
|
+
expect(result).to have_key('refreshSessionToken')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'extracts refresh token from cookies when refreshJwt is empty' do
|
|
77
|
+
stub_const('Descope::Api::V1::Auth::REFRESH_SESSION_COOKIE_NAME', 'DSR')
|
|
78
|
+
|
|
79
|
+
response_body = {
|
|
80
|
+
'sessionJwt' => valid_session_token,
|
|
81
|
+
'refreshJwt' => '',
|
|
82
|
+
'cookies' => {
|
|
83
|
+
'DSR' => valid_refresh_token
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
|
|
88
|
+
|
|
89
|
+
result = client.send(:generate_auth_info, response_body, nil, true, nil)
|
|
90
|
+
|
|
91
|
+
expect(result).to have_key('refreshSessionToken')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'falls back to parameter refresh token when cookie has empty string' do
|
|
95
|
+
response_body = {
|
|
96
|
+
'sessionJwt' => valid_session_token,
|
|
97
|
+
'refreshJwt' => '',
|
|
98
|
+
'cookies' => {
|
|
99
|
+
'DSR' => ''
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
|
|
104
|
+
|
|
105
|
+
result = client.send(:generate_auth_info, response_body, valid_refresh_token, true, nil)
|
|
106
|
+
|
|
107
|
+
expect(result).to have_key('refreshSessionToken')
|
|
108
|
+
expect(client).to have_received(:validate_token).with(valid_refresh_token, nil)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'raises error when no refresh token is available' do
|
|
112
|
+
response_body = {
|
|
113
|
+
'sessionJwt' => valid_session_token,
|
|
114
|
+
'refreshJwt' => '',
|
|
115
|
+
'cookies' => {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123', 'iss' => 'test_project_id' })
|
|
119
|
+
|
|
120
|
+
expect {
|
|
121
|
+
client.send(:generate_auth_info, response_body, nil, true, nil)
|
|
122
|
+
}.to raise_error(Descope::AuthException, /Could not find refresh token/)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Descope::Api::V1::Session do
|
|
6
|
+
let(:client) { Descope::Client.new(project_id: 'test_project_id', management_key: 'test_key') }
|
|
7
|
+
let(:valid_token) { 'valid.jwt.token' }
|
|
8
|
+
let(:refresh_token) { 'refresh.jwt.token' }
|
|
9
|
+
|
|
10
|
+
describe '#refresh_session' do
|
|
11
|
+
context 'when refresh token is in cookie' do
|
|
12
|
+
it 'uses the cookie refresh token' do
|
|
13
|
+
response_body = {
|
|
14
|
+
'sessionJwt' => '',
|
|
15
|
+
'refreshJwt' => '',
|
|
16
|
+
'cookieData' => {
|
|
17
|
+
'DSR' => refresh_token
|
|
18
|
+
},
|
|
19
|
+
'cookies' => {
|
|
20
|
+
'DS' => valid_token
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
allow(client).to receive(:post).and_return(response_body)
|
|
25
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123' })
|
|
26
|
+
|
|
27
|
+
result = client.refresh_session(refresh_token: refresh_token)
|
|
28
|
+
|
|
29
|
+
expect(result).to be_a(Hash)
|
|
30
|
+
expect(client).to have_received(:validate_token).with(refresh_token, nil)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context 'when refresh token is in response body' do
|
|
35
|
+
it 'uses the refreshJwt from response body' do
|
|
36
|
+
response_body = {
|
|
37
|
+
'sessionJwt' => '',
|
|
38
|
+
'refreshJwt' => refresh_token,
|
|
39
|
+
'cookieData' => {},
|
|
40
|
+
'cookies' => {
|
|
41
|
+
'DS' => valid_token
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
allow(client).to receive(:post).and_return(response_body)
|
|
46
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123' })
|
|
47
|
+
|
|
48
|
+
result = client.refresh_session(refresh_token: 'old_refresh_token')
|
|
49
|
+
|
|
50
|
+
expect(result).to be_a(Hash)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context 'when refresh token is only in parameter' do
|
|
55
|
+
it 'falls back to the parameter refresh token' do
|
|
56
|
+
response_body = {
|
|
57
|
+
'sessionJwt' => '',
|
|
58
|
+
'refreshJwt' => '',
|
|
59
|
+
'cookieData' => {},
|
|
60
|
+
'cookies' => {
|
|
61
|
+
'DS' => valid_token
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
allow(client).to receive(:post).and_return(response_body)
|
|
66
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123' })
|
|
67
|
+
|
|
68
|
+
result = client.refresh_session(refresh_token: refresh_token)
|
|
69
|
+
|
|
70
|
+
expect(result).to be_a(Hash)
|
|
71
|
+
expect(client).to have_received(:validate_token).with(refresh_token, nil).at_least(:once)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'when refresh token values are empty strings' do
|
|
76
|
+
it 'falls back to parameter refresh token when cookie and body have empty strings' do
|
|
77
|
+
response_body = {
|
|
78
|
+
'sessionJwt' => '',
|
|
79
|
+
'refreshJwt' => '',
|
|
80
|
+
'cookieData' => {
|
|
81
|
+
'DSR' => ''
|
|
82
|
+
},
|
|
83
|
+
'cookies' => {
|
|
84
|
+
'DS' => valid_token
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
allow(client).to receive(:post).and_return(response_body)
|
|
89
|
+
allow(client).to receive(:validate_token).and_return({ 'sub' => 'user123' })
|
|
90
|
+
|
|
91
|
+
result = client.refresh_session(refresh_token: refresh_token)
|
|
92
|
+
|
|
93
|
+
expect(result).to be_a(Hash)
|
|
94
|
+
expect(client).to have_received(:validate_token).with(refresh_token, nil).at_least(:once)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/spec/factories/user.rb
CHANGED
|
@@ -10,7 +10,7 @@ FactoryBot.define do
|
|
|
10
10
|
phone { "+1#{Faker::Number.number(digits: 10)}" }
|
|
11
11
|
name { Faker::Name.name }
|
|
12
12
|
given_name { Faker::Name.first_name }
|
|
13
|
-
middle_name {
|
|
13
|
+
middle_name { "#{SpecUtils.build_prefix}Ruby-SDK-User" }
|
|
14
14
|
family_name { Faker::Name.last_name }
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -3,31 +3,25 @@
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
|
|
5
5
|
def poll_for_session(descope_client, pending_ref)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
while !done && i < max_tries
|
|
6
|
+
@client.logger.info('Waiting for session to be created...')
|
|
7
|
+
|
|
8
|
+
SpecUtils.wait_for_condition(max_wait: 60, interval: 3, description: 'enchanted link session') do
|
|
10
9
|
begin
|
|
11
|
-
i += 1
|
|
12
|
-
@client.logger.info('waiting 4 seconds for session to be created...')
|
|
13
|
-
sleep(4)
|
|
14
|
-
print '.'
|
|
15
10
|
@client.logger.info("Getting session for pending_ref: #{pending_ref}...")
|
|
16
11
|
jwt_response = descope_client.enchanted_link_get_session(pending_ref)
|
|
17
|
-
|
|
12
|
+
|
|
13
|
+
if jwt_response
|
|
14
|
+
@client.logger.info("jwt_response: #{jwt_response}")
|
|
15
|
+
refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME]['jwt']
|
|
16
|
+
@client.logger.info("refresh_token: #{refresh_token}")
|
|
17
|
+
return refresh_token
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
false
|
|
18
21
|
rescue Descope::AuthException, Descope::Unauthorized => e
|
|
19
|
-
@client.logger.info("
|
|
20
|
-
|
|
22
|
+
@client.logger.info("Waiting for session, err: #{e}")
|
|
23
|
+
false
|
|
21
24
|
end
|
|
22
|
-
|
|
23
|
-
next unless jwt_response
|
|
24
|
-
|
|
25
|
-
@client.logger.info("jwt_response: #{jwt_response}")
|
|
26
|
-
refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME]['jwt']
|
|
27
|
-
|
|
28
|
-
@client.logger.info("refresh_token: #{refresh_token}")
|
|
29
|
-
done = true
|
|
30
|
-
return refresh_token
|
|
31
25
|
end
|
|
32
26
|
end
|
|
33
27
|
|
|
@@ -60,9 +54,13 @@ describe Descope::Api::V1::Auth::EnchantedLink do
|
|
|
60
54
|
@client.logger.info('Cleaning up test users...')
|
|
61
55
|
all_users = @client.search_all_users
|
|
62
56
|
all_users['users'].each do |user|
|
|
63
|
-
if user['middleName'] ==
|
|
57
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
64
58
|
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
|
|
65
|
-
|
|
59
|
+
begin
|
|
60
|
+
@client.delete_user(user['loginIds'][0])
|
|
61
|
+
rescue Descope::NotFound => e
|
|
62
|
+
@client.logger.info("User already deleted: #{e.message}")
|
|
63
|
+
end
|
|
66
64
|
end
|
|
67
65
|
end
|
|
68
66
|
end
|
|
@@ -11,9 +11,13 @@ describe Descope::Api::V1::Auth::MagicLink do
|
|
|
11
11
|
@client.logger.info('Cleaning up test users...')
|
|
12
12
|
all_users = @client.search_all_users
|
|
13
13
|
all_users['users'].each do |user|
|
|
14
|
-
if user['middleName'] ==
|
|
14
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
15
15
|
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
|
|
16
|
-
|
|
16
|
+
begin
|
|
17
|
+
@client.delete_user(user['loginIds'][0])
|
|
18
|
+
rescue Descope::NotFound => e
|
|
19
|
+
@client.logger.info("User already deleted: #{e.message}")
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
end
|
|
@@ -19,9 +19,13 @@ describe Descope::Api::V1::Auth::OTP do
|
|
|
19
19
|
@client.logger.info('Cleaning up test users...')
|
|
20
20
|
all_users = @client.search_all_users
|
|
21
21
|
all_users['users'].each do |user|
|
|
22
|
-
if user['middleName'] ==
|
|
22
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
23
23
|
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
|
|
24
|
-
|
|
24
|
+
begin
|
|
25
|
+
@client.delete_user(user['loginIds'][0])
|
|
26
|
+
rescue Descope::NotFound => e
|
|
27
|
+
@client.logger.info("User already deleted: #{e.message}")
|
|
28
|
+
end
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Descope::Api::V1::Session do
|
|
6
|
+
before(:all) do
|
|
7
|
+
@client = DescopeClient.new(Configuration.config)
|
|
8
|
+
|
|
9
|
+
# Cleanup any existing test users before starting
|
|
10
|
+
@client.logger.info('Cleaning up any existing test users before starting...')
|
|
11
|
+
all_users = @client.search_all_users
|
|
12
|
+
all_users['users'].each do |user|
|
|
13
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
14
|
+
@client.logger.info("Deleting existing ruby spec test user #{user['loginIds'][0]}")
|
|
15
|
+
begin
|
|
16
|
+
@client.delete_user(user['loginIds'][0])
|
|
17
|
+
rescue Descope::NotFound => e
|
|
18
|
+
@client.logger.info("User already deleted: #{e.message}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
after(:all) do
|
|
25
|
+
@client.logger.info('Cleaning up test users...')
|
|
26
|
+
all_users = @client.search_all_users
|
|
27
|
+
all_users['users'].each do |user|
|
|
28
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
29
|
+
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
|
|
30
|
+
begin
|
|
31
|
+
@client.delete_user(user['loginIds'][0])
|
|
32
|
+
rescue Descope::NotFound => e
|
|
33
|
+
@client.logger.info("User already deleted: #{e.message}")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context 'test session methods' do
|
|
40
|
+
it 'should refresh session with refresh token' do
|
|
41
|
+
@password = SpecUtils.generate_password
|
|
42
|
+
# Add timestamp to ensure unique login_id across multiple test runs
|
|
43
|
+
user = build(:user, login_id: "session_test_#{Time.now.to_i}_#{SecureRandom.hex(4)}")
|
|
44
|
+
|
|
45
|
+
@client.logger.info("1. Sign up with password for user: #{user[:login_id]}")
|
|
46
|
+
res = @client.password_sign_up(login_id: user[:login_id], password: @password, user:)
|
|
47
|
+
@client.logger.info("sign up with password res: #{res}")
|
|
48
|
+
original_refresh_token = res[REFRESH_SESSION_TOKEN_NAME]['jwt']
|
|
49
|
+
|
|
50
|
+
@client.logger.info('2. Sign in with password')
|
|
51
|
+
login_res = @client.password_sign_in(login_id: user[:login_id], password: @password)
|
|
52
|
+
@client.logger.info("sign_in res: #{login_res}")
|
|
53
|
+
|
|
54
|
+
@client.logger.info('3. Wait briefly to ensure token timestamps differ')
|
|
55
|
+
sleep(2) # Wait 2 seconds to ensure new token will have different 'iat' timestamp
|
|
56
|
+
|
|
57
|
+
@client.logger.info('4. Refresh session')
|
|
58
|
+
refresh_session_res = @client.refresh_session(refresh_token: login_res[REFRESH_SESSION_TOKEN_NAME]['jwt'])
|
|
59
|
+
@client.logger.info("refresh_session_res: #{refresh_session_res}")
|
|
60
|
+
|
|
61
|
+
new_refresh_token = refresh_session_res[REFRESH_SESSION_TOKEN_NAME]['jwt']
|
|
62
|
+
@client.logger.info("new_refresh_token: #{new_refresh_token}")
|
|
63
|
+
|
|
64
|
+
@client.logger.info('5. Check new refresh token is not the same as the original one')
|
|
65
|
+
expect(original_refresh_token).not_to eq(new_refresh_token)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -12,9 +12,13 @@ describe Descope::Api::V1::Auth::TOTP do
|
|
|
12
12
|
@client.logger.info('Cleaning up test users...')
|
|
13
13
|
all_users = @client.search_all_users
|
|
14
14
|
all_users['users'].each do |user|
|
|
15
|
-
if user['middleName'] ==
|
|
15
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
16
16
|
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
|
|
17
|
-
|
|
17
|
+
begin
|
|
18
|
+
@client.delete_user(user['loginIds'][0])
|
|
19
|
+
rescue Descope::NotFound => e
|
|
20
|
+
@client.logger.info("User already deleted: #{e.message}")
|
|
21
|
+
end
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -4,6 +4,8 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
describe Descope::Api::V1::Management::AccessKey do
|
|
6
6
|
before(:all) do
|
|
7
|
+
raise 'DESCOPE_MANAGEMENT_KEY is not set' if ENV['DESCOPE_MANAGEMENT_KEY'].nil?
|
|
8
|
+
|
|
7
9
|
@client = DescopeClient.new(Configuration.config)
|
|
8
10
|
end
|
|
9
11
|
|
|
@@ -28,7 +30,16 @@ describe Descope::Api::V1::Management::AccessKey do
|
|
|
28
30
|
@tenant_id = @client.create_tenant(name: 'some-new-tenant')['id']
|
|
29
31
|
@client.logger.info('creating access key')
|
|
30
32
|
@access_key = @client.create_access_key(name: @key_name, key_tenants: [{ tenant_id: @tenant_id }])
|
|
31
|
-
|
|
33
|
+
@client.logger.info("waiting for access key #{@access_key['key']['id']} to be active")
|
|
34
|
+
SpecUtils.wait_for_condition(max_wait: 65, interval: 3, description: 'access key to be active') do
|
|
35
|
+
begin
|
|
36
|
+
key = @client.load_access_key(@access_key['key']['id'])
|
|
37
|
+
key['key']['status'] == 'active'
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
@client.logger.info("Waiting for key activation: #{e.message}")
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
it 'should create the access key and load it' do
|