th7-clerk-sdk-ruby 4.2.2

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 (195) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +3 -0
  3. data/.github/workflows/main.yml +30 -0
  4. data/.github/workflows/semgrep.yml +24 -0
  5. data/.gitignore +21 -0
  6. data/.rspec +3 -0
  7. data/.ruby-version +1 -0
  8. data/CHANGELOG.md +212 -0
  9. data/Gemfile +33 -0
  10. data/Gemfile.lock +300 -0
  11. data/Guardfile +14 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +278 -0
  14. data/Rakefile +56 -0
  15. data/apps/rack/app.rb +67 -0
  16. data/apps/rack/config.ru +17 -0
  17. data/apps/rack/middleware/disable_paths.rb +13 -0
  18. data/apps/rails-api/.dockerignore +41 -0
  19. data/apps/rails-api/.gitattributes +9 -0
  20. data/apps/rails-api/.gitignore +32 -0
  21. data/apps/rails-api/.kamal/hooks/docker-setup.sample +3 -0
  22. data/apps/rails-api/.kamal/hooks/post-deploy.sample +14 -0
  23. data/apps/rails-api/.kamal/hooks/post-proxy-reboot.sample +3 -0
  24. data/apps/rails-api/.kamal/hooks/pre-build.sample +51 -0
  25. data/apps/rails-api/.kamal/hooks/pre-connect.sample +47 -0
  26. data/apps/rails-api/.kamal/hooks/pre-deploy.sample +109 -0
  27. data/apps/rails-api/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  28. data/apps/rails-api/.kamal/secrets +17 -0
  29. data/apps/rails-api/.rubocop.yml +8 -0
  30. data/apps/rails-api/.ruby-version +1 -0
  31. data/apps/rails-api/Dockerfile +69 -0
  32. data/apps/rails-api/Gemfile +54 -0
  33. data/apps/rails-api/Gemfile.lock +374 -0
  34. data/apps/rails-api/README.md +24 -0
  35. data/apps/rails-api/Rakefile +6 -0
  36. data/apps/rails-api/app/controllers/application_controller.rb +3 -0
  37. data/apps/rails-api/app/controllers/home_controller.rb +5 -0
  38. data/apps/rails-api/app/jobs/application_job.rb +7 -0
  39. data/apps/rails-api/app/mailers/application_mailer.rb +4 -0
  40. data/apps/rails-api/app/models/application_record.rb +3 -0
  41. data/apps/rails-api/app/views/layouts/mailer.html.erb +13 -0
  42. data/apps/rails-api/app/views/layouts/mailer.text.erb +1 -0
  43. data/apps/rails-api/bin/brakeman +7 -0
  44. data/apps/rails-api/bin/bundle +109 -0
  45. data/apps/rails-api/bin/dev +2 -0
  46. data/apps/rails-api/bin/docker-entrypoint +14 -0
  47. data/apps/rails-api/bin/jobs +6 -0
  48. data/apps/rails-api/bin/kamal +27 -0
  49. data/apps/rails-api/bin/rails +4 -0
  50. data/apps/rails-api/bin/rake +4 -0
  51. data/apps/rails-api/bin/rubocop +8 -0
  52. data/apps/rails-api/bin/setup +34 -0
  53. data/apps/rails-api/bin/thrust +5 -0
  54. data/apps/rails-api/config/application.rb +36 -0
  55. data/apps/rails-api/config/boot.rb +4 -0
  56. data/apps/rails-api/config/cable.yml +17 -0
  57. data/apps/rails-api/config/cache.yml +16 -0
  58. data/apps/rails-api/config/credentials.yml.enc +1 -0
  59. data/apps/rails-api/config/database.yml +41 -0
  60. data/apps/rails-api/config/deploy.yml +116 -0
  61. data/apps/rails-api/config/environment.rb +5 -0
  62. data/apps/rails-api/config/environments/development.rb +70 -0
  63. data/apps/rails-api/config/environments/production.rb +88 -0
  64. data/apps/rails-api/config/environments/test.rb +53 -0
  65. data/apps/rails-api/config/initializers/cors.rb +16 -0
  66. data/apps/rails-api/config/initializers/filter_parameter_logging.rb +8 -0
  67. data/apps/rails-api/config/initializers/inflections.rb +16 -0
  68. data/apps/rails-api/config/locales/en.yml +31 -0
  69. data/apps/rails-api/config/puma.rb +41 -0
  70. data/apps/rails-api/config/queue.yml +18 -0
  71. data/apps/rails-api/config/recurring.yml +10 -0
  72. data/apps/rails-api/config/routes.rb +10 -0
  73. data/apps/rails-api/config/storage.yml +34 -0
  74. data/apps/rails-api/config.ru +6 -0
  75. data/apps/rails-api/db/cable_schema.rb +11 -0
  76. data/apps/rails-api/db/cache_schema.rb +14 -0
  77. data/apps/rails-api/db/queue_schema.rb +129 -0
  78. data/apps/rails-api/db/seeds.rb +9 -0
  79. data/apps/rails-api/public/robots.txt +1 -0
  80. data/apps/rails-api/test/controllers/home_controller_test.rb +7 -0
  81. data/apps/rails-api/test/test_helper.rb +15 -0
  82. data/apps/rails-full/.dockerignore +47 -0
  83. data/apps/rails-full/.gitattributes +9 -0
  84. data/apps/rails-full/.gitignore +34 -0
  85. data/apps/rails-full/.kamal/hooks/docker-setup.sample +3 -0
  86. data/apps/rails-full/.kamal/hooks/post-deploy.sample +14 -0
  87. data/apps/rails-full/.kamal/hooks/post-proxy-reboot.sample +3 -0
  88. data/apps/rails-full/.kamal/hooks/pre-build.sample +51 -0
  89. data/apps/rails-full/.kamal/hooks/pre-connect.sample +47 -0
  90. data/apps/rails-full/.kamal/hooks/pre-deploy.sample +109 -0
  91. data/apps/rails-full/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  92. data/apps/rails-full/.kamal/secrets +17 -0
  93. data/apps/rails-full/.rubocop.yml +8 -0
  94. data/apps/rails-full/.ruby-version +1 -0
  95. data/apps/rails-full/Dockerfile +72 -0
  96. data/apps/rails-full/Gemfile +70 -0
  97. data/apps/rails-full/Gemfile.lock +429 -0
  98. data/apps/rails-full/README.md +24 -0
  99. data/apps/rails-full/Rakefile +6 -0
  100. data/apps/rails-full/app/assets/stylesheets/application.css +10 -0
  101. data/apps/rails-full/app/controllers/application_controller.rb +6 -0
  102. data/apps/rails-full/app/controllers/home_controller.rb +11 -0
  103. data/apps/rails-full/app/helpers/application_helper.rb +2 -0
  104. data/apps/rails-full/app/helpers/home_helper.rb +2 -0
  105. data/apps/rails-full/app/javascript/application.js +3 -0
  106. data/apps/rails-full/app/javascript/controllers/application.js +9 -0
  107. data/apps/rails-full/app/javascript/controllers/hello_controller.js +7 -0
  108. data/apps/rails-full/app/javascript/controllers/index.js +4 -0
  109. data/apps/rails-full/app/jobs/application_job.rb +7 -0
  110. data/apps/rails-full/app/mailers/application_mailer.rb +4 -0
  111. data/apps/rails-full/app/models/application_record.rb +3 -0
  112. data/apps/rails-full/app/views/home/index.html.erb +7 -0
  113. data/apps/rails-full/app/views/layouts/application.html.erb +60 -0
  114. data/apps/rails-full/app/views/layouts/mailer.html.erb +13 -0
  115. data/apps/rails-full/app/views/layouts/mailer.text.erb +1 -0
  116. data/apps/rails-full/app/views/pwa/manifest.json.erb +22 -0
  117. data/apps/rails-full/app/views/pwa/service-worker.js +26 -0
  118. data/apps/rails-full/bin/brakeman +7 -0
  119. data/apps/rails-full/bin/bundle +109 -0
  120. data/apps/rails-full/bin/dev +2 -0
  121. data/apps/rails-full/bin/docker-entrypoint +14 -0
  122. data/apps/rails-full/bin/importmap +4 -0
  123. data/apps/rails-full/bin/jobs +6 -0
  124. data/apps/rails-full/bin/kamal +27 -0
  125. data/apps/rails-full/bin/rails +4 -0
  126. data/apps/rails-full/bin/rake +4 -0
  127. data/apps/rails-full/bin/rubocop +8 -0
  128. data/apps/rails-full/bin/setup +34 -0
  129. data/apps/rails-full/bin/thrust +5 -0
  130. data/apps/rails-full/config/application.rb +31 -0
  131. data/apps/rails-full/config/boot.rb +4 -0
  132. data/apps/rails-full/config/cable.yml +17 -0
  133. data/apps/rails-full/config/cache.yml +16 -0
  134. data/apps/rails-full/config/credentials.yml.enc +1 -0
  135. data/apps/rails-full/config/database.yml +41 -0
  136. data/apps/rails-full/config/deploy.yml +116 -0
  137. data/apps/rails-full/config/environment.rb +5 -0
  138. data/apps/rails-full/config/environments/development.rb +72 -0
  139. data/apps/rails-full/config/environments/production.rb +91 -0
  140. data/apps/rails-full/config/environments/test.rb +53 -0
  141. data/apps/rails-full/config/importmap.rb +7 -0
  142. data/apps/rails-full/config/initializers/assets.rb +7 -0
  143. data/apps/rails-full/config/initializers/clerk.rb +4 -0
  144. data/apps/rails-full/config/initializers/content_security_policy.rb +25 -0
  145. data/apps/rails-full/config/initializers/filter_parameter_logging.rb +8 -0
  146. data/apps/rails-full/config/initializers/inflections.rb +16 -0
  147. data/apps/rails-full/config/locales/en.yml +31 -0
  148. data/apps/rails-full/config/puma.rb +41 -0
  149. data/apps/rails-full/config/queue.yml +18 -0
  150. data/apps/rails-full/config/recurring.yml +10 -0
  151. data/apps/rails-full/config/routes.rb +15 -0
  152. data/apps/rails-full/config/storage.yml +34 -0
  153. data/apps/rails-full/config.ru +6 -0
  154. data/apps/rails-full/db/cable_schema.rb +11 -0
  155. data/apps/rails-full/db/cache_schema.rb +14 -0
  156. data/apps/rails-full/db/queue_schema.rb +129 -0
  157. data/apps/rails-full/db/seeds.rb +9 -0
  158. data/apps/rails-full/public/400.html +114 -0
  159. data/apps/rails-full/public/404.html +114 -0
  160. data/apps/rails-full/public/406-unsupported-browser.html +114 -0
  161. data/apps/rails-full/public/422.html +114 -0
  162. data/apps/rails-full/public/500.html +114 -0
  163. data/apps/rails-full/public/icon.png +0 -0
  164. data/apps/rails-full/public/icon.svg +3 -0
  165. data/apps/rails-full/public/robots.txt +1 -0
  166. data/apps/rails-full/test/application_system_test_case.rb +5 -0
  167. data/apps/rails-full/test/controllers/home_controller_test.rb +7 -0
  168. data/apps/rails-full/test/test_helper.rb +15 -0
  169. data/apps/sinatra/app.rb +29 -0
  170. data/apps/sinatra/config.ru +8 -0
  171. data/apps/sinatra/views/index.erb +44 -0
  172. data/bin/console +16 -0
  173. data/bin/release +21 -0
  174. data/bin/setup +8 -0
  175. data/clerk-sdk-ruby.gemspec +38 -0
  176. data/docs/clerk-logo-dark.png +0 -0
  177. data/docs/clerk-logo-light.png +0 -0
  178. data/lib/clerk/authenticatable.rb +32 -0
  179. data/lib/clerk/authenticate_context.rb +168 -0
  180. data/lib/clerk/authenticate_request.rb +261 -0
  181. data/lib/clerk/configuration.rb +84 -0
  182. data/lib/clerk/constants.rb +74 -0
  183. data/lib/clerk/error.rb +17 -0
  184. data/lib/clerk/jwks_cache.rb +37 -0
  185. data/lib/clerk/proxy.rb +135 -0
  186. data/lib/clerk/rack.rb +2 -0
  187. data/lib/clerk/rack_middleware.rb +112 -0
  188. data/lib/clerk/rails.rb +3 -0
  189. data/lib/clerk/railtie.rb +15 -0
  190. data/lib/clerk/sdk.rb +84 -0
  191. data/lib/clerk/sinatra.rb +52 -0
  192. data/lib/clerk/utils.rb +73 -0
  193. data/lib/clerk/version.rb +5 -0
  194. data/lib/clerk.rb +27 -0
  195. metadata +340 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/clerk/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "th7-clerk-sdk-ruby"
