descope 1.0.4 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +15 -27
  3. data/.github/workflows/publish-gem.yaml +61 -0
  4. data/Gemfile +12 -14
  5. data/Gemfile.lock +46 -101
  6. data/README.md +56 -19
  7. data/descope.gemspec +25 -20
  8. data/examples/ruby/.ruby-version +1 -0
  9. data/examples/ruby/access_key_app.rb +7 -4
  10. data/examples/ruby/enchantedlink_app.rb +1 -0
  11. data/examples/ruby/magiclink_app.rb +1 -0
  12. data/examples/ruby/management/.ruby-version +1 -0
  13. data/examples/ruby/management/Gemfile +2 -2
  14. data/examples/ruby/management/Gemfile.lock +2 -2
  15. data/examples/ruby/management/access_key_app.rb +2 -0
  16. data/examples/ruby/management/audit_app.rb +32 -8
  17. data/examples/ruby/management/authz_app.rb +1 -0
  18. data/examples/ruby/management/flow_app.rb +1 -0
  19. data/examples/ruby/management/permission_app.rb +3 -2
  20. data/examples/ruby/management/role_app.rb +10 -4
  21. data/examples/ruby/management/tenant_app.rb +1 -0
  22. data/examples/ruby/management/user_app.rb +1 -0
  23. data/examples/ruby/oauth_app.rb +1 -0
  24. data/examples/ruby/otp_app.rb +38 -12
  25. data/examples/ruby/password_app.rb +8 -7
  26. data/examples/ruby/saml_app.rb +1 -0
  27. data/examples/ruby/version_check.rb +17 -0
  28. data/examples/ruby-on-rails-api/descope/.gitignore +58 -28
  29. data/examples/ruby-on-rails-api/descope/Gemfile +3 -1
  30. data/examples/ruby-on-rails-api/descope/Gemfile.lock +121 -90
  31. data/examples/ruby-on-rails-api/descope/README.md +18 -18
  32. data/examples/ruby-on-rails-api/descope/app/assets/builds/App.css +62 -0
  33. data/examples/ruby-on-rails-api/descope/app/assets/builds/App.css.map +7 -0
  34. data/examples/ruby-on-rails-api/descope/app/assets/builds/application.css +20131 -0
  35. data/examples/ruby-on-rails-api/descope/app/assets/builds/application.css.map +7 -0
  36. data/examples/ruby-on-rails-api/descope/app/assets/builds/application.js +40368 -0
  37. data/examples/ruby-on-rails-api/descope/app/assets/builds/application.js.map +7 -0
  38. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/App.css +62 -0
  39. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/App.css.map +7 -0
  40. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/App.js +27979 -0
  41. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/App.js.map +7 -0
  42. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Dashboard.css +62 -0
  43. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Dashboard.css.map +7 -0
  44. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Dashboard.js +27118 -0
  45. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Dashboard.js.map +7 -0
  46. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Home.css +62 -0
  47. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Home.css.map +7 -0
  48. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Home.js +27113 -0
  49. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Home.js.map +7 -0
  50. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Login.css +62 -0
  51. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Login.css.map +7 -0
  52. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Login.js +27131 -0
  53. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Login.js.map +7 -0
  54. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Profile.css +62 -0
  55. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Profile.css.map +7 -0
  56. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Profile.js +27168 -0
  57. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/Profile.js.map +7 -0
  58. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/index.css +62 -0
  59. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/index.css.map +7 -0
  60. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/index.js +28236 -0
  61. data/examples/ruby-on-rails-api/descope/app/assets/builds/components/index.js.map +7 -0
  62. data/examples/ruby-on-rails-api/descope/app/assets/builds/controllers/application.js +2456 -0
  63. data/examples/ruby-on-rails-api/descope/app/assets/builds/controllers/application.js.map +7 -0
  64. data/examples/ruby-on-rails-api/descope/app/assets/builds/controllers/index.js +2453 -0
  65. data/examples/ruby-on-rails-api/descope/app/assets/builds/controllers/index.js.map +7 -0
  66. data/examples/ruby-on-rails-api/descope/app/assets/builds/routes/index.css +62 -0
  67. data/examples/ruby-on-rails-api/descope/app/assets/builds/routes/index.css.map +7 -0
  68. data/examples/ruby-on-rails-api/descope/app/assets/builds/routes/index.js +27973 -0
  69. data/examples/ruby-on-rails-api/descope/app/assets/builds/routes/index.js.map +7 -0
  70. data/examples/ruby-on-rails-api/descope/package-lock.json +1021 -19307
  71. data/examples/ruby-on-rails-api/descope/package.json +8 -16
  72. data/examples/ruby-on-rails-api/descope/yarn.lock +459 -10641
  73. data/lib/descope/api/v1/auth/otp.rb +21 -14
  74. data/lib/descope/api/v1/auth.rb +37 -25
  75. data/lib/descope/api/v1/management/access_key.rb +5 -4
  76. data/lib/descope/api/v1/management/audit.rb +24 -0
  77. data/lib/descope/api/v1/management/common.rb +5 -1
  78. data/lib/descope/api/v1/management/role.rb +22 -6
  79. data/lib/descope/api/v1/management/user.rb +17 -0
  80. data/lib/descope/mixins/common.rb +6 -13
  81. data/lib/descope/mixins/http.rb +1 -1
  82. data/lib/descope/mixins/validation.rb +21 -6
  83. data/lib/descope/version.rb +1 -1
  84. data/spec/integration/lib.descope/api/v1/auth/enchantedlink_spec.rb +81 -0
  85. data/spec/integration/lib.descope/api/v1/auth/magiclink_spec.rb +49 -0
  86. data/spec/integration/lib.descope/api/v1/auth/otp_spec.rb +103 -0
  87. data/spec/integration/lib.descope/api/v1/auth/password_spec.rb +41 -0
  88. data/spec/integration/lib.descope/api/v1/auth/totp_spec.rb +76 -0
  89. data/spec/integration/lib.descope/api/v1/management/access_key_spec.rb +62 -0
  90. data/spec/integration/lib.descope/api/v1/management/audit_spec.rb +52 -0
  91. data/spec/integration/lib.descope/api/v1/management/authz_spec.rb +187 -0
  92. data/spec/integration/lib.descope/api/v1/management/flow_spec.rb +44 -0
  93. data/spec/integration/lib.descope/api/v1/management/permissions_spec.rb +27 -0
  94. data/spec/integration/lib.descope/api/v1/management/project_spec.rb +29 -0
  95. data/spec/integration/lib.descope/api/v1/management/roles_spec.rb +116 -0
  96. data/spec/integration/lib.descope/api/v1/management/user_spec.rb +262 -0
  97. data/spec/lib.descope/api/v1/auth/otp_spec.rb +176 -18
  98. data/spec/lib.descope/api/v1/auth_spec.rb +50 -1
  99. data/spec/lib.descope/api/v1/management/access_key_spec.rb +4 -2
  100. data/spec/lib.descope/api/v1/management/audit_spec.rb +92 -0
  101. data/spec/lib.descope/api/v1/management/role_spec.rb +35 -6
  102. data/spec/lib.descope/api/v1/management/user_spec.rb +40 -0
  103. data/spec/spec_helper.rb +9 -38
  104. data/spec/support/client_config.rb +5 -1
  105. data/spec/support/dummy_class.rb +15 -1
  106. data/spec/support/utils.rb +1 -1
  107. metadata +77 -133
  108. data/examples/ruby-on-rails-api/descope/tmp/pids/.keep +0 -0
  109. 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, provider_id: 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. (See 'sign_up' function for an explanation of the
16
- # login_id field.) Provide the DeliveryMethod required for this user. If the login_id value cannot be used for the
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. Choose a delivery method for OTP
27
- # verification, for example email, SMS, or WhatsApp.
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
- user ||= {}
31
+ validate_login_id(login_id)
30
32
 
31
- raise AuthException unless adjust_and_verify_delivery_method(method, login_id, user)
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. Sign-up_or_in will first
42
- # determine if login_id is a new or existing end user. If login_id is new, a new end user user will be
43
- # created and then authenticated using the OTP DeliveryMethod specified.
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, middle_name: nil, family_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
@@ -41,8 +41,21 @@ module Descope
41
41
  jwt_response
42
42
  end
43
43
 
44
- def exchange_access_key(access_key = nil)
45
- post(EXCHANGE_AUTH_ACCESS_KEY_PATH, {}, {}, access_key)
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
- if st_jwt
227
- jwt_response[SESSION_TOKEN_NAME] = validate_token(st_jwt, audience)
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
- return false if login_id.nil?
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
- return false unless user.is_a?(Hash)
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
- user[:email] ||= login_id
411
- begin
412
- validate_email(user[:email])
413
- return true
414
- rescue AuthException
415
- return false
416
- end
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
- return false
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
- # userUSER_CREATE_PATH
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
- post(ROLE_DELETE_PATH, { name: })
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
- PHONE_REGEX = %r{^(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[-./ \\]?(?:(?:\(?\d{1,}\)?[-./ \\]?){0,})(?:[-./ \\]?(?:#|ext\.?|extension|x)[-./ \\]?(\d+))?$}.freeze
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
- # accesskey
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
@@ -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 "http status code: #{result.code}"
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('Invalid phone number', code: 400) unless phone.match?(PHONE_REGEX)
51
- raise AuthException.new('Invalid delivery method', code: 400) unless [
52
- DeliveryMethod::WHATSAPP, DeliveryMethod::SMS
53
- ].include?(method)
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
- raise AuthException.new('Return_url cannot be empty', code: 400) unless return_url.is_a?(String) && !return_url.empty?
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
- raise AuthException.new('SCIM Group ID cannot be empty', code: 400) unless group_id.is_a?(String) && !group_id.empty?
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  # Current version of gem
4
4
  module Descope
5
- VERSION = '1.0.4'
5
+ VERSION = '1.0.6'
6
6
  SDK_VERSION = '1.0.0'
7
7
  end
@@ -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