descope 1.0.4 → 1.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +15 -27
- data/.github/workflows/publish-gem.yaml +61 -0
- data/Gemfile +12 -14
- data/Gemfile.lock +46 -101
- data/README.md +56 -19
- data/descope.gemspec +25 -20
- data/examples/ruby/.ruby-version +1 -0
- data/examples/ruby/access_key_app.rb +7 -4
- 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/Gemfile.lock +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 +10 -4
- 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/.gitignore +58 -28
- data/examples/ruby-on-rails-api/descope/Gemfile +3 -1
- 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/App.css +62 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/App.css.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/application.css +20131 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/application.css.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/application.js +40368 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/application.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/App.css +62 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/App.css.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/App.js +27979 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/App.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Dashboard.css +62 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Dashboard.css.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Dashboard.js +27118 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Dashboard.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Home.css +62 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Home.css.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Home.js +27113 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Home.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Login.css +62 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Login.css.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Login.js +27131 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Login.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Profile.css +62 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Profile.css.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Profile.js +27168 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Profile.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/index.css +62 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/index.css.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/index.js +28236 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/components/index.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/controllers/application.js +2456 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/controllers/application.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/controllers/index.js +2453 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/controllers/index.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/routes/index.css +62 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/routes/index.css.map +7 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/routes/index.js +27973 -0
- data/examples/ruby-on-rails-api/descope/app/assets/builds/routes/index.js.map +7 -0
- data/examples/ruby-on-rails-api/descope/package-lock.json +1021 -19307
- data/examples/ruby-on-rails-api/descope/package.json +8 -16
- data/examples/ruby-on-rails-api/descope/yarn.lock +459 -10641
- data/lib/descope/api/v1/auth/otp.rb +21 -14
- data/lib/descope/api/v1/auth.rb +37 -25
- data/lib/descope/api/v1/management/access_key.rb +5 -4
- data/lib/descope/api/v1/management/audit.rb +24 -0
- data/lib/descope/api/v1/management/common.rb +5 -1
- data/lib/descope/api/v1/management/role.rb +22 -6
- data/lib/descope/api/v1/management/user.rb +17 -0
- data/lib/descope/mixins/common.rb +6 -13
- data/lib/descope/mixins/http.rb +1 -1
- data/lib/descope/mixins/validation.rb +21 -6
- data/lib/descope/version.rb +1 -1
- data/spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb +81 -0
- data/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb +49 -0
- data/spec/integration/lib.descope/api/v1/auth/otp_spec.rb +103 -0
- data/spec/integration/lib.descope/api/v1/auth/password_spec.rb +41 -0
- data/spec/integration/lib.descope/api/v1/auth/totp_spec.rb +76 -0
- data/spec/integration/lib.descope/api/v1/management/access_key_spec.rb +62 -0
- data/spec/integration/lib.descope/api/v1/management/audit_spec.rb +52 -0
- data/spec/integration/lib.descope/api/v1/management/authz_spec.rb +187 -0
- data/spec/integration/lib.descope/api/v1/management/flow_spec.rb +44 -0
- data/spec/integration/lib.descope/api/v1/management/permissions_spec.rb +27 -0
- data/spec/integration/lib.descope/api/v1/management/project_spec.rb +29 -0
- data/spec/integration/lib.descope/api/v1/management/roles_spec.rb +116 -0
- data/spec/integration/lib.descope/api/v1/management/user_spec.rb +262 -0
- data/spec/lib.descope/api/v1/auth/otp_spec.rb +176 -18
- data/spec/lib.descope/api/v1/auth_spec.rb +50 -1
- data/spec/lib.descope/api/v1/management/access_key_spec.rb +4 -2
- data/spec/lib.descope/api/v1/management/audit_spec.rb +92 -0
- data/spec/lib.descope/api/v1/management/role_spec.rb +35 -6
- data/spec/lib.descope/api/v1/management/user_spec.rb +40 -0
- data/spec/spec_helper.rb +9 -38
- data/spec/support/client_config.rb +5 -1
- data/spec/support/dummy_class.rb +15 -1
- data/spec/support/utils.rb +1 -1
- metadata +77 -133
- data/examples/ruby-on-rails-api/descope/tmp/pids/.keep +0 -0
- data/examples/ruby-on-rails-api/descope/tmp/storage/.keep +0 -0
@@ -10,10 +10,11 @@ module Descope
|
|
10
10
|
include Descope::Mixins::Common::EndpointsV1
|
11
11
|
include Descope::Mixins::Common::EndpointsV2
|
12
12
|
|
13
|
-
def otp_sign_in(method: nil, login_id: nil, login_options: nil, refresh_token: nil,
|
13
|
+
def otp_sign_in(method: nil, login_id: nil, login_options: nil, refresh_token: nil, provider_id: nil,
|
14
14
|
template_id: nil, sso_app_id: nil)
|
15
|
-
# Sign in (log in) an existing user with the unique login_id you provide.
|
16
|
-
# login_id field
|
15
|
+
# Sign in (log in) an existing user with the unique login_id you provide.
|
16
|
+
# The login_id field is used to identify the user. It can be an email address or a phone number.
|
17
|
+
# Provide the DeliveryMethod required for this user. If the login_id value cannot be used for the
|
17
18
|
# DeliverMethod selected (for example, 'login_id = 4567qq445km' and 'DeliveryMethod = email')
|
18
19
|
validate_login_id(login_id)
|
19
20
|
uri = otp_compose_signin_url(method)
|
@@ -23,12 +24,15 @@ module Descope
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def otp_sign_up(method: nil, login_id: nil, user: {}, provider_id: nil, template_id: nil)
|
26
|
-
# Sign up (create) a new user using their email or phone number.
|
27
|
-
#
|
27
|
+
# Sign up (create) a new user using their email or phone number.
|
28
|
+
# The login_id field is used to identify the user. It can be an email address or a phone number.
|
29
|
+
# Choose a delivery method for OTP verification, for example email, SMS, or Voice.
|
28
30
|
# (optional) Include additional user metadata that you wish to preserve.
|
29
|
-
|
31
|
+
validate_login_id(login_id)
|
30
32
|
|
31
|
-
|
33
|
+
unless adjust_and_verify_delivery_method(method, login_id, user)
|
34
|
+
raise Descope::AuthException.new('Could not verify delivery method', code: 400)
|
35
|
+
end
|
32
36
|
|
33
37
|
uri = otp_compose_signup_url(method)
|
34
38
|
body = otp_compose_signup_body(method, login_id, user, provider_id, template_id)
|
@@ -38,9 +42,11 @@ module Descope
|
|
38
42
|
|
39
43
|
def otp_sign_up_or_in(method: nil, login_id: nil, login_options: nil, provider_id: nil, template_id: nil,
|
40
44
|
sso_app_id: nil)
|
41
|
-
# Sign_up_or_in lets you handle both sign up and sign in with a single call.
|
42
|
-
#
|
43
|
-
#
|
45
|
+
# Sign_up_or_in lets you handle both sign up and sign in with a single call.
|
46
|
+
# The login_id field is used to identify the user. It can be an email address or a phone number.
|
47
|
+
# Sign-up_or_in will first determine if login_id is a new or existing end user.
|
48
|
+
# If login_id is new, a new end user user will be created and then authenticated using the
|
49
|
+
# OTP DeliveryMethod specified.
|
44
50
|
# If login_id exists, the end user will be authenticated using the OTP DeliveryMethod specified.
|
45
51
|
validate_login_id(login_id)
|
46
52
|
uri = otp_compose_sign_up_or_in_url(method)
|
@@ -81,9 +87,10 @@ module Descope
|
|
81
87
|
method: nil, login_id: nil, phone: nil, refresh_token: nil, add_to_login_ids: false,
|
82
88
|
on_merge_use_existing: false, provider_id: nil, template_id: nil
|
83
89
|
)
|
84
|
-
# Update the phone number of an existing end user, after verifying the authenticity of the end user using OTP
|
90
|
+
# Update the phone number of an existing end user, after verifying the authenticity of the end user using OTP
|
85
91
|
validate_login_id(login_id)
|
86
92
|
validate_phone(method, phone)
|
93
|
+
|
87
94
|
uri = otp_compose_update_phone_url(method)
|
88
95
|
request_params = {
|
89
96
|
loginId: login_id,
|
@@ -127,7 +134,7 @@ module Descope
|
|
127
134
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
128
135
|
def otp_compose_signup_body(method, login_id, user, provider_id, template_id)
|
129
136
|
body = {
|
130
|
-
loginId: login_id
|
137
|
+
loginId: login_id
|
131
138
|
}
|
132
139
|
|
133
140
|
unless user.nil?
|
@@ -167,7 +174,8 @@ module Descope
|
|
167
174
|
end
|
168
175
|
|
169
176
|
private
|
170
|
-
def otp_user_compose_update_body(login_id: nil, name: nil, phone: nil, email: nil, given_name: nil,
|
177
|
+
def otp_user_compose_update_body(login_id: nil, name: nil, phone: nil, email: nil, given_name: nil,
|
178
|
+
middle_name: nil, family_name: nil)
|
171
179
|
user = {}
|
172
180
|
user[:loginId] = login_id if login_id
|
173
181
|
user[:name] = name if name
|
@@ -176,7 +184,6 @@ module Descope
|
|
176
184
|
user[:givenName] = given_name if given_name
|
177
185
|
user[:middleName] = middle_name if middle_name
|
178
186
|
user[:familyName] = family_name if family_name
|
179
|
-
|
180
187
|
user
|
181
188
|
end
|
182
189
|
end
|
data/lib/descope/api/v1/auth.rb
CHANGED
@@ -41,8 +41,21 @@ module Descope
|
|
41
41
|
jwt_response
|
42
42
|
end
|
43
43
|
|
44
|
-
def exchange_access_key(access_key
|
45
|
-
|
44
|
+
def exchange_access_key(access_key: nil, login_options: {}, audience: nil)
|
45
|
+
# Return a new session token for the given access key
|
46
|
+
# Args:
|
47
|
+
# access_key (str): The access key
|
48
|
+
# audience (str|Iterable[str]|nil): Optional recipients that the JWT is intended for
|
49
|
+
# (must be equal to the 'aud' claim on the provided token)
|
50
|
+
# login_options (hash): Optional advanced controls over login parameters
|
51
|
+
# Return value (Hash): returns the session token from the server together with the expiry and key id
|
52
|
+
# (sessionToken:Hash, keyId:str, expiration:int)
|
53
|
+
unless (access_key.is_a?(String) || access_key.nil?) && !access_key.to_s.empty?
|
54
|
+
raise AuthException.new('Access key should be a string!', code: 400)
|
55
|
+
end
|
56
|
+
|
57
|
+
res = post(EXCHANGE_AUTH_ACCESS_KEY_PATH, { loginOptions: login_options, audience: }, {}, access_key)
|
58
|
+
generate_auth_info(res, nil, false, audience)
|
46
59
|
end
|
47
60
|
|
48
61
|
def select_tenant(tenant_id: nil, refresh_token: nil)
|
@@ -223,16 +236,18 @@ module Descope
|
|
223
236
|
|
224
237
|
# validate the session token if sessionJwt is not empty
|
225
238
|
st_jwt = response_body.fetch('sessionJwt', '')
|
226
|
-
|
227
|
-
|
239
|
+
unless st_jwt.empty?
|
240
|
+
@logger.debug "validating session token with refresh_token: #{refresh_token}" if st_jwt
|
241
|
+
jwt_response[SESSION_TOKEN_NAME] = validate_token(st_jwt, audience) if st_jwt
|
228
242
|
end
|
229
243
|
|
230
244
|
# validate refresh token if refresh_token was passed or if refreshJwt is not empty
|
231
245
|
rt_jwt = response_body.fetch('refreshJwt', '')
|
232
246
|
|
233
|
-
if refresh_token
|
247
|
+
if !refresh_token.nil? || !refresh_token.to_s.empty?
|
248
|
+
@logger.debug "validating refresh token: #{refresh_token}" if refresh_token
|
234
249
|
jwt_response[REFRESH_SESSION_TOKEN_NAME] = validate_token(refresh_token, audience)
|
235
|
-
elsif rt_jwt
|
250
|
+
elsif !rt_jwt.empty?
|
236
251
|
jwt_response[REFRESH_SESSION_TOKEN_NAME] = validate_token(rt_jwt, audience)
|
237
252
|
end
|
238
253
|
|
@@ -392,6 +407,7 @@ module Descope
|
|
392
407
|
login_id = {
|
393
408
|
DeliveryMethod::WHATSAPP => ['whatsapp', user.fetch(:phone, '')],
|
394
409
|
DeliveryMethod::SMS => ['phone', user.fetch(:phone, '')],
|
410
|
+
DeliveryMethod::VOICE => ['phone', user.fetch(:phone, '')],
|
395
411
|
DeliveryMethod::EMAIL => ['email', user.fetch(:email, '')]
|
396
412
|
}[method]
|
397
413
|
|
@@ -401,34 +417,30 @@ module Descope
|
|
401
417
|
end
|
402
418
|
|
403
419
|
def adjust_and_verify_delivery_method(method, login_id, user)
|
404
|
-
|
420
|
+
@logger.debug("adjust_and_verify_delivery_method: method: #{method}, login_id: #{login_id}, user: #{user}")
|
421
|
+
raise AuthException.new("Could not verify delivery method for method: #{method}", code: 400) if method.nil?
|
422
|
+
raise AuthException.new('Could not verify delivery method without login_id', code: 400) if login_id.nil?
|
405
423
|
|
406
|
-
|
424
|
+
unless user.is_a?(Hash)
|
425
|
+
raise AuthException.new('Could not verify delivery method, user is not a Hash', code: 400)
|
426
|
+
end
|
407
427
|
|
408
428
|
case method
|
409
429
|
when DeliveryMethod::EMAIL
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
when DeliveryMethod::SMS
|
418
|
-
user[:phone] ||= login_id
|
419
|
-
return false unless /^#{PHONE_REGEX}$/.match(user[:phone])
|
420
|
-
when DeliveryMethod::WHATSAPP
|
421
|
-
user[:phone] ||= login_id
|
422
|
-
return false unless /^#{PHONE_REGEX}$/.match(user[:phone])
|
430
|
+
validate_email(login_id)
|
431
|
+
@logger.debug("email: #{login_id} is valid")
|
432
|
+
true
|
433
|
+
when DeliveryMethod::SMS, DeliveryMethod::WHATSAPP, DeliveryMethod::VOICE
|
434
|
+
validate_phone(method, login_id)
|
435
|
+
@logger.debug("phone number (login_id): #{login_id} is valid")
|
436
|
+
true
|
423
437
|
else
|
424
|
-
|
438
|
+
false
|
425
439
|
end
|
426
|
-
|
427
|
-
true
|
428
440
|
end
|
429
441
|
|
430
442
|
def extract_masked_address(response, method)
|
431
|
-
if [DeliveryMethod::SMS, DeliveryMethod::WHATSAPP].include?(method)
|
443
|
+
if [DeliveryMethod::SMS, DeliveryMethod::WHATSAPP, DeliveryMethod::VOICE].include?(method)
|
432
444
|
response['maskedPhone']
|
433
445
|
elsif method == DeliveryMethod::EMAIL
|
434
446
|
response['maskedEmail']
|
@@ -9,22 +9,23 @@ module Descope
|
|
9
9
|
include Descope::Mixins::Validation
|
10
10
|
include Descope::Api::V1::Management::Common
|
11
11
|
|
12
|
-
def create_access_key(name: nil, expire_time: nil, role_names: nil, key_tenants: nil)
|
12
|
+
def create_access_key(name: nil, expire_time: nil, role_names: nil, key_tenants: nil, custom_claims: nil)
|
13
13
|
# Create a new access key.'
|
14
14
|
# @see https://docs.descope.com/api/openapi/accesskeymanagement/operation/CreateAccessKey/
|
15
15
|
|
16
16
|
role_names ||= []
|
17
17
|
key_tenants ||= []
|
18
18
|
validate_tenants(key_tenants)
|
19
|
-
post(ACCESS_KEY_CREATE_PATH, access_key_compose_create_body(name, expire_time, role_names, key_tenants))
|
19
|
+
post(ACCESS_KEY_CREATE_PATH, access_key_compose_create_body(name, expire_time, role_names, key_tenants, custom_claims))
|
20
20
|
end
|
21
21
|
|
22
|
-
def access_key_compose_create_body(name, expire_time, role_names, key_tenants)
|
22
|
+
def access_key_compose_create_body(name, expire_time, role_names, key_tenants, custom_claims)
|
23
23
|
{
|
24
24
|
name:,
|
25
25
|
expireTime: expire_time,
|
26
26
|
roleNames: role_names,
|
27
|
-
keyTenants: associated_tenants_to_hash_array(key_tenants)
|
27
|
+
keyTenants: associated_tenants_to_hash_array(key_tenants),
|
28
|
+
customClaims: custom_claims
|
28
29
|
}
|
29
30
|
end
|
30
31
|
|
@@ -58,6 +58,30 @@ module Descope
|
|
58
58
|
{ 'audits' => res['audits'].map { |audit| convert_audit_record(audit) } }
|
59
59
|
end
|
60
60
|
|
61
|
+
def audit_create_event(action: nil, type: nil, data: nil, user_id: nil, actor_id: nil, tenant_id: nil)
|
62
|
+
# Create an audit event
|
63
|
+
unless %w[info warn error].include?(type)
|
64
|
+
raise Descope::AuthException, 'type must be either info, warn or error'
|
65
|
+
end
|
66
|
+
|
67
|
+
# validation
|
68
|
+
raise Descope::AuthException, 'data must be provided as a key, value Hash' unless data.is_a?(Hash)
|
69
|
+
raise Descope::AuthException, 'action must be provided' if action.nil?
|
70
|
+
raise Descope::AuthException, 'actor_id must be provided' if actor_id.nil?
|
71
|
+
raise Descope::AuthException, 'tenant_id must be provided' if tenant_id.nil?
|
72
|
+
|
73
|
+
request_params = {
|
74
|
+
action:,
|
75
|
+
tenantId: tenant_id,
|
76
|
+
type:,
|
77
|
+
actorId: actor_id,
|
78
|
+
data:
|
79
|
+
}
|
80
|
+
request_params[:userId] = user_id unless user_id.nil?
|
81
|
+
|
82
|
+
post(AUDIT_CREATE_EVENT, request_params)
|
83
|
+
end
|
84
|
+
|
61
85
|
private
|
62
86
|
|
63
87
|
def convert_audit_record(audit)
|
@@ -15,7 +15,7 @@ module Descope
|
|
15
15
|
TENANT_SEARCH_ALL_PATH = '/v1/mgmt/tenant/search'
|
16
16
|
PASSWORD_SETTINGS_PATH = '/v1/mgmt/password/settings'
|
17
17
|
|
18
|
-
#
|
18
|
+
# user
|
19
19
|
USER_CREATE_PATH = '/v1/mgmt/user/create'
|
20
20
|
USER_CREATE_BATCH_PATH = '/v1/mgmt/user/create/batch'
|
21
21
|
USER_UPDATE_PATH = '/v1/mgmt/user/update'
|
@@ -34,6 +34,8 @@ module Descope
|
|
34
34
|
USER_UPDATE_CUSTOM_ATTRIBUTE_PATH = '/v1/mgmt/user/update/customAttribute'
|
35
35
|
USER_ADD_ROLE_PATH = '/v1/mgmt/user/update/role/add'
|
36
36
|
USER_REMOVE_ROLE_PATH = '/v1/mgmt/user/update/role/remove'
|
37
|
+
USER_SET_TEMPORARY_PASSWORD_PATH = '/v1/mgmt/user/password/set/temporary'
|
38
|
+
USER_SET_ACTIVE_PASSWORD_PATH = '/v1/mgmt/user/password/set/active'
|
37
39
|
USER_SET_PASSWORD_PATH = '/v1/mgmt/user/password/set'
|
38
40
|
USER_EXPIRE_PASSWORD_PATH = '/v1/mgmt/user/password/expire'
|
39
41
|
USER_ADD_TENANT_PATH = '/v1/mgmt/user/update/tenant/add'
|
@@ -80,6 +82,7 @@ module Descope
|
|
80
82
|
ROLE_UPDATE_PATH = '/v1/mgmt/role/update'
|
81
83
|
ROLE_DELETE_PATH = '/v1/mgmt/role/delete'
|
82
84
|
ROLE_LOAD_ALL_PATH = '/v1/mgmt/role/all'
|
85
|
+
ROLE_SEARCH_PATH = '/v1/mgmt/role/search'
|
83
86
|
|
84
87
|
# flow
|
85
88
|
FLOW_LIST_PATH = '/v1/mgmt/flow/list'
|
@@ -97,6 +100,7 @@ module Descope
|
|
97
100
|
|
98
101
|
# Audit
|
99
102
|
AUDIT_SEARCH = '/v1/mgmt/audit/search'
|
103
|
+
AUDIT_CREATE_EVENT = '/v1/mgmt/audit/event'
|
100
104
|
|
101
105
|
# Authz ReBAC
|
102
106
|
AUTHZ_SCHEMA_SAVE = '/v1/mgmt/authz/schema/save'
|
@@ -8,18 +8,19 @@ module Descope
|
|
8
8
|
module Role
|
9
9
|
include Descope::Api::V1::Management::Common
|
10
10
|
|
11
|
-
def create_role(name: nil, description: nil, permission_names: nil)
|
11
|
+
def create_role(name: nil, description: nil, permission_names: nil, tenant_id: nil)
|
12
12
|
# Create a new role.
|
13
13
|
permission_names ||= []
|
14
14
|
request_params = {
|
15
15
|
name:,
|
16
16
|
description:,
|
17
|
-
permissionNames: permission_names
|
17
|
+
permissionNames: permission_names,
|
18
|
+
tenantId: tenant_id
|
18
19
|
}
|
19
20
|
post(ROLE_CREATE_PATH, request_params)
|
20
21
|
end
|
21
22
|
|
22
|
-
def update_role(name: nil, new_name: nil, description: nil, permission_names: nil)
|
23
|
+
def update_role(name: nil, new_name: nil, description: nil, permission_names: nil, tenant_id: nil)
|
23
24
|
# Update an existing role with the given various fields. IMPORTANT: All parameters are used as overrides
|
24
25
|
# to the existing role. Empty fields will override populated fields. Use carefully.
|
25
26
|
permission_names ||= []
|
@@ -27,20 +28,35 @@ module Descope
|
|
27
28
|
name:,
|
28
29
|
newName: new_name,
|
29
30
|
description:,
|
30
|
-
permissionNames: permission_names
|
31
|
+
permissionNames: permission_names,
|
32
|
+
tenantId: tenant_id
|
31
33
|
}
|
32
34
|
post(ROLE_UPDATE_PATH, request_params)
|
33
35
|
end
|
34
36
|
|
35
|
-
def delete_role(name)
|
37
|
+
def delete_role(name: nil, tenant_id: nil)
|
36
38
|
# Delete an existing role. IMPORTANT: This action is irreversible. Use carefully.
|
37
|
-
|
39
|
+
raise Descope::ArgumentError, 'name is required' if name.nil? || name.empty?
|
40
|
+
|
41
|
+
request_params = { name: }
|
42
|
+
request_params[:tenantId] = tenant_id if tenant_id
|
43
|
+
post(ROLE_DELETE_PATH, request_params)
|
38
44
|
end
|
39
45
|
|
40
46
|
def load_all_roles
|
41
47
|
# Load all roles.
|
42
48
|
get(ROLE_LOAD_ALL_PATH)
|
43
49
|
end
|
50
|
+
|
51
|
+
def search_roles(role_names: nil, tenant_ids: nil, role_name_like: nil, permission_names: nil)
|
52
|
+
# Search for roles using the given parameters.
|
53
|
+
request_params = {}
|
54
|
+
request_params[:roleNames] = role_names if role_names
|
55
|
+
request_params[:tenantIds] = tenant_ids if tenant_ids
|
56
|
+
request_params[:roleNameLike] = role_name_like if role_name_like
|
57
|
+
request_params[:permissionNames] = permission_names if permission_names
|
58
|
+
post(ROLE_SEARCH_PATH, request_params)
|
59
|
+
end
|
44
60
|
end
|
45
61
|
end
|
46
62
|
end
|
@@ -385,6 +385,23 @@ module Descope
|
|
385
385
|
post(Common::USER_REMOVE_TENANT_PATH, body)
|
386
386
|
end
|
387
387
|
|
388
|
+
def set_temporary_password(login_id: nil, password: nil)
|
389
|
+
body = {
|
390
|
+
loginId: login_id,
|
391
|
+
password:
|
392
|
+
}
|
393
|
+
post(Common::USER_SET_TEMPORARY_PASSWORD_PATH, body)
|
394
|
+
end
|
395
|
+
|
396
|
+
def set_active_password(login_id: nil, password: nil)
|
397
|
+
body = {
|
398
|
+
loginId: login_id,
|
399
|
+
password:
|
400
|
+
}
|
401
|
+
post(Common::USER_SET_ACTIVE_PASSWORD_PATH, body)
|
402
|
+
end
|
403
|
+
|
404
|
+
# Deprecated (use set_temporary_password(..) instead)
|
388
405
|
def set_password(login_id: nil, password: nil)
|
389
406
|
body = {
|
390
407
|
loginId: login_id,
|
@@ -9,7 +9,8 @@ module Descope
|
|
9
9
|
DEFAULT_BASE_URL = 'https://api.descope.com' # pragma: no cover
|
10
10
|
DEFAULT_TIMEOUT_SECONDS = 60
|
11
11
|
DEFAULT_JWT_VALIDATION_LEEWAY = 5
|
12
|
-
|
12
|
+
# Using E164 format,\A and \z are start and end of string respectively, to prevent multiline matching
|
13
|
+
PHONE_REGEX = /\A\+[1-9]\d{1,14}\z/
|
13
14
|
|
14
15
|
SESSION_COOKIE_NAME = 'DS'
|
15
16
|
REFRESH_SESSION_COOKIE_NAME = 'DSR'
|
@@ -24,13 +25,15 @@ module Descope
|
|
24
25
|
WHATSAPP = 1
|
25
26
|
SMS = 2
|
26
27
|
EMAIL = 3
|
28
|
+
VOICE = 4
|
27
29
|
end
|
28
30
|
|
29
31
|
def get_method_string(method)
|
30
32
|
name = {
|
31
33
|
DeliveryMethod::WHATSAPP => 'whatsapp',
|
32
34
|
DeliveryMethod::SMS => 'sms',
|
33
|
-
DeliveryMethod::EMAIL => 'email'
|
35
|
+
DeliveryMethod::EMAIL => 'email',
|
36
|
+
DeliveryMethod::VOICE => 'voice'
|
34
37
|
}[method]
|
35
38
|
|
36
39
|
raise ArgumentException, "Unknown delivery method: #{method}" if name.nil?
|
@@ -50,7 +53,7 @@ module Descope
|
|
50
53
|
VALIDATE_SESSION_PATH = '/v1/auth/validate'
|
51
54
|
ME_PATH = '/v1/auth/me'
|
52
55
|
|
53
|
-
#
|
56
|
+
# access key
|
54
57
|
EXCHANGE_AUTH_ACCESS_KEY_PATH = '/v1/auth/accesskey/exchange'
|
55
58
|
|
56
59
|
# otp
|
@@ -114,16 +117,6 @@ module Descope
|
|
114
117
|
module EndpointsV2
|
115
118
|
PUBLIC_KEY_PATH = '/v2/keys'
|
116
119
|
end
|
117
|
-
|
118
|
-
module LoginOptions
|
119
|
-
attr_accessor :stepup, :mfa, :custom_claims
|
120
|
-
|
121
|
-
def initialize
|
122
|
-
@stepup = stepup || false
|
123
|
-
@mfa ||= false
|
124
|
-
@custom_claims ||= {}
|
125
|
-
end
|
126
|
-
end
|
127
120
|
end
|
128
121
|
end
|
129
122
|
end
|
data/lib/descope/mixins/http.rb
CHANGED
@@ -96,7 +96,7 @@ module Descope
|
|
96
96
|
|
97
97
|
raise Descope::Unsupported.new("No response from server", code: 400) unless result && result.respond_to?(:code)
|
98
98
|
|
99
|
-
@logger.info
|
99
|
+
@logger.info("API Request: [#{method}] #{uri} - Response Code: #{result.code}")
|
100
100
|
case result.code
|
101
101
|
when 200...226 then safe_parse_json(result.body)
|
102
102
|
when 400 then raise Descope::BadRequest.new(result.body, code: result.code, headers: result.headers)
|
@@ -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,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
def poll_for_session(descope_client, pending_ref)
|
6
|
+
max_tries = 15
|
7
|
+
i = 0
|
8
|
+
done = false
|
9
|
+
while !done && i < max_tries
|
10
|
+
begin
|
11
|
+
i += 1
|
12
|
+
@client.logger.info('waiting 4 seconds for session to be created...')
|
13
|
+
sleep(4)
|
14
|
+
print '.'
|
15
|
+
@client.logger.info("Getting session for pending_ref: #{pending_ref}...")
|
16
|
+
jwt_response = descope_client.enchanted_link_get_session(pending_ref)
|
17
|
+
done = true
|
18
|
+
rescue Descope::AuthException, Descope::Unauthorized => e
|
19
|
+
@client.logger.info("Failed pending session, err: #{e}")
|
20
|
+
nil
|
21
|
+
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
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def verify_session(descope_client: nil, res: nil, user: nil)
|
35
|
+
raise StandardError, 'Missing required parameters' if descope_client.nil? || res.nil? || user.nil?
|
36
|
+
|
37
|
+
token = res['link'].match(/.+verify\?t=(.+)/)[1]
|
38
|
+
@client.logger.info("token: #{token}")
|
39
|
+
|
40
|
+
expect do
|
41
|
+
descope_client.enchanted_link_verify_token(token)
|
42
|
+
@client.logger.info('EnchantedLink Token Verified! now getting session information...')
|
43
|
+
@client.logger.info('Polling for session...')
|
44
|
+
refresh_token = poll_for_session(descope_client, res['pendingRef'])
|
45
|
+
my_details = descope_client.me(refresh_token)
|
46
|
+
expect(my_details['email']).to eq(user['email'])
|
47
|
+
@client.logger.info('EnchantedLink Token Verified via sign in!')
|
48
|
+
rescue StandardError => e
|
49
|
+
raise StandardError, "Verification failed - Could not verify token #{e.message}"
|
50
|
+
|
51
|
+
end.to_not raise_error
|
52
|
+
end
|
53
|
+
|
54
|
+
describe Descope::Api::V1::Auth::EnchantedLink do
|
55
|
+
before(:all) do
|
56
|
+
@client = DescopeClient.new(Configuration.config)
|
57
|
+
end
|
58
|
+
|
59
|
+
after(:all) do
|
60
|
+
@client.logger.info('Cleaning up test users...')
|
61
|
+
all_users = @client.search_all_users
|
62
|
+
all_users['users'].each do |user|
|
63
|
+
if user['middleName'] == 'Ruby SDK User'
|
64
|
+
@client.logger.info("Deleting ruby spec test user #{user['loginIds'][0]}")
|
65
|
+
@client.delete_user(user['loginIds'][0])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'test EnchantedLink for test user' do
|
71
|
+
it 'should sign in with enchanted link' do
|
72
|
+
user = build(:user)
|
73
|
+
test_user = @client.create_test_user(**user)['user']
|
74
|
+
@client.logger.info("Should sign in a test user => #{test_user['loginIds'][0]} with enchanted link...")
|
75
|
+
res = @client.generate_enchanted_link_for_test_user(login_id: test_user['loginIds'][0], uri: 'http://localhost:3000/verify')
|
76
|
+
@client.logger.info("res: #{res}")
|
77
|
+
@client.logger.info('Verifying session...')
|
78
|
+
verify_session(descope_client: @client, res:, user: test_user)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Descope::Api::V1::Auth::MagicLink 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'] == '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 Magiclink for test user' do
|
22
|
+
it 'should sign in with magiclink' do
|
23
|
+
user = build(:user)
|
24
|
+
test_user = @client.create_test_user(**user)['user']
|
25
|
+
@client.create_test_user(**user)
|
26
|
+
res = @client.generate_magic_link_for_test_user(
|
27
|
+
method: Descope::Mixins::Common::DeliveryMethod::EMAIL,
|
28
|
+
login_id: test_user['loginIds'][0],
|
29
|
+
uri: 'http://localhost:3000/verify'
|
30
|
+
)
|
31
|
+
@client.logger.info("res: #{res}")
|
32
|
+
token = res['link'].match(/^http.+verify\?t=(.+)/)[1]
|
33
|
+
@client.logger.info("token: #{token}")
|
34
|
+
|
35
|
+
expect do
|
36
|
+
@client.logger.info('Verifying token...')
|
37
|
+
jwt_response = @client.magiclink_verify_token(token)
|
38
|
+
@client.logger.info("jwt_response #{jwt_response}")
|
39
|
+
my_details = @client.me(jwt_response['refreshSessionToken']['jwt'])
|
40
|
+
@client.logger.info('verifying session...')
|
41
|
+
expect(my_details['email']).to eq(test_user['email'])
|
42
|
+
@client.logger.info('Magiclink Token Verified via sign in!')
|
43
|
+
rescue StandardError => e
|
44
|
+
raise StandardError, "Verification failed - Could not verify token: #{e.message}"
|
45
|
+
|
46
|
+
end.to_not raise_error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|