better_auth 0.3.0 → 0.5.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 +17 -0
- data/README.md +24 -0
- data/lib/better_auth/adapters/internal_adapter.rb +10 -7
- data/lib/better_auth/adapters/memory.rb +57 -11
- data/lib/better_auth/adapters/sql.rb +123 -20
- data/lib/better_auth/api.rb +114 -9
- data/lib/better_auth/async.rb +70 -0
- data/lib/better_auth/configuration.rb +97 -7
- data/lib/better_auth/context.rb +165 -12
- data/lib/better_auth/cookies.rb +6 -4
- data/lib/better_auth/core.rb +2 -0
- data/lib/better_auth/crypto/jwe.rb +27 -5
- data/lib/better_auth/crypto.rb +32 -0
- data/lib/better_auth/database_hooks.rb +8 -8
- data/lib/better_auth/deprecate.rb +28 -0
- data/lib/better_auth/endpoint.rb +92 -5
- data/lib/better_auth/error.rb +8 -1
- data/lib/better_auth/host.rb +166 -0
- data/lib/better_auth/instrumentation.rb +74 -0
- data/lib/better_auth/logger.rb +31 -0
- data/lib/better_auth/middleware/origin_check.rb +2 -2
- data/lib/better_auth/oauth2.rb +94 -0
- data/lib/better_auth/plugins/admin/schema.rb +2 -2
- data/lib/better_auth/plugins/admin.rb +344 -16
- data/lib/better_auth/plugins/anonymous.rb +37 -3
- data/lib/better_auth/plugins/device_authorization.rb +102 -5
- data/lib/better_auth/plugins/dub.rb +148 -0
- data/lib/better_auth/plugins/email_otp.rb +261 -19
- data/lib/better_auth/plugins/expo.rb +17 -1
- data/lib/better_auth/plugins/generic_oauth.rb +67 -35
- data/lib/better_auth/plugins/jwt.rb +37 -4
- data/lib/better_auth/plugins/last_login_method.rb +2 -2
- data/lib/better_auth/plugins/magic_link.rb +66 -3
- data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
- data/lib/better_auth/plugins/mcp/config.rb +51 -0
- data/lib/better_auth/plugins/mcp/consent.rb +31 -0
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
- data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
- data/lib/better_auth/plugins/mcp/registration.rb +31 -0
- data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
- data/lib/better_auth/plugins/mcp/schema.rb +91 -0
- data/lib/better_auth/plugins/mcp/token.rb +108 -0
- data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
- data/lib/better_auth/plugins/mcp.rb +111 -263
- data/lib/better_auth/plugins/multi_session.rb +61 -3
- data/lib/better_auth/plugins/oauth_protocol.rb +173 -30
- data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
- data/lib/better_auth/plugins/oidc_provider.rb +118 -14
- data/lib/better_auth/plugins/one_tap.rb +7 -2
- data/lib/better_auth/plugins/one_time_token.rb +42 -2
- data/lib/better_auth/plugins/open_api.rb +163 -318
- data/lib/better_auth/plugins/organization/schema.rb +6 -0
- data/lib/better_auth/plugins/organization.rb +186 -56
- data/lib/better_auth/plugins/phone_number.rb +141 -6
- data/lib/better_auth/plugins/siwe.rb +69 -3
- data/lib/better_auth/plugins/two_factor.rb +118 -41
- data/lib/better_auth/plugins/username.rb +57 -2
- data/lib/better_auth/rate_limiter.rb +38 -0
- data/lib/better_auth/request_state.rb +44 -0
- data/lib/better_auth/response.rb +42 -0
- data/lib/better_auth/router.rb +7 -1
- data/lib/better_auth/routes/account.rb +220 -42
- data/lib/better_auth/routes/email_verification.rb +98 -14
- data/lib/better_auth/routes/password.rb +126 -8
- data/lib/better_auth/routes/session.rb +128 -13
- data/lib/better_auth/routes/sign_in.rb +26 -2
- data/lib/better_auth/routes/sign_out.rb +13 -1
- data/lib/better_auth/routes/sign_up.rb +70 -4
- data/lib/better_auth/routes/social.rb +132 -7
- data/lib/better_auth/routes/user.rb +228 -20
- data/lib/better_auth/routes/validation.rb +50 -0
- data/lib/better_auth/secret_config.rb +115 -0
- data/lib/better_auth/session.rb +13 -2
- data/lib/better_auth/url_helpers.rb +206 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +12 -0
- metadata +23 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Routes
|
|
5
|
+
REQUEST_EMAIL_PATTERN = /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
|
|
6
|
+
|
|
7
|
+
def self.request_body_schema(required_strings: [], required_nonempty_strings: [], email_strings: [], optional_strings: [])
|
|
8
|
+
->(body) {
|
|
9
|
+
data = request_validation_hash(body)
|
|
10
|
+
return false unless required_strings.all? { |key| request_string?(data, key) }
|
|
11
|
+
return false unless required_nonempty_strings.all? { |key| request_string?(data, key) && !data[request_storage_key(key)].empty? }
|
|
12
|
+
return false unless email_strings.all? { |key| request_string?(data, key) && REQUEST_EMAIL_PATTERN.match?(data[request_storage_key(key)]) }
|
|
13
|
+
return false unless optional_strings.all? { |key| !data.key?(request_storage_key(key)) || request_string?(data, key) }
|
|
14
|
+
|
|
15
|
+
data
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.request_query_schema(required_strings: [], optional_strings: [])
|
|
20
|
+
->(query) {
|
|
21
|
+
data = request_validation_hash(query)
|
|
22
|
+
return false unless required_strings.all? { |key| request_string?(data, key) }
|
|
23
|
+
return false unless optional_strings.all? { |key| !data.key?(request_storage_key(key)) || request_string?(data, key) }
|
|
24
|
+
|
|
25
|
+
data
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.request_validation_hash(value)
|
|
30
|
+
return {} unless value.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
value.each_with_object({}) do |(key, object_value), result|
|
|
33
|
+
result[request_storage_key(key)] = object_value
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.request_string?(data, key)
|
|
38
|
+
data[request_storage_key(key)].is_a?(String)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.request_storage_key(key)
|
|
42
|
+
key.to_s
|
|
43
|
+
.gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
|
|
44
|
+
.tr("-", "_")
|
|
45
|
+
.downcase
|
|
46
|
+
.split("_")
|
|
47
|
+
.then { |parts| ([parts.first] + parts.drop(1).map(&:capitalize)).join }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
class SecretConfig
|
|
5
|
+
ENVELOPE_PREFIX = "$ba$"
|
|
6
|
+
|
|
7
|
+
attr_reader :keys, :current_version, :legacy_secret
|
|
8
|
+
|
|
9
|
+
def initialize(keys:, current_version:, legacy_secret: nil)
|
|
10
|
+
normalized_keys = keys.each_with_object({}) do |(version, value), result|
|
|
11
|
+
result[normalize_version!(version)] = value.to_s
|
|
12
|
+
end
|
|
13
|
+
@keys = normalized_keys.freeze
|
|
14
|
+
@current_version = normalize_version!(current_version)
|
|
15
|
+
@legacy_secret = legacy_secret unless legacy_secret.to_s.empty?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current_secret
|
|
19
|
+
keys.fetch(current_version) do
|
|
20
|
+
raise Error, "Secret version #{current_version} not found in keys"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def all_secrets
|
|
25
|
+
entries = keys.map { |version, value| [version, value] }
|
|
26
|
+
entries << [-1, legacy_secret] if legacy_secret && !keys.value?(legacy_secret)
|
|
27
|
+
entries
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.parse_env(value)
|
|
31
|
+
return nil if value.to_s.empty?
|
|
32
|
+
|
|
33
|
+
value.to_s.split(",").map do |entry|
|
|
34
|
+
entry = entry.strip
|
|
35
|
+
colon_index = entry.index(":")
|
|
36
|
+
raise Error, "Invalid BETTER_AUTH_SECRETS entry: \"#{entry}\". Expected format: \"<version>:<secret>\"" unless colon_index
|
|
37
|
+
|
|
38
|
+
version = entry[0...colon_index].strip
|
|
39
|
+
secret = entry[(colon_index + 1)..].to_s.strip
|
|
40
|
+
raise Error, "Empty secret value for version #{version} in BETTER_AUTH_SECRETS." if secret.empty?
|
|
41
|
+
|
|
42
|
+
{version: parse_version!(version, source: "BETTER_AUTH_SECRETS"), value: secret}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.validate_secrets!(secrets, logger: nil)
|
|
47
|
+
entries = Array(secrets)
|
|
48
|
+
raise Error, "`secrets` array must contain at least one entry." if entries.empty?
|
|
49
|
+
|
|
50
|
+
seen = {}
|
|
51
|
+
entries.each do |entry|
|
|
52
|
+
data = normalize_entry(entry)
|
|
53
|
+
version = parse_version!(data.fetch(:version), source: "`secrets`")
|
|
54
|
+
value = data.fetch(:value, nil).to_s
|
|
55
|
+
raise Error, "Empty secret value for version #{version} in `secrets`." if value.empty?
|
|
56
|
+
raise Error, "Duplicate version #{version} in `secrets`. Each version must be unique." if seen[version]
|
|
57
|
+
|
|
58
|
+
seen[version] = true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
current = normalize_entry(entries.first)
|
|
62
|
+
current_version = parse_version!(current.fetch(:version), source: "`secrets`")
|
|
63
|
+
current_value = current.fetch(:value).to_s
|
|
64
|
+
warn(logger, "[better-auth] Warning: the current secret (version #{current_version}) should be at least 32 characters long for adequate security.") if current_value.length < 32
|
|
65
|
+
warn(logger, "[better-auth] Warning: the current secret appears low-entropy. Use a randomly generated secret for production.") if entropy(current_value) < 120
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.build(secrets, legacy_secret, logger: nil)
|
|
69
|
+
validate_secrets!(secrets, logger: logger)
|
|
70
|
+
entries = Array(secrets).map { |entry| normalize_entry(entry) }
|
|
71
|
+
keys = entries.each_with_object({}) do |entry, result|
|
|
72
|
+
result[parse_version!(entry.fetch(:version), source: "`secrets`")] = entry.fetch(:value).to_s
|
|
73
|
+
end
|
|
74
|
+
current_version = parse_version!(entries.first.fetch(:version), source: "`secrets`")
|
|
75
|
+
legacy = (legacy_secret && legacy_secret != Configuration::DEFAULT_SECRET) ? legacy_secret : nil
|
|
76
|
+
new(keys: keys, current_version: current_version, legacy_secret: legacy)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.normalize_entry(entry)
|
|
80
|
+
raise Error, "Invalid `secrets` entry. Expected a hash with `version` and `value`." unless entry.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
entry.each_with_object({}) do |(key, value), result|
|
|
83
|
+
result[key.to_s.tr("-", "_").to_sym] = value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.parse_version!(value, source:)
|
|
88
|
+
text = value.to_s.strip
|
|
89
|
+
unless text.match?(/\A(?:0|[1-9]\d*)\z/)
|
|
90
|
+
raise Error, "Invalid version #{value} in #{source}. Version must be a non-negative integer."
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
text.to_i
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.entropy(value)
|
|
97
|
+
unique = value.to_s.chars.uniq.length
|
|
98
|
+
return 0 if unique.zero?
|
|
99
|
+
|
|
100
|
+
Math.log2(unique**value.to_s.length)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.warn(logger, message)
|
|
104
|
+
if logger.respond_to?(:call)
|
|
105
|
+
logger.call(:warn, message)
|
|
106
|
+
elsif logger.respond_to?(:warn)
|
|
107
|
+
logger.warn(message)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def normalize_version!(version)
|
|
112
|
+
self.class.parse_version!(version, source: "`secrets`")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/better_auth/session.rb
CHANGED
|
@@ -51,7 +51,7 @@ module BetterAuth
|
|
|
51
51
|
return nil if payload["session"]["token"] && payload["session"]["token"] != token
|
|
52
52
|
|
|
53
53
|
result = {session: payload["session"], user: payload["user"]}
|
|
54
|
-
|
|
54
|
+
result = refresh_cached_session(ctx, result) if should_refresh_cookie_cache?(config, payload)
|
|
55
55
|
result
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -89,6 +89,17 @@ module BetterAuth
|
|
|
89
89
|
refreshed
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
def refresh_cached_session(ctx, result)
|
|
93
|
+
now = Time.now
|
|
94
|
+
session = stringify_keys(result[:session]).merge(
|
|
95
|
+
"expiresAt" => now + ctx.context.session_config[:expires_in].to_i,
|
|
96
|
+
"updatedAt" => now
|
|
97
|
+
)
|
|
98
|
+
refreshed = {session: session, user: result[:user]}
|
|
99
|
+
Cookies.set_session_cookie(ctx, refreshed, Cookies.dont_remember?(ctx))
|
|
100
|
+
refreshed
|
|
101
|
+
end
|
|
102
|
+
|
|
92
103
|
def should_refresh_cookie_cache?(config, payload)
|
|
93
104
|
refresh_cache = config[:refresh_cache]
|
|
94
105
|
return false if refresh_cache == false || refresh_cache.nil?
|
|
@@ -97,7 +108,7 @@ module BetterAuth
|
|
|
97
108
|
update_age = if refresh_cache.is_a?(Hash)
|
|
98
109
|
(refresh_cache[:update_age] || refresh_cache["updateAge"] || refresh_cache["update_age"]).to_i
|
|
99
110
|
else
|
|
100
|
-
(max_age * 0.
|
|
111
|
+
(max_age * 0.2).to_i
|
|
101
112
|
end
|
|
102
113
|
updated_at = payload["updatedAt"].to_i
|
|
103
114
|
updated_at.positive? && updated_at + (update_age * 1000) <= (Time.now.to_f * 1000).to_i
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module URLHelpers
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def valid_proxy_header?(header, type)
|
|
10
|
+
value = header.to_s
|
|
11
|
+
return false if value.strip.empty?
|
|
12
|
+
|
|
13
|
+
case type.to_sym
|
|
14
|
+
when :proto
|
|
15
|
+
["http", "https"].include?(value)
|
|
16
|
+
when :host
|
|
17
|
+
return false if value.match?(/\.\.|\0|\s|\A[.]|[<>'"]|javascript:|file:|data:/i)
|
|
18
|
+
return false if value.match?(%r{[/\\]})
|
|
19
|
+
|
|
20
|
+
patterns = [
|
|
21
|
+
/\A[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(:[0-9]{1,5})?\z/,
|
|
22
|
+
/\A(\d{1,3}\.){3}\d{1,3}(:[0-9]{1,5})?\z/,
|
|
23
|
+
/\A\[[0-9a-fA-F:]+\](:[0-9]{1,5})?\z/,
|
|
24
|
+
/\Alocalhost(:[0-9]{1,5})?\z/i
|
|
25
|
+
]
|
|
26
|
+
patterns.any? { |pattern| value.match?(pattern) } && valid_port?(value)
|
|
27
|
+
else
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def matches_host_pattern?(host, pattern)
|
|
33
|
+
return false if host.to_s.empty? || pattern.to_s.empty?
|
|
34
|
+
|
|
35
|
+
normalized_host = normalize_host_pattern_value(host)
|
|
36
|
+
normalized_pattern = normalize_host_pattern_value(pattern)
|
|
37
|
+
regex = Regexp.escape(normalized_pattern)
|
|
38
|
+
.gsub("\\*", ".*")
|
|
39
|
+
.gsub("\\?", ".")
|
|
40
|
+
!!normalized_host.match?(/\A#{regex}\z/i)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def host_from_source(source, trusted_proxy_headers: false)
|
|
44
|
+
headers = headers_from_source(source)
|
|
45
|
+
if trusted_proxy_headers
|
|
46
|
+
forwarded_host = header_value(headers, "x-forwarded-host")
|
|
47
|
+
return forwarded_host if forwarded_host && valid_proxy_header?(forwarded_host, :host)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
host = header_value(headers, "host")
|
|
51
|
+
return host if host && valid_proxy_header?(host, :host)
|
|
52
|
+
|
|
53
|
+
uri_host(source_url(source))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def protocol_from_source(source, config_protocol: nil, trusted_proxy_headers: false)
|
|
57
|
+
return config_protocol if ["http", "https"].include?(config_protocol)
|
|
58
|
+
|
|
59
|
+
headers = headers_from_source(source)
|
|
60
|
+
if trusted_proxy_headers
|
|
61
|
+
forwarded_proto = header_value(headers, "x-forwarded-proto")
|
|
62
|
+
return forwarded_proto if forwarded_proto && valid_proxy_header?(forwarded_proto, :proto)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
protocol = uri_scheme(source_url(source))
|
|
66
|
+
return protocol if ["http", "https"].include?(protocol)
|
|
67
|
+
|
|
68
|
+
host = host_from_source(source, trusted_proxy_headers: trusted_proxy_headers)
|
|
69
|
+
return "http" if host && loopback_for_dev_scheme?(host)
|
|
70
|
+
|
|
71
|
+
"https"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def resolve_base_url(config, base_path, source = nil, load_env: true, trusted_proxy_headers: false)
|
|
75
|
+
if dynamic_config?(config)
|
|
76
|
+
return resolve_dynamic_base_url(config, source, base_path, trusted_proxy_headers: trusted_proxy_headers) if source
|
|
77
|
+
return with_path(config[:fallback] || config["fallback"], base_path) if config[:fallback] || config["fallback"]
|
|
78
|
+
|
|
79
|
+
return env_base_url(base_path) if load_env
|
|
80
|
+
return nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
return with_path(config, base_path) if config.is_a?(String)
|
|
84
|
+
return env_base_url(base_path) if load_env
|
|
85
|
+
return with_path(origin(source_url(source)), base_path) if source
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def resolve_dynamic_base_url(config, source, base_path, trusted_proxy_headers: false)
|
|
91
|
+
host = host_from_source(source, trusted_proxy_headers: trusted_proxy_headers)
|
|
92
|
+
fallback = config[:fallback] || config["fallback"]
|
|
93
|
+
raise Error, "Could not determine host from request headers. Please provide a fallback URL in your baseURL config." unless host || fallback
|
|
94
|
+
|
|
95
|
+
allowed_hosts = config[:allowed_hosts] || config["allowed_hosts"] || config[:allowedHosts] || config["allowedHosts"] || []
|
|
96
|
+
if host && allowed_hosts.any? { |pattern| matches_host_pattern?(host, pattern) }
|
|
97
|
+
protocol = protocol_from_source(source, config_protocol: config[:protocol] || config["protocol"], trusted_proxy_headers: trusted_proxy_headers)
|
|
98
|
+
return with_path("#{protocol}://#{host}", base_path)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
return with_path(fallback, base_path) if fallback
|
|
102
|
+
|
|
103
|
+
raise Error, "Host \"#{host}\" is not in the allowed hosts list."
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def with_path(url, path = "/api/auth")
|
|
107
|
+
parsed = URI.parse(url.to_s)
|
|
108
|
+
raise Error, "Invalid base URL: #{url}. URL must include 'http://' or 'https://'" unless ["http", "https"].include?(parsed.scheme)
|
|
109
|
+
|
|
110
|
+
current_path = parsed.path.to_s.gsub(%r{/+\z}, "")
|
|
111
|
+
return url.to_s if !current_path.empty? && current_path != "/"
|
|
112
|
+
|
|
113
|
+
trimmed = url.to_s.gsub(%r{/+\z}, "")
|
|
114
|
+
return trimmed if path.to_s.empty? || path == "/"
|
|
115
|
+
|
|
116
|
+
suffix = path.start_with?("/") ? path : "/#{path}"
|
|
117
|
+
"#{trimmed}#{suffix}"
|
|
118
|
+
rescue URI::InvalidURIError
|
|
119
|
+
raise Error, "Invalid base URL: #{url}. Please provide a valid base URL."
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def origin(url)
|
|
123
|
+
parsed = URI.parse(url.to_s)
|
|
124
|
+
return nil unless ["http", "https"].include?(parsed.scheme)
|
|
125
|
+
|
|
126
|
+
port = parsed.port
|
|
127
|
+
default_port = (parsed.scheme == "http" && port == 80) || (parsed.scheme == "https" && port == 443)
|
|
128
|
+
default_port ? "#{parsed.scheme}://#{parsed.host}" : "#{parsed.scheme}://#{parsed.host}:#{port}"
|
|
129
|
+
rescue URI::InvalidURIError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def uri_host(url)
|
|
134
|
+
parsed = URI.parse(url.to_s)
|
|
135
|
+
return nil unless parsed.host
|
|
136
|
+
|
|
137
|
+
default_port = (parsed.scheme == "http" && parsed.port == 80) || (parsed.scheme == "https" && parsed.port == 443)
|
|
138
|
+
default_port ? parsed.host : "#{parsed.host}:#{parsed.port}"
|
|
139
|
+
rescue URI::InvalidURIError
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def uri_scheme(url)
|
|
144
|
+
URI.parse(url.to_s).scheme
|
|
145
|
+
rescue URI::InvalidURIError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def normalize_host_pattern_value(value)
|
|
150
|
+
value.to_s.sub(%r{\Ahttps?://}i, "").split("/").first.to_s.downcase
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def headers_from_source(source)
|
|
154
|
+
return {} unless source
|
|
155
|
+
return source.headers if source.respond_to?(:headers)
|
|
156
|
+
return rack_request_headers(source) if source.respond_to?(:get_header)
|
|
157
|
+
return source if source.is_a?(Hash)
|
|
158
|
+
|
|
159
|
+
{}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def header_value(headers, key)
|
|
163
|
+
if headers.respond_to?(:get)
|
|
164
|
+
headers.get(key)
|
|
165
|
+
else
|
|
166
|
+
headers[key] || headers[key.to_s] || headers[key.to_s.downcase] || headers[key.to_s.upcase] || headers[key.tr("-", "_").upcase]
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def source_url(source)
|
|
171
|
+
return source.url if source.respond_to?(:url)
|
|
172
|
+
|
|
173
|
+
source.get_header("REQUEST_URI") if source.respond_to?(:get_header)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def dynamic_config?(config)
|
|
177
|
+
config.is_a?(Hash) && (config.key?(:allowed_hosts) || config.key?("allowed_hosts") || config.key?(:allowedHosts) || config.key?("allowedHosts"))
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def env_base_url(base_path)
|
|
181
|
+
url = ENV["BETTER_AUTH_URL"] || ENV["NEXT_PUBLIC_BETTER_AUTH_URL"] || ENV["PUBLIC_BETTER_AUTH_URL"] || ENV["NUXT_PUBLIC_BETTER_AUTH_URL"] || ENV["NUXT_PUBLIC_AUTH_URL"]
|
|
182
|
+
url ||= ENV["BASE_URL"] if ENV["BASE_URL"] && ENV["BASE_URL"] != "/"
|
|
183
|
+
url ? with_path(url, base_path) : nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def loopback_for_dev_scheme?(host)
|
|
187
|
+
hostname = host.to_s.sub(/:\d+\z/, "").sub(/\A\[/, "").sub(/\]\z/, "").downcase
|
|
188
|
+
hostname == "localhost" || hostname.end_with?(".localhost") || hostname == "::1" || hostname.start_with?("127.")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def valid_port?(host)
|
|
192
|
+
port = host[/:(\d{1,5})\z/, 1]
|
|
193
|
+
return true unless port
|
|
194
|
+
|
|
195
|
+
port.to_i.between?(1, 65_535)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def rack_request_headers(source)
|
|
199
|
+
{
|
|
200
|
+
"x-forwarded-host" => source.get_header("HTTP_X_FORWARDED_HOST"),
|
|
201
|
+
"x-forwarded-proto" => source.get_header("HTTP_X_FORWARDED_PROTO"),
|
|
202
|
+
"host" => source.get_header("HTTP_HOST")
|
|
203
|
+
}.compact
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
data/lib/better_auth/version.rb
CHANGED
data/lib/better_auth.rb
CHANGED
|
@@ -4,7 +4,17 @@ require_relative "better_auth/version"
|
|
|
4
4
|
require_relative "better_auth/core"
|
|
5
5
|
require_relative "better_auth/error"
|
|
6
6
|
require_relative "better_auth/api_error"
|
|
7
|
+
require_relative "better_auth/secret_config"
|
|
7
8
|
require_relative "better_auth/crypto"
|
|
9
|
+
require_relative "better_auth/host"
|
|
10
|
+
require_relative "better_auth/url_helpers"
|
|
11
|
+
require_relative "better_auth/request_state"
|
|
12
|
+
require_relative "better_auth/response"
|
|
13
|
+
require_relative "better_auth/async"
|
|
14
|
+
require_relative "better_auth/deprecate"
|
|
15
|
+
require_relative "better_auth/logger"
|
|
16
|
+
require_relative "better_auth/instrumentation"
|
|
17
|
+
require_relative "better_auth/oauth2"
|
|
8
18
|
require_relative "better_auth/password"
|
|
9
19
|
require_relative "better_auth/plugin"
|
|
10
20
|
require_relative "better_auth/configuration"
|
|
@@ -40,6 +50,7 @@ require_relative "better_auth/plugins/one_time_token"
|
|
|
40
50
|
require_relative "better_auth/plugins/one_tap"
|
|
41
51
|
require_relative "better_auth/plugins/siwe"
|
|
42
52
|
require_relative "better_auth/plugins/generic_oauth"
|
|
53
|
+
require_relative "better_auth/plugins/dub"
|
|
43
54
|
require_relative "better_auth/plugins/oauth_proxy"
|
|
44
55
|
require_relative "better_auth/plugins/passkey"
|
|
45
56
|
require_relative "better_auth/plugins/organization/schema"
|
|
@@ -68,6 +79,7 @@ require_relative "better_auth/session_store"
|
|
|
68
79
|
require_relative "better_auth/cookies"
|
|
69
80
|
require_relative "better_auth/session"
|
|
70
81
|
require_relative "better_auth/endpoint"
|
|
82
|
+
require_relative "better_auth/routes/validation"
|
|
71
83
|
require_relative "better_auth/routes/ok"
|
|
72
84
|
require_relative "better_auth/routes/error"
|
|
73
85
|
require_relative "better_auth/routes/sign_up"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_auth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Sala
|
|
@@ -268,6 +268,7 @@ files:
|
|
|
268
268
|
- lib/better_auth/adapters/sqlite.rb
|
|
269
269
|
- lib/better_auth/api.rb
|
|
270
270
|
- lib/better_auth/api_error.rb
|
|
271
|
+
- lib/better_auth/async.rb
|
|
271
272
|
- lib/better_auth/auth.rb
|
|
272
273
|
- lib/better_auth/configuration.rb
|
|
273
274
|
- lib/better_auth/context.rb
|
|
@@ -276,9 +277,14 @@ files:
|
|
|
276
277
|
- lib/better_auth/crypto.rb
|
|
277
278
|
- lib/better_auth/crypto/jwe.rb
|
|
278
279
|
- lib/better_auth/database_hooks.rb
|
|
280
|
+
- lib/better_auth/deprecate.rb
|
|
279
281
|
- lib/better_auth/endpoint.rb
|
|
280
282
|
- lib/better_auth/error.rb
|
|
283
|
+
- lib/better_auth/host.rb
|
|
284
|
+
- lib/better_auth/instrumentation.rb
|
|
285
|
+
- lib/better_auth/logger.rb
|
|
281
286
|
- lib/better_auth/middleware/origin_check.rb
|
|
287
|
+
- lib/better_auth/oauth2.rb
|
|
282
288
|
- lib/better_auth/password.rb
|
|
283
289
|
- lib/better_auth/plugin.rb
|
|
284
290
|
- lib/better_auth/plugin_context.rb
|
|
@@ -294,6 +300,7 @@ files:
|
|
|
294
300
|
- lib/better_auth/plugins/captcha.rb
|
|
295
301
|
- lib/better_auth/plugins/custom_session.rb
|
|
296
302
|
- lib/better_auth/plugins/device_authorization.rb
|
|
303
|
+
- lib/better_auth/plugins/dub.rb
|
|
297
304
|
- lib/better_auth/plugins/email_otp.rb
|
|
298
305
|
- lib/better_auth/plugins/expo.rb
|
|
299
306
|
- lib/better_auth/plugins/generic_oauth.rb
|
|
@@ -302,6 +309,16 @@ files:
|
|
|
302
309
|
- lib/better_auth/plugins/last_login_method.rb
|
|
303
310
|
- lib/better_auth/plugins/magic_link.rb
|
|
304
311
|
- lib/better_auth/plugins/mcp.rb
|
|
312
|
+
- lib/better_auth/plugins/mcp/authorization.rb
|
|
313
|
+
- lib/better_auth/plugins/mcp/config.rb
|
|
314
|
+
- lib/better_auth/plugins/mcp/consent.rb
|
|
315
|
+
- lib/better_auth/plugins/mcp/legacy_aliases.rb
|
|
316
|
+
- lib/better_auth/plugins/mcp/metadata.rb
|
|
317
|
+
- lib/better_auth/plugins/mcp/registration.rb
|
|
318
|
+
- lib/better_auth/plugins/mcp/resource_handler.rb
|
|
319
|
+
- lib/better_auth/plugins/mcp/schema.rb
|
|
320
|
+
- lib/better_auth/plugins/mcp/token.rb
|
|
321
|
+
- lib/better_auth/plugins/mcp/userinfo.rb
|
|
305
322
|
- lib/better_auth/plugins/multi_session.rb
|
|
306
323
|
- lib/better_auth/plugins/oauth_protocol.rb
|
|
307
324
|
- lib/better_auth/plugins/oauth_provider.rb
|
|
@@ -322,6 +339,8 @@ files:
|
|
|
322
339
|
- lib/better_auth/plugins/username.rb
|
|
323
340
|
- lib/better_auth/rate_limiter.rb
|
|
324
341
|
- lib/better_auth/request_ip.rb
|
|
342
|
+
- lib/better_auth/request_state.rb
|
|
343
|
+
- lib/better_auth/response.rb
|
|
325
344
|
- lib/better_auth/router.rb
|
|
326
345
|
- lib/better_auth/routes/account.rb
|
|
327
346
|
- lib/better_auth/routes/email_verification.rb
|
|
@@ -334,8 +353,10 @@ files:
|
|
|
334
353
|
- lib/better_auth/routes/sign_up.rb
|
|
335
354
|
- lib/better_auth/routes/social.rb
|
|
336
355
|
- lib/better_auth/routes/user.rb
|
|
356
|
+
- lib/better_auth/routes/validation.rb
|
|
337
357
|
- lib/better_auth/schema.rb
|
|
338
358
|
- lib/better_auth/schema/sql.rb
|
|
359
|
+
- lib/better_auth/secret_config.rb
|
|
339
360
|
- lib/better_auth/session.rb
|
|
340
361
|
- lib/better_auth/session_store.rb
|
|
341
362
|
- lib/better_auth/social_providers.rb
|
|
@@ -375,6 +396,7 @@ files:
|
|
|
375
396
|
- lib/better_auth/social_providers/vk.rb
|
|
376
397
|
- lib/better_auth/social_providers/wechat.rb
|
|
377
398
|
- lib/better_auth/social_providers/zoom.rb
|
|
399
|
+
- lib/better_auth/url_helpers.rb
|
|
378
400
|
- lib/better_auth/version.rb
|
|
379
401
|
homepage: https://github.com/sebasxsala/better-auth
|
|
380
402
|
licenses:
|