descope 1.0.5 → 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 +39 -7
- data/.gitignore +2 -0
- data/.ruby-version +1 -1
- data/Gemfile +7 -7
- data/Gemfile.lock +70 -65
- data/README.md +175 -52
- data/descope.gemspec +25 -20
- data/examples/ruby/.ruby-version +1 -0
- data/examples/ruby/access_key_app.rb +4 -3
- data/examples/ruby/enchantedlink_app.rb +1 -0
- data/examples/ruby/magiclink_app.rb +1 -0
- data/examples/ruby/management/.ruby-version +1 -0
- data/examples/ruby/management/Gemfile +2 -2
- data/examples/ruby/management/access_key_app.rb +2 -0
- data/examples/ruby/management/audit_app.rb +32 -8
- data/examples/ruby/management/authz_app.rb +1 -0
- data/examples/ruby/management/flow_app.rb +1 -0
- data/examples/ruby/management/permission_app.rb +3 -2
- data/examples/ruby/management/role_app.rb +3 -2
- data/examples/ruby/management/tenant_app.rb +1 -0
- data/examples/ruby/management/user_app.rb +1 -0
- data/examples/ruby/oauth_app.rb +1 -0
- data/examples/ruby/otp_app.rb +38 -12
- data/examples/ruby/password_app.rb +8 -7
- data/examples/ruby/saml_app.rb +1 -0
- data/examples/ruby/version_check.rb +17 -0
- data/examples/ruby-on-rails-api/descope/Gemfile +9 -7
- data/examples/ruby-on-rails-api/descope/Gemfile.lock +121 -90
- data/examples/ruby-on-rails-api/descope/README.md +18 -18
- data/examples/ruby-on-rails-api/descope/app/assets/builds/application.css +20092 -23
- data/examples/ruby-on-rails-api/descope/app/assets/builds/application.js +0 -1
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/index.js +0 -14
- data/examples/ruby-on-rails-api/descope/package-lock.json +1073 -19302
- data/examples/ruby-on-rails-api/descope/package.json +8 -16
- data/examples/ruby-on-rails-api/descope/yarn.lock +557 -10641
- 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 +24 -15
- 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 +64 -32
- data/lib/descope/api/v1/management/audit.rb +24 -0
- data/lib/descope/api/v1/management/common.rb +21 -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 +6 -2
- 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/mixins/validation.rb +21 -6
- 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 +73 -8
- 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 +38 -0
- 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 +3 -1
- 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/otp_spec.rb +176 -18
- data/spec/lib.descope/api/v1/auth/password_spec.rb +10 -1
- data/spec/lib.descope/api/v1/auth_spec.rb +168 -6
- data/spec/lib.descope/api/v1/cookie_domain_fix_integration_spec.rb +245 -0
- data/spec/lib.descope/api/v1/management/audit_spec.rb +92 -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 +34 -137
- data/examples/ruby-on-rails-api/descope/app/assets/builds/reportWebVitals.js +0 -211
- data/examples/ruby-on-rails-api/descope/app/assets/builds/reportWebVitals.js.map +0 -7
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'] || []
|
|
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,11 @@ 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
|
log_level = options[:log_level] || ENV['DESCOPE_LOG_LEVEL'] || 'info'
|
|
19
|
-
@logger ||= Descope::Mixins::Logging.logger_for(self.class.name, log_level)
|
|
20
|
+
@logger ||= Descope::Mixins::Logging.logger_for(self.class.name, log_level, @project_id)
|
|
20
21
|
|
|
21
22
|
@logger.debug("Initializing Descope API with project_id: #{@project_id} and base_uri: #{@base_uri}")
|
|
22
23
|
|
|
@@ -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
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'descope/mixins/common'
|
|
4
|
+
|
|
3
5
|
module Descope
|
|
4
6
|
module Mixins
|
|
5
7
|
# Module to provide validation for specific data structures.
|
|
6
8
|
module Validation
|
|
9
|
+
include Descope::Mixins::Common
|
|
7
10
|
def validate_tenants(key_tenants)
|
|
8
11
|
raise ArgumentError, 'key_tenants should be an Array of hashes' unless key_tenants.is_a? Array
|
|
9
12
|
|
|
@@ -46,11 +49,18 @@ module Descope
|
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
def validate_phone(method, phone)
|
|
52
|
+
phone_number_is_invalid = !phone.match?(PHONE_REGEX) unless phone.nil?
|
|
53
|
+
|
|
49
54
|
raise AuthException.new('Phone number cannot be empty', code: 400) unless phone.is_a?(String) && !phone.empty?
|
|
50
|
-
raise AuthException.new(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
raise AuthException.new("Invalid pattern for phone number: #{phone}", code: 400) if phone_number_is_invalid
|
|
56
|
+
|
|
57
|
+
valid_methods = DeliveryMethod.constants.map { |constant| DeliveryMethod.const_get(constant) }
|
|
58
|
+
|
|
59
|
+
# rubocop:disable Style/LineLength
|
|
60
|
+
unless valid_methods.include?(method)
|
|
61
|
+
valid_methods_names = valid_methods.map { |m| "DeliveryMethod::#{DeliveryMethod.constants[valid_methods.index(m)]}" }.join(', ')
|
|
62
|
+
raise AuthException.new("Delivery method should be one of the following: #{valid_methods_names}", code: 400)
|
|
63
|
+
end
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
def verify_provider(oauth_provider)
|
|
@@ -64,7 +74,9 @@ module Descope
|
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
def validate_redirect_url(return_url)
|
|
67
|
-
|
|
77
|
+
return if return_url.is_a?(String) && !return_url.empty?
|
|
78
|
+
|
|
79
|
+
raise AuthException.new('Return_url cannot be empty', code: 400)
|
|
68
80
|
end
|
|
69
81
|
|
|
70
82
|
def validate_code(code)
|
|
@@ -72,7 +84,10 @@ module Descope
|
|
|
72
84
|
end
|
|
73
85
|
|
|
74
86
|
def validate_scim_group_id(group_id)
|
|
75
|
-
|
|
87
|
+
return if group_id.is_a?(String) && !group_id.empty?
|
|
88
|
+
|
|
89
|
+
raise AuthException.new('SCIM Group ID cannot be empty', code: 400)
|
|
90
|
+
|
|
76
91
|
end
|
|
77
92
|
end
|
|
78
93
|
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
|
|
@@ -60,7 +60,7 @@ describe Descope::Api::V1::Auth::EnchantedLink do
|
|
|
60
60
|
@client.logger.info('Cleaning up test users...')
|
|
61
61
|
all_users = @client.search_all_users
|
|
62
62
|
all_users['users'].each do |user|
|
|
63
|
-
if user['middleName'] ==
|
|
63
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
64
64
|
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
|
|
65
65
|
@client.delete_user(user['loginIds'][0])
|
|
66
66
|
end
|
|
@@ -11,7 +11,7 @@ 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
|
@client.delete_user(user['loginIds'][0])
|
|
17
17
|
end
|
|
@@ -5,34 +5,99 @@ require 'spec_helper'
|
|
|
5
5
|
describe Descope::Api::V1::Auth::OTP do
|
|
6
6
|
before(:all) do
|
|
7
7
|
@client = DescopeClient.new(Configuration.config)
|
|
8
|
+
|
|
9
|
+
dummy_instance = DummyClass.new
|
|
10
|
+
dummy_instance.extend(Descope::Api::V1::Session)
|
|
11
|
+
dummy_instance.extend(Descope::Api::V1::Auth::OTP)
|
|
12
|
+
@instance = dummy_instance
|
|
13
|
+
@user = build(:user)
|
|
14
|
+
@test_user = @client.create_test_user(**@user)['user']
|
|
15
|
+
@client.create_test_user(**@user)
|
|
8
16
|
end
|
|
9
17
|
|
|
10
18
|
after(:all) do
|
|
11
19
|
@client.logger.info('Cleaning up test users...')
|
|
12
20
|
all_users = @client.search_all_users
|
|
13
21
|
all_users['users'].each do |user|
|
|
14
|
-
if user['middleName'] ==
|
|
22
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
15
23
|
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
|
|
16
24
|
@client.delete_user(user['loginIds'][0])
|
|
17
25
|
end
|
|
18
26
|
end
|
|
19
27
|
end
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
test_user = @client.create_test_user(**user)['user']
|
|
25
|
-
@client.create_test_user(**user)
|
|
29
|
+
# SIGN INs
|
|
30
|
+
context 'test otp sign-in methods' do
|
|
31
|
+
it 'should sign in a new test user with otp via EMAIL' do
|
|
26
32
|
res = @client.generate_otp_for_test_user(
|
|
27
33
|
method: Descope::Mixins::Common::DeliveryMethod::EMAIL,
|
|
28
|
-
login_id: test_user['loginIds'][0]
|
|
34
|
+
login_id: @test_user['loginIds'][0]
|
|
29
35
|
)
|
|
30
36
|
@client.logger.info("res: #{res}")
|
|
31
37
|
@client.otp_verify_code(
|
|
32
38
|
method: Descope::Mixins::Common::DeliveryMethod::EMAIL,
|
|
33
|
-
login_id: user[:login_id],
|
|
39
|
+
login_id: @user[:login_id],
|
|
40
|
+
code: res['code']
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'should sign in a new test user with otp via SMS' do
|
|
45
|
+
res = @client.generate_otp_for_test_user(
|
|
46
|
+
method: Descope::Mixins::Common::DeliveryMethod::SMS,
|
|
47
|
+
login_id: @test_user['loginIds'][0]
|
|
48
|
+
)
|
|
49
|
+
@client.logger.info("res: #{res}")
|
|
50
|
+
@client.otp_verify_code(
|
|
51
|
+
method: Descope::Mixins::Common::DeliveryMethod::SMS,
|
|
52
|
+
login_id: @user[:login_id],
|
|
34
53
|
code: res['code']
|
|
35
54
|
)
|
|
36
55
|
end
|
|
37
56
|
end
|
|
57
|
+
|
|
58
|
+
# SIGN UPs
|
|
59
|
+
context 'test otp sign-up methods' do
|
|
60
|
+
it 'should sign up with otp via email' do
|
|
61
|
+
email = 'someone@example.com'
|
|
62
|
+
allow_any_instance_of(Descope::Api::V1::Auth).to receive(:extract_masked_address).and_return({})
|
|
63
|
+
expect(@instance).to receive(:post).with(
|
|
64
|
+
otp_compose_signup_url, { loginId: email, email: '' }
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect do
|
|
68
|
+
@instance.otp_sign_up(method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: email)
|
|
69
|
+
end.not_to raise_error
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'should sign up with otp via SMS' do
|
|
73
|
+
phone = '+12123354465'
|
|
74
|
+
allow_any_instance_of(Descope::Api::V1::Auth).to receive(:extract_masked_address).and_return({})
|
|
75
|
+
expect(@instance).to receive(:post).with(
|
|
76
|
+
otp_compose_signup_url(Descope::Mixins::Common::DeliveryMethod::SMS), { loginId: phone, phone: '' }
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
expect do
|
|
80
|
+
@instance.otp_sign_up(method: Descope::Mixins::Common::DeliveryMethod::SMS, login_id: phone)
|
|
81
|
+
end.not_to raise_error
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'should sign up with otp via voice' do
|
|
85
|
+
phone = '+12123354465'
|
|
86
|
+
allow_any_instance_of(Descope::Api::V1::Auth).to receive(:extract_masked_address).and_return({})
|
|
87
|
+
expect(@instance).to receive(:post).with(
|
|
88
|
+
otp_compose_signup_url(Descope::Mixins::Common::DeliveryMethod::VOICE), { loginId: phone, phone: '' }
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
expect do
|
|
92
|
+
@instance.otp_sign_up(method: Descope::Mixins::Common::DeliveryMethod::VOICE, login_id: phone)
|
|
93
|
+
end.not_to raise_error
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'should fail to signup with invalid phone number via SMS' do
|
|
97
|
+
phone = '1$234.90'
|
|
98
|
+
expect do
|
|
99
|
+
@instance.otp_sign_up(method: Descope::Mixins::Common::DeliveryMethod::SMS, login_id: phone)
|
|
100
|
+
end.to raise_error(Descope::AuthException, "Invalid pattern for phone number: #{phone}")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
38
103
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
end
|
|
9
|
+
|
|
10
|
+
after(:all) do
|
|
11
|
+
@client.logger.info('Cleaning up test users...')
|
|
12
|
+
all_users = @client.search_all_users
|
|
13
|
+
all_users['users'].each do |user|
|
|
14
|
+
if user['middleName'] == "#{SpecUtils.build_prefix}Ruby-SDK-User"
|
|
15
|
+
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
|
|
16
|
+
@client.delete_user(user['loginIds'][0])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context 'test session methods' do
|
|
22
|
+
it 'should refresh session with refresh token' do
|
|
23
|
+
@password = SpecUtils.generate_password
|
|
24
|
+
user = build(:user)
|
|
25
|
+
|
|
26
|
+
@client.logger.info('1. Sign up with password')
|
|
27
|
+
res = @client.password_sign_up(login_id: user[:login_id], password: @password, user:)
|
|
28
|
+
@client.logger.info("sign up with password res: #{res}")
|
|
29
|
+
original_refresh_token = res[REFRESH_SESSION_TOKEN_NAME]['jwt']
|
|
30
|
+
|
|
31
|
+
@client.logger.info('2. Sign in with password')
|
|
32
|
+
login_res = @client.password_sign_in(login_id: user[:login_id], password: @password)
|
|
33
|
+
@client.logger.info("sign_in res: #{login_res}")
|
|
34
|
+
|
|
35
|
+
@client.logger.info('3. sleep 1 second before calling refresh_session')
|
|
36
|
+
sleep(1)
|
|
37
|
+
|
|
38
|
+
@client.logger.info('4. Refresh session')
|
|
39
|
+
refresh_session_res = @client.refresh_session(refresh_token: login_res[REFRESH_SESSION_TOKEN_NAME]['jwt'])
|
|
40
|
+
@client.logger.info("refresh_session_res: #{refresh_session_res}")
|
|
41
|
+
|
|
42
|
+
new_refresh_token = refresh_session_res[REFRESH_SESSION_TOKEN_NAME]['jwt']
|
|
43
|
+
@client.logger.info("new_refresh_token: #{new_refresh_token}")
|
|
44
|
+
|
|
45
|
+
@client.logger.info('5. Check new refresh token is not the same as the original one')
|
|
46
|
+
expect(original_refresh_token).not_to eq(new_refresh_token)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|