descope 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
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