7
+ spec.version = Clerk::VERSION
8
+ spec.authors = ["Tyler"]
9
+ spec.email = ["tylerhartland7@gmail.com"]
10
+
11
+ spec.summary = "Fork of Clerk SDK for Ruby."
12
+ spec.description = "Fork of Client SDK for the Clerk"
13
+ spec.homepage = "https://github.com/th7/clerk-sdk-ruby"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/th7/clerk-sdk-ruby"
19
+ spec.metadata["changelog_uri"] = "https://github.com/th7/clerk-sdk-ruby/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "faraday", ">= 1.4.1", "< 3.0"
31
+ spec.add_dependency "jwt", '~> 2.5'
32
+ spec.add_dependency "clerk-http-client", "~> 2.0"
33
+ spec.add_dependency "concurrent-ruby", "~> 1.1"
34
+ spec.add_dependency "ostruct", "~> 0.6.1"
35
+
36
+ spec.add_development_dependency "byebug", "~> 11.1"
37
+ spec.add_development_dependency "timecop", "~> 0.9.4"
38
+ end
Binary file
Binary file
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Clerk
6
+ module Authenticatable
7
+ extend ActiveSupport::Concern
8
+
9
+ protected
10
+
11
+ def clerk
12
+ request.env["clerk"]
13
+ end
14
+
15
+ def require_reverification!(preset = StepUp::Preset::STRICT, &block)
16
+ clerk.user_require_reverification!(preset) do
17
+ return yield(preset) if block_given?
18
+ render_reverification!(preset)
19
+ end
20
+ end
21
+
22
+ def render_reverification!(preset = nil)
23
+ render status: 403, json: StepUp::Reverification.error_payload(preset)
24
+ end
25
+
26
+ included do
27
+ if respond_to?(:helper_method)
28
+ helper_method :clerk, :require_reverification!, :render_reverification!
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+ require "forwardable"
5
+
6
+ module Clerk
7
+ # This class represents a parameter object used to contain all request and configuration
8
+ # information required by the middleware to resolve the current request state.
9
+ # link: https://refactoring.guru/introduce-parameter-object
10
+ class AuthenticateContext
11
+ extend Forwardable
12
+
13
+ # Expose the url of the request that this parameter object was created from as a URI object.
14
+ attr_reader :clerk_url
15
+
16
+ # Expose properties that does not require validations or complex logic to retrieve
17
+ # values by delegating them to the cookies or headers variables.
18
+ def_delegators :@cookies, :session_token_in_cookie, :client_uat
19
+ def_delegators :@headers, :session_token_in_header, :sec_fetch_dest
20
+
21
+ # Creates a new parameter object using ::Rack::Request and Clerk::Config objects.
22
+ def initialize(request, config)
23
+ @clerk_url = URI.parse(request.url)
24
+ @config = config
25
+
26
+ @cookies = OpenStruct.new({
27
+ client_uat: request.cookies[CLIENT_UAT_COOKIE],
28
+ dev_browser: request.cookies[DEV_BROWSER_COOKIE],
29
+ handshake_token: request.cookies[HANDSHAKE_COOKIE],
30
+ session_token_in_cookie: request.cookies[SESSION_COOKIE]
31
+ })
32
+
33
+ @headers = OpenStruct.new({
34
+ accept: Utils.retrieve_header_from_request(request, ACCEPT_HEADER),
35
+ host: request.host,
36
+ origin: Utils.retrieve_header_from_request(request, ORIGIN_HEADER),
37
+ port: request.port,
38
+ sec_fetch_dest: Utils.retrieve_header_from_request(request, SEC_FETCH_DEST_HEADER),
39
+ session_token_in_header: Utils.retrieve_header_from_request(request, AUTHORIZATION_HEADER).gsub(/bearer/i, "").strip
40
+ })
41
+ end
42
+
43
+ # The following properties are part of the props supported in all the AuthenticateContext
44
+ # objects across all of our SDKs (eg JS, Go)
45
+ def secret_key
46
+ raise ConfigurationError, "Clerk secret key is not set" if @config.secret_key.to_s.empty?
47
+
48
+ @config.secret_key.to_s
49
+ end
50
+
51
+ def publishable_key
52
+ raise ConfigurationError, "Clerk publishable key is not set" if @config.publishable_key.to_s.to_s.empty?
53
+
54
+ @config.publishable_key.to_s
55
+ end
56
+
57
+ def proxy_url?
58
+ !proxy_url.empty?
59
+ end
60
+
61
+ def handshake_token
62
+ @handshake_token ||= Utils.retrieve_from_query_string(@clerk_url, HANDSHAKE_COOKIE) || @cookies.handshake_token.to_s
63
+ end
64
+
65
+ def dev_browser
66
+ @dev_browser ||= dev_browser_in_url || @cookies.dev_browser.to_s
67
+ end
68
+
69
+ # The frontend_api returned is without protocol prefix
70
+ def frontend_api
71
+ return "" unless Utils.valid_publishable_key?(publishable_key.to_s)
72
+
73
+ @frontend_api ||= if proxy_url?
74
+ proxy_url
75
+ elsif development_instance? && !domain.empty?
76
+ "clerk.#{domain}"
77
+ else
78
+ # remove $ postfix
79
+ Utils.decode_publishable_key(publishable_key).chop.to_s
80
+ end
81
+ end
82
+
83
+ def development_instance?
84
+ secret_key.start_with?("sk_test_")
85
+ end
86
+
87
+ def production_instance?
88
+ secret_key.start_with?("sk_live_")
89
+ end
90
+
91
+ def document_request?
92
+ @headers.sec_fetch_dest == "document"
93
+ end
94
+
95
+ def accepts_html?
96
+ @headers.accept&.start_with?("text/html")
97
+ end
98
+
99
+ def eligible_for_multi_domain?
100
+ is_satellite? && document_request? && !clerk_synced?
101
+ end
102
+
103
+ def active_client?
104
+ @cookies.client_uat.to_i.positive?
105
+ end
106
+
107
+ def cross_origin_request?
108
+ # origin contains scheme+host and optionally port (omitted if 80 or 443)
109
+ # ref. https://www.rfc-editor.org/rfc/rfc6454#section-6.1
110
+ return false if @headers.origin.nil?
111
+
112
+ # strip scheme
113
+ origin = @headers.origin.strip.sub(%r{\A(\w+:)?//}, "")
114
+ return false if origin.empty?
115
+
116
+ # Rack's host and port helpers are reverse-proxy-aware; that
117
+ # is, they prefer the de-facto X-Forwarded-* headers if they're set
118
+ request_host = @headers.host
119
+ request_host << ":#{@headers.port}" if @headers.port != 80 && @headers.port != 443
120
+
121
+ origin != request_host
122
+ end
123
+
124
+ def dev_browser?
125
+ !dev_browser.empty?
126
+ end
127
+
128
+ def session_token_in_header?
129
+ !session_token_in_header.to_s.empty?
130
+ end
131
+
132
+ def handshake_token?
133
+ !handshake_token.to_s.empty?
134
+ end
135
+
136
+ def session_token_in_cookie?
137
+ !session_token_in_cookie.to_s.empty?
138
+ end
139
+
140
+ def dev_browser_in_url
141
+ Utils.retrieve_from_query_string(@clerk_url, DEV_BROWSER_COOKIE)
142
+ end
143
+
144
+ def dev_browser_in_url?
145
+ !!dev_browser_in_url
146
+ end
147
+
148
+ def domain
149
+ "" # TODO: Add multi-domain support
150
+ end
151
+
152
+ def is_satellite?
153
+ false # TODO: Add multi-domain support
154
+ end
155
+
156
+ def proxy_url
157
+ "" # TODO: Add multi-domain support
158
+ end
159
+
160
+ def clerk_synced?
161
+ false # TODO: Add multi-domain support
162
+ end
163
+
164
+ def clerk_redirect_url
165
+ "" # TODO: Add multi-domain support
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "clerk/proxy"
4
+ require "rack/utils"
5
+
6
+ module Clerk
7
+ # This class represents a service object used to determine the current request state
8
+ # for the current env passed based on a provided Clerk::AuthenticateContext.
9
+ # There is only 1 public method exposed (`resolve`) to be invoked with a env parameter.
10
+ class AuthenticateRequest
11
+ attr_reader :auth_context
12
+
13
+ # Creates a new instance using Clerk::AuthenticateContext object.
14
+ def initialize(auth_context)
15
+ @auth_context = auth_context
16
+ end
17
+
18
+ # Determines the current request state by verifying a Clerk token in headers or cookies.
19
+ # The possible outcomes of this method are `signed-in`, `signed-out` or `handshake` states.
20
+ # The return values are the same as a return value of a rack middleware `[http_status_code, headers, body]`.
21
+ # When used in a middleware the consumer of this service should return the return value when there is an
22
+ # `http_status_code` provided otherwise the should continue with the middleware chain.
23
+ # The headers provided in the return value is a hash of { header_key => header_value } and in the case
24
+ # of a `Set-Cookie` header the `header_value` used is a list of raw HTTP Set-Cookie directives.
25
+ def resolve(env)
26
+ if auth_context.session_token_in_header?
27
+ resolve_header_token(env)
28
+ else
29
+ resolve_cookie_token(env)
30
+ end
31
+ end
32
+
33
+ protected
34
+
35
+ def resolve_header_token(env)
36
+ begin
37
+ # malformed JWT
38
+ unless sdk.decode_token(auth_context.session_token_in_header)
39
+ return signed_out(reason: TokenVerificationErrorReason::TOKEN_INVALID)
40
+ end
41
+
42
+ claims = verify_token(auth_context.session_token_in_header)
43
+ return signed_in(env, claims, auth_context.session_token_in_header) if claims
44
+ rescue JWT::ExpiredSignature
45
+ # Expired token
46
+ return signed_out(enforce_auth: true, reason: TokenVerificationErrorReason::TOKEN_EXPIRED)
47
+ rescue JWT::InvalidIatError
48
+ # Token not active yet
49
+ return signed_out(enforce_auth: true, reason: TokenVerificationErrorReason::TOKEN_NOT_ACTIVE_YET)
50
+ rescue JWT::DecodeError
51
+ # Malformed JWT (NOTE: Must be the last rescue block as it catches all decoding errors)
52
+ return signed_out(reason: TokenVerificationErrorReason::TOKEN_INVALID)
53
+ end
54
+
55
+ # Clerk.js should refresh the token and retry
56
+ signed_out(enforce_auth: true)
57
+ end
58
+
59
+ def resolve_cookie_token(env)
60
+ # in cross-origin XHRs the use of Authorization header is mandatory.
61
+ # TODO: add reason
62
+ return signed_out if auth_context.cross_origin_request?
63
+
64
+ return resolve_handshake(env) if auth_context.handshake_token?
65
+
66
+ if auth_context.development_instance? && auth_context.dev_browser_in_url?
67
+ return handle_handshake_maybe_status(env, reason: AuthErrorReason::DEV_BROWSER_SYNC)
68
+ end
69
+
70
+ if auth_context.development_instance? && !auth_context.dev_browser?
71
+ return handle_handshake_maybe_status(env, reason: AuthErrorReason::DEV_BROWSER_MISSING)
72
+ end
73
+
74
+ # TODO: Add multi-domain support for production
75
+ # if auth_context.production_instance? && auth_context.eligible_for_multi_domain?
76
+ # return handle_handshake_maybe_status(env, reason: AuthErrorReason::SATELLITE_COOKIE_NEEDS_SYNCING)
77
+ # end
78
+
79
+ # TODO: Add multi-domain support for development
80
+ # if auth_context.development_instance? && auth_context.eligible_for_multi_domain?
81
+ # # trigger handshake using auth_context.sign_in_url as base redirect_url
82
+ # # return handle_handshake_maybe_status(env, reason: AuthErrorReason::SATELLITE_COOKIE_NEEDS_SYNCING, '', headers)
83
+ # end
84
+
85
+ # TODO: Add multi-domain support for development in primary
86
+ # if auth_context.development_instance? && !auth_context.is_satellite? && auth_context.clerk_redirect_url
87
+ # # trigger handshake using auth_context.clerk_redirect_url as base redirect_url + mark it as clerk_synced
88
+ # # return handle_handshake_maybe_status(env, reason: AuthErrorReason::PRIMARY_RESPONDS_TO_SYNCING, '', headers)
89
+ # end
90
+
91
+ if !auth_context.active_client? && !auth_context.session_token_in_cookie?
92
+ return signed_out(reason: AuthErrorReason::SESSION_TOKEN_AND_UAT_MISSING)
93
+ end
94
+
95
+ # This can eagerly run handshake since client_uat is SameSite=Strict in dev
96
+ if !auth_context.active_client? && auth_context.session_token_in_cookie?
97
+ return handle_handshake_maybe_status(env, reason: AuthErrorReason::SESSION_TOKEN_WITHOUT_CLIENT_UAT)
98
+ end
99
+
100
+ if auth_context.active_client? && !auth_context.session_token_in_cookie?
101
+ return handle_handshake_maybe_status(env, reason: AuthErrorReason::CLIENT_UAT_WITHOUT_SESSION_TOKEN)
102
+ end
103
+
104
+ begin
105
+ claims = verify_token(auth_context.session_token_in_cookie)
106
+ return signed_out unless claims
107
+
108
+ if claims["iat"] < auth_context.client_uat.to_i
109
+ return handle_handshake_maybe_status(env, reason: AuthErrorReason::SESSION_TOKEN_OUTDATED)
110
+ end
111
+
112
+ signed_in(env, claims, auth_context.session_token_in_cookie)
113
+ rescue JWT::ExpiredSignature
114
+ handshake(env, reason: TokenVerificationErrorReason::TOKEN_EXPIRED)
115
+ rescue JWT::InvalidIatError
116
+ handshake(env, reason: TokenVerificationErrorReason::TOKEN_NOT_ACTIVE_YET)
117
+ rescue JWT::DecodeError
118
+ signed_out(reason: TokenVerificationErrorReason::TOKEN_INVALID)
119
+ rescue
120
+ signed_out
121
+ end
122
+ end
123
+
124
+ def resolve_handshake(env)
125
+ headers = {
126
+ Clerk::ACCESS_CONTROL_ALLOW_ORIGIN_HEADER => "null",
127
+ Clerk::ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER => "true"
128
+ }
129
+ session_token = nil
130
+
131
+ # Return signed-out outcome if the handshake verification fails
132
+ handshake_payload = verify_token(auth_context.handshake_token)
133
+ unless handshake_payload
134
+ return signed_out(enforce_auth: true, reason: TokenVerificationErrorReason::JWK_FAILED_TO_RESOLVE)
135
+ end
136
+
137
+ # Retrieve the cookie directives included in handshake token payload and convert it to set-cookie headers
138
+ # Also retrieve the session token separately to determine the outcome of the request
139
+ cookies_to_set = handshake_payload[HANDSHAKE_COOKIE_DIRECTIVES_KEY] || []
140
+ cookies_to_set.each do |cookie|
141
+ headers[SET_COOKIE_HEADER] ||= []
142
+ headers[SET_COOKIE_HEADER] << cookie
143
+
144
+ session_token = cookie.split(";")[0].split("=")[1] if cookie.start_with?("#{SESSION_COOKIE}=")
145
+ end
146
+
147
+ # Clear handshake token from query params and set headers to redirect to the initial request url
148
+ if auth_context.development_instance?
149
+ redirect_url = auth_context.clerk_url.dup
150
+ remove_from_query_string(redirect_url, HANDSHAKE_COOKIE)
151
+
152
+ headers[LOCATION_HEADER] = redirect_url.to_s
153
+ end
154
+
155
+ return signed_out(reason: AuthErrorReason::SESSION_TOKEN_MISSING, headers: headers) unless session_token
156
+
157
+ verify_token_with_retry(env, session_token)
158
+ end
159
+
160
+ def handle_handshake_maybe_status(env, **opts)
161
+ return signed_out unless eligible_for_handshake?
162
+
163
+ handshake(env, **opts)
164
+ end
165
+
166
+ # A outcome
167
+ def handshake(_env, **opts)
168
+ redirect_headers = {LOCATION_HEADER => redirect_to_handshake}
169
+ [307, debug_auth_headers(**opts).merge(redirect_headers), []]
170
+ end
171
+
172
+ # B outcome
173
+ def signed_out(**opts)
174
+ headers = opts.delete(:headers) || {}
175
+ enforce_auth = opts.delete(:enforce_auth)
176
+
177
+ [
178
+ enforce_auth ? 401 : nil,
179
+ debug_auth_headers(**opts).merge(headers),
180
+ []
181
+ ]
182
+ end
183
+
184
+ # C outcome
185
+ def signed_in(env, claims, token, **headers)
186
+ env["clerk"] = Proxy.new(session_claims: claims, session_token: token)
187
+ [nil, headers, []]
188
+ end
189
+
190
+ def eligible_for_handshake?
191
+ auth_context.document_request? || (!auth_context.document_request? && auth_context.accepts_html?)
192
+ end
193
+
194
+ private
195
+
196
+ def redirect_to_handshake
197
+ redirect_url = auth_context.clerk_url.dup
198
+ remove_from_query_string(redirect_url, DEV_BROWSER_COOKIE)
199
+
200
+ handshake_url = URI.parse("https://#{auth_context.frontend_api}/v1/client/handshake")
201
+ handshake_url_qs = ::Rack::Utils.parse_query(handshake_url.query)
202
+ handshake_url_qs["redirect_url"] = redirect_url
203
+
204
+ if auth_context.development_instance? && auth_context.dev_browser?
205
+ handshake_url_qs[DEV_BROWSER_COOKIE] = auth_context.dev_browser
206
+ end
207
+
208
+ handshake_url.query = ::Rack::Utils.build_query(handshake_url_qs)
209
+ handshake_url.to_s
210
+ end
211
+
212
+ def remove_from_query_string(url, key)
213
+ qs = ::Rack::Utils.parse_query(url.query)
214
+ qs.delete(key)
215
+
216
+ url.query = ::Rack::Utils.build_query(qs)
217
+ end
218
+
219
+ def verify_token(token, **opts)
220
+ return false if token.nil? || token.strip.empty?
221
+
222
+ begin
223
+ sdk.verify_token(token, **opts)
224
+ rescue JWT::ExpiredSignature, JWT::InvalidIatError => e
225
+ raise e
226
+ rescue JWT::DecodeError, JWT::RequiredDependencyError => _
227
+ false
228
+ end
229
+ end
230
+
231
+ # Verify session token and provide a 1-day leeway for development if initial verification
232
+ # fails for development instance due to invalid exp or iat
233
+ def verify_token_with_retry(env, token)
234
+ claims = verify_token(token)
235
+ signed_in(env, claims, token) if claims
236
+ rescue JWT::ExpiredSignature, JWT::InvalidIatError => e
237
+ if auth_context.development_instance?
238
+ # TODO: log possible Clock skew detected
239
+
240
+ # Retry with a generous clock skew allowance (1 day)
241
+ claims = verify_token(token, timeout: 86_400)
242
+ return signed_in(env, claims, token) if claims
243
+ end
244
+
245
+ # Raise error if handshake resolution fails in production
246
+ raise e
247
+ end
248
+
249
+ def sdk
250
+ SDK.new
251
+ end
252
+
253
+ def debug_auth_headers(reason: nil, message: nil, status: nil)
254
+ {
255
+ AUTH_REASON_HEADER => reason,
256
+ AUTH_MESSAGE_HEADER => message,
257
+ AUTH_STATUS_HEADER => status
258
+ }.compact
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "clerk-http-client"
4
+
5
+ module Clerk
6
+ class Configuration
7
+ attr_reader :cache_store
8
+ attr_reader :debug
9
+ attr_reader :logger
10
+ attr_reader :excluded_routes
11
+ attr_reader :publishable_key
12
+ attr_reader :secret_key
13
+
14
+ def initialize
15
+ @excluded_routes = []
16
+ @publishable_key = ENV["CLERK_PUBLISHABLE_KEY"]
17
+ @secret_key = ENV["CLERK_SECRET_KEY"]
18
+
19
+ # Default to Rails.cache or ActiveSupport::Cache::MemoryStore, if available, otherwise nil
20
+ @cache_store = if defined?(::Rails)
21
+ ::Rails.cache
22
+ elsif defined?(::ActiveSupport::Cache::MemoryStore)
23
+ ::ActiveSupport::Cache::MemoryStore.new
24
+ end
25
+
26
+ ClerkHttpClient.configure do |config|
27
+ unless secret_key.nil? || secret_key.empty?
28
+ config.access_token = @secret_key
29
+ end
30
+ end
31
+ end
32
+
33
+ def self.default
34
+ @@default ||= new
35
+ end
36
+
37
+ def update(options)
38
+ options.each do |key, value|
39
+ send(:"#{key}=", value)
40
+ end
41
+ end
42
+
43
+ def debug=(value)
44
+ ClerkHttpClient::Configuration.default.debugging = value
45
+ @debug = value
46
+ end
47
+
48
+ def cache_store=(store)
49
+ if !store
50
+ @cache_store = nil
51
+ return
52
+ end
53
+
54
+ raise ArgumentError, "cache_store must respond to :fetch" unless store.respond_to?(:fetch)
55
+
56
+ @cache_store = store
57
+ end
58
+
59
+ def excluded_routes=(routes)
60
+ raise ArgumentError, "excluded_routes must be an array" unless routes.is_a?(Array)
61
+ raise ArgumentError, "All elements in the excluded_routes array must be strings" unless routes.all? { |r| r.is_a?(String) }
62
+
63
+ @excluded_routes = routes
64
+ end
65
+
66
+ def publishable_key=(pk)
67
+ raise ArgumentError, "publishable_key must start with 'pk_'" unless pk.start_with?("pk_")
68
+
69
+ @publishable_key = pk
70
+ end
71
+
72
+ def secret_key=(sk)
73
+ raise ArgumentError, "secret_key must start with 'sk_'" unless sk.start_with?("sk_")
74
+
75
+ ClerkHttpClient::Configuration.default.access_token = sk
76
+ @secret_key = sk
77
+ end
78
+
79
+ def logger=(logger)
80
+ ClerkHttpClient::Configuration.default.logger = logger
81
+ @logger = logger
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clerk
4
+ SESSION_COOKIE = "__session"
5
+ CLIENT_UAT_COOKIE = "__client_uat"
6
+
7
+ # Dev Browser
8
+ DEV_BROWSER_COOKIE = "__clerk_db_jwt"
9
+
10
+ # Handshake
11
+ HANDSHAKE_COOKIE = "__clerk_handshake"
12
+ HANDSHAKE_COOKIE_DIRECTIVES_KEY = "handshake"
13
+
14
+ # auth debug response headers
15
+ AUTH_STATUS_HEADER = "x-clerk-auth-status"
16
+ AUTH_REASON_HEADER = "x-clerk-auth-reason"
17
+ AUTH_MESSAGE_HEADER = "x-clerk-auth-message"
18
+
19
+ SEC_FETCH_DEST_HEADER = "HTTP_SEC_FETCH_DEST"
20
+
21
+ # headers used in response - should be lowered case and without http prefix
22
+ ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "access-control-allow-credentials"
23
+ ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "access-control-allow-origin"
24
+ CONTENT_TYPE_HEADER = "content-type"
25
+ LOCATION_HEADER = "location"
26
+ SET_COOKIE_HEADER = "set-cookie"
27
+
28
+ # clerk url related headers
29
+ AUTHORIZATION_HEADER = "HTTP_AUTHORIZATION"
30
+ ACCEPT_HEADER = "HTTP_ACCEPT"
31
+ USER_AGENT_HEADER = "HTTP_USER_AGENT"
32
+ ORIGIN_HEADER = "HTTP_ORIGIN"
33
+
34
+ module TokenVerificationErrorReason
35
+ TOKEN_INVALID = "token-invalid"
36
+ TOKEN_EXPIRED = "token-expired"
37
+ TOKEN_NOT_ACTIVE_YET = "token-not-active-yet"
38
+ JWK_FAILED_TO_RESOLVE = "jwk-failed-to-resolve"
39
+ end
40
+
41
+ module AuthErrorReason
42
+ CLIENT_UAT_WITHOUT_SESSION_TOKEN = "client-uat-but-no-session-token"
43
+ DEV_BROWSER_SYNC = "dev-browser-sync"
44
+ DEV_BROWSER_MISSING = "dev-browser-missing"
45
+ PRIMARY_RESPONDS_TO_SYNCING = "primary-responds-to-syncing"
46
+ SATELLITE_COOKIE_NEEDS_SYNCING = "satellite-needs-syncing"
47
+ SESSION_TOKEN_AND_UAT_MISSING = "session-token-and-uat-missing"
48
+ SESSION_TOKEN_MISSING = "session-token-missing"
49
+ SESSION_TOKEN_OUTDATED = "session-token-outdated"
50
+ SESSION_TOKEN_WITHOUT_CLIENT_UAT = "session-token-but-no-client-uat"
51
+ UNEXPECTED_ERROR = "unexpected-error"
52
+ end
53
+
54
+ module StepUp
55
+ module Preset
56
+ STRICT_MFA = {after_minutes: 10, level: :multi_factor}
57
+ STRICT = {after_minutes: 10, level: :second_factor}
58
+ MODERATE = {after_minutes: 60, level: :second_factor}
59
+ LAX = {after_minutes: 1440, level: :second_factor}
60
+ end
61
+
62
+ module Reverification
63
+ def self.error_payload(missing_config)
64
+ {
65
+ clerk_error: {
66
+ type: "forbidden",
67
+ reason: "reverification-error",
68
+ metadata: {reverification: missing_config}
69
+ }
70
+ }
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,17 @@
1
+ module Clerk
2
+ class Error < StandardError
3
+ attr_reader :status
4
+
5
+ def initialize(msg, status:)
6
+ @errors = msg["errors"]
7
+ @status = status
8
+ super(msg.merge(status: status))
9
+ end
10
+ end
11
+
12
+ class AuthenticationError < Error; end
13
+
14
+ class ConfigurationError < StandardError; end
15
+
16
+ class FatalError < Error; end
17
+ end