descope 1.0.4

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.
Files changed (197) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yaml +54 -0
  3. data/.gitignore +59 -0
  4. data/.release-please-manifest.json +3 -0
  5. data/.rubocop.yml +10 -0
  6. data/.rubocop_todo.yml +10 -0
  7. data/.ruby-version +1 -0
  8. data/CHANGELOG.md +90 -0
  9. data/Gemfile +22 -0
  10. data/Gemfile.lock +204 -0
  11. data/LICENSE +21 -0
  12. data/README.md +1171 -0
  13. data/Rakefile +31 -0
  14. data/descope.gemspec +34 -0
  15. data/examples/ruby/Gemfile +4 -0
  16. data/examples/ruby/Gemfile.lock +41 -0
  17. data/examples/ruby/access_key_app.rb +45 -0
  18. data/examples/ruby/enchantedlink_app.rb +65 -0
  19. data/examples/ruby/magiclink_app.rb +81 -0
  20. data/examples/ruby/management/Gemfile +5 -0
  21. data/examples/ruby/management/Gemfile.lock +38 -0
  22. data/examples/ruby/management/access_key_app.rb +71 -0
  23. data/examples/ruby/management/audit_app.rb +25 -0
  24. data/examples/ruby/management/authz_app.rb +135 -0
  25. data/examples/ruby/management/authz_files.json +229 -0
  26. data/examples/ruby/management/flow_app.rb +57 -0
  27. data/examples/ruby/management/permission_app.rb +56 -0
  28. data/examples/ruby/management/role_app.rb +58 -0
  29. data/examples/ruby/management/tenant_app.rb +60 -0
  30. data/examples/ruby/management/user_app.rb +60 -0
  31. data/examples/ruby/oauth_app.rb +39 -0
  32. data/examples/ruby/otp_app.rb +50 -0
  33. data/examples/ruby/password_app.rb +76 -0
  34. data/examples/ruby/saml_app.rb +38 -0
  35. data/examples/ruby-on-rails-api/descope/.dockerignore +37 -0
  36. data/examples/ruby-on-rails-api/descope/.gitattributes +9 -0
  37. data/examples/ruby-on-rails-api/descope/.gitignore +40 -0
  38. data/examples/ruby-on-rails-api/descope/.node-version +1 -0
  39. data/examples/ruby-on-rails-api/descope/.ruby-version +1 -0
  40. data/examples/ruby-on-rails-api/descope/Dockerfile +75 -0
  41. data/examples/ruby-on-rails-api/descope/Gemfile +67 -0
  42. data/examples/ruby-on-rails-api/descope/Gemfile.lock +284 -0
  43. data/examples/ruby-on-rails-api/descope/Procfile.dev +3 -0
  44. data/examples/ruby-on-rails-api/descope/README.md +54 -0
  45. data/examples/ruby-on-rails-api/descope/Rakefile +6 -0
  46. data/examples/ruby-on-rails-api/descope/app/assets/builds/.keep +0 -0
  47. data/examples/ruby-on-rails-api/descope/app/assets/config/manifest.js +3 -0
  48. data/examples/ruby-on-rails-api/descope/app/assets/images/.keep +0 -0
  49. data/examples/ruby-on-rails-api/descope/app/assets/images/descope.jpeg +0 -0
  50. data/examples/ruby-on-rails-api/descope/app/assets/images/favicon.ico +0 -0
  51. data/examples/ruby-on-rails-api/descope/app/assets/images/logo192.png +0 -0
  52. data/examples/ruby-on-rails-api/descope/app/assets/images/logo512.png +0 -0
  53. data/examples/ruby-on-rails-api/descope/app/assets/stylesheets/application.bootstrap.scss +67 -0
  54. data/examples/ruby-on-rails-api/descope/app/channels/application_cable/channel.rb +4 -0
  55. data/examples/ruby-on-rails-api/descope/app/channels/application_cable/connection.rb +4 -0
  56. data/examples/ruby-on-rails-api/descope/app/controllers/application_controller.rb +2 -0
  57. data/examples/ruby-on-rails-api/descope/app/controllers/concerns/.keep +0 -0
  58. data/examples/ruby-on-rails-api/descope/app/controllers/homepage_controller.rb +4 -0
  59. data/examples/ruby-on-rails-api/descope/app/controllers/session_controller.rb +66 -0
  60. data/examples/ruby-on-rails-api/descope/app/helpers/application_helper.rb +2 -0
  61. data/examples/ruby-on-rails-api/descope/app/helpers/homepage_helper.rb +2 -0
  62. data/examples/ruby-on-rails-api/descope/app/helpers/session_helper.rb +2 -0
  63. data/examples/ruby-on-rails-api/descope/app/javascript/App.css +53 -0
  64. data/examples/ruby-on-rails-api/descope/app/javascript/application.js +5 -0
  65. data/examples/ruby-on-rails-api/descope/app/javascript/components/App.jsx +4 -0
  66. data/examples/ruby-on-rails-api/descope/app/javascript/components/Dashboard.jsx +60 -0
  67. data/examples/ruby-on-rails-api/descope/app/javascript/components/Home.jsx +27 -0
  68. data/examples/ruby-on-rails-api/descope/app/javascript/components/Login.jsx +45 -0
  69. data/examples/ruby-on-rails-api/descope/app/javascript/components/Profile.jsx +81 -0
  70. data/examples/ruby-on-rails-api/descope/app/javascript/components/index.html +11 -0
  71. data/examples/ruby-on-rails-api/descope/app/javascript/components/index.jsx +24 -0
  72. data/examples/ruby-on-rails-api/descope/app/javascript/controllers/application.js +9 -0
  73. data/examples/ruby-on-rails-api/descope/app/javascript/controllers/index.js +5 -0
  74. data/examples/ruby-on-rails-api/descope/app/javascript/reportWebVitals.js +13 -0
  75. data/examples/ruby-on-rails-api/descope/app/javascript/routes/index.jsx +17 -0
  76. data/examples/ruby-on-rails-api/descope/app/jobs/application_job.rb +7 -0
  77. data/examples/ruby-on-rails-api/descope/app/mailers/application_mailer.rb +4 -0
  78. data/examples/ruby-on-rails-api/descope/app/models/application_record.rb +3 -0
  79. data/examples/ruby-on-rails-api/descope/app/models/concerns/.keep +0 -0
  80. data/examples/ruby-on-rails-api/descope/app/views/homepage/index.html.erb +2 -0
  81. data/examples/ruby-on-rails-api/descope/app/views/layouts/application.html.erb +16 -0
  82. data/examples/ruby-on-rails-api/descope/app/views/layouts/mailer.html.erb +13 -0
  83. data/examples/ruby-on-rails-api/descope/app/views/layouts/mailer.text.erb +1 -0
  84. data/examples/ruby-on-rails-api/descope/app/views/session/index.html.erb +2 -0
  85. data/examples/ruby-on-rails-api/descope/bin/bundle +109 -0
  86. data/examples/ruby-on-rails-api/descope/bin/dev +11 -0
  87. data/examples/ruby-on-rails-api/descope/bin/docker-entrypoint +8 -0
  88. data/examples/ruby-on-rails-api/descope/bin/rails +4 -0
  89. data/examples/ruby-on-rails-api/descope/bin/rake +4 -0
  90. data/examples/ruby-on-rails-api/descope/bin/setup +36 -0
  91. data/examples/ruby-on-rails-api/descope/build.js +30 -0
  92. data/examples/ruby-on-rails-api/descope/config/application.rb +42 -0
  93. data/examples/ruby-on-rails-api/descope/config/boot.rb +4 -0
  94. data/examples/ruby-on-rails-api/descope/config/cable.yml +10 -0
  95. data/examples/ruby-on-rails-api/descope/config/config.yml +9 -0
  96. data/examples/ruby-on-rails-api/descope/config/credentials.yml.enc +1 -0
  97. data/examples/ruby-on-rails-api/descope/config/database.yml +25 -0
  98. data/examples/ruby-on-rails-api/descope/config/environment.rb +5 -0
  99. data/examples/ruby-on-rails-api/descope/config/environments/development.rb +76 -0
  100. data/examples/ruby-on-rails-api/descope/config/environments/production.rb +97 -0
  101. data/examples/ruby-on-rails-api/descope/config/environments/test.rb +64 -0
  102. data/examples/ruby-on-rails-api/descope/config/initializers/assets.rb +13 -0
  103. data/examples/ruby-on-rails-api/descope/config/initializers/content_security_policy.rb +25 -0
  104. data/examples/ruby-on-rails-api/descope/config/initializers/filter_parameter_logging.rb +8 -0
  105. data/examples/ruby-on-rails-api/descope/config/initializers/inflections.rb +16 -0
  106. data/examples/ruby-on-rails-api/descope/config/initializers/load_config.rb +12 -0
  107. data/examples/ruby-on-rails-api/descope/config/initializers/permissions_policy.rb +13 -0
  108. data/examples/ruby-on-rails-api/descope/config/locales/en.yml +31 -0
  109. data/examples/ruby-on-rails-api/descope/config/puma.rb +35 -0
  110. data/examples/ruby-on-rails-api/descope/config/routes.rb +18 -0
  111. data/examples/ruby-on-rails-api/descope/config/storage.yml +34 -0
  112. data/examples/ruby-on-rails-api/descope/config.ru +6 -0
  113. data/examples/ruby-on-rails-api/descope/db/seeds.rb +9 -0
  114. data/examples/ruby-on-rails-api/descope/lib/assets/.keep +0 -0
  115. data/examples/ruby-on-rails-api/descope/lib/tasks/.keep +0 -0
  116. data/examples/ruby-on-rails-api/descope/log/.keep +0 -0
  117. data/examples/ruby-on-rails-api/descope/package-lock.json +19680 -0
  118. data/examples/ruby-on-rails-api/descope/package.json +51 -0
  119. data/examples/ruby-on-rails-api/descope/public/404.html +67 -0
  120. data/examples/ruby-on-rails-api/descope/public/422.html +67 -0
  121. data/examples/ruby-on-rails-api/descope/public/500.html +66 -0
  122. data/examples/ruby-on-rails-api/descope/public/apple-touch-icon-precomposed.png +0 -0
  123. data/examples/ruby-on-rails-api/descope/public/apple-touch-icon.png +0 -0
  124. data/examples/ruby-on-rails-api/descope/public/favicon.ico +0 -0
  125. data/examples/ruby-on-rails-api/descope/public/robots.txt +1 -0
  126. data/examples/ruby-on-rails-api/descope/storage/.keep +0 -0
  127. data/examples/ruby-on-rails-api/descope/tmp/.keep +0 -0
  128. data/examples/ruby-on-rails-api/descope/tmp/pids/.keep +0 -0
  129. data/examples/ruby-on-rails-api/descope/tmp/storage/.keep +0 -0
  130. data/examples/ruby-on-rails-api/descope/vendor/.keep +0 -0
  131. data/examples/ruby-on-rails-api/descope/yarn.lock +10780 -0
  132. data/lib/descope/api/v1/auth/enchantedlink.rb +156 -0
  133. data/lib/descope/api/v1/auth/magiclink.rb +170 -0
  134. data/lib/descope/api/v1/auth/oauth.rb +72 -0
  135. data/lib/descope/api/v1/auth/otp.rb +186 -0
  136. data/lib/descope/api/v1/auth/password.rb +100 -0
  137. data/lib/descope/api/v1/auth/saml.rb +48 -0
  138. data/lib/descope/api/v1/auth/totp.rb +72 -0
  139. data/lib/descope/api/v1/auth.rb +452 -0
  140. data/lib/descope/api/v1/management/access_key.rb +81 -0
  141. data/lib/descope/api/v1/management/audit.rb +82 -0
  142. data/lib/descope/api/v1/management/authz.rb +165 -0
  143. data/lib/descope/api/v1/management/common.rb +147 -0
  144. data/lib/descope/api/v1/management/flow.rb +55 -0
  145. data/lib/descope/api/v1/management/password.rb +58 -0
  146. data/lib/descope/api/v1/management/permission.rb +48 -0
  147. data/lib/descope/api/v1/management/project.rb +53 -0
  148. data/lib/descope/api/v1/management/role.rb +48 -0
  149. data/lib/descope/api/v1/management/scim.rb +206 -0
  150. data/lib/descope/api/v1/management/sso_settings.rb +153 -0
  151. data/lib/descope/api/v1/management/tenant.rb +71 -0
  152. data/lib/descope/api/v1/management/user.rb +619 -0
  153. data/lib/descope/api/v1/management.rb +38 -0
  154. data/lib/descope/api/v1/session.rb +84 -0
  155. data/lib/descope/api/v1.rb +13 -0
  156. data/lib/descope/client.rb +6 -0
  157. data/lib/descope/exception.rb +50 -0
  158. data/lib/descope/mixins/common.rb +129 -0
  159. data/lib/descope/mixins/headers.rb +15 -0
  160. data/lib/descope/mixins/http.rb +133 -0
  161. data/lib/descope/mixins/initializer.rb +80 -0
  162. data/lib/descope/mixins/logging.rb +30 -0
  163. data/lib/descope/mixins/validation.rb +79 -0
  164. data/lib/descope/mixins.rb +22 -0
  165. data/lib/descope/version.rb +7 -0
  166. data/lib/descope.rb +9 -0
  167. data/lib/descope_client.rb +5 -0
  168. data/release-please-config.json +18 -0
  169. data/renovate.json +6 -0
  170. data/spec/factories/user.rb +16 -0
  171. data/spec/lib.descope/api/v1/auth/enchantedlink_spec.rb +159 -0
  172. data/spec/lib.descope/api/v1/auth/magiclink_spec.rb +282 -0
  173. data/spec/lib.descope/api/v1/auth/oauth_spec.rb +117 -0
  174. data/spec/lib.descope/api/v1/auth/otp_spec.rb +285 -0
  175. data/spec/lib.descope/api/v1/auth/password_spec.rb +124 -0
  176. data/spec/lib.descope/api/v1/auth/saml_spec.rb +55 -0
  177. data/spec/lib.descope/api/v1/auth/totp_spec.rb +70 -0
  178. data/spec/lib.descope/api/v1/auth_spec.rb +372 -0
  179. data/spec/lib.descope/api/v1/management/access_key_spec.rb +118 -0
  180. data/spec/lib.descope/api/v1/management/audit_spec.rb +78 -0
  181. data/spec/lib.descope/api/v1/management/authz_spec.rb +336 -0
  182. data/spec/lib.descope/api/v1/management/flow_spec.rb +78 -0
  183. data/spec/lib.descope/api/v1/management/password_spec.rb +25 -0
  184. data/spec/lib.descope/api/v1/management/permission_spec.rb +81 -0
  185. data/spec/lib.descope/api/v1/management/project_spec.rb +63 -0
  186. data/spec/lib.descope/api/v1/management/role_spec.rb +85 -0
  187. data/spec/lib.descope/api/v1/management/scim_spec.rb +312 -0
  188. data/spec/lib.descope/api/v1/management/sso_settings_spec.rb +172 -0
  189. data/spec/lib.descope/api/v1/management/tenant_spec.rb +141 -0
  190. data/spec/lib.descope/api/v1/management/user_spec.rb +667 -0
  191. data/spec/lib.descope/api/v1/session_spec.rb +117 -0
  192. data/spec/lib.descope/client_spec.rb +40 -0
  193. data/spec/spec_helper.rb +72 -0
  194. data/spec/support/client_config.rb +14 -0
  195. data/spec/support/dummy_class.rb +36 -0
  196. data/spec/support/utils.rb +32 -0
  197. metadata +420 -0
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Descope
4
+ module Api
5
+ module V1
6
+ # Holds all session methods
7
+ module Session
8
+ include Descope::Mixins::Common
9
+ include Descope::Mixins::Common::EndpointsV1
10
+ include Descope::Mixins::Common::EndpointsV2
11
+
12
+ def token_validation_key(project_id)
13
+ get("#{PUBLIC_KEY_PATH}/#{project_id}")
14
+ end
15
+
16
+ def refresh_session(refresh_token: nil, audience: nil)
17
+ # Validate a session token. Call this function for every incoming request to your
18
+ # private endpoints. Alternatively, use validate_and_refresh_session in order to
19
+ # automatically refresh expired sessions. If you need to use these specific claims
20
+ # [amr, drn, exp, iss, rexp, sub, jwt] in the top level of the response dict, please use
21
+ # them from the sessionToken key instead, as these claims will soon be deprecated from the top level
22
+ # of the response dict.
23
+
24
+ validate_refresh_token_not_nil(refresh_token)
25
+ validate_token(refresh_token, audience)
26
+ res = post(REFRESH_TOKEN_PATH, {}, {}, refresh_token)
27
+ generate_jwt_response(response_body: res, refresh_cookie: refresh_token, audience:)
28
+ end
29
+
30
+ def me(refresh_token = nil)
31
+ get(ME_PATH, {}, {}, refresh_token)
32
+ end
33
+
34
+ def sign_out(refresh_token = nil)
35
+ post(LOGOUT_PATH, {}, {}, refresh_token)
36
+ end
37
+
38
+ def sign_out_all(refresh_token = nil)
39
+ post(LOGOUT_ALL_PATH, {}, {}, refresh_token)
40
+ end
41
+
42
+ def validate_session(session_token: nil, audience: nil)
43
+ # Validate a session token. Call this function for every incoming request to your
44
+ # private endpoints. Alternatively, use validate_and_refresh_session in order to
45
+ # automatically refresh expired sessions. If you need to use these specific claims
46
+ # [amr, drn, exp, iss, rexp, sub, jwt] in the top level of the response dict, please use
47
+ # them from the sessionToken key instead, as these claims will soon be deprecated from the top level
48
+ # of the response dict.
49
+ # Return a hash includes the session token and all JWT claims
50
+
51
+ if session_token.nil? || session_token.empty?
52
+ raise Descope::AuthException.new('Session token is required for validation', code: 400)
53
+ end
54
+
55
+ @logger.debug("Validating session token: #{session_token}")
56
+ res = validate_token(session_token, audience)
57
+ @logger.debug("Session token validation response: #{res}")
58
+ # Duplicate for saving backward compatibility but keep the same structure as the refresh operation response
59
+ res[SESSION_TOKEN_NAME] = deep_copy(res)
60
+ session_props = adjust_properties(res, true)
61
+ @logger.debug("session validation jwt response properties: #{session_props}")
62
+ session_props
63
+ end
64
+
65
+ def validate_and_refresh_session(session_token: nil, refresh_token: nil, audience: nil)
66
+ # Validate the session token and refresh it if it has expired, the session token will automatically be refreshed.
67
+ # Either the session_token or the refresh_token must be provided.
68
+ # Call this function for every incoming request to your
69
+ # private endpoints. Alternatively, use validate_session to only validate the session.
70
+
71
+ raise Descope::AuthException.new('Session token is missing', code: 400) if session_token.nil?
72
+
73
+ begin
74
+ @logger.debug("Validating session token: #{session_token}")
75
+ validate_session(session_token:, audience:)
76
+ rescue Descope::AuthException
77
+ @logger.debug("Session is invalid, refreshing session with refresh token: #{refresh_token}")
78
+ refresh_session(refresh_token:, audience:)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,13 @@
1
+ require 'descope/api/v1/management'
2
+ require 'descope/api/v1/session'
3
+ require 'descope/api/v1/auth'
4
+
5
+ module Descope
6
+ module Api
7
+ module V1
8
+ include Descope::Api::V1::Management
9
+ include Descope::Api::V1::Session
10
+ include Descope::Api::V1::Auth
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ module Descope
2
+ # Main class for descope client
3
+ class Client
4
+ include Descope::Mixins
5
+ end
6
+ end
@@ -0,0 +1,50 @@
1
+ module Descope
2
+ # Default exception in namespace of Descope
3
+ # If you want to catch all exceptions, then you should use this one.
4
+ # Network exceptions are not included
5
+ class Exception < StandardError
6
+ attr_reader :error_data
7
+
8
+ def initialize(message, error_data = {})
9
+ super(message)
10
+ @error_data = error_data
11
+ end
12
+ end
13
+
14
+ # Parent for all exceptions that arise out of HTTP error responses.
15
+ class HTTPError < Descope::Exception
16
+ def headers
17
+ error_data[:headers]
18
+ end
19
+
20
+ def http_code
21
+ error_data[:code]
22
+ end
23
+ end
24
+
25
+ class AuthException < Descope::Exception; end
26
+ # exception for unauthorized requests, if you see it,
27
+ # probably Bearer Token is not set correctly
28
+
29
+ # exception for unset user_id, this might cause removal of
30
+ # all users, or other unexpected behaviour
31
+ class ArgumentException < Descope::Exception; end
32
+
33
+ # exception for invalid token when its empty
34
+ class InvalidToken < Descope::Exception; end
35
+ class InvalidParameter < Descope::Exception; end
36
+ class Unauthorized < Descope::HTTPError; end
37
+ # exception for not found resource, you query for an
38
+ # non-existent resource, or wrong path
39
+ class NotFound < Descope::HTTPError; end
40
+ class MethodNotAllowed < Descope::HTTPError; end
41
+ # exception for unknown error
42
+ class Unsupported < Descope::HTTPError; end
43
+ # exception for server error
44
+ class ServerError < Descope::HTTPError; end
45
+ # exception for incorrect request, you've sent wrong params
46
+ class BadRequest < Descope::HTTPError; end
47
+ class AccessDenied < Descope::HTTPError; end
48
+ class RateLimitException < Descope::HTTPError; end
49
+ class RequestTimeout < Descope::HTTPError; end
50
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../exception'
4
+
5
+ module Descope
6
+ module Mixins
7
+ # Common values and methods
8
+ module Common
9
+ DEFAULT_BASE_URL = 'https://api.descope.com' # pragma: no cover
10
+ DEFAULT_TIMEOUT_SECONDS = 60
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
13
+
14
+ SESSION_COOKIE_NAME = 'DS'
15
+ REFRESH_SESSION_COOKIE_NAME = 'DSR'
16
+
17
+ SESSION_TOKEN_NAME = 'sessionToken'
18
+ REFRESH_SESSION_TOKEN_NAME = 'refreshSessionToken'
19
+ COOKIE_DATA_NAME = 'cookieData'
20
+
21
+ REDIRECT_LOCATION_COOKIE_NAME = 'Location'
22
+
23
+ module DeliveryMethod
24
+ WHATSAPP = 1
25
+ SMS = 2
26
+ EMAIL = 3
27
+ end
28
+
29
+ def get_method_string(method)
30
+ name = {
31
+ DeliveryMethod::WHATSAPP => 'whatsapp',
32
+ DeliveryMethod::SMS => 'sms',
33
+ DeliveryMethod::EMAIL => 'email'
34
+ }[method]
35
+
36
+ raise ArgumentException, "Unknown delivery method: #{method}" if name.nil?
37
+
38
+ name
39
+ end
40
+
41
+ def deep_copy(obj)
42
+ Marshal.load(Marshal.dump(obj))
43
+ end
44
+
45
+ module EndpointsV1
46
+ REFRESH_TOKEN_PATH = '/v1/auth/refresh'
47
+ SELECT_TENANT_PATH = '/v1/auth/tenant/select'
48
+ LOGOUT_PATH = '/v1/auth/logout'
49
+ LOGOUT_ALL_PATH = '/v1/auth/logoutall'
50
+ VALIDATE_SESSION_PATH = '/v1/auth/validate'
51
+ ME_PATH = '/v1/auth/me'
52
+
53
+ # accesskey
54
+ EXCHANGE_AUTH_ACCESS_KEY_PATH = '/v1/auth/accesskey/exchange'
55
+
56
+ # otp
57
+ SIGN_UP_AUTH_OTP_PATH = '/v1/auth/otp/signup'
58
+ SIGN_IN_AUTH_OTP_PATH = '/v1/auth/otp/signin'
59
+ SIGN_UP_OR_IN_AUTH_OTP_PATH = '/v1/auth/otp/signup-in'
60
+ VERIFY_CODE_AUTH_PATH = '/v1/auth/otp/verify'
61
+ UPDATE_USER_EMAIL_OTP_PATH = '/v1/auth/otp/update/email'
62
+ UPDATE_USER_PHONE_OTP_PATH = '/v1/auth/otp/update/phone'
63
+
64
+ # magiclink
65
+ SIGN_UP_AUTH_MAGICLINK_PATH = '/v1/auth/magiclink/signup'
66
+ SIGN_IN_AUTH_MAGICLINK_PATH = '/v1/auth/magiclink/signin'
67
+ SIGN_UP_OR_IN_AUTH_MAGICLINK_PATH = '/v1/auth/magiclink/signup-in'
68
+ VERIFY_MAGICLINK_AUTH_PATH = '/v1/auth/magiclink/verify'
69
+ GET_SESSION_MAGICLINK_AUTH_PATH = '/v1/auth/magiclink/pending-session'
70
+ UPDATE_USER_EMAIL_MAGICLINK_PATH = '/v1/auth/magiclink/update/email'
71
+ UPDATE_USER_PHONE_MAGICLINK_PATH = '/v1/auth/magiclink/update/phone'
72
+
73
+ # enchantedlink
74
+ SIGN_UP_AUTH_ENCHANTEDLINK_PATH = '/v1/auth/enchantedlink/signup'
75
+ SIGN_IN_AUTH_ENCHANTEDLINK_PATH = '/v1/auth/enchantedlink/signin'
76
+ SIGN_UP_OR_IN_AUTH_ENCHANTEDLINK_PATH = '/v1/auth/enchantedlink/signup-in'
77
+ VERIFY_ENCHANTEDLINK_AUTH_PATH = '/v1/auth/enchantedlink/verify'
78
+ GET_SESSION_ENCHANTEDLINK_AUTH_PATH = '/v1/auth/enchantedlink/pending-session'
79
+ UPDATE_USER_EMAIL_ENCHANTEDLINK_PATH = '/v1/auth/enchantedlink/update/email'
80
+
81
+ # oauth
82
+ OAUTH_START_PATH = '/v1/auth/oauth/authorize'
83
+ OAUTH_EXCHANGE_TOKEN_PATH = '/v1/auth/oauth/exchange'
84
+ OAUTH_CREATE_REDIRECT_URL_FOR_SIGN_IN_REQUEST_PATH = 'v1/auth/oauth/authorize/signin'
85
+ OAUTH_CREATE_REDIRECT_URL_FOR_SIGN_UP_REQUEST_PATH = 'v1/auth/oauth/authorize/signup'
86
+
87
+ # saml
88
+ AUTH_SAML_START_PATH = '/v1/auth/saml/authorize'
89
+ SAML_EXCHANGE_TOKEN_PATH = '/v1/auth/saml/exchange'
90
+
91
+ # totp
92
+ SIGN_UP_AUTH_TOTP_PATH = '/v1/auth/totp/signup'
93
+ VERIFY_TOTP_PATH = '/v1/auth/totp/verify'
94
+ UPDATE_TOTP_PATH = '/v1/auth/totp/update'
95
+
96
+ # webauthn
97
+ SIGN_UP_AUTH_WEBAUTHN_START_PATH = '/v1/auth/webauthn/signup/start'
98
+ SIGN_UP_AUTH_WEBAUTHN_FINISH_PATH = '/v1/auth/webauthn/signup/finish'
99
+ SIGN_IN_AUTH_WEBAUTHN_START_PATH = '/v1/auth/webauthn/signin/start'
100
+ SIGN_IN_AUTH_WEBAUTHN_FINISH_PATH = '/v1/auth/webauthn/signin/finish'
101
+ SIGN_UP_OR_IN_AUTH_WEBAUTHN_START_PATH = '/v1/auth/webauthn/signup-in/start'
102
+ UPDATE_AUTH_WEBAUTHN_START_PATH = '/v1/auth/webauthn/update/start'
103
+ UPDATE_AUTH_WEBAUTHN_FINISH_PATH = '/v1/auth/webauthn/update/finish'
104
+
105
+ # password
106
+ SIGN_UP_PASSWORD_PATH = '/v1/auth/password/signup'
107
+ SIGN_IN_PASSWORD_PATH = '/v1/auth/password/signin'
108
+ SEND_RESET_PASSWORD_PATH = '/v1/auth/password/reset'
109
+ UPDATE_PASSWORD_PATH = '/v1/auth/password/update'
110
+ REPLACE_PASSWORD_PATH = '/v1/auth/password/replace'
111
+ PASSWORD_POLICY_PATH = '/v1/auth/password/policy'
112
+ end
113
+
114
+ module EndpointsV2
115
+ PUBLIC_KEY_PATH = '/v2/keys'
116
+ 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
+ end
128
+ end
129
+ end
@@ -0,0 +1,15 @@
1
+ module Descope
2
+ module Mixins
3
+ module Headers
4
+ # Descope default headers
5
+ def client_headers
6
+ {
7
+ 'Content-Type' => 'application/json',
8
+ 'x-descope-sdk-name': 'ruby',
9
+ 'x-descope-sdk-ruby-version': RUBY_VERSION,
10
+ 'x-descope-sdk-version': Descope::SDK_VERSION,
11
+ }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+ require "addressable/uri"
3
+ require 'retryable'
4
+ require_relative '../exception'
5
+
6
+ module Descope
7
+ module Mixins
8
+ # HTTP-related methods
9
+ module HTTP
10
+ attr_accessor :headers, :base_uri, :timeout, :retry_count
11
+
12
+ DEFAULT_RETRIES = 3
13
+ MAX_ALLOWED_RETRIES = 10
14
+ MAX_REQUEST_RETRY_JITTER = 250
15
+ MAX_REQUEST_RETRY_DELAY = 1000
16
+ MIN_REQUEST_RETRY_DELAY = 250
17
+ BASE_DELAY = 100
18
+
19
+ %i[get post post_file post_form put patch delete delete_with_body].each do |method|
20
+ define_method(method) do |uri, body = {}, extra_headers = {}, pswd = nil|
21
+ body = body.delete_if { |_, v| v.nil? }
22
+ authorization_header(pswd) # This will set the pswd if provided, else default to the @default_pswd
23
+
24
+ @logger.debug "request => method: #{method}, uri: #{uri}, body: #{body}, extra_headers: #{extra_headers}}"
25
+ request_with_retry(method, uri, body, extra_headers)
26
+ end
27
+ end
28
+
29
+ def retry_options
30
+ sleep_timer = lambda do |attempt|
31
+ wait = BASE_DELAY * (2**attempt - 1) # Exponential delay with each subsequent request attempt.
32
+ wait += rand(wait + 1..wait + MAX_REQUEST_RETRY_JITTER) # Add jitter to the delay window.
33
+ wait = [MAX_REQUEST_RETRY_DELAY, wait].min # Cap delay at MAX_REQUEST_RETRY_DELAY.
34
+ wait = [MIN_REQUEST_RETRY_DELAY, wait].max # Ensure delay is no less than MIN_REQUEST_RETRY_DELAY.
35
+ wait / 1000.to_f.round(2) # convert ms to seconds
36
+ end
37
+
38
+ tries = 1 + [Integer(retry_count || DEFAULT_RETRIES), MAX_ALLOWED_RETRIES].min # Cap retries at MAX_ALLOWED_RETRIES
39
+
40
+ {
41
+ tries: tries,
42
+ sleep: sleep_timer,
43
+ on: Descope::RateLimitException
44
+ }
45
+ end
46
+
47
+ def safe_parse_json(body)
48
+ @logger.debug "response => #{JSON.parse(body.to_s)}"
49
+ JSON.parse(body.to_s)
50
+ rescue JSON::ParserError
51
+ body
52
+ end
53
+
54
+ def encode_uri(uri)
55
+ encoded_uri = base_uri ? Addressable::URI.parse(uri).normalize : Addressable::URI.escape(uri)
56
+ @logger.debug "will call #{url(encoded_uri)}"
57
+ url(encoded_uri)
58
+ end
59
+
60
+ def url(path)
61
+ "#{@base_uri}#{path}"
62
+ end
63
+
64
+ def add_headers(h = {})
65
+ raise ArgumentError, 'Headers must be an object which responds to #to_hash' unless h.respond_to?(:to_hash)
66
+
67
+ @headers ||= {}
68
+ @headers.merge!(h.to_hash)
69
+ end
70
+
71
+ def request_with_retry(method, uri, body = {}, extra_headers = {}, pswd = nil)
72
+ Retryable.retryable(retry_options) do
73
+ request(method, uri, body, extra_headers)
74
+ end
75
+ end
76
+
77
+ def request(method, uri, body = {}, extra_headers = {})
78
+ # @headers is getting the authorization header merged in initializer.rb
79
+ headers_debug = @headers.dup
80
+ if headers_debug['Authorization']
81
+ headers_debug['Authorization'] = headers_debug['Authorization'].gsub(/(.{10})\z/, '***********')
82
+ end
83
+
84
+ @logger.debug "base url: #{@base_uri}"
85
+ @logger.debug "request method: #{method}, uri: #{uri}, body: #{body}, extra_headers: #{extra_headers}, headers: #{headers_debug}"
86
+ result = case method
87
+ when :get
88
+ get_headers = @headers.merge({ params: body }).merge(extra_headers)
89
+ call(:get, encode_uri(uri), timeout, get_headers)
90
+ when :delete
91
+ delete_headers = @headers.merge({ params: body })
92
+ call(:delete, encode_uri(uri), timeout, delete_headers)
93
+ else
94
+ call(method, encode_uri(uri), timeout, @headers, body.to_json)
95
+ end
96
+
97
+ raise Descope::Unsupported.new("No response from server", code: 400) unless result && result.respond_to?(:code)
98
+
99
+ @logger.info "http status code: #{result.code}"
100
+ case result.code
101
+ when 200...226 then safe_parse_json(result.body)
102
+ when 400 then raise Descope::BadRequest.new(result.body, code: result.code, headers: result.headers)
103
+ when 401 then raise Descope::Unauthorized.new(result.body, code: result.code, headers: result.headers)
104
+ when 403 then raise Descope::AccessDenied.new(result.body, code: result.code, headers: result.headers)
105
+ when 404 then raise Descope::NotFound.new(result.body, code: result.code, headers: result.headers)
106
+ when 405 then raise Descope::MethodNotAllowed.new(result.body, code: result.code, headers: result.headers)
107
+ when 429 then raise Descope::RateLimitException.new(result.body, code: result.code, headers: result.headers)
108
+ when 500 then raise Descope::ServerError.new(result.body, code: result.code, headers: result.headers)
109
+ else
110
+ raise Descope::Unsupported.new(result.body, code: result.code, headers: result.headers)
111
+ end
112
+ end
113
+
114
+ def call(method, url, timeout, headers, body = nil)
115
+ RestClient::Request.execute(
116
+ method:,
117
+ url:,
118
+ timeout:,
119
+ headers:,
120
+ payload: body
121
+ )
122
+ rescue RestClient::Exception => e
123
+ case e
124
+ when RestClient::RequestTimeout
125
+ raise Descope::RequestTimeout.new(e.message)
126
+ else
127
+ return e.response
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Descope
6
+ module Mixins
7
+ # Helper class for initializing the Descope API
8
+ module Initializer
9
+ attr_accessor :public_keys, :mlock
10
+
11
+ def initialize(config)
12
+ options = Hash[config.map { |(k, v)| [k.to_sym, v] }]
13
+ @base_uri = base_url(options)
14
+ @headers = client_headers
15
+ @project_id = options[:project_id] || ENV['DESCOPE_PROJECT_ID'] || ''
16
+ @public_key = options[:public_key] || ENV['DESCOPE_PUBLIC_KEY']
17
+ @mlock = Mutex.new
18
+ log_level = options[:log_level] || ENV['DESCOPE_LOG_LEVEL'] || 'info'
19
+ @logger ||= Descope::Mixins::Logging.logger_for(self.class.name, log_level)
20
+
21
+ @logger.debug("Initializing Descope API with project_id: #{@project_id} and base_uri: #{@base_uri}")
22
+
23
+ if @public_key.nil?
24
+ @public_keys = {}
25
+ else
26
+ kid, pub_key, alg = validate_and_load_public_key(@public_key)
27
+ @public_keys = { kid => [pub_key, alg] }
28
+ end
29
+
30
+ @skip_verify = options[:skip_verify]
31
+ @secure = !@skip_verify
32
+ @management_key = options[:management_key] || ENV['DESCOPE_MANAGEMENT_KEY']
33
+ @logger.debug("Management Key ID: #{@management_key}")
34
+ @timeout_seconds = options[:timeout_seconds] || Common::DEFAULT_TIMEOUT_SECONDS
35
+ @jwt_validation_leeway = options[:jwt_validation_leeway] || Common::DEFAULT_JWT_VALIDATION_LEEWAY
36
+
37
+ if @project_id.to_s.empty?
38
+ raise AuthException.new(
39
+ 'Unable to init Auth object because project_id cannot be empty. '\
40
+ 'Set environment variable DESCOPE_PROJECT_ID or pass your Project ID to the init function.',
41
+ code: 400
42
+ )
43
+ else
44
+ initialize_api(options)
45
+ end
46
+ end
47
+
48
+ def self.included(klass)
49
+ klass.send :prepend, Initializer
50
+ end
51
+
52
+ def base_url(options)
53
+ url = options[:descope_base_uri] || ENV['DESCOPE_BASE_URI'] || Common::DEFAULT_BASE_URL
54
+ return url if url.start_with? 'http'
55
+
56
+ raise AuthException.new('base url must start with http or https', code: 400)
57
+
58
+ end
59
+
60
+ def authorization_header(pswd = nil)
61
+ pswd = @default_pswd if pswd.nil? || pswd.empty?
62
+ bearer = "#{@project_id}:#{pswd}"
63
+ add_headers('Authorization' => "Bearer #{bearer}")
64
+ end
65
+
66
+ def initialize_api(options)
67
+ initialize_v1(options)
68
+ @default_pswd = options.fetch(:management_key, ENV['DESCOPE_MANAGEMENT_KEY'])
69
+ authorization_header
70
+ end
71
+
72
+ def initialize_v1(_options)
73
+ extend Descope::Api::V1
74
+ extend Descope::Api::V1::Management
75
+ extend Descope::Api::V1::Auth
76
+ extend Descope::Api::V1::Session
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Descope
4
+ module Mixins
5
+ # Module to provide logger.
6
+ module Logging
7
+
8
+ def logger
9
+ # This is the magical bit that gets mixed into the other modules
10
+ @logger ||= Logging.logger_for(self.class.name, 'info')
11
+ end
12
+
13
+ # Use a hash class-ivar to cache a unique Logger per class:
14
+ @loggers = {}
15
+
16
+ class << self
17
+ def logger_for(classname, level)
18
+ @loggers[classname] ||= configure_logger_for(classname, level)
19
+ end
20
+
21
+ def configure_logger_for(classname, level = 'info')
22
+ logger = Logger.new(STDOUT)
23
+ logger.level = Object.const_get("Logger::#{level.upcase}")
24
+ logger.progname = classname
25
+ logger
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Descope
4
+ module Mixins
5
+ # Module to provide validation for specific data structures.
6
+ module Validation
7
+ def validate_tenants(key_tenants)
8
+ raise ArgumentError, 'key_tenants should be an Array of hashes' unless key_tenants.is_a? Array
9
+
10
+ key_tenants.each do |tenant|
11
+ unless tenant.is_a? Hash
12
+ raise ArgumentError,
13
+ 'Each tenant should be a Hash of tenant_id and optional role_names array'
14
+ end
15
+
16
+ tenant_symbolized = tenant.transform_keys(&:to_sym)
17
+
18
+ raise ArgumentError, "Missing tenant_id key in tenant: #{tenant}" unless tenant_symbolized.key?(:tenant_id)
19
+ end
20
+ end
21
+
22
+ def validate_login_id(login_id)
23
+ raise AuthException, 'login_id cannot be empty' unless login_id.is_a?(String) && !login_id.empty?
24
+ end
25
+
26
+ def validate_user_id(user_id)
27
+ raise Descope::ArgumentException, 'Missing user id' if user_id.nil? || user_id.to_s.empty?
28
+ end
29
+
30
+ def validate_password(password)
31
+ raise AuthException, 'password cannot be empty' unless password.is_a?(String) && !password.empty?
32
+ end
33
+
34
+ def validate_email(email)
35
+ raise AuthException.new('email cannot be empty', code: 400) unless email.is_a?(String) && !email.empty?
36
+ end
37
+
38
+ def validate_token_not_empty(token)
39
+ raise AuthException.new('Token cannot be empty', code: 400) unless token.is_a?(String) && !token.empty?
40
+ end
41
+
42
+ def validate_refresh_token_not_nil(refresh_token)
43
+ return unless refresh_token.nil? || refresh_token.empty?
44
+
45
+ raise AuthException.new('Refresh token is required to refresh a session', code: 400)
46
+ end
47
+
48
+ def validate_phone(method, phone)
49
+ 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)
54
+ end
55
+
56
+ def verify_provider(oauth_provider)
57
+ return false if oauth_provider.to_s.empty? || oauth_provider.nil?
58
+
59
+ true
60
+ end
61
+
62
+ def validate_tenant(tenant)
63
+ raise AuthException.new('Tenant cannot be empty', code: 400) unless tenant.is_a?(String) && !tenant.empty?
64
+ end
65
+
66
+ 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?
68
+ end
69
+
70
+ def validate_code(code)
71
+ raise AuthException.new('Code cannot be empty', code: 400) unless code.is_a?(String) && !code.empty?
72
+ end
73
+
74
+ 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?
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,22 @@
1
+ require 'rest-client'
2
+ require 'uri'
3
+ require 'logger'
4
+ require 'jwt'
5
+ require 'descope/mixins/headers'
6
+ require 'descope/mixins/http'
7
+ require 'descope/mixins/initializer'
8
+ require 'descope/mixins/validation'
9
+ require 'descope/mixins/logging'
10
+ require 'descope/mixins/common'
11
+ require 'descope/api/v1'
12
+
13
+ module Descope
14
+ # Collecting dependencies here
15
+ module Mixins
16
+ include Descope::Mixins::Common
17
+ include Descope::Mixins::Headers
18
+ include Descope::Mixins::HTTP
19
+ include Descope::Mixins::Initializer
20
+ include Descope::Mixins::Logging
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Current version of gem
4
+ module Descope
5
+ VERSION = '1.0.4'
6
+ SDK_VERSION = '1.0.0'
7
+ end
data/lib/descope.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'descope/version'
2
+ require 'descope/mixins'
3
+ require 'descope/exception'
4
+ require 'descope/client'
5
+ require 'descope_client'
6
+
7
+ # Namespace for Ruby Descope logic
8
+ module Descope
9
+ end
@@ -0,0 +1,5 @@
1
+ # This class is made for backward compatibility of gem
2
+
3
+ class DescopeClient < Descope::Client
4
+
5
+ end