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.
- checksums.yaml +7 -0
- data/.env.example +3 -0
- data/.github/workflows/main.yml +30 -0
- data/.github/workflows/semgrep.yml +24 -0
- data/.gitignore +21 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +212 -0
- data/Gemfile +33 -0
- data/Gemfile.lock +300 -0
- data/Guardfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +278 -0
- data/Rakefile +56 -0
- data/apps/rack/app.rb +67 -0
- data/apps/rack/config.ru +17 -0
- data/apps/rack/middleware/disable_paths.rb +13 -0
- data/apps/rails-api/.dockerignore +41 -0
- data/apps/rails-api/.gitattributes +9 -0
- data/apps/rails-api/.gitignore +32 -0
- data/apps/rails-api/.kamal/hooks/docker-setup.sample +3 -0
- data/apps/rails-api/.kamal/hooks/post-deploy.sample +14 -0
- data/apps/rails-api/.kamal/hooks/post-proxy-reboot.sample +3 -0
- data/apps/rails-api/.kamal/hooks/pre-build.sample +51 -0
- data/apps/rails-api/.kamal/hooks/pre-connect.sample +47 -0
- data/apps/rails-api/.kamal/hooks/pre-deploy.sample +109 -0
- data/apps/rails-api/.kamal/hooks/pre-proxy-reboot.sample +3 -0
- data/apps/rails-api/.kamal/secrets +17 -0
- data/apps/rails-api/.rubocop.yml +8 -0
- data/apps/rails-api/.ruby-version +1 -0
- data/apps/rails-api/Dockerfile +69 -0
- data/apps/rails-api/Gemfile +54 -0
- data/apps/rails-api/Gemfile.lock +374 -0
- data/apps/rails-api/README.md +24 -0
- data/apps/rails-api/Rakefile +6 -0
- data/apps/rails-api/app/controllers/application_controller.rb +3 -0
- data/apps/rails-api/app/controllers/home_controller.rb +5 -0
- data/apps/rails-api/app/jobs/application_job.rb +7 -0
- data/apps/rails-api/app/mailers/application_mailer.rb +4 -0
- data/apps/rails-api/app/models/application_record.rb +3 -0
- data/apps/rails-api/app/views/layouts/mailer.html.erb +13 -0
- data/apps/rails-api/app/views/layouts/mailer.text.erb +1 -0
- data/apps/rails-api/bin/brakeman +7 -0
- data/apps/rails-api/bin/bundle +109 -0
- data/apps/rails-api/bin/dev +2 -0
- data/apps/rails-api/bin/docker-entrypoint +14 -0
- data/apps/rails-api/bin/jobs +6 -0
- data/apps/rails-api/bin/kamal +27 -0
- data/apps/rails-api/bin/rails +4 -0
- data/apps/rails-api/bin/rake +4 -0
- data/apps/rails-api/bin/rubocop +8 -0
- data/apps/rails-api/bin/setup +34 -0
- data/apps/rails-api/bin/thrust +5 -0
- data/apps/rails-api/config/application.rb +36 -0
- data/apps/rails-api/config/boot.rb +4 -0
- data/apps/rails-api/config/cable.yml +17 -0
- data/apps/rails-api/config/cache.yml +16 -0
- data/apps/rails-api/config/credentials.yml.enc +1 -0
- data/apps/rails-api/config/database.yml +41 -0
- data/apps/rails-api/config/deploy.yml +116 -0
- data/apps/rails-api/config/environment.rb +5 -0
- data/apps/rails-api/config/environments/development.rb +70 -0
- data/apps/rails-api/config/environments/production.rb +88 -0
- data/apps/rails-api/config/environments/test.rb +53 -0
- data/apps/rails-api/config/initializers/cors.rb +16 -0
- data/apps/rails-api/config/initializers/filter_parameter_logging.rb +8 -0
- data/apps/rails-api/config/initializers/inflections.rb +16 -0
- data/apps/rails-api/config/locales/en.yml +31 -0
- data/apps/rails-api/config/puma.rb +41 -0
- data/apps/rails-api/config/queue.yml +18 -0
- data/apps/rails-api/config/recurring.yml +10 -0
- data/apps/rails-api/config/routes.rb +10 -0
- data/apps/rails-api/config/storage.yml +34 -0
- data/apps/rails-api/config.ru +6 -0
- data/apps/rails-api/db/cable_schema.rb +11 -0
- data/apps/rails-api/db/cache_schema.rb +14 -0
- data/apps/rails-api/db/queue_schema.rb +129 -0
- data/apps/rails-api/db/seeds.rb +9 -0
- data/apps/rails-api/public/robots.txt +1 -0
- data/apps/rails-api/test/controllers/home_controller_test.rb +7 -0
- data/apps/rails-api/test/test_helper.rb +15 -0
- data/apps/rails-full/.dockerignore +47 -0
- data/apps/rails-full/.gitattributes +9 -0
- data/apps/rails-full/.gitignore +34 -0
- data/apps/rails-full/.kamal/hooks/docker-setup.sample +3 -0
- data/apps/rails-full/.kamal/hooks/post-deploy.sample +14 -0
- data/apps/rails-full/.kamal/hooks/post-proxy-reboot.sample +3 -0
- data/apps/rails-full/.kamal/hooks/pre-build.sample +51 -0
- data/apps/rails-full/.kamal/hooks/pre-connect.sample +47 -0
- data/apps/rails-full/.kamal/hooks/pre-deploy.sample +109 -0
- data/apps/rails-full/.kamal/hooks/pre-proxy-reboot.sample +3 -0
- data/apps/rails-full/.kamal/secrets +17 -0
- data/apps/rails-full/.rubocop.yml +8 -0
- data/apps/rails-full/.ruby-version +1 -0
- data/apps/rails-full/Dockerfile +72 -0
- data/apps/rails-full/Gemfile +70 -0
- data/apps/rails-full/Gemfile.lock +429 -0
- data/apps/rails-full/README.md +24 -0
- data/apps/rails-full/Rakefile +6 -0
- data/apps/rails-full/app/assets/stylesheets/application.css +10 -0
- data/apps/rails-full/app/controllers/application_controller.rb +6 -0
- data/apps/rails-full/app/controllers/home_controller.rb +11 -0
- data/apps/rails-full/app/helpers/application_helper.rb +2 -0
- data/apps/rails-full/app/helpers/home_helper.rb +2 -0
- data/apps/rails-full/app/javascript/application.js +3 -0
- data/apps/rails-full/app/javascript/controllers/application.js +9 -0
- data/apps/rails-full/app/javascript/controllers/hello_controller.js +7 -0
- data/apps/rails-full/app/javascript/controllers/index.js +4 -0
- data/apps/rails-full/app/jobs/application_job.rb +7 -0
- data/apps/rails-full/app/mailers/application_mailer.rb +4 -0
- data/apps/rails-full/app/models/application_record.rb +3 -0
- data/apps/rails-full/app/views/home/index.html.erb +7 -0
- data/apps/rails-full/app/views/layouts/application.html.erb +60 -0
- data/apps/rails-full/app/views/layouts/mailer.html.erb +13 -0
- data/apps/rails-full/app/views/layouts/mailer.text.erb +1 -0
- data/apps/rails-full/app/views/pwa/manifest.json.erb +22 -0
- data/apps/rails-full/app/views/pwa/service-worker.js +26 -0
- data/apps/rails-full/bin/brakeman +7 -0
- data/apps/rails-full/bin/bundle +109 -0
- data/apps/rails-full/bin/dev +2 -0
- data/apps/rails-full/bin/docker-entrypoint +14 -0
- data/apps/rails-full/bin/importmap +4 -0
- data/apps/rails-full/bin/jobs +6 -0
- data/apps/rails-full/bin/kamal +27 -0
- data/apps/rails-full/bin/rails +4 -0
- data/apps/rails-full/bin/rake +4 -0
- data/apps/rails-full/bin/rubocop +8 -0
- data/apps/rails-full/bin/setup +34 -0
- data/apps/rails-full/bin/thrust +5 -0
- data/apps/rails-full/config/application.rb +31 -0
- data/apps/rails-full/config/boot.rb +4 -0
- data/apps/rails-full/config/cable.yml +17 -0
- data/apps/rails-full/config/cache.yml +16 -0
- data/apps/rails-full/config/credentials.yml.enc +1 -0
- data/apps/rails-full/config/database.yml +41 -0
- data/apps/rails-full/config/deploy.yml +116 -0
- data/apps/rails-full/config/environment.rb +5 -0
- data/apps/rails-full/config/environments/development.rb +72 -0
- data/apps/rails-full/config/environments/production.rb +91 -0
- data/apps/rails-full/config/environments/test.rb +53 -0
- data/apps/rails-full/config/importmap.rb +7 -0
- data/apps/rails-full/config/initializers/assets.rb +7 -0
- data/apps/rails-full/config/initializers/clerk.rb +4 -0
- data/apps/rails-full/config/initializers/content_security_policy.rb +25 -0
- data/apps/rails-full/config/initializers/filter_parameter_logging.rb +8 -0
- data/apps/rails-full/config/initializers/inflections.rb +16 -0
- data/apps/rails-full/config/locales/en.yml +31 -0
- data/apps/rails-full/config/puma.rb +41 -0
- data/apps/rails-full/config/queue.yml +18 -0
- data/apps/rails-full/config/recurring.yml +10 -0
- data/apps/rails-full/config/routes.rb +15 -0
- data/apps/rails-full/config/storage.yml +34 -0
- data/apps/rails-full/config.ru +6 -0
- data/apps/rails-full/db/cable_schema.rb +11 -0
- data/apps/rails-full/db/cache_schema.rb +14 -0
- data/apps/rails-full/db/queue_schema.rb +129 -0
- data/apps/rails-full/db/seeds.rb +9 -0
- data/apps/rails-full/public/400.html +114 -0
- data/apps/rails-full/public/404.html +114 -0
- data/apps/rails-full/public/406-unsupported-browser.html +114 -0
- data/apps/rails-full/public/422.html +114 -0
- data/apps/rails-full/public/500.html +114 -0
- data/apps/rails-full/public/icon.png +0 -0
- data/apps/rails-full/public/icon.svg +3 -0
- data/apps/rails-full/public/robots.txt +1 -0
- data/apps/rails-full/test/application_system_test_case.rb +5 -0
- data/apps/rails-full/test/controllers/home_controller_test.rb +7 -0
- data/apps/rails-full/test/test_helper.rb +15 -0
- data/apps/sinatra/app.rb +29 -0
- data/apps/sinatra/config.ru +8 -0
- data/apps/sinatra/views/index.erb +44 -0
- data/bin/console +16 -0
- data/bin/release +21 -0
- data/bin/setup +8 -0
- data/clerk-sdk-ruby.gemspec +38 -0
- data/docs/clerk-logo-dark.png +0 -0
- data/docs/clerk-logo-light.png +0 -0
- data/lib/clerk/authenticatable.rb +32 -0
- data/lib/clerk/authenticate_context.rb +168 -0
- data/lib/clerk/authenticate_request.rb +261 -0
- data/lib/clerk/configuration.rb +84 -0
- data/lib/clerk/constants.rb +74 -0
- data/lib/clerk/error.rb +17 -0
- data/lib/clerk/jwks_cache.rb +37 -0
- data/lib/clerk/proxy.rb +135 -0
- data/lib/clerk/rack.rb +2 -0
- data/lib/clerk/rack_middleware.rb +112 -0
- data/lib/clerk/rails.rb +3 -0
- data/lib/clerk/railtie.rb +15 -0
- data/lib/clerk/sdk.rb +84 -0
- data/lib/clerk/sinatra.rb +52 -0
- data/lib/clerk/utils.rb +73 -0
- data/lib/clerk/version.rb +5 -0
- data/lib/clerk.rb +27 -0
- 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
|
data/lib/clerk/error.rb
ADDED
@@ -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
|