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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +24 -0
  4. data/lib/better_auth/adapters/internal_adapter.rb +10 -7
  5. data/lib/better_auth/adapters/memory.rb +57 -11
  6. data/lib/better_auth/adapters/sql.rb +123 -20
  7. data/lib/better_auth/api.rb +114 -9
  8. data/lib/better_auth/async.rb +70 -0
  9. data/lib/better_auth/configuration.rb +97 -7
  10. data/lib/better_auth/context.rb +165 -12
  11. data/lib/better_auth/cookies.rb +6 -4
  12. data/lib/better_auth/core.rb +2 -0
  13. data/lib/better_auth/crypto/jwe.rb +27 -5
  14. data/lib/better_auth/crypto.rb +32 -0
  15. data/lib/better_auth/database_hooks.rb +8 -8
  16. data/lib/better_auth/deprecate.rb +28 -0
  17. data/lib/better_auth/endpoint.rb +92 -5
  18. data/lib/better_auth/error.rb +8 -1
  19. data/lib/better_auth/host.rb +166 -0
  20. data/lib/better_auth/instrumentation.rb +74 -0
  21. data/lib/better_auth/logger.rb +31 -0
  22. data/lib/better_auth/middleware/origin_check.rb +2 -2
  23. data/lib/better_auth/oauth2.rb +94 -0
  24. data/lib/better_auth/plugins/admin/schema.rb +2 -2
  25. data/lib/better_auth/plugins/admin.rb +344 -16
  26. data/lib/better_auth/plugins/anonymous.rb +37 -3
  27. data/lib/better_auth/plugins/device_authorization.rb +102 -5
  28. data/lib/better_auth/plugins/dub.rb +148 -0
  29. data/lib/better_auth/plugins/email_otp.rb +261 -19
  30. data/lib/better_auth/plugins/expo.rb +17 -1
  31. data/lib/better_auth/plugins/generic_oauth.rb +67 -35
  32. data/lib/better_auth/plugins/jwt.rb +37 -4
  33. data/lib/better_auth/plugins/last_login_method.rb +2 -2
  34. data/lib/better_auth/plugins/magic_link.rb +66 -3
  35. data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
  36. data/lib/better_auth/plugins/mcp/config.rb +51 -0
  37. data/lib/better_auth/plugins/mcp/consent.rb +31 -0
  38. data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
  39. data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
  40. data/lib/better_auth/plugins/mcp/registration.rb +31 -0
  41. data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
  42. data/lib/better_auth/plugins/mcp/schema.rb +91 -0
  43. data/lib/better_auth/plugins/mcp/token.rb +108 -0
  44. data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
  45. data/lib/better_auth/plugins/mcp.rb +111 -263
  46. data/lib/better_auth/plugins/multi_session.rb +61 -3
  47. data/lib/better_auth/plugins/oauth_protocol.rb +173 -30
  48. data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
  49. data/lib/better_auth/plugins/oidc_provider.rb +118 -14
  50. data/lib/better_auth/plugins/one_tap.rb +7 -2
  51. data/lib/better_auth/plugins/one_time_token.rb +42 -2
  52. data/lib/better_auth/plugins/open_api.rb +163 -318
  53. data/lib/better_auth/plugins/organization/schema.rb +6 -0
  54. data/lib/better_auth/plugins/organization.rb +186 -56
  55. data/lib/better_auth/plugins/phone_number.rb +141 -6
  56. data/lib/better_auth/plugins/siwe.rb +69 -3
  57. data/lib/better_auth/plugins/two_factor.rb +118 -41
  58. data/lib/better_auth/plugins/username.rb +57 -2
  59. data/lib/better_auth/rate_limiter.rb +38 -0
  60. data/lib/better_auth/request_state.rb +44 -0
  61. data/lib/better_auth/response.rb +42 -0
  62. data/lib/better_auth/router.rb +7 -1
  63. data/lib/better_auth/routes/account.rb +220 -42
  64. data/lib/better_auth/routes/email_verification.rb +98 -14
  65. data/lib/better_auth/routes/password.rb +126 -8
  66. data/lib/better_auth/routes/session.rb +128 -13
  67. data/lib/better_auth/routes/sign_in.rb +26 -2
  68. data/lib/better_auth/routes/sign_out.rb +13 -1
  69. data/lib/better_auth/routes/sign_up.rb +70 -4
  70. data/lib/better_auth/routes/social.rb +132 -7
  71. data/lib/better_auth/routes/user.rb +228 -20
  72. data/lib/better_auth/routes/validation.rb +50 -0
  73. data/lib/better_auth/secret_config.rb +115 -0
  74. data/lib/better_auth/session.rb +13 -2
  75. data/lib/better_auth/url_helpers.rb +206 -0
  76. data/lib/better_auth/version.rb +1 -1
  77. data/lib/better_auth.rb +12 -0
  78. metadata +23 -1
@@ -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
- return @raw_response if raw_response?
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
- [status, response_headers, body]
233
+ Response.new(status: status, headers: response_headers, body: body)
147
234
  end
148
235
 
149
236
  private
@@ -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
- !!endpoint_context.context.options.advanced[:disable_origin_check]
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: {