better_auth 0.7.0 → 0.10.0
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 +4 -4
- data/CHANGELOG.md +10 -1
- data/README.md +6 -6
- data/lib/better_auth/adapters/memory.rb +131 -17
- data/lib/better_auth/adapters/mongodb.rb +3 -3
- data/lib/better_auth/adapters/sql.rb +139 -57
- data/lib/better_auth/auth.rb +68 -1
- data/lib/better_auth/configuration.rb +15 -10
- data/lib/better_auth/context.rb +1 -1
- data/lib/better_auth/cookies.rb +11 -3
- data/lib/better_auth/doctor.rb +97 -0
- data/lib/better_auth/endpoint.rb +88 -5
- data/lib/better_auth/env.rb +38 -0
- data/lib/better_auth/http_client.rb +46 -0
- data/lib/better_auth/migration_plan.rb +15 -0
- data/lib/better_auth/oauth2.rb +1 -1
- data/lib/better_auth/plugins/admin.rb +6 -1
- data/lib/better_auth/plugins/anonymous.rb +2 -0
- data/lib/better_auth/plugins/captcha.rb +1 -1
- data/lib/better_auth/plugins/device_authorization.rb +34 -0
- data/lib/better_auth/plugins/dub.rb +8 -0
- data/lib/better_auth/plugins/generic_oauth.rb +34 -7
- data/lib/better_auth/plugins/have_i_been_pwned.rb +1 -1
- data/lib/better_auth/plugins/jwt.rb +10 -3
- data/lib/better_auth/plugins/mcp/schema.rb +13 -13
- data/lib/better_auth/plugins/mcp.rb +41 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +98 -21
- data/lib/better_auth/plugins/oidc_provider.rb +62 -3
- data/lib/better_auth/plugins/one_tap.rb +17 -5
- data/lib/better_auth/plugins/open_api.rb +42 -2
- data/lib/better_auth/plugins/organization.rb +122 -11
- data/lib/better_auth/plugins/phone_number.rb +1 -1
- data/lib/better_auth/plugins/two_factor.rb +21 -0
- data/lib/better_auth/rate_limiter.rb +7 -2
- data/lib/better_auth/routes/account.rb +4 -0
- data/lib/better_auth/routes/email_verification.rb +5 -1
- data/lib/better_auth/routes/password.rb +1 -0
- data/lib/better_auth/routes/social.rb +29 -1
- data/lib/better_auth/routes/user.rb +6 -2
- data/lib/better_auth/schema/sql.rb +104 -15
- data/lib/better_auth/schema.rb +35 -2
- data/lib/better_auth/session.rb +2 -1
- data/lib/better_auth/social_providers/base.rb +4 -9
- data/lib/better_auth/social_providers/facebook.rb +1 -1
- data/lib/better_auth/social_providers/github.rb +2 -0
- data/lib/better_auth/social_providers/line.rb +1 -1
- data/lib/better_auth/social_providers/paypal.rb +1 -1
- data/lib/better_auth/sql_migration.rb +566 -0
- data/lib/better_auth/url_helpers.rb +1 -1
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +4 -0
- metadata +15 -8
data/lib/better_auth/auth.rb
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module BetterAuth
|
|
4
4
|
class Auth
|
|
5
|
-
|
|
5
|
+
TELEMETRY_ADAPTER_IDS = {
|
|
6
|
+
"BetterAuth::Adapters::Memory" => "memory",
|
|
7
|
+
"BetterAuth::Adapters::Postgres" => "postgres",
|
|
8
|
+
"BetterAuth::Adapters::MySQL" => "mysql",
|
|
9
|
+
"BetterAuth::Adapters::SQLite" => "sqlite",
|
|
10
|
+
"BetterAuth::Adapters::MSSQL" => "mssql"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
TELEMETRY_DATABASE_IDS = {
|
|
14
|
+
memory: "memory",
|
|
15
|
+
postgres: "postgres",
|
|
16
|
+
mysql: "mysql",
|
|
17
|
+
sqlite: "sqlite",
|
|
18
|
+
mssql: "mssql"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :handler, :api, :options, :context, :error_codes, :telemetry
|
|
6
22
|
|
|
7
23
|
def initialize(options = {})
|
|
8
24
|
@options = Configuration.new(options)
|
|
@@ -16,6 +32,7 @@ module BetterAuth
|
|
|
16
32
|
Router.check_endpoint_conflicts(@options, @options.logger)
|
|
17
33
|
@api = API.new(@context, @endpoints)
|
|
18
34
|
@handler = Router.new(@context, @endpoints)
|
|
35
|
+
@telemetry = build_telemetry_publisher
|
|
19
36
|
end
|
|
20
37
|
|
|
21
38
|
def call(env)
|
|
@@ -38,5 +55,55 @@ module BetterAuth
|
|
|
38
55
|
def build_endpoints
|
|
39
56
|
Core.base_endpoints.merge(@plugin_registry.endpoints)
|
|
40
57
|
end
|
|
58
|
+
|
|
59
|
+
def build_telemetry_publisher
|
|
60
|
+
require "better_auth/telemetry"
|
|
61
|
+
BetterAuth::Telemetry.create(@options, telemetry_context)
|
|
62
|
+
rescue LoadError
|
|
63
|
+
noop_telemetry_publisher
|
|
64
|
+
rescue => e
|
|
65
|
+
log_telemetry_error(e)
|
|
66
|
+
noop_telemetry_publisher
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def telemetry_context
|
|
70
|
+
{
|
|
71
|
+
database: telemetry_database_id,
|
|
72
|
+
adapter: telemetry_adapter_id,
|
|
73
|
+
custom_track: nil,
|
|
74
|
+
skip_test_check: false
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def telemetry_database_id
|
|
79
|
+
configured = @options.database
|
|
80
|
+
return "memory" if configured.nil?
|
|
81
|
+
return TELEMETRY_DATABASE_IDS[configured] if configured.is_a?(Symbol) && TELEMETRY_DATABASE_IDS.key?(configured)
|
|
82
|
+
return "adapter" if configured.respond_to?(:call)
|
|
83
|
+
|
|
84
|
+
TELEMETRY_ADAPTER_IDS[configured.class.name] || "adapter"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def telemetry_adapter_id
|
|
88
|
+
TELEMETRY_ADAPTER_IDS[@context.adapter.class.name] || @context.adapter.class.name
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def noop_telemetry_publisher
|
|
92
|
+
Class.new do
|
|
93
|
+
def publish(_event) = nil
|
|
94
|
+
|
|
95
|
+
def enabled? = false
|
|
96
|
+
end.new
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def log_telemetry_error(error)
|
|
100
|
+
logger = @options.logger
|
|
101
|
+
message = "[better-auth] telemetry creation failed: #{error.class}: #{error.message}"
|
|
102
|
+
if logger.respond_to?(:error)
|
|
103
|
+
logger.error(message)
|
|
104
|
+
else
|
|
105
|
+
Kernel.warn(message)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
41
108
|
end
|
|
42
109
|
end
|
|
@@ -56,6 +56,7 @@ module BetterAuth
|
|
|
56
56
|
:on_api_error,
|
|
57
57
|
:disabled_paths,
|
|
58
58
|
:trusted_origins_callback,
|
|
59
|
+
:telemetry,
|
|
59
60
|
:logger
|
|
60
61
|
|
|
61
62
|
def initialize(options = {})
|
|
@@ -73,12 +74,13 @@ module BetterAuth
|
|
|
73
74
|
@database_hooks = options[:database_hooks]
|
|
74
75
|
@hooks = options[:hooks]
|
|
75
76
|
@on_api_error = symbolize_keys(options[:on_api_error] || options[:on_apierror] || {})
|
|
76
|
-
@
|
|
77
|
+
@telemetry = symbolize_keys(options[:telemetry] || {})
|
|
78
|
+
@social_providers = normalize_social_providers(options[:social_providers])
|
|
77
79
|
@trusted_origins_callbacks = []
|
|
78
80
|
@trusted_origins_callbacks << options[:trusted_origins] if options[:trusted_origins].respond_to?(:call)
|
|
79
81
|
@trusted_origins_callback = combined_trusted_origins_callback
|
|
80
82
|
legacy_secret = resolve_secret(options, allow_test_default: false)
|
|
81
|
-
secrets = options.key?(:secrets) ? options[:secrets] : SecretConfig.parse_env(
|
|
83
|
+
secrets = options.key?(:secrets) ? options[:secrets] : SecretConfig.parse_env(Env.get("BETTER_AUTH_SECRETS"))
|
|
82
84
|
if secrets
|
|
83
85
|
@secret_config = SecretConfig.build(secrets, legacy_secret, logger: logger)
|
|
84
86
|
@secret = @secret_config.current_secret
|
|
@@ -153,7 +155,8 @@ module BetterAuth
|
|
|
153
155
|
database_hooks: database_hooks,
|
|
154
156
|
hooks: hooks,
|
|
155
157
|
on_api_error: on_api_error,
|
|
156
|
-
disabled_paths: disabled_paths
|
|
158
|
+
disabled_paths: disabled_paths,
|
|
159
|
+
telemetry: telemetry
|
|
157
160
|
}
|
|
158
161
|
end
|
|
159
162
|
|
|
@@ -279,17 +282,13 @@ module BetterAuth
|
|
|
279
282
|
def env_base_url
|
|
280
283
|
base_url = ENV["BASE_URL"]
|
|
281
284
|
[
|
|
282
|
-
|
|
283
|
-
ENV["NEXT_PUBLIC_BETTER_AUTH_URL"],
|
|
284
|
-
ENV["PUBLIC_BETTER_AUTH_URL"],
|
|
285
|
-
ENV["NUXT_PUBLIC_BETTER_AUTH_URL"],
|
|
286
|
-
ENV["NUXT_PUBLIC_AUTH_URL"],
|
|
285
|
+
Env.get("BETTER_AUTH_URL"),
|
|
287
286
|
(base_url unless base_url == "/")
|
|
288
287
|
].find { |value| value && !value.empty? }
|
|
289
288
|
end
|
|
290
289
|
|
|
291
290
|
def resolve_secret(options, allow_test_default: true)
|
|
292
|
-
[options[:secret],
|
|
291
|
+
[options[:secret], Env.get("BETTER_AUTH_SECRET"), ENV["AUTH_SECRET"]].find { |value| value && !value.empty? } ||
|
|
293
292
|
((allow_test_default && test_environment?) ? DEFAULT_SECRET : nil)
|
|
294
293
|
end
|
|
295
294
|
|
|
@@ -372,6 +371,12 @@ module BetterAuth
|
|
|
372
371
|
Array(value).compact.reject { |plugin| plugin == false }.map { |plugin| Plugin.coerce(plugin) }
|
|
373
372
|
end
|
|
374
373
|
|
|
374
|
+
def normalize_social_providers(value)
|
|
375
|
+
symbolize_keys(value || {}).reject do |_id, provider|
|
|
376
|
+
provider.nil? || provider == false || (provider.is_a?(Hash) && provider[:enabled] == false)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
375
380
|
def normalize_trusted_origins(value)
|
|
376
381
|
origins = []
|
|
377
382
|
origins << base_url unless base_url.nil? || base_url.empty?
|
|
@@ -408,7 +413,7 @@ module BetterAuth
|
|
|
408
413
|
end
|
|
409
414
|
|
|
410
415
|
def env_trusted_origins
|
|
411
|
-
|
|
416
|
+
Env.csv("BETTER_AUTH_TRUSTED_ORIGINS")
|
|
412
417
|
end
|
|
413
418
|
|
|
414
419
|
def symbolize_keys(value)
|
data/lib/better_auth/context.rb
CHANGED
|
@@ -321,7 +321,7 @@ module BetterAuth
|
|
|
321
321
|
if options.trusted_origins_callback
|
|
322
322
|
origins.concat(Array(options.trusted_origins_callback.call(request)).compact)
|
|
323
323
|
end
|
|
324
|
-
origins.concat(
|
|
324
|
+
origins.concat(Env.csv("BETTER_AUTH_TRUSTED_ORIGINS"))
|
|
325
325
|
origins.map(&:to_s).reject(&:empty?).uniq
|
|
326
326
|
rescue URI::InvalidURIError
|
|
327
327
|
options.trusted_origins
|
data/lib/better_auth/cookies.rb
CHANGED
|
@@ -67,7 +67,7 @@ module BetterAuth
|
|
|
67
67
|
name, value = pair.split("=", 2)
|
|
68
68
|
next if name.to_s.empty? || value.nil?
|
|
69
69
|
|
|
70
|
-
result[name.strip] = value.strip
|
|
70
|
+
result[name.strip] = decode_cookie_value(value.strip)
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -150,12 +150,14 @@ module BetterAuth
|
|
|
150
150
|
Crypto.symmetric_decode_jwt(value, ctx.context.secret_config, "better-auth-account")
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
def get_cookie_cache(request_or_cookie_header, secret:, strategy: "compact", version: nil, cookie_prefix: "better-auth", cookie_name: "session_data", is_secure: nil)
|
|
153
|
+
def get_cookie_cache(request_or_cookie_header, secret:, strategy: "compact", version: nil, cookie_prefix: "better-auth", cookie_name: "session_data", is_secure: nil, cookie_full_name: nil)
|
|
154
154
|
cookie_header = header_value(request_or_cookie_header)
|
|
155
155
|
return nil if cookie_header.to_s.empty?
|
|
156
156
|
|
|
157
157
|
parsed = parse_cookies(cookie_header)
|
|
158
|
-
name = if
|
|
158
|
+
name = if cookie_full_name
|
|
159
|
+
cookie_full_name
|
|
160
|
+
elsif is_secure.nil?
|
|
159
161
|
production_environment? ? "#{SECURE_COOKIE_PREFIX}#{cookie_prefix}.#{cookie_name}" : "#{cookie_prefix}.#{cookie_name}"
|
|
160
162
|
else
|
|
161
163
|
secure_prefix = is_secure ? SECURE_COOKIE_PREFIX : ""
|
|
@@ -248,6 +250,12 @@ module BetterAuth
|
|
|
248
250
|
chunks.sort_by(&:first).map(&:last).join
|
|
249
251
|
end
|
|
250
252
|
|
|
253
|
+
def decode_cookie_value(value)
|
|
254
|
+
URI.decode_uri_component(value)
|
|
255
|
+
rescue ArgumentError
|
|
256
|
+
value
|
|
257
|
+
end
|
|
258
|
+
|
|
251
259
|
def header_value(request_or_cookie_header)
|
|
252
260
|
return request_or_cookie_header.headers["cookie"] if request_or_cookie_header.respond_to?(:headers)
|
|
253
261
|
return request_or_cookie_header.get_header("HTTP_COOKIE") if request_or_cookie_header.respond_to?(:get_header)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "better_auth/sql_migration"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Doctor
|
|
7
|
+
Result = Struct.new(:ok, :warnings, :errors, keyword_init: true) do
|
|
8
|
+
def success?
|
|
9
|
+
errors.empty?
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def check(config_or_options)
|
|
16
|
+
config = BetterAuth::SQLMigration.configuration_for(config_or_options)
|
|
17
|
+
result = Result.new(ok: ["config loaded"], warnings: [], errors: [])
|
|
18
|
+
|
|
19
|
+
check_secret(config, result)
|
|
20
|
+
check_base_url(config, result)
|
|
21
|
+
check_rate_limit(config, result)
|
|
22
|
+
check_database(config, result)
|
|
23
|
+
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def print(result, stdout:, stderr:)
|
|
28
|
+
result.ok.each { |message| stdout.puts "OK #{message}" }
|
|
29
|
+
result.warnings.each { |message| stdout.puts "WARN #{message}" }
|
|
30
|
+
result.errors.each { |message| stderr.puts "ERROR #{message}" }
|
|
31
|
+
result.success? ? 0 : 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def check_secret(config, result)
|
|
35
|
+
secret = config.secret.to_s
|
|
36
|
+
if secret.empty?
|
|
37
|
+
result.errors << "secret is missing"
|
|
38
|
+
elsif secret == BetterAuth::Configuration::DEFAULT_SECRET
|
|
39
|
+
result.errors << "secret uses the default development value"
|
|
40
|
+
elsif secret.length < 32
|
|
41
|
+
result.errors << "secret should be at least 32 characters"
|
|
42
|
+
elsif entropy(secret) < 120
|
|
43
|
+
result.errors << "secret appears low-entropy; use a random production secret"
|
|
44
|
+
else
|
|
45
|
+
result.ok << "secret length and entropy look acceptable"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def check_base_url(config, result)
|
|
50
|
+
base_url = config.base_url.to_s
|
|
51
|
+
if base_url.empty?
|
|
52
|
+
result.warnings << "base_url is not configured; set it explicitly in production"
|
|
53
|
+
elsif !base_url.start_with?("https://")
|
|
54
|
+
result.warnings << "base_url is not HTTPS"
|
|
55
|
+
else
|
|
56
|
+
result.ok << "base_url uses HTTPS"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def check_rate_limit(config, result)
|
|
61
|
+
rate_limit = config.rate_limit || {}
|
|
62
|
+
result.warnings << "rate_limit is disabled" unless rate_limit[:enabled]
|
|
63
|
+
if rate_limit[:storage].to_s == "memory"
|
|
64
|
+
result.warnings << "rate_limit uses memory storage; use database or secondary-storage for multi-process production deployments"
|
|
65
|
+
else
|
|
66
|
+
result.ok << "rate_limit storage is #{rate_limit[:storage]}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_database(config, result)
|
|
71
|
+
auth = BetterAuth.auth(config.to_h)
|
|
72
|
+
adapter = auth.context.adapter
|
|
73
|
+
unless adapter.respond_to?(:dialect) && adapter.respond_to?(:connection)
|
|
74
|
+
result.warnings << "database adapter does not expose SQL migration introspection; schema drift check skipped"
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
result.ok << "database adapter supports SQL migrations"
|
|
79
|
+
plan = BetterAuth::SQLMigration.plan(config, connection: adapter.connection, dialect: adapter.dialect)
|
|
80
|
+
if plan.empty?
|
|
81
|
+
result.ok << "database schema is up to date"
|
|
82
|
+
else
|
|
83
|
+
result.warnings << "database has pending Better Auth migrations"
|
|
84
|
+
plan.warnings.each { |warning| result.warnings << warning }
|
|
85
|
+
end
|
|
86
|
+
rescue BetterAuth::SQLMigration::UnsupportedAdapterError => error
|
|
87
|
+
result.warnings << error.message
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def entropy(value)
|
|
91
|
+
unique = value.chars.uniq.length
|
|
92
|
+
return 0 if unique.zero?
|
|
93
|
+
|
|
94
|
+
Math.log2(unique**value.length)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/better_auth/endpoint.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
module BetterAuth
|
|
6
7
|
class Endpoint
|
|
@@ -24,6 +25,7 @@ module BetterAuth
|
|
|
24
25
|
@headers_schema = headers_schema
|
|
25
26
|
@metadata = metadata || {}
|
|
26
27
|
apply_default_open_api_metadata!
|
|
28
|
+
apply_open_api_defaults!
|
|
27
29
|
apply_open_api_schemas!
|
|
28
30
|
@options = endpoint_options
|
|
29
31
|
@use = Array(use)
|
|
@@ -40,13 +42,12 @@ module BetterAuth
|
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def call(context)
|
|
43
|
-
apply_schemas!(context)
|
|
44
|
-
|
|
45
45
|
use.each do |middleware|
|
|
46
46
|
middleware_result = middleware.call(context)
|
|
47
47
|
return Result.from_value(middleware_result, context) if middleware_result
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
apply_schemas!(context)
|
|
50
51
|
Result.from_value(handler.call(context), context)
|
|
51
52
|
end
|
|
52
53
|
|
|
@@ -72,6 +73,41 @@ module BetterAuth
|
|
|
72
73
|
metadata[:openapi] = BetterAuth::OpenAPI.default_metadata(path, methods)
|
|
73
74
|
end
|
|
74
75
|
|
|
76
|
+
def apply_open_api_defaults!
|
|
77
|
+
return unless path
|
|
78
|
+
return unless defined?(BetterAuth::OpenAPI)
|
|
79
|
+
|
|
80
|
+
openapi = fetch_key(metadata, :openapi)
|
|
81
|
+
return unless openapi.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
defaults = BetterAuth::OpenAPI.default_metadata(path, methods)
|
|
84
|
+
openapi[:operationId] = defaults[:operationId] if fetch_key(openapi, :operationId).to_s.empty?
|
|
85
|
+
openapi[:description] = defaults[:description] if default_open_api_description?(fetch_key(openapi, :description))
|
|
86
|
+
openapi[:parameters] = merge_open_api_parameters(defaults[:parameters], fetch_key(openapi, :parameters))
|
|
87
|
+
openapi[:responses] = defaults[:responses].merge(fetch_key(openapi, :responses) || {})
|
|
88
|
+
if request_body_method? && !fetch_key(openapi, :requestBody).is_a?(Hash)
|
|
89
|
+
openapi[:requestBody] = defaults[:requestBody] || BetterAuth::OpenAPI.default_request_body
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def default_open_api_description?(description)
|
|
94
|
+
methods.any? { |method| description.to_s == "#{method} #{path}" }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def request_body_method?
|
|
98
|
+
methods.any? { |method| %w[POST PUT PATCH].include?(method) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def merge_open_api_parameters(default_parameters, custom_parameters)
|
|
102
|
+
merged = Array(custom_parameters).dup
|
|
103
|
+
Array(default_parameters).each do |parameter|
|
|
104
|
+
next if merged.any? { |entry| fetch_key(entry, :name).to_s == fetch_key(parameter, :name).to_s && fetch_key(entry, :in).to_s == fetch_key(parameter, :in).to_s }
|
|
105
|
+
|
|
106
|
+
merged << parameter
|
|
107
|
+
end
|
|
108
|
+
merged
|
|
109
|
+
end
|
|
110
|
+
|
|
75
111
|
def apply_open_api_schemas!
|
|
76
112
|
openapi = fetch_key(metadata, :openapi)
|
|
77
113
|
return unless openapi.is_a?(Hash)
|
|
@@ -172,6 +208,42 @@ module BetterAuth
|
|
|
172
208
|
class Result
|
|
173
209
|
attr_accessor :response, :status, :headers
|
|
174
210
|
|
|
211
|
+
class SetCookieHeader < Array
|
|
212
|
+
def self.from(value)
|
|
213
|
+
new(value.lines.map(&:chomp))
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def include?(value)
|
|
217
|
+
return super unless value.is_a?(String)
|
|
218
|
+
|
|
219
|
+
any? { |line| line.include?(value) }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def lines
|
|
223
|
+
self
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def match(...)
|
|
227
|
+
to_s.match(...)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def =~(pattern)
|
|
231
|
+
to_s =~ pattern
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def split(...)
|
|
235
|
+
to_s.split(...)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def to_s
|
|
239
|
+
join("\n")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def to_str
|
|
243
|
+
to_s
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
175
247
|
def initialize(response:, status: 200, headers: {}, raw_response: nil)
|
|
176
248
|
@response = response
|
|
177
249
|
@status = status
|
|
@@ -223,7 +295,7 @@ module BetterAuth
|
|
|
223
295
|
end
|
|
224
296
|
|
|
225
297
|
def to_response
|
|
226
|
-
return Response.from_rack(@raw_response) if raw_response?
|
|
298
|
+
return Response.from_rack([@raw_response[0], rack_headers(@raw_response[1]), @raw_response[2]]) if raw_response?
|
|
227
299
|
|
|
228
300
|
body = if response.nil?
|
|
229
301
|
[JSON.generate(nil)]
|
|
@@ -232,12 +304,23 @@ module BetterAuth
|
|
|
232
304
|
else
|
|
233
305
|
[JSON.generate(response)]
|
|
234
306
|
end
|
|
235
|
-
response_headers = {"content-type" => "application/json"}.merge(headers)
|
|
307
|
+
response_headers = rack_headers({"content-type" => "application/json"}.merge(headers))
|
|
236
308
|
Response.new(status: status, headers: response_headers, body: body)
|
|
237
309
|
end
|
|
238
310
|
|
|
239
311
|
private
|
|
240
312
|
|
|
313
|
+
def rack_headers(value)
|
|
314
|
+
value.each_with_object({}) do |(key, header_value), result|
|
|
315
|
+
normalized = key.to_s.downcase
|
|
316
|
+
result[normalized] = if normalized == "set-cookie" && header_value.is_a?(String) && header_value.include?("\n")
|
|
317
|
+
SetCookieHeader.from(header_value)
|
|
318
|
+
else
|
|
319
|
+
header_value
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
241
324
|
def normalize_headers(headers)
|
|
242
325
|
headers.each_with_object({}) do |(key, value), result|
|
|
243
326
|
result[key.to_s.downcase] = value
|
|
@@ -290,7 +373,7 @@ module BetterAuth
|
|
|
290
373
|
|
|
291
374
|
def set_cookie(name, value, options = {})
|
|
292
375
|
attributes = cookie_attributes(options)
|
|
293
|
-
cookie = (["#{name}=#{value}"] + attributes).join("; ")
|
|
376
|
+
cookie = (["#{name}=#{URI.encode_uri_component(value.to_s)}"] + attributes).join("; ")
|
|
294
377
|
set_header("set-cookie", cookie)
|
|
295
378
|
end
|
|
296
379
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Env
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def get(name)
|
|
8
|
+
open_auth_name = open_auth_name(name)
|
|
9
|
+
return nil unless open_auth_name
|
|
10
|
+
|
|
11
|
+
value = ENV[open_auth_name]
|
|
12
|
+
return value if present?(value)
|
|
13
|
+
|
|
14
|
+
value = ENV[name]
|
|
15
|
+
present?(value) ? value : nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def fetch(name, default = nil)
|
|
19
|
+
value = get(name)
|
|
20
|
+
value.nil? ? default : value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def csv(name)
|
|
24
|
+
fetch(name, "").split(",").map(&:strip).reject(&:empty?)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def open_auth_name(name)
|
|
28
|
+
text = name.to_s
|
|
29
|
+
return nil unless text.start_with?("BETTER_AUTH_")
|
|
30
|
+
|
|
31
|
+
text.sub(/\ABETTER_AUTH_/, "OPEN_AUTH_")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def present?(value)
|
|
35
|
+
value && !value.empty?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module BetterAuth
|
|
8
|
+
module HTTPClient
|
|
9
|
+
DEFAULT_OPEN_TIMEOUT = 5
|
|
10
|
+
DEFAULT_READ_TIMEOUT = 5
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def request(uri, request, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT)
|
|
15
|
+
Net::HTTP.start(
|
|
16
|
+
uri.hostname || uri.host,
|
|
17
|
+
uri.port,
|
|
18
|
+
use_ssl: uri.scheme == "https",
|
|
19
|
+
open_timeout: open_timeout,
|
|
20
|
+
read_timeout: read_timeout
|
|
21
|
+
) do |http|
|
|
22
|
+
http.request(request)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get_response(uri, headers = {})
|
|
27
|
+
request = Net::HTTP::Get.new(uri)
|
|
28
|
+
headers.each { |key, value| request[key.to_s] = value.to_s }
|
|
29
|
+
request(uri, request)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def post_form(uri, form_body, headers = {})
|
|
33
|
+
request = Net::HTTP::Post.new(uri)
|
|
34
|
+
headers.each { |key, value| request[key.to_s] = value.to_s }
|
|
35
|
+
request["Content-Type"] ||= "application/x-www-form-urlencoded"
|
|
36
|
+
request.body = form_body
|
|
37
|
+
request(uri, request)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_json(url, headers = {})
|
|
41
|
+
uri = url.is_a?(URI) ? url : URI.parse(url.to_s)
|
|
42
|
+
response = get_response(uri, headers)
|
|
43
|
+
response.is_a?(Net::HTTPSuccess) ? JSON.parse(response.body.to_s) : nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module MigrationPlan
|
|
5
|
+
TableChange = Struct.new(:logical_name, :table_name, :table, :order, keyword_init: true)
|
|
6
|
+
FieldChange = Struct.new(:logical_name, :table_name, :fields, :table, :order, keyword_init: true)
|
|
7
|
+
IndexChange = Struct.new(:table_name, :field_name, :name, :unique, :field, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
Plan = Struct.new(:to_create, :to_add, :to_index, :warnings, :dialect, :tables, keyword_init: true) do
|
|
10
|
+
def empty?
|
|
11
|
+
to_create.empty? && to_add.empty? && to_index.empty?
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/better_auth/oauth2.rb
CHANGED
|
@@ -81,7 +81,7 @@ module BetterAuth
|
|
|
81
81
|
|
|
82
82
|
def post_form(token_endpoint, request)
|
|
83
83
|
uri = URI.parse(token_endpoint)
|
|
84
|
-
response =
|
|
84
|
+
response = HTTPClient.post_form(uri, URI.encode_www_form(request[:body]), request[:headers])
|
|
85
85
|
JSON.parse(response.body)
|
|
86
86
|
end
|
|
87
87
|
|
|
@@ -66,7 +66,11 @@ module BetterAuth
|
|
|
66
66
|
after: [
|
|
67
67
|
{
|
|
68
68
|
matcher: ->(ctx) { ctx.path == "/list-sessions" },
|
|
69
|
-
handler:
|
|
69
|
+
handler: lambda do |ctx|
|
|
70
|
+
next unless ctx.returned.is_a?(Array)
|
|
71
|
+
|
|
72
|
+
ctx.json(ctx.returned.reject { |session| session["impersonatedBy"] || session[:impersonatedBy] })
|
|
73
|
+
end
|
|
70
74
|
}
|
|
71
75
|
]
|
|
72
76
|
},
|
|
@@ -436,6 +440,7 @@ module BetterAuth
|
|
|
436
440
|
openapi: {
|
|
437
441
|
operationId: "stopImpersonating",
|
|
438
442
|
description: "Stop impersonating a user",
|
|
443
|
+
requestBody: OpenAPI.empty_request_body,
|
|
439
444
|
responses: {
|
|
440
445
|
"200" => OpenAPI.json_response("Impersonation stopped", OpenAPI.session_response_schema_pair)
|
|
441
446
|
}
|
|
@@ -47,6 +47,7 @@ module BetterAuth
|
|
|
47
47
|
openapi: {
|
|
48
48
|
operationId: "signInAnonymous",
|
|
49
49
|
description: "Sign in anonymously",
|
|
50
|
+
requestBody: OpenAPI.empty_request_body,
|
|
50
51
|
responses: {
|
|
51
52
|
"200" => OpenAPI.json_response(
|
|
52
53
|
"Anonymous session created",
|
|
@@ -96,6 +97,7 @@ module BetterAuth
|
|
|
96
97
|
openapi: {
|
|
97
98
|
operationId: "deleteAnonymousUser",
|
|
98
99
|
description: "Delete the current anonymous user",
|
|
100
|
+
requestBody: OpenAPI.empty_request_body,
|
|
99
101
|
responses: {
|
|
100
102
|
"200" => OpenAPI.json_response("Anonymous user deleted", OpenAPI.success_response_schema)
|
|
101
103
|
}
|
|
@@ -103,7 +103,7 @@ module BetterAuth
|
|
|
103
103
|
else
|
|
104
104
|
URI.encode_www_form(verifier[:payload])
|
|
105
105
|
end
|
|
106
|
-
response =
|
|
106
|
+
response = HTTPClient.request(uri, request)
|
|
107
107
|
raise CAPTCHA_INTERNAL_ERROR_CODES["SERVICE_UNAVAILABLE"] unless response.is_a?(Net::HTTPSuccess)
|
|
108
108
|
|
|
109
109
|
JSON.parse(response.body.to_s)
|
|
@@ -54,6 +54,14 @@ module BetterAuth
|
|
|
54
54
|
openapi: {
|
|
55
55
|
operationId: "requestDeviceCode",
|
|
56
56
|
description: "Request a device and user code",
|
|
57
|
+
requestBody: OpenAPI.json_request_body(
|
|
58
|
+
OpenAPI.object_schema(
|
|
59
|
+
{
|
|
60
|
+
client_id: {type: "string", description: "OAuth client ID"},
|
|
61
|
+
scope: {type: "string", description: "Requested scopes"}
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
),
|
|
57
65
|
responses: {
|
|
58
66
|
"200" => OpenAPI.json_response("Success", device_code_response_schema)
|
|
59
67
|
}
|
|
@@ -106,6 +114,16 @@ module BetterAuth
|
|
|
106
114
|
openapi: {
|
|
107
115
|
operationId: "exchangeDeviceToken",
|
|
108
116
|
description: "Exchange device code for access token",
|
|
117
|
+
requestBody: OpenAPI.json_request_body(
|
|
118
|
+
OpenAPI.object_schema(
|
|
119
|
+
{
|
|
120
|
+
grant_type: {type: "string", enum: [OAuthProtocol::DEVICE_CODE_GRANT]},
|
|
121
|
+
device_code: {type: "string"},
|
|
122
|
+
client_id: {type: "string"}
|
|
123
|
+
},
|
|
124
|
+
required: ["grant_type", "device_code"]
|
|
125
|
+
)
|
|
126
|
+
),
|
|
109
127
|
responses: {
|
|
110
128
|
"200" => OpenAPI.json_response("Success", device_token_response_schema)
|
|
111
129
|
}
|
|
@@ -197,6 +215,14 @@ module BetterAuth
|
|
|
197
215
|
openapi: {
|
|
198
216
|
operationId: "approveDevice",
|
|
199
217
|
description: "Approve a device authorization request",
|
|
218
|
+
requestBody: OpenAPI.json_request_body(
|
|
219
|
+
OpenAPI.object_schema(
|
|
220
|
+
{
|
|
221
|
+
user_code: {type: "string", description: "User code shown on the device"},
|
|
222
|
+
userCode: {type: "string", description: "User code shown on the device"}
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
),
|
|
200
226
|
responses: {
|
|
201
227
|
"200" => OpenAPI.json_response("Success", OpenAPI.success_response_schema)
|
|
202
228
|
}
|
|
@@ -218,6 +244,14 @@ module BetterAuth
|
|
|
218
244
|
openapi: {
|
|
219
245
|
operationId: "denyDevice",
|
|
220
246
|
description: "Deny a device authorization request",
|
|
247
|
+
requestBody: OpenAPI.json_request_body(
|
|
248
|
+
OpenAPI.object_schema(
|
|
249
|
+
{
|
|
250
|
+
user_code: {type: "string", description: "User code shown on the device"},
|
|
251
|
+
userCode: {type: "string", description: "User code shown on the device"}
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
),
|
|
221
255
|
responses: {
|
|
222
256
|
"200" => OpenAPI.json_response("Success", OpenAPI.success_response_schema)
|
|
223
257
|
}
|