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
data/lib/better_auth/endpoint.rb
CHANGED
|
@@ -7,18 +7,24 @@ module BetterAuth
|
|
|
7
7
|
attr_reader :path,
|
|
8
8
|
:body_schema,
|
|
9
9
|
:query_schema,
|
|
10
|
+
:params_schema,
|
|
10
11
|
:headers_schema,
|
|
11
12
|
:metadata,
|
|
13
|
+
:options,
|
|
12
14
|
:use,
|
|
13
15
|
:handler
|
|
14
16
|
|
|
15
|
-
def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, headers_schema: nil, metadata: {}, use: [], &handler)
|
|
17
|
+
def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, params_schema: nil, headers_schema: nil, metadata: {}, use: [], &handler)
|
|
16
18
|
@path = path
|
|
17
19
|
@methods = Array(method || "*").map { |value| value.to_s.upcase }
|
|
18
20
|
@body_schema = body_schema
|
|
19
21
|
@query_schema = query_schema
|
|
22
|
+
@params_schema = params_schema
|
|
20
23
|
@headers_schema = headers_schema
|
|
21
24
|
@metadata = metadata || {}
|
|
25
|
+
apply_default_open_api_metadata!
|
|
26
|
+
apply_open_api_schemas!
|
|
27
|
+
@options = endpoint_options
|
|
22
28
|
@use = Array(use)
|
|
23
29
|
@handler = handler || ->(_ctx) {}
|
|
24
30
|
end
|
|
@@ -44,9 +50,38 @@ module BetterAuth
|
|
|
44
50
|
|
|
45
51
|
private
|
|
46
52
|
|
|
53
|
+
def endpoint_options
|
|
54
|
+
{
|
|
55
|
+
method: (methods.length == 1) ? methods.first : methods,
|
|
56
|
+
body: body_schema,
|
|
57
|
+
query: query_schema,
|
|
58
|
+
params: params_schema,
|
|
59
|
+
headers: headers_schema,
|
|
60
|
+
metadata: metadata
|
|
61
|
+
}.compact
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def apply_default_open_api_metadata!
|
|
65
|
+
return unless path
|
|
66
|
+
return if metadata[:openapi] || metadata[:hide] || metadata[:SERVER_ONLY] || metadata[:server_only]
|
|
67
|
+
return unless defined?(BetterAuth::OpenAPI)
|
|
68
|
+
|
|
69
|
+
metadata[:openapi] = BetterAuth::OpenAPI.default_metadata(path, methods)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def apply_open_api_schemas!
|
|
73
|
+
openapi = fetch_key(metadata, :openapi)
|
|
74
|
+
return unless openapi.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
@body_schema ||= schema_for_open_api_request_body(openapi)
|
|
77
|
+
@query_schema ||= schema_for_open_api_parameters(openapi, "query")
|
|
78
|
+
@headers_schema ||= schema_for_open_api_parameters(openapi, "header")
|
|
79
|
+
end
|
|
80
|
+
|
|
47
81
|
def apply_schemas!(context)
|
|
48
82
|
context.body = validate_schema(:body, body_schema, context.body)
|
|
49
83
|
context.query = validate_schema(:query, query_schema, context.query)
|
|
84
|
+
context.params = validate_schema(:params, params_schema, context.params)
|
|
50
85
|
context.headers = context.send(:normalize_headers, validate_schema(:headers, headers_schema, context.headers))
|
|
51
86
|
end
|
|
52
87
|
|
|
@@ -83,6 +118,54 @@ module BetterAuth
|
|
|
83
118
|
result
|
|
84
119
|
end
|
|
85
120
|
|
|
121
|
+
def schema_for_open_api_request_body(openapi)
|
|
122
|
+
schema = fetch_key(fetch_key(fetch_key(fetch_key(openapi, :requestBody), :content), "application/json"), :schema)
|
|
123
|
+
required = Array(fetch_key(schema, :required)).map(&:to_s)
|
|
124
|
+
return nil if required.empty?
|
|
125
|
+
|
|
126
|
+
->(value) { validate_required_open_api_fields(value, required) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def schema_for_open_api_parameters(openapi, location)
|
|
130
|
+
required = Array(fetch_key(openapi, :parameters))
|
|
131
|
+
.select { |parameter| parameter.is_a?(Hash) && fetch_key(parameter, :in).to_s == location && fetch_key(parameter, :required) == true }
|
|
132
|
+
.filter_map { |parameter| fetch_key(parameter, :name) }
|
|
133
|
+
.map(&:to_s)
|
|
134
|
+
return nil if required.empty?
|
|
135
|
+
|
|
136
|
+
->(value) { validate_required_open_api_fields(value, required) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def validate_required_open_api_fields(value, required)
|
|
140
|
+
data = normalize_open_api_input(value)
|
|
141
|
+
return false unless required.all? { |key| data.key?(open_api_storage_key(key)) && !data[open_api_storage_key(key)].nil? }
|
|
142
|
+
|
|
143
|
+
value
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def normalize_open_api_input(value)
|
|
147
|
+
return {} unless value.is_a?(Hash)
|
|
148
|
+
|
|
149
|
+
value.each_with_object({}) do |(key, object_value), result|
|
|
150
|
+
result[open_api_storage_key(key)] = object_value
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def open_api_storage_key(key)
|
|
155
|
+
key.to_s
|
|
156
|
+
.gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
|
|
157
|
+
.tr("-", "_")
|
|
158
|
+
.downcase
|
|
159
|
+
.split("_")
|
|
160
|
+
.then { |parts| ([parts.first] + parts.drop(1).map(&:capitalize)).join }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def fetch_key(hash, key)
|
|
164
|
+
return nil unless hash.respond_to?(:[])
|
|
165
|
+
|
|
166
|
+
hash[key] || hash[key.to_s]
|
|
167
|
+
end
|
|
168
|
+
|
|
86
169
|
class Result
|
|
87
170
|
attr_accessor :response, :status, :headers
|
|
88
171
|
|
|
@@ -97,7 +180,7 @@ module BetterAuth
|
|
|
97
180
|
return value if value.is_a?(self)
|
|
98
181
|
|
|
99
182
|
if value.is_a?(APIError)
|
|
100
|
-
return new(response: value, status: value.status_code, headers: value.headers)
|
|
183
|
+
return new(response: value, status: value.status_code, headers: merge_headers(context.response_headers, value.headers))
|
|
101
184
|
end
|
|
102
185
|
|
|
103
186
|
if rack_response?(value)
|
|
@@ -133,17 +216,21 @@ module BetterAuth
|
|
|
133
216
|
end
|
|
134
217
|
|
|
135
218
|
def to_rack_response
|
|
136
|
-
|
|
219
|
+
to_response.to_a
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def to_response
|
|
223
|
+
return Response.from_rack(@raw_response) if raw_response?
|
|
137
224
|
|
|
138
225
|
body = if response.nil?
|
|
139
|
-
[
|
|
226
|
+
[JSON.generate(nil)]
|
|
140
227
|
elsif response.is_a?(String)
|
|
141
228
|
[response]
|
|
142
229
|
else
|
|
143
230
|
[JSON.generate(response)]
|
|
144
231
|
end
|
|
145
232
|
response_headers = {"content-type" => "application/json"}.merge(headers)
|
|
146
|
-
|
|
233
|
+
Response.new(status: status, headers: response_headers, body: body)
|
|
147
234
|
end
|
|
148
235
|
|
|
149
236
|
private
|
data/lib/better_auth/error.rb
CHANGED
|
@@ -16,6 +16,7 @@ module BetterAuth
|
|
|
16
16
|
"SOCIAL_ACCOUNT_ALREADY_LINKED" => "Social account already linked",
|
|
17
17
|
"PROVIDER_NOT_FOUND" => "Provider not found",
|
|
18
18
|
"INVALID_TOKEN" => "Invalid token",
|
|
19
|
+
"TOKEN_EXPIRED" => "Token expired",
|
|
19
20
|
"ID_TOKEN_NOT_SUPPORTED" => "id_token not supported",
|
|
20
21
|
"FAILED_TO_GET_USER_INFO" => "Failed to get user info",
|
|
21
22
|
"USER_EMAIL_NOT_FOUND" => "User email not found",
|
|
@@ -47,6 +48,12 @@ module BetterAuth
|
|
|
47
48
|
"FIELD_NOT_ALLOWED" => "Field not allowed to be set",
|
|
48
49
|
"ASYNC_VALIDATION_NOT_SUPPORTED" => "Async validation is not supported",
|
|
49
50
|
"VALIDATION_ERROR" => "Validation Error",
|
|
50
|
-
"MISSING_FIELD" => "Field is required"
|
|
51
|
+
"MISSING_FIELD" => "Field is required",
|
|
52
|
+
"BODY_MUST_BE_AN_OBJECT" => "Body must be an object",
|
|
53
|
+
"METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED" => "POST method requires deferSessionRefresh to be enabled in session config",
|
|
54
|
+
"PASSWORD_ALREADY_SET" => "User already has a password set",
|
|
55
|
+
"RESET_PASSWORD_DISABLED" => "Reset password isn't enabled",
|
|
56
|
+
"EMAIL_PASSWORD_DISABLED" => "Email and password is not enabled",
|
|
57
|
+
"EMAIL_PASSWORD_SIGN_UP_DISABLED" => "Email and password sign up is not enabled"
|
|
51
58
|
}.freeze
|
|
52
59
|
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Host
|
|
7
|
+
CLOUD_METADATA_HOSTS = [
|
|
8
|
+
"metadata.google.internal",
|
|
9
|
+
"metadata.goog",
|
|
10
|
+
"metadata",
|
|
11
|
+
"instance-data",
|
|
12
|
+
"instance-data.ec2.internal"
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def classify_host(host)
|
|
18
|
+
canonical_input = normalize_input(host)
|
|
19
|
+
lowered = canonical_input.downcase
|
|
20
|
+
return {kind: :reserved, literal: :fqdn, canonical: ""} if lowered.empty?
|
|
21
|
+
|
|
22
|
+
address = parse_ip(lowered)
|
|
23
|
+
unless address
|
|
24
|
+
return {kind: :localhost, literal: :fqdn, canonical: lowered} if lowered == "localhost" || lowered.end_with?(".localhost")
|
|
25
|
+
return {kind: :cloud_metadata, literal: :fqdn, canonical: lowered} if CLOUD_METADATA_HOSTS.include?(lowered)
|
|
26
|
+
|
|
27
|
+
return {kind: :public, literal: :fqdn, canonical: lowered}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
native = address.respond_to?(:native) ? address.native : address
|
|
31
|
+
if native.ipv4?
|
|
32
|
+
canonical = native.to_s
|
|
33
|
+
return {kind: classify_ipv4(canonical), literal: :ipv4, canonical: canonical}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
canonical = expanded_ipv6(native)
|
|
37
|
+
{kind: classify_ipv6(canonical), literal: :ipv6, canonical: canonical}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def loopback_ip?(host)
|
|
41
|
+
classify_host(host)[:kind] == :loopback
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def loopback_host?(host)
|
|
45
|
+
[:loopback, :localhost].include?(classify_host(host)[:kind])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def public_routable_host?(host)
|
|
49
|
+
classify_host(host)[:kind] == :public
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalize_input(host)
|
|
53
|
+
value = host.to_s.strip
|
|
54
|
+
value = strip_port(value)
|
|
55
|
+
value = value[1...-1] if value.start_with?("[") && value.end_with?("]")
|
|
56
|
+
value = value.split("%", 2).first || ""
|
|
57
|
+
value.gsub(/\.+\z/, "")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def strip_port(host)
|
|
61
|
+
if host.start_with?("[")
|
|
62
|
+
closing = host.index("]")
|
|
63
|
+
return host unless closing
|
|
64
|
+
|
|
65
|
+
return host[0..closing] if host[(closing + 1)..]&.match?(/\A:\d+\z/)
|
|
66
|
+
return host
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
first_colon = host.index(":")
|
|
70
|
+
return host unless first_colon
|
|
71
|
+
return host if host.index(":", first_colon + 1)
|
|
72
|
+
|
|
73
|
+
host[0...first_colon]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_ip(host)
|
|
77
|
+
IPAddr.new(host)
|
|
78
|
+
rescue ArgumentError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def classify_ipv4(ip)
|
|
83
|
+
return :unspecified if ip == "0.0.0.0"
|
|
84
|
+
return :broadcast if ip == "255.255.255.255"
|
|
85
|
+
|
|
86
|
+
value = ipv4_to_i(ip)
|
|
87
|
+
return :loopback if ipv4_range?(value, "127.0.0.0", 8)
|
|
88
|
+
return :private if ipv4_range?(value, "10.0.0.0", 8)
|
|
89
|
+
return :private if ipv4_range?(value, "172.16.0.0", 12)
|
|
90
|
+
return :private if ipv4_range?(value, "192.168.0.0", 16)
|
|
91
|
+
return :link_local if ipv4_range?(value, "169.254.0.0", 16)
|
|
92
|
+
return :shared_address_space if ipv4_range?(value, "100.64.0.0", 10)
|
|
93
|
+
return :documentation if ipv4_range?(value, "192.0.2.0", 24)
|
|
94
|
+
return :documentation if ipv4_range?(value, "198.51.100.0", 24)
|
|
95
|
+
return :documentation if ipv4_range?(value, "203.0.113.0", 24)
|
|
96
|
+
return :benchmarking if ipv4_range?(value, "198.18.0.0", 15)
|
|
97
|
+
return :multicast if ipv4_range?(value, "224.0.0.0", 4)
|
|
98
|
+
return :reserved if ipv4_range?(value, "0.0.0.0", 8)
|
|
99
|
+
return :reserved if ipv4_range?(value, "192.0.0.0", 24)
|
|
100
|
+
return :reserved if ipv4_range?(value, "240.0.0.0", 4)
|
|
101
|
+
|
|
102
|
+
:public
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ipv4_to_i(ip)
|
|
106
|
+
ip.split(".").map(&:to_i).reduce(0) { |sum, part| (sum << 8) + part }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def ipv4_range?(value, prefix, length)
|
|
110
|
+
mask = (length == 32) ? 0xffffffff : ((0xffffffff << (32 - length)) & 0xffffffff)
|
|
111
|
+
(value & mask) == (ipv4_to_i(prefix) & mask)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def classify_ipv6(expanded)
|
|
115
|
+
return :unspecified if expanded == "0000:0000:0000:0000:0000:0000:0000:0000"
|
|
116
|
+
return :loopback if expanded == "0000:0000:0000:0000:0000:0000:0000:0001"
|
|
117
|
+
|
|
118
|
+
first_byte = expanded[0, 2].to_i(16)
|
|
119
|
+
second_byte = expanded[2, 2].to_i(16)
|
|
120
|
+
|
|
121
|
+
return :multicast if first_byte == 0xff
|
|
122
|
+
return :link_local if first_byte == 0xfe && (second_byte & 0xc0) == 0x80
|
|
123
|
+
return :private if (first_byte & 0xfe) == 0xfc
|
|
124
|
+
return :documentation if expanded.start_with?("2001:0db8:")
|
|
125
|
+
|
|
126
|
+
if expanded.start_with?("2002:")
|
|
127
|
+
embedded = embedded_ipv4(expanded, 1)
|
|
128
|
+
return (classify_ipv4(embedded) == :public) ? :public : :reserved if embedded
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if expanded.start_with?("0064:ff9b:0000:0000:0000:0000:")
|
|
132
|
+
embedded = embedded_ipv4(expanded, 6)
|
|
133
|
+
return :reserved if embedded
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if expanded.start_with?("2001:0000:")
|
|
137
|
+
embedded = embedded_ipv4(expanded, 6, xor: true)
|
|
138
|
+
return :reserved if embedded
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
return :reserved if expanded.start_with?("0100:0000:0000:0000:")
|
|
142
|
+
|
|
143
|
+
:public
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def embedded_ipv4(expanded, start_group, xor: false)
|
|
147
|
+
groups = expanded.split(":")
|
|
148
|
+
combined = (groups.fetch(start_group).to_i(16) << 16) | groups.fetch(start_group + 1).to_i(16)
|
|
149
|
+
combined ^= 0xffffffff if xor
|
|
150
|
+
[
|
|
151
|
+
(combined >> 24) & 0xff,
|
|
152
|
+
(combined >> 16) & 0xff,
|
|
153
|
+
(combined >> 8) & 0xff,
|
|
154
|
+
combined & 0xff
|
|
155
|
+
].join(".")
|
|
156
|
+
rescue IndexError
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def expanded_ipv6(address)
|
|
161
|
+
address.hton.bytes.each_slice(2).map do |high, low|
|
|
162
|
+
((high << 8) + low).to_s(16).rjust(4, "0")
|
|
163
|
+
end.join(":")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module SpanStatusCode
|
|
6
|
+
UNSET = 0
|
|
7
|
+
OK = 1
|
|
8
|
+
ERROR = 2
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class NoopSpan
|
|
12
|
+
def set_attribute(_key, _value)
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def set_attributes(_attributes)
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def record_exception(_error)
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def set_status(_status)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add_event(_name, _attributes = nil)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def end
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class NoopTracer
|
|
38
|
+
def start_active_span(_name, attributes: {}, &block)
|
|
39
|
+
span = NoopSpan.new
|
|
40
|
+
return span unless block
|
|
41
|
+
|
|
42
|
+
block.call(span)
|
|
43
|
+
ensure
|
|
44
|
+
span&.end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Trace
|
|
49
|
+
def get_tracer(_name = "better-auth")
|
|
50
|
+
NoopTracer.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_active_span
|
|
54
|
+
NoopSpan.new
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
module_function
|
|
59
|
+
|
|
60
|
+
def trace
|
|
61
|
+
@trace ||= Trace.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def with_span(name, attributes: {}, &block)
|
|
65
|
+
trace.get_tracer("better-auth").start_active_span(name, attributes: attributes) do |span|
|
|
66
|
+
block.call(span)
|
|
67
|
+
rescue => error
|
|
68
|
+
span.record_exception(error)
|
|
69
|
+
span.set_status(SpanStatusCode::ERROR)
|
|
70
|
+
raise
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Logger
|
|
5
|
+
LEVELS = [:debug, :info, :success, :warn, :error].freeze
|
|
6
|
+
|
|
7
|
+
Internal = Struct.new(:level, :disabled, :handler, keyword_init: true) do
|
|
8
|
+
LEVELS.each do |log_level|
|
|
9
|
+
define_method(log_level) do |message, *args|
|
|
10
|
+
return if disabled || !Logger.should_publish?(level, log_level)
|
|
11
|
+
|
|
12
|
+
if handler
|
|
13
|
+
handler.call((log_level == :success) ? :info : log_level, message, *args)
|
|
14
|
+
else
|
|
15
|
+
Kernel.warn("#{log_level.upcase} [Better Auth]: #{message}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def should_publish?(current_log_level, log_level)
|
|
24
|
+
LEVELS.index(log_level.to_sym).to_i >= LEVELS.index(current_log_level.to_sym).to_i
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create(level: :warn, disabled: false, log: nil, **)
|
|
28
|
+
Internal.new(level: level.to_sym, disabled: disabled, handler: log)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -14,7 +14,7 @@ module BetterAuth
|
|
|
14
14
|
|
|
15
15
|
validate_origin(endpoint_context)
|
|
16
16
|
validate_fetch_metadata(endpoint_context)
|
|
17
|
-
return if skip_origin_check?(endpoint_context)
|
|
17
|
+
return if skip_origin_check?(endpoint_context) || skip_origin_path?(endpoint_context)
|
|
18
18
|
|
|
19
19
|
validate_callback_urls(endpoint_context)
|
|
20
20
|
nil
|
|
@@ -87,7 +87,7 @@ module BetterAuth
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def skip_origin_check?(endpoint_context)
|
|
90
|
-
|
|
90
|
+
endpoint_context.context.options.advanced[:disable_origin_check] == true
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def skip_csrf_for_backward_compat?(endpoint_context)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "jwt"
|
|
7
|
+
|
|
8
|
+
module BetterAuth
|
|
9
|
+
module OAuth2
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def validate_token(token, jwks:, audience: nil, issuer: nil)
|
|
13
|
+
header = JWT.decode(token, nil, false).last
|
|
14
|
+
kid = header["kid"]
|
|
15
|
+
raise APIError.new("UNAUTHORIZED", message: "Missing jwt kid") if kid.to_s.empty?
|
|
16
|
+
|
|
17
|
+
key_data = Array(jwks["keys"] || jwks[:keys]).find { |key| (key["kid"] || key[:kid]).to_s == kid.to_s }
|
|
18
|
+
raise APIError.new("UNAUTHORIZED", message: "kid doesn't match any key") unless key_data
|
|
19
|
+
|
|
20
|
+
public_key = JWT::JWK.import(stringify_keys(key_data)).public_key
|
|
21
|
+
algorithm = header["alg"] || key_data["alg"] || key_data[:alg]
|
|
22
|
+
options = {algorithm: algorithm}
|
|
23
|
+
options[:aud] = audience if audience
|
|
24
|
+
options[:verify_aud] = true if audience
|
|
25
|
+
options[:iss] = issuer if issuer
|
|
26
|
+
options[:verify_iss] = true if issuer
|
|
27
|
+
JWT.decode(token, public_key, true, **options).first
|
|
28
|
+
rescue JWT::DecodeError => error
|
|
29
|
+
raise APIError.new("UNAUTHORIZED", message: error.message)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def refresh_access_token(refresh_token:, token_endpoint:, options:, authentication: nil, extra_params: nil, resource: nil, fetcher: nil)
|
|
33
|
+
request = create_refresh_access_token_request(
|
|
34
|
+
refresh_token: refresh_token,
|
|
35
|
+
options: options,
|
|
36
|
+
authentication: authentication,
|
|
37
|
+
extra_params: extra_params,
|
|
38
|
+
resource: resource
|
|
39
|
+
)
|
|
40
|
+
data = fetcher ? fetcher.call(token_endpoint, request) : post_form(token_endpoint, request)
|
|
41
|
+
now = Time.now
|
|
42
|
+
tokens = {
|
|
43
|
+
access_token: data["access_token"] || data[:access_token],
|
|
44
|
+
refresh_token: data["refresh_token"] || data[:refresh_token],
|
|
45
|
+
token_type: data["token_type"] || data[:token_type],
|
|
46
|
+
scopes: (data["scope"] || data[:scope])&.split(" "),
|
|
47
|
+
id_token: data["id_token"] || data[:id_token]
|
|
48
|
+
}.compact
|
|
49
|
+
|
|
50
|
+
expires_in = data["expires_in"] || data[:expires_in]
|
|
51
|
+
tokens[:access_token_expires_at] = now + expires_in.to_i if expires_in
|
|
52
|
+
|
|
53
|
+
refresh_expires_in = data["refresh_token_expires_in"] || data[:refresh_token_expires_in]
|
|
54
|
+
tokens[:refresh_token_expires_at] = now + refresh_expires_in.to_i if refresh_expires_in
|
|
55
|
+
tokens
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def create_refresh_access_token_request(refresh_token:, options:, authentication: nil, extra_params: nil, resource: nil)
|
|
59
|
+
body = {
|
|
60
|
+
"grant_type" => "refresh_token",
|
|
61
|
+
"refresh_token" => refresh_token
|
|
62
|
+
}
|
|
63
|
+
headers = {
|
|
64
|
+
"content-type" => "application/x-www-form-urlencoded",
|
|
65
|
+
"accept" => "application/json"
|
|
66
|
+
}
|
|
67
|
+
client_id = Array(options[:client_id] || options["client_id"] || options[:clientId] || options["clientId"]).first
|
|
68
|
+
client_secret = options[:client_secret] || options["client_secret"] || options[:clientSecret] || options["clientSecret"]
|
|
69
|
+
|
|
70
|
+
if authentication.to_s == "basic"
|
|
71
|
+
headers["authorization"] = "Basic #{Base64.strict_encode64("#{client_id}:#{client_secret}")}"
|
|
72
|
+
else
|
|
73
|
+
body["client_id"] = client_id if client_id
|
|
74
|
+
body["client_secret"] = client_secret if client_secret
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Array(resource).each { |entry| (body["resource"] ||= []) << entry } if resource
|
|
78
|
+
extra_params&.each { |key, value| body[key.to_s] = value }
|
|
79
|
+
{body: body, headers: headers}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def post_form(token_endpoint, request)
|
|
83
|
+
uri = URI.parse(token_endpoint)
|
|
84
|
+
response = Net::HTTP.post(uri, URI.encode_www_form(request[:body]), request[:headers])
|
|
85
|
+
JSON.parse(response.body)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def stringify_keys(hash)
|
|
89
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
90
|
+
result[key.to_s] = value.is_a?(Hash) ? stringify_keys(value) : value
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -11,8 +11,8 @@ module BetterAuth
|
|
|
11
11
|
fields: {
|
|
12
12
|
role: {type: "string", required: false, input: false},
|
|
13
13
|
banned: {type: "boolean", required: false, input: false, default_value: false},
|
|
14
|
-
banReason: {type: "string", required: false, input: false},
|
|
15
|
-
banExpires: {type: "date", required: false, input: false}
|
|
14
|
+
banReason: {type: "string", required: false, input: false, default_value: nil},
|
|
15
|
+
banExpires: {type: "date", required: false, input: false, default_value: nil}
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
session: {
|