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,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
5
+ module Descope
6
+ module Api
7
+ module V1
8
+ module Auth
9
+ # Holds all the password API calls
10
+ module SAML
11
+ include Descope::Mixins::Validation
12
+ include Descope::Mixins::Common::EndpointsV1
13
+ include Descope::Mixins::Common::EndpointsV2
14
+
15
+ # rubocop:disable Metrics/AbcSize
16
+ def saml_sign_in(tenant: nil, redirect_url: nil, prompt: nil, stepup: false,
17
+ mfa: false, custom_claims: {}, sso_app_id: nil)
18
+ validate_tenant(tenant)
19
+ validate_redirect_url(redirect_url)
20
+ uri = compose_saml_signin_url(tenant, redirect_url, prompt)
21
+
22
+ request_params = {}
23
+ request_params[:stepup] = stepup
24
+ request_params[:mfa] = mfa
25
+ request_params[:customClaims] = custom_claims
26
+ request_params[:ssoAppId] = sso_app_id unless sso_app_id.nil?
27
+
28
+ post(uri, request_params)
29
+ end
30
+
31
+ def saml_exchange_token(code = nil)
32
+ exchange_token(SAML_EXCHANGE_TOKEN_PATH, code)
33
+ end
34
+
35
+ private
36
+
37
+ def compose_saml_signin_url(tenant, redirect_url, prompt)
38
+ uri = AUTH_SAML_START_PATH
39
+ uri += "?tenant=#{CGI.escape(tenant)}" unless tenant.nil?
40
+ uri += "&redirectUrl=#{CGI.escape(redirect_url)}" unless redirect_url.nil?
41
+ uri += "&prompt=#{CGI.escape(prompt)}" unless prompt.nil?
42
+ uri
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Descope
4
+ module Api
5
+ module V1
6
+ module Auth
7
+ # Holds all the password API calls
8
+ module TOTP
9
+ include Descope::Mixins::Validation
10
+ include Descope::Mixins::Common::EndpointsV1
11
+ include Descope::Mixins::Common::EndpointsV2
12
+
13
+ def totp_sign_in_code(login_id: nil, login_options: nil, code: nil)
14
+ # Sign in by verifying the validity of a TOTP code entered by an end user.
15
+ validate_login_id(login_id)
16
+ validate_code(code)
17
+ uri = VERIFY_TOTP_PATH
18
+ body = totp_compose_signin_body(login_id, code, login_options)
19
+ res = post(uri, body, {}, nil)
20
+ generate_jwt_response(response_body: res, refresh_cookie: res.fetch('refreshJwt', {}))
21
+ end
22
+
23
+ def totp_sign_up(login_id: nil, user: nil, sso_app_id: nil)
24
+ # Sign up (create) a new user using their email or phone number.
25
+ # (optional) Include additional user metadata that you wish to save.
26
+ user ||= {}
27
+ validate_login_id(login_id)
28
+
29
+ request_params = {
30
+ loginId: login_id
31
+ }
32
+ request_params[:user] = user_compose_update_body(**user) unless user.empty?
33
+ request_params[:ssoAppId] = sso_app_id unless sso_app_id.nil?
34
+ post(SIGN_UP_AUTH_TOTP_PATH, request_params)
35
+ end
36
+
37
+ def totp_add_update_key(login_id: nil, refresh_token: nil)
38
+ # Add or update TOTP key for existing end userUpdate the email address of an end user,
39
+ # after verifying the authenticity of the end user using OTP.
40
+ validate_login_id(login_id)
41
+ post(UPDATE_TOTP_PATH, { loginId: login_id }, {}, refresh_token)
42
+ end
43
+
44
+ private
45
+
46
+ # rubocop:disable Metrics/MethodLength
47
+ def totp_compose_signin_body(login_id, code, login_options)
48
+ login_options ||= {}
49
+ unless login_options.is_a?(Hash)
50
+ raise Descope::ArgumentException.new(
51
+ 'Unable to read login_option, not a Hash',
52
+ code: 400
53
+ )
54
+ end
55
+
56
+ body = {
57
+ loginId: login_id,
58
+ code:,
59
+ loginOptions: {}
60
+ }
61
+ body[:loginOptions][:stepup] = login_options.fetch(:stepup, false)
62
+ body[:loginOptions][:mfa] = login_options.fetch(:mfa, false)
63
+ body[:loginOptions][:customClaims] = login_options.fetch(:custom_claims, {})
64
+ body[:loginOptions][:ssoAppId] = login_options.fetch(:sso_app_id, nil)
65
+
66
+ body
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,452 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'descope/mixins/common'
4
+ require 'descope/api/v1/auth/password'
5
+ require 'descope/api/v1/auth/enchantedlink'
6
+ require 'descope/api/v1/auth/magiclink'
7
+ require 'descope/api/v1/auth/oauth'
8
+ require 'descope/api/v1/auth/otp'
9
+ require 'descope/api/v1/auth/saml'
10
+ require 'descope/api/v1/auth/totp'
11
+
12
+ module Descope
13
+ module Api
14
+ module V1
15
+ # Holds all the management API calls
16
+ module Auth
17
+ include Descope::Mixins::Common
18
+ include Descope::Mixins::Common::EndpointsV1
19
+ include Descope::Mixins::Common::EndpointsV2
20
+ include Descope::Api::V1::Auth::Password
21
+ include Descope::Api::V1::Auth::EnchantedLink
22
+ include Descope::Api::V1::Auth::MagicLink
23
+ include Descope::Api::V1::Auth::OAuth
24
+ include Descope::Api::V1::Auth::OTP
25
+ include Descope::Api::V1::Auth::SAML
26
+ include Descope::Api::V1::Auth::TOTP
27
+
28
+
29
+ ALGORITHM_KEY = 'alg'
30
+
31
+ def generate_jwt_response(response_body: nil, refresh_cookie: nil, audience: nil)
32
+ if response_body.nil? || response_body.empty?
33
+ raise AuthException.new('Unable to generate jwt response. Response body is empty', code: 500)
34
+ end
35
+
36
+ jwt_response = generate_auth_info(response_body, refresh_cookie, true, audience)
37
+ @logger.debug "jwt_response: #{jwt_response}"
38
+ jwt_response['user'] = response_body.key?('user') ? response_body['user'] : {}
39
+ jwt_response['firstSeen'] = response_body.key?('firstSeen') ? response_body['firstSeen'] : true
40
+
41
+ jwt_response
42
+ end
43
+
44
+ def exchange_access_key(access_key = nil)
45
+ post(EXCHANGE_AUTH_ACCESS_KEY_PATH, {}, {}, access_key)
46
+ end
47
+
48
+ def select_tenant(tenant_id: nil, refresh_token: nil)
49
+ validate_refresh_token_not_nil(refresh_token)
50
+ res = post(SELECT_TENANT_PATH, { tenantId: tenant_id }, {}, refresh_token)
51
+ @logger.debug "select_tenant response: #{res}"
52
+ generate_jwt_response(
53
+ response_body: res,
54
+ refresh_cookie: res['refreshJwt']
55
+ )
56
+ end
57
+
58
+ def validate_permissions(jwt_response: nil, permissions: nil)
59
+ # Validate that a jwt_response has been granted the specified permissions.
60
+ # For a multi-tenant environment use validate_tenant_permissions function
61
+ validate_tenant_permissions(jwt_response:, permissions:)
62
+ end
63
+
64
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/MethodLength
65
+ def validate_tenant_permissions(jwt_response: nil, tenant: nil, permissions: nil)
66
+ # Validate that a jwt_response has been granted the specified permissions on the specified tenant.
67
+ # For a multi-tenant environment use validate_tenant_permissions function
68
+ if permissions.is_a?(String)
69
+ permissions = [permissions]
70
+ else
71
+ permissions ||= []
72
+ end
73
+
74
+ unless jwt_response.is_a?(Hash)
75
+ raise Descope::ArgumentException.new(
76
+ 'Invalid JWT response hash', code: 400
77
+ )
78
+ end
79
+
80
+ return false unless jwt_response
81
+
82
+ granted_permissions = if tenant.nil? || tenant.to_s.empty?
83
+ jwt_response.fetch('permissions', [])
84
+ else
85
+ # ensure that the tenant is associated with the jwt_response
86
+ @logger.debug "tenant associated jwt: #{jwt_response['tenants']&.key?(tenant)}"
87
+ return false unless jwt_response['tenants'].key?(tenant)
88
+
89
+ # dig is a method in Ruby for safely navigating nested data structures like hashes
90
+ # and arrays. It allows you to access deeply nested values without worrying about
91
+ # raising an error if a middle value is nil.
92
+ tenant_permission = jwt_response.dig('tenants', tenant, 'permissions') || []
93
+ tenant_permission = [] if tenant_permission.nil?
94
+ if tenant_permission.is_a?(String)
95
+ @logger.debug "tenant_permission string: #{tenant_permission}"
96
+ [tenant_permission]
97
+ else
98
+ @logger.debug "tenant_permission array: #{tenant_permission}"
99
+ tenant_permission
100
+ end
101
+ end
102
+
103
+ # Validate all permissions are granted
104
+ permissions.all? do |permission|
105
+ granted_permissions.include?(permission)
106
+ end
107
+ end
108
+
109
+ def validate_roles(jwt_response: nil, roles: nil)
110
+ # Validate that a jwt_response has been granted the specified roles.
111
+ # For a multi-tenant environment use validate_tenant_roles function
112
+ validate_tenant_roles(jwt_response:, tenant: '', roles:)
113
+ end
114
+
115
+ def validate_tenant_roles(jwt_response: nil, tenant: nil, roles: nil)
116
+ # Validate that a jwt_response has been granted the specified roles on the specified tenant.
117
+ # For a multi-tenant environment use validate_tenant_roles function
118
+ @logger.debug "Validate_tenant_roles: #{jwt_response}, #{tenant}, #{roles}"
119
+ if roles.is_a?(String)
120
+ roles = [roles]
121
+ else
122
+ roles ||= []
123
+ end
124
+
125
+ unless jwt_response.is_a?(Hash)
126
+ raise Descope::ArgumentException.new(
127
+ 'Invalid JWT response hash', code: 400
128
+ )
129
+ end
130
+
131
+ return false unless jwt_response
132
+
133
+ granted_roles = if tenant.nil? || tenant.to_s.empty?
134
+ jwt_response.fetch('roles', [])
135
+ else
136
+ # ensure that the tenant is associated with the jwt_response
137
+ return false unless jwt_response['tenants'].key?(tenant)
138
+
139
+ # dig is a method in Ruby for safely navigating nested data structures like hashes
140
+ # and arrays. It allows you to access deeply nested values without worrying about
141
+ # raising an error if a middle value is nil.
142
+ tenant_roles = jwt_response.dig('tenants', tenant, 'roles') || []
143
+ tenant_roles = [] if tenant_roles.nil?
144
+ if tenant_roles.is_a?(String)
145
+ [tenant_roles]
146
+ else
147
+ tenant_roles
148
+ end
149
+ end
150
+
151
+ @logger.debug "granted_roles: #{granted_roles}"
152
+ # Validate all roles are granted
153
+ roles.all? do |role|
154
+ @logger.debug "granted_roles.include?(#{role}): #{granted_roles.include?(role)}"
155
+ granted_roles.include?(role)
156
+ end
157
+ end
158
+
159
+ def validate_token(token, _audience = nil)
160
+ @logger.debug "validating token: #{token}"
161
+ raise AuthException.new('Token validation received empty token', code: 500) if token.nil? || token.to_s.empty?
162
+
163
+ unverified_header = jwt_get_unverified_header(token)
164
+ @logger.debug "unverified_header: #{unverified_header}"
165
+ alg_header = unverified_header[ALGORITHM_KEY]
166
+ @logger.debug "alg_header: #{alg_header}"
167
+
168
+ if alg_header.nil? || alg_header == 'none'
169
+ raise AuthException.new('Token header is missing property: alg', code: 500)
170
+ end
171
+
172
+ kid = unverified_header['kid']
173
+ @logger.debug "kid: #{kid}"
174
+ raise AuthException.new('Token header is missing property: kid', code: 500) if kid.nil?
175
+
176
+ found_key = nil
177
+ @mlock.synchronize do
178
+ if @public_keys.nil? || @public_keys == {} || @public_keys.to_s.empty? || @public_keys[kid].nil?
179
+ @logger.debug 'fetching public keys'
180
+ # fetch keys from /v2/keys and set them in @public_keys
181
+ fetch_public_keys
182
+ end
183
+
184
+ found_key = @public_keys[kid]
185
+ @logger.debug "found_key: #{found_key}"
186
+ raise AuthException.new('Unable to validate public key. Public key not found.', code: 500) if found_key.nil?
187
+ end
188
+
189
+ # save reference to the found key
190
+ # (as another thread can change the self.public_keys hash)
191
+ @logger.debug 'checking if alg_header matches alg_from_key'
192
+ alg_from_key = found_key[1]
193
+ if alg_header != alg_from_key
194
+ raise AuthException.new(
195
+ 'Algorithm signature in JWT header does not match the algorithm signature in the Public key.',
196
+ code: 500
197
+ )
198
+ end
199
+
200
+ begin
201
+ @logger.debug 'decoding token'
202
+ claims = JWT.decode(
203
+ token,
204
+ found_key[0].public_key,
205
+ true,
206
+ { algorithm: alg_header, exp_leeway: @jwt_validation_leeway }
207
+ )[0] # the payload is the first index in the decoded array
208
+ rescue JWT::ExpiredSignature => e
209
+ raise AuthException.new(
210
+ "Received Invalid token times error due to time glitch (between machines) during jwt validation, try to set the jwt_validation_leeway parameter (in DescopeClient) to higher value than 5sec which is the default: #{e.message}", code: 500
211
+ )
212
+ end
213
+ claims['jwt'] = token
214
+ @logger.debug "claims: #{claims}"
215
+ claims
216
+ end
217
+
218
+ private
219
+
220
+ def generate_auth_info(response_body, refresh_token, user_jwt, audience = nil)
221
+ @logger.debug "generating auth info: #{response_body}, #{refresh_token}, #{user_jwt}, #{audience}"
222
+ jwt_response = {}
223
+
224
+ # validate the session token if sessionJwt is not empty
225
+ st_jwt = response_body.fetch('sessionJwt', '')
226
+ if st_jwt
227
+ jwt_response[SESSION_TOKEN_NAME] = validate_token(st_jwt, audience)
228
+ end
229
+
230
+ # validate refresh token if refresh_token was passed or if refreshJwt is not empty
231
+ rt_jwt = response_body.fetch('refreshJwt', '')
232
+
233
+ if refresh_token
234
+ jwt_response[REFRESH_SESSION_TOKEN_NAME] = validate_token(refresh_token, audience)
235
+ elsif rt_jwt
236
+ jwt_response[REFRESH_SESSION_TOKEN_NAME] = validate_token(rt_jwt, audience)
237
+ end
238
+
239
+ jwt_response = adjust_properties(jwt_response, user_jwt)
240
+
241
+ if user_jwt
242
+ jwt_response[COOKIE_DATA_NAME] = {
243
+ exp: response_body.fetch('cookieExpiration', 0),
244
+ maxAge: response_body.fetch('cookieMaxAge', 0),
245
+ domain: response_body.fetch('cookieDomain', ''),
246
+ path: response_body.fetch('cookiePath', '/')
247
+ }
248
+ end
249
+
250
+ jwt_response
251
+ end
252
+
253
+ def adjust_properties(jwt_response, user_jwt)
254
+ # Save permissions, roles and tenants info from Session token or from refresh token on the json top level
255
+ if jwt_response[SESSION_TOKEN_NAME]
256
+ jwt_response['permissions'] = jwt_response[SESSION_TOKEN_NAME].fetch('permissions', [])
257
+ jwt_response['roles'] = jwt_response[SESSION_TOKEN_NAME].fetch('roles', [])
258
+ jwt_response['tenants'] = jwt_response[SESSION_TOKEN_NAME].fetch('tenants', {})
259
+ elsif jwt_response[REFRESH_SESSION_TOKEN_NAME]
260
+ jwt_response['permissions'] = jwt_response[REFRESH_SESSION_TOKEN_NAME].fetch('permissions', [])
261
+ jwt_response['roles'] = jwt_response[REFRESH_SESSION_TOKEN_NAME].fetch('roles', [])
262
+ jwt_response['tenants'] = jwt_response[REFRESH_SESSION_TOKEN_NAME].fetch('tenants', {})
263
+ else
264
+ jwt_response['permissions'] = jwt_response.fetch('permissions', [])
265
+ jwt_response['roles'] = jwt_response.fetch('roles', [])
266
+ jwt_response['tenants'] = jwt_response.fetch('tenants', {})
267
+ end
268
+
269
+ # Save the projectID also in the dict top level
270
+ issuer =
271
+ jwt_response.fetch(SESSION_TOKEN_NAME, {}).fetch('iss', nil) ||
272
+ jwt_response.fetch(REFRESH_SESSION_TOKEN_NAME, {}).fetch('iss', nil) ||
273
+ jwt_response.fetch('iss', '')
274
+
275
+ jwt_response['projectId'] = issuer.split('/').last # support both url issuer and project ID issuer
276
+
277
+ sub =
278
+ jwt_response.fetch(SESSION_TOKEN_NAME, {}).fetch('iss', nil) ||
279
+ jwt_response.fetch(REFRESH_SESSION_TOKEN_NAME, {}).fetch('iss', nil) ||
280
+ jwt_response.fetch('sub', '')
281
+
282
+ if user_jwt
283
+ jwt_response['userId'] = sub # Save the userID also in the dict top level
284
+ else
285
+ jwt_response['keyId'] = sub # Save the AccessKeyID also in the dict top level
286
+ end
287
+
288
+ jwt_response
289
+ end
290
+
291
+ def jwt_get_unverified_header(token)
292
+ begin
293
+ decode_response = JWT.decode(token, nil, false)
294
+ rescue JWT::DecodeError => e
295
+ raise AuthException.new("Unable to parse token. #{e.message}", code: 500)
296
+ end
297
+
298
+ # The JWT.decode method returns an array where
299
+ # the first element is the payload and the second element is the header.
300
+ decode_response[1]
301
+ end
302
+
303
+ def fetch_public_keys
304
+ response = token_validation_key(@project_id)
305
+ unless response.is_a?(Hash) && response.key?('keys')
306
+ raise AuthException.new("Unable to fetch public keys. #{response}", code: 500)
307
+ end
308
+
309
+ jwkeys_wrapper = response
310
+ jwkeys = jwkeys_wrapper['keys']
311
+ @public_keys = {}
312
+
313
+ jwkeys.each do |key|
314
+ loaded_kid, pub_key, alg = validate_and_load_public_key(key)
315
+ @public_keys[loaded_kid] = [pub_key, alg]
316
+ rescue AuthException
317
+ nil
318
+ end
319
+ end
320
+
321
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
322
+ def validate_and_load_public_key(public_key)
323
+ unless public_key.is_a?(String) || public_key.is_a?(Hash)
324
+ raise AuthException.new(
325
+ 'Unable to load public key. Invalid public key error: (unknown type)',
326
+ code: 500
327
+ )
328
+ end
329
+
330
+ if public_key.is_a? String
331
+ begin
332
+ public_key = JSON.parse(public_key)
333
+ rescue JSON::ParserError => e
334
+ raise AuthException.new(
335
+ "Unable to parse public key json, error: #{e.message}",
336
+ code: 500
337
+ )
338
+ end
339
+ end
340
+
341
+ alg = public_key[ALGORITHM_KEY]
342
+ if alg.nil?
343
+ raise AuthException.new(
344
+ 'Unable to load public key. Missing property: alg',
345
+ code: 500
346
+ )
347
+ end
348
+
349
+ kid = public_key['kid']
350
+ if kid.nil?
351
+ raise AuthException.new(
352
+ 'Unable to load public key. Missing property: kid',
353
+ code: 500
354
+ )
355
+ end
356
+
357
+ begin
358
+ # Load and validate public key
359
+ [kid, JWT::JWK.new(public_key), alg]
360
+ rescue JWT::JWKError => e
361
+ raise AuthException.new(
362
+ "Unable to load public key #{e.message}",
363
+ code: 500
364
+ )
365
+ end
366
+ end
367
+
368
+ def validate_refresh_token_provided(login_options, refresh_token)
369
+ refresh_required = !login_options.nil? && (login_options[:mfa] || login_options[:stepup])
370
+ refresh_missing = refresh_token.nil? || refresh_token.to_s.empty?
371
+
372
+ return unless refresh_required && refresh_missing
373
+
374
+ raise AuthException.new(
375
+ 'Missing refresh token for stepup/mfa',
376
+ code: 400
377
+ )
378
+ end
379
+
380
+ def compose_url(base, method)
381
+ suffix = get_method_string(method)
382
+ unless suffix
383
+ raise AuthException.new(
384
+ "Unable to compose url. Unknown delivery method: #{method}",
385
+ code: 500
386
+ )
387
+ end
388
+ "#{base}/#{suffix}"
389
+ end
390
+
391
+ def get_login_id_by_method(method: nil, user: {})
392
+ login_id = {
393
+ DeliveryMethod::WHATSAPP => ['whatsapp', user.fetch(:phone, '')],
394
+ DeliveryMethod::SMS => ['phone', user.fetch(:phone, '')],
395
+ DeliveryMethod::EMAIL => ['email', user.fetch(:email, '')]
396
+ }[method]
397
+
398
+ raise AuthException.new("Unknown delivery method: #{method}", code: 400) if login_id.nil?
399
+
400
+ login_id
401
+ end
402
+
403
+ def adjust_and_verify_delivery_method(method, login_id, user)
404
+ return false if login_id.nil?
405
+
406
+ return false unless user.is_a?(Hash)
407
+
408
+ case method
409
+ 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])
423
+ else
424
+ return false
425
+ end
426
+
427
+ true
428
+ end
429
+
430
+ def extract_masked_address(response, method)
431
+ if [DeliveryMethod::SMS, DeliveryMethod::WHATSAPP].include?(method)
432
+ response['maskedPhone']
433
+ elsif method == DeliveryMethod::EMAIL
434
+ response['maskedEmail']
435
+ else
436
+ ''
437
+ end
438
+ end
439
+
440
+ def exchange_token(uri, code)
441
+ raise Descope::ArgumentException.new("Code can't be empty", code: 400) if code.nil? || code.empty?
442
+
443
+ res = post(uri, { code: })
444
+ generate_jwt_response(
445
+ response_body: res,
446
+ refresh_cookie: res['refreshJwt']
447
+ )
448
+ end
449
+ end
450
+ end
451
+ end
452
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Descope
4
+ module Api
5
+ module V1
6
+ module Management
7
+ # Management API calls
8
+ module AccessKey
9
+ include Descope::Mixins::Validation
10
+ include Descope::Api::V1::Management::Common
11
+
12
+ def create_access_key(name: nil, expire_time: nil, role_names: nil, key_tenants: nil)
13
+ # Create a new access key.'
14
+ # @see https://docs.descope.com/api/openapi/accesskeymanagement/operation/CreateAccessKey/
15
+
16
+ role_names ||= []
17
+ key_tenants ||= []
18
+ validate_tenants(key_tenants)
19
+ post(ACCESS_KEY_CREATE_PATH, access_key_compose_create_body(name, expire_time, role_names, key_tenants))
20
+ end
21
+
22
+ def access_key_compose_create_body(name, expire_time, role_names, key_tenants)
23
+ {
24
+ name:,
25
+ expireTime: expire_time,
26
+ roleNames: role_names,
27
+ keyTenants: associated_tenants_to_hash_array(key_tenants)
28
+ }
29
+ end
30
+
31
+ def load_access_key(id)
32
+ # Load an access key.'
33
+ # @param id [string] The access key id.
34
+ # @see https://docs.descope.com/api/openapi/accesskeymanagement/operation/LoadAccessKey/
35
+
36
+ get(ACCESS_KEY_LOAD_PATH, { id: })
37
+ end
38
+
39
+ def search_all_access_keys(tenant_ids = nil)
40
+ # Search all access keys.'
41
+ # @see https://docs.descope.com/api/openapi/accesskeymanagement/operation/SearchAccessKeys/
42
+ request_params = {
43
+ tenantIds: tenant_ids
44
+ }
45
+ post(ACCESS_KEYS_SEARCH_PATH, request_params)
46
+ end
47
+
48
+ def update_access_key(id: nil, name: nil)
49
+ # Update an existing access key name
50
+ # @see https://docs.descope.com/api/openapi/accesskeymanagement/operation/UpdateAccessKey/
51
+ request_params = {
52
+ id:,
53
+ name:
54
+ }
55
+ post(ACCESS_KEY_UPDATE_PATH, request_params)
56
+ end
57
+
58
+ def deactivate_access_key(id)
59
+ # Deactivate an existing access key. IMPORTANT: This deactivated key will not be usable from this stage.
60
+ # It will, however, persist, and can be activated again if needed.
61
+ # @see https://docs.descope.com/api/openapi/accesskeymanagement/operation/DeactivateAccessKey/
62
+ post(ACCESS_KEY_DEACTIVATE_PATH, { id: })
63
+ end
64
+
65
+ def activate_access_key(id)
66
+ # Activate an existing access key. IMPORTANT: Only deactivated keys can be activated again,
67
+ # and become usable once more. New access keys are active by default.
68
+ # @see https://docs.descope.com/api/openapi/accesskeymanagement/operation/ActivateAccessKey/
69
+ post(ACCESS_KEY_ACTIVATE_PATH, { id: })
70
+ end
71
+
72
+ def delete_access_key(id)
73
+ # Delete an existing access key. IMPORTANT: This action is irreversible. Use carefully.
74
+ # @see https://docs.descope.com/api/openapi/accesskeymanagement/operation/DeleteAccessKey/
75
+ post(ACCESS_KEY_DELETE_PATH, { id: })
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end