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,37 @@
|
|
1
|
+
require "concurrent"
|
2
|
+
|
3
|
+
module Clerk
|
4
|
+
class JWKSCache
|
5
|
+
def initialize(lifetime)
|
6
|
+
@lifetime = lifetime
|
7
|
+
@jwks = nil
|
8
|
+
@last_update = nil
|
9
|
+
@lock = Concurrent::ReadWriteLock.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def fetch(sdk, force_refresh: false, kid_not_found: false)
|
13
|
+
should_refresh = @lock.with_read_lock do
|
14
|
+
now = Time.now.to_i
|
15
|
+
|
16
|
+
@jwks.nil? || @last_update.nil? || force_refresh ||
|
17
|
+
(now - @last_update > @lifetime) ||
|
18
|
+
(kid_not_found && now - @last_update > 300)
|
19
|
+
end
|
20
|
+
|
21
|
+
if should_refresh
|
22
|
+
@lock.with_write_lock do
|
23
|
+
@last_update = Time.now.to_i
|
24
|
+
@jwks = begin
|
25
|
+
sdk.jwks.get_jwks.keys.map(&:to_hash)
|
26
|
+
rescue Clerk::Error, ClerkHttpClient::ApiError
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@lock.with_read_lock do
|
33
|
+
@jwks
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/clerk/proxy.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require "clerk"
|
2
|
+
require "clerk/authenticate_context"
|
3
|
+
require "clerk/authenticate_request"
|
4
|
+
|
5
|
+
module Clerk
|
6
|
+
class Proxy
|
7
|
+
CACHE_TTL = 60 # seconds
|
8
|
+
|
9
|
+
attr_reader :session_claims, :session_token
|
10
|
+
|
11
|
+
def initialize(session_claims: nil, session_token: nil)
|
12
|
+
@session_claims = session_claims
|
13
|
+
@session_token = session_token
|
14
|
+
end
|
15
|
+
|
16
|
+
def user?
|
17
|
+
!@session_claims.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def user
|
21
|
+
return nil unless user?
|
22
|
+
|
23
|
+
@user ||= fetch_user(user_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def user_id
|
27
|
+
return nil unless user?
|
28
|
+
|
29
|
+
@session_claims["sub"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def organization?
|
33
|
+
!organization_id.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def organization
|
37
|
+
return nil unless organization?
|
38
|
+
|
39
|
+
@org ||= fetch_org(organization_id)
|
40
|
+
end
|
41
|
+
|
42
|
+
def organization_id
|
43
|
+
return nil unless user?
|
44
|
+
|
45
|
+
@session_claims["org_id"]
|
46
|
+
end
|
47
|
+
|
48
|
+
def organization_role
|
49
|
+
return nil if @session_claims.nil?
|
50
|
+
|
51
|
+
@session_claims["org_role"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def organization_permissions
|
55
|
+
return nil if @session_claims.nil?
|
56
|
+
|
57
|
+
@session_claims["org_permissions"]
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns true if the session needs to perform step up verification
|
61
|
+
def user_reverified?(params)
|
62
|
+
return false unless user?
|
63
|
+
|
64
|
+
fva = session_claims["fva"]
|
65
|
+
|
66
|
+
# the feature is disabled
|
67
|
+
return true if fva.nil?
|
68
|
+
|
69
|
+
level = params[:level]
|
70
|
+
after_minutes = params[:after_minutes].to_i
|
71
|
+
|
72
|
+
return false if after_minutes.nil? || level.nil?
|
73
|
+
|
74
|
+
factor1_age, factor2_age = fva
|
75
|
+
is_valid_factor1 = factor1_age != -1 && after_minutes > factor1_age
|
76
|
+
is_valid_factor2 = factor2_age != -1 && after_minutes > factor2_age
|
77
|
+
|
78
|
+
case level
|
79
|
+
when :first_factor
|
80
|
+
is_valid_factor1
|
81
|
+
when :second_factor
|
82
|
+
(factor2_age == -1) ? is_valid_factor1 : is_valid_factor2
|
83
|
+
when :multi_factor
|
84
|
+
(factor2_age == -1) ? is_valid_factor1 : is_valid_factor1 && is_valid_factor2
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def user_needs_reverification?(preset = StepUp::Preset::STRICT)
|
89
|
+
!user_reverified?(preset)
|
90
|
+
end
|
91
|
+
|
92
|
+
def user_require_reverification!(preset = StepUp::Preset::STRICT, &block)
|
93
|
+
return unless user_needs_reverification?(preset)
|
94
|
+
yield(preset) if block_given?
|
95
|
+
end
|
96
|
+
|
97
|
+
def user_reverification_rack_response(config = nil)
|
98
|
+
raise ArgumentError, "Missing config, please pass a preset a la `Clerk::StepUp::Preset::*`" if config.nil?
|
99
|
+
|
100
|
+
[
|
101
|
+
403,
|
102
|
+
{Clerk::CONTENT_TYPE_HEADER => "application/json"},
|
103
|
+
[StepUp::Reverification.error_payload(config).to_json]
|
104
|
+
]
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def fetch_user(user_id)
|
110
|
+
cached_fetch("clerk:user:#{user_id}") do
|
111
|
+
sdk.users.get_user(user_id)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def fetch_org(org_id)
|
116
|
+
cached_fetch("clerk:org:#{org_id}") do
|
117
|
+
sdk.organizations.get_organization(org_id)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def cached_fetch(key, &block)
|
122
|
+
store = Clerk.configuration.cache_store
|
123
|
+
|
124
|
+
if store
|
125
|
+
store.fetch(key, expires_in: CACHE_TTL, &block)
|
126
|
+
else
|
127
|
+
yield
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def sdk
|
132
|
+
@sdk ||= Clerk::SDK.new
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/clerk/rack.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require "clerk"
|
2
|
+
require "clerk/authenticate_context"
|
3
|
+
require "clerk/authenticate_request"
|
4
|
+
require "clerk/proxy"
|
5
|
+
require "clerk/utils"
|
6
|
+
|
7
|
+
module Clerk
|
8
|
+
module Rack
|
9
|
+
class Middleware
|
10
|
+
def initialize(app, options = {})
|
11
|
+
@app = app
|
12
|
+
|
13
|
+
Clerk.configuration.update(options) if options
|
14
|
+
@excluded_routes, @excluded_routes_wildcards = Clerk::Utils.filter_routes(Clerk.configuration.excluded_routes)
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
env["clerk.initialized"] = true
|
19
|
+
|
20
|
+
req = ::Rack::Request.new(env)
|
21
|
+
|
22
|
+
if @excluded_routes[req.path]
|
23
|
+
env["clerk.excluded_route"] = true
|
24
|
+
return @app.call(env)
|
25
|
+
end
|
26
|
+
|
27
|
+
@excluded_routes_wildcards.each do |route|
|
28
|
+
if req.path.start_with?(route)
|
29
|
+
env["clerk.excluded_route"] = true
|
30
|
+
return @app.call(env)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
env["clerk"] = Clerk::Proxy.new
|
35
|
+
|
36
|
+
auth_context = AuthenticateContext.new(req, Clerk.configuration)
|
37
|
+
auth_request = AuthenticateRequest.new(auth_context)
|
38
|
+
|
39
|
+
status, auth_request_headers, body = auth_request.resolve(env)
|
40
|
+
|
41
|
+
return [status, auth_request_headers, body] if status
|
42
|
+
|
43
|
+
status, headers, body = @app.call(env)
|
44
|
+
|
45
|
+
unless auth_request_headers.empty?
|
46
|
+
# Remove them to avoid overriding existing cookies set in headers by other middlewares
|
47
|
+
auth_request_cookies = auth_request_headers.delete(SET_COOKIE_HEADER.downcase)
|
48
|
+
# merge non-cookie related headers into response headers
|
49
|
+
headers.merge!(auth_request_headers)
|
50
|
+
|
51
|
+
set_cookie_headers!(headers, auth_request_cookies) if auth_request_cookies
|
52
|
+
end
|
53
|
+
|
54
|
+
[status, headers, body]
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def parse_cookie_key(cookie_header)
|
60
|
+
cookie_header.split(";")[0].split("=")[0]
|
61
|
+
end
|
62
|
+
|
63
|
+
def set_cookie_headers!(headers, cookie_headers)
|
64
|
+
cookie_headers.each do |cookie_header|
|
65
|
+
cookie_key = parse_cookie_key(cookie_header)
|
66
|
+
cookie = ::Clerk::Utils.parse_cookies_header(cookie_header)
|
67
|
+
cookie_params = convert_http_cookie_to_cookie_setter_params(cookie_key, cookie)
|
68
|
+
::Rack::Utils.set_cookie_header!(headers, cookie_key, cookie_params)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def convert_http_cookie_to_cookie_setter_params(cookie_key, cookie)
|
73
|
+
# convert cookie to to match cookie setter method params (lowercase symbolized keys with `:value` key)
|
74
|
+
cookie_params = cookie.transform_keys { |k| k.downcase.to_sym }
|
75
|
+
# drop the current cookie name key to avoid polluting the expected cookie params
|
76
|
+
cookie_params[:value] = cookie_params.delete(cookie_key.to_sym)
|
77
|
+
|
78
|
+
# Ensure secure and httponly are set to true if present
|
79
|
+
cookie_params[:secure] = cookie_params.has_key?(:secure)
|
80
|
+
cookie_params[:httponly] = cookie_params.has_key?(:httponly)
|
81
|
+
|
82
|
+
# fix issue with cookie expiration expected to be Date type
|
83
|
+
cookie_params[:expires] = Date.parse(cookie_params[:expires]) if cookie_params[:expires]
|
84
|
+
|
85
|
+
cookie_params
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Reverification
|
90
|
+
def initialize(app, routes: ["/*"], preset: Clerk::StepUp::Preset::STRICT)
|
91
|
+
@app = app
|
92
|
+
@preset = preset
|
93
|
+
|
94
|
+
@included_routes, @included_routes_wildcards = Clerk::Utils.filter_routes(routes)
|
95
|
+
end
|
96
|
+
|
97
|
+
def call(env)
|
98
|
+
raise Clerk::ConfigurationError, "`Clerk::Rack::Reverification` must be initialized after `Clerk::Rack::Middleware`" unless env["clerk.initialized"]
|
99
|
+
return @app.call(env) if env["clerk.excluded_route"]
|
100
|
+
|
101
|
+
req = ::Rack::Request.new(env)
|
102
|
+
valid_route = @included_routes[req.path] || @included_routes_wildcards.any? { |route| req.path.start_with?(route) }
|
103
|
+
|
104
|
+
if valid_route && env["clerk"].user_needs_reverification?(@preset)
|
105
|
+
return env["clerk"].user_reverification_rack_response(@preset)
|
106
|
+
end
|
107
|
+
|
108
|
+
@app.call(env)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/clerk/rails.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "clerk/rack_middleware"
|
4
|
+
|
5
|
+
module Clerk
|
6
|
+
module Rails
|
7
|
+
class Railtie < ::Rails::Railtie
|
8
|
+
initializer "clerk.configure_rails_initialization" do |app|
|
9
|
+
unless ENV["CLERK_SKIP_RAILTIE"]
|
10
|
+
app.middleware.use Clerk::Rack::Middleware
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/clerk/sdk.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require "clerk-http-client"
|
2
|
+
require "clerk/jwks_cache"
|
3
|
+
require "clerk/version"
|
4
|
+
require "jwt"
|
5
|
+
|
6
|
+
module Clerk
|
7
|
+
class SDK < ClerkHttpClient::SDK
|
8
|
+
DEFAULT_HEADERS = {
|
9
|
+
"User-Agent": "Clerk/#{Clerk::VERSION}; Faraday/#{Faraday::VERSION}; Ruby/#{RUBY_VERSION}",
|
10
|
+
"X-Clerk-SDK": "ruby/#{Clerk::VERSION}",
|
11
|
+
"Clerk-API-Version": "2025-04-10",
|
12
|
+
}
|
13
|
+
|
14
|
+
# How often (in seconds) should JWKs be refreshed
|
15
|
+
JWKS_CACHE_LIFETIME = 3600 # 1 hour
|
16
|
+
|
17
|
+
@@jwks_cache = JWKSCache.new(JWKS_CACHE_LIFETIME)
|
18
|
+
|
19
|
+
def self.jwks_cache
|
20
|
+
@@jwks_cache
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the decoded JWT payload without verifying if the signature is valid.
|
24
|
+
#
|
25
|
+
# WARNING: This will not verify whether the signature is valid. You should not
|
26
|
+
# use this for untrusted messages! You most likely want to use `verify_token`.
|
27
|
+
def decode_token(token)
|
28
|
+
JWT.decode(token, nil, false).first
|
29
|
+
end
|
30
|
+
|
31
|
+
# Decode the JWT and verify it's valid (verify claims, signature etc.) using the provided algorithms.
|
32
|
+
#
|
33
|
+
# JWKS are cached for JWKS_CACHE_LIFETIME seconds, in order to avoid unecessary roundtrips.
|
34
|
+
# In order to invalidate the cache, pass `force_refresh_jwks: true`.
|
35
|
+
#
|
36
|
+
# A timeout for the request to the JWKs endpoint can be set with the `timeout` argument.
|
37
|
+
def verify_token(token, force_refresh_jwks: false, algorithms: ["RS256"], timeout: 5)
|
38
|
+
jwk_loader = ->(options) do
|
39
|
+
# JWT.decode requires that the 'keys' key in the Hash is a symbol (as
|
40
|
+
# opposed to a string which our SDK returns by default)
|
41
|
+
{keys: SDK.jwks_cache.fetch(self, kid_not_found: options[:invalidate] || options[:kid_not_found], force_refresh: force_refresh_jwks)}
|
42
|
+
end
|
43
|
+
|
44
|
+
claims = JWT.decode(token, nil, true, algorithms: algorithms, exp_leeway: timeout, jwks: jwk_loader).first
|
45
|
+
|
46
|
+
# orgs
|
47
|
+
if claims["v"].nil? || claims["v"] == 1
|
48
|
+
claims["v"] = 1
|
49
|
+
elsif claims["v"] == 2 && claims["o"]
|
50
|
+
claims["org_id"] = claims["o"].fetch("id", nil)
|
51
|
+
claims["org_slug"] = claims["o"].fetch("slg", nil)
|
52
|
+
claims["org_role"] = "org:#{claims["o"].fetch("rol", nil)}"
|
53
|
+
|
54
|
+
org_permissions = compute_org_permissions_from_v2_token(claims)
|
55
|
+
claims["org_permissions"] = org_permissions if org_permissions.any?
|
56
|
+
claims.delete("o")
|
57
|
+
claims.delete("fea")
|
58
|
+
end
|
59
|
+
|
60
|
+
claims
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def compute_org_permissions_from_v2_token(claims)
|
66
|
+
features = claims["fea"] ? claims["fea"].split(",") : []
|
67
|
+
permissions = claims["o"]["per"] ? claims["o"]["per"].split(",") : []
|
68
|
+
mappings = claims["o"]["fpm"] ? claims["o"]["fpm"].split(",") : []
|
69
|
+
org_permissions = []
|
70
|
+
|
71
|
+
mappings.each_with_index do |mapping, i|
|
72
|
+
scope, feature = features[i].split(":")
|
73
|
+
|
74
|
+
next if !scope.include?("o") # not an orgs-related permission
|
75
|
+
|
76
|
+
mapping.to_i.to_s(2).reverse.each_char.each_with_index do |bit, i|
|
77
|
+
org_permissions << "org:#{feature}:#{permissions[i]}" if bit == "1"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
org_permissions
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
require "clerk/rack"
|
3
|
+
|
4
|
+
module Sinatra
|
5
|
+
module Clerk
|
6
|
+
module Helpers
|
7
|
+
def clerk
|
8
|
+
env["clerk"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def require_reverification!(preset = ::Clerk::StepUp::Preset::STRICT, &block)
|
12
|
+
clerk.user_require_reverification!(preset) do
|
13
|
+
return yield(preset) if block_given?
|
14
|
+
render_reverification!(preset)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def render_reverification!(preset = nil)
|
19
|
+
halt 403, ::Clerk::StepUp::Reverification.error_payload(preset).to_json
|
20
|
+
end
|
21
|
+
|
22
|
+
def clerk_sdk
|
23
|
+
@@sdk ||= ::Clerk::SDK.new
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.registered(app)
|
28
|
+
app.helpers Clerk::Helpers
|
29
|
+
app.use ::Clerk::Rack::Middleware
|
30
|
+
|
31
|
+
app.set(:auth) do |active|
|
32
|
+
condition do
|
33
|
+
redirect clerk.sign_in_url if active && !clerk.session
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
app.set(:reverify) do |preset|
|
38
|
+
condition do
|
39
|
+
if preset === true
|
40
|
+
preset = ::Clerk::StepUp::Preset::STRICT
|
41
|
+
end
|
42
|
+
|
43
|
+
if preset
|
44
|
+
require_reverification!(preset)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
register Clerk
|
52
|
+
end
|
data/lib/clerk/utils.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module Clerk
|
6
|
+
module Utils
|
7
|
+
class << self
|
8
|
+
def decode_publishable_key(publishable_key)
|
9
|
+
Base64.decode64(publishable_key.split("_")[2].to_s)
|
10
|
+
end
|
11
|
+
|
12
|
+
def filter_routes(routes)
|
13
|
+
filtered_routes = {}
|
14
|
+
filtered_wildcard_routes = []
|
15
|
+
|
16
|
+
routes.each do |route|
|
17
|
+
route = route.strip
|
18
|
+
|
19
|
+
if route.end_with?("/*")
|
20
|
+
filtered_wildcard_routes << route[0..-2]
|
21
|
+
else
|
22
|
+
filtered_routes[route] = true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
filtered_wildcard_routes.uniq!
|
27
|
+
|
28
|
+
[filtered_routes, filtered_wildcard_routes]
|
29
|
+
end
|
30
|
+
|
31
|
+
def retrieve_header_from_request(request, key)
|
32
|
+
(request.env[key] || request.env[key.downcase]).to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def retrieve_from_query_string(url, key)
|
36
|
+
::Rack::Utils.parse_query(url.query)[key]
|
37
|
+
end
|
38
|
+
|
39
|
+
def valid_publishable_key?(publishable_key)
|
40
|
+
raise ArgumentError, "publishable_key must be a string" unless publishable_key.is_a?(String)
|
41
|
+
|
42
|
+
key = publishable_key.to_s
|
43
|
+
valid_publishable_key_prefix?(key) && valid_publishable_key_postfix?(key)
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_publishable_key_postfix?(publishable_key)
|
47
|
+
decode_publishable_key(publishable_key).end_with?("$")
|
48
|
+
end
|
49
|
+
|
50
|
+
def valid_publishable_key_prefix?(publishable_key)
|
51
|
+
publishable_key.start_with?("pk_live_", "pk_test_")
|
52
|
+
end
|
53
|
+
|
54
|
+
# NOTE: This is a copy of Rack::Utils.parse_cookies_header to allow for
|
55
|
+
# compatibility with older versions of Rack.
|
56
|
+
def parse_cookies_header(value)
|
57
|
+
return {} unless value
|
58
|
+
|
59
|
+
value.split(/; */n).each_with_object({}) do |cookie, cookies|
|
60
|
+
next if cookie.empty?
|
61
|
+
key, value = cookie.split('=', 2)
|
62
|
+
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def unescape(s, encoding = Encoding::UTF_8)
|
69
|
+
URI.decode_www_form_component(s, encoding)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/clerk.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "clerk/configuration"
|
4
|
+
require "clerk/constants"
|
5
|
+
require "clerk/error"
|
6
|
+
require "clerk/sdk"
|
7
|
+
require "clerk/version"
|
8
|
+
|
9
|
+
if defined?(::Rails)
|
10
|
+
require "clerk/rails"
|
11
|
+
end
|
12
|
+
|
13
|
+
module Clerk
|
14
|
+
class << self
|
15
|
+
def configure
|
16
|
+
if block_given?
|
17
|
+
yield(configuration)
|
18
|
+
else
|
19
|
+
configuration
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def configuration
|
24
|
+
@configuration ||= Clerk::Configuration.default
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|