better_auth 0.10.0 → 0.11.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 +83 -0
- data/LICENSE.md +1 -1
- data/README.md +138 -91
- data/lib/better_auth/adapters/base.rb +64 -1
- data/lib/better_auth/adapters/internal_adapter.rb +197 -11
- data/lib/better_auth/adapters/join_support.rb +10 -0
- data/lib/better_auth/adapters/memory.rb +191 -56
- data/lib/better_auth/adapters/mssql.rb +7 -3
- data/lib/better_auth/adapters/sql.rb +294 -49
- data/lib/better_auth/api.rb +3 -24
- data/lib/better_auth/configuration.rb +41 -46
- data/lib/better_auth/context.rb +43 -67
- data/lib/better_auth/cookies.rb +110 -14
- data/lib/better_auth/core.rb +0 -2
- data/lib/better_auth/crypto.rb +4 -3
- data/lib/better_auth/doctor.rb +14 -3
- data/lib/better_auth/error.rb +3 -1
- data/lib/better_auth/logger.rb +1 -1
- data/lib/better_auth/middleware/origin_check.rb +9 -1
- data/lib/better_auth/migration_plan.rb +4 -4
- data/lib/better_auth/oauth_state.rb +147 -0
- data/lib/better_auth/plugin_loader.rb +220 -0
- data/lib/better_auth/plugins/access.rb +10 -13
- data/lib/better_auth/plugins/admin.rb +79 -28
- data/lib/better_auth/plugins/api_key.rb +4 -7
- data/lib/better_auth/plugins/bearer.rb +8 -22
- data/lib/better_auth/plugins/captcha.rb +24 -6
- data/lib/better_auth/plugins/device_authorization.rb +47 -9
- data/lib/better_auth/plugins/dub.rb +2 -2
- data/lib/better_auth/plugins/email_otp.rb +30 -47
- data/lib/better_auth/plugins/generic_oauth.rb +141 -146
- data/lib/better_auth/plugins/i18n.rb +215 -0
- data/lib/better_auth/plugins/jwt.rb +37 -19
- data/lib/better_auth/plugins/magic_link.rb +18 -21
- data/lib/better_auth/plugins/oauth_popup.rb +330 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +142 -32
- data/lib/better_auth/plugins/oauth_provider.rb +4 -7
- data/lib/better_auth/plugins/oauth_proxy.rb +1 -1
- data/lib/better_auth/plugins/one_tap.rb +61 -60
- data/lib/better_auth/plugins/one_time_token.rb +1 -4
- data/lib/better_auth/plugins/open_api.rb +35 -7
- data/lib/better_auth/plugins/organization.rb +455 -122
- data/lib/better_auth/plugins/passkey.rb +4 -7
- data/lib/better_auth/plugins/phone_number.rb +24 -26
- data/lib/better_auth/plugins/scim.rb +4 -7
- data/lib/better_auth/plugins/siwe.rb +126 -16
- data/lib/better_auth/plugins/sso.rb +4 -7
- data/lib/better_auth/plugins/stripe.rb +4 -7
- data/lib/better_auth/plugins/two_factor.rb +186 -21
- data/lib/better_auth/plugins/username.rb +56 -13
- data/lib/better_auth/plugins.rb +57 -0
- data/lib/better_auth/rate_limiter.rb +297 -135
- data/lib/better_auth/request_ip.rb +123 -13
- data/lib/better_auth/router.rb +3 -3
- data/lib/better_auth/routes/account.rb +40 -8
- data/lib/better_auth/routes/email_verification.rb +88 -5
- data/lib/better_auth/routes/password.rb +5 -6
- data/lib/better_auth/routes/session.rb +7 -2
- data/lib/better_auth/routes/sign_in.rb +10 -1
- data/lib/better_auth/routes/sign_up.rb +45 -22
- data/lib/better_auth/routes/social.rb +169 -54
- data/lib/better_auth/routes/user.rb +11 -8
- data/lib/better_auth/schema/sql.rb +90 -24
- data/lib/better_auth/schema.rb +124 -3
- data/lib/better_auth/session.rb +27 -9
- data/lib/better_auth/session_store.rb +1 -1
- data/lib/better_auth/social_providers/apple.rb +6 -1
- data/lib/better_auth/social_providers/atlassian.rb +1 -0
- data/lib/better_auth/social_providers/base.rb +41 -14
- data/lib/better_auth/social_providers/cognito.rb +75 -5
- data/lib/better_auth/social_providers/facebook.rb +95 -1
- data/lib/better_auth/social_providers/figma.rb +2 -0
- data/lib/better_auth/social_providers/google.rb +12 -1
- data/lib/better_auth/social_providers/line.rb +35 -1
- data/lib/better_auth/social_providers/linear.rb +1 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +9 -0
- data/lib/better_auth/social_providers/naver.rb +24 -1
- data/lib/better_auth/social_providers/notion.rb +1 -0
- data/lib/better_auth/social_providers/paybin.rb +25 -1
- data/lib/better_auth/social_providers/paypal.rb +118 -2
- data/lib/better_auth/social_providers/reddit.rb +25 -4
- data/lib/better_auth/social_providers/salesforce.rb +1 -0
- data/lib/better_auth/social_providers/twitch.rb +24 -1
- data/lib/better_auth/social_providers/twitter.rb +36 -1
- data/lib/better_auth/social_providers/vk.rb +34 -1
- data/lib/better_auth/social_providers/wechat.rb +6 -4
- data/lib/better_auth/sql_migration.rb +5 -5
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +11 -41
- metadata +5 -13
- data/lib/better_auth/plugins/mcp/authorization.rb +0 -111
- data/lib/better_auth/plugins/mcp/config.rb +0 -51
- data/lib/better_auth/plugins/mcp/consent.rb +0 -31
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +0 -43
- data/lib/better_auth/plugins/mcp/metadata.rb +0 -81
- data/lib/better_auth/plugins/mcp/registration.rb +0 -31
- data/lib/better_auth/plugins/mcp/resource_handler.rb +0 -37
- data/lib/better_auth/plugins/mcp/schema.rb +0 -91
- data/lib/better_auth/plugins/mcp/token.rb +0 -108
- data/lib/better_auth/plugins/mcp/userinfo.rb +0 -37
- data/lib/better_auth/plugins/mcp.rb +0 -256
- data/lib/better_auth/plugins/oidc_provider.rb +0 -769
data/lib/better_auth/schema.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
3
5
|
module BetterAuth
|
|
4
6
|
module Schema
|
|
5
7
|
module_function
|
|
@@ -22,6 +24,40 @@ module BetterAuth
|
|
|
22
24
|
tables.sort_by { |_name, table| table[:order] || Float::INFINITY }.to_h
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
def migration_tables(options)
|
|
28
|
+
tables = auth_tables(options)
|
|
29
|
+
grouped = {}
|
|
30
|
+
|
|
31
|
+
tables.each_with_index do |(logical_name, table), index|
|
|
32
|
+
table_name = table.fetch(:model_name)
|
|
33
|
+
target = grouped[table_name] ||= {
|
|
34
|
+
model_name: table_name,
|
|
35
|
+
fields: {},
|
|
36
|
+
order: table[:order] || Float::INFINITY,
|
|
37
|
+
source_order: index,
|
|
38
|
+
logical_names: [],
|
|
39
|
+
disable_migration: false
|
|
40
|
+
}
|
|
41
|
+
target[:order] = [target[:order], table[:order] || Float::INFINITY].min
|
|
42
|
+
target[:source_order] = [target[:source_order], index].min
|
|
43
|
+
target[:logical_names] << logical_name
|
|
44
|
+
target[:disable_migration] ||= table[:disable_migration] == true
|
|
45
|
+
|
|
46
|
+
table.fetch(:fields).each do |logical_field, attributes|
|
|
47
|
+
column = attributes[:field_name] || physical_name(logical_field)
|
|
48
|
+
physical_attributes = physical_field_attributes(logical_field, attributes, tables)
|
|
49
|
+
merge_physical_field!(target, column, physical_attributes)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
grouped
|
|
54
|
+
.reject { |_name, table| table[:disable_migration] }
|
|
55
|
+
.sort_by { |_name, table| [table[:order], table[:source_order]] }
|
|
56
|
+
.to_h do |name, table|
|
|
57
|
+
[name, table.except(:source_order, :disable_migration)]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
25
61
|
def storage_model_name(options, model)
|
|
26
62
|
table = auth_tables(options).fetch(model.to_s)
|
|
27
63
|
table[:model_name]
|
|
@@ -49,10 +85,48 @@ module BetterAuth
|
|
|
49
85
|
data
|
|
50
86
|
end
|
|
51
87
|
|
|
88
|
+
def parse_provider_profile_user_input(options, profile, action:)
|
|
89
|
+
action = action.to_sym
|
|
90
|
+
raise ArgumentError, "action must be :create or :update" unless [:create, :update].include?(action)
|
|
91
|
+
|
|
92
|
+
input = (profile || {}).each_with_object({}) do |(key, value), result|
|
|
93
|
+
result[storage_key(key)] = value
|
|
94
|
+
end
|
|
95
|
+
core_fields = %w[id email emailVerified name image createdAt updatedAt]
|
|
96
|
+
fields = auth_tables(options).fetch("user").fetch(:fields).except(*core_fields)
|
|
97
|
+
|
|
98
|
+
fields.each_with_object({}) do |(field, attributes), result|
|
|
99
|
+
provider_value_allowed = attributes[:input] != false && input.key?(field)
|
|
100
|
+
if provider_value_allowed
|
|
101
|
+
value = coerce_provider_profile_value(input[field], attributes)
|
|
102
|
+
transform = attributes[:transform]
|
|
103
|
+
value = transform.call(value) if transform.respond_to?(:call)
|
|
104
|
+
validator = attributes[:validator]
|
|
105
|
+
if validator.respond_to?(:call) && !validator.call(value)
|
|
106
|
+
raise APIError.new("BAD_REQUEST", message: "#{field} is invalid")
|
|
107
|
+
end
|
|
108
|
+
result[field] = value
|
|
109
|
+
elsif action == :create && attributes.key?(:default_value)
|
|
110
|
+
default = attributes[:default_value]
|
|
111
|
+
result[field] = default.respond_to?(:call) ? default.call : default
|
|
112
|
+
elsif action == :create && attributes[:required]
|
|
113
|
+
raise APIError.new("BAD_REQUEST", message: "#{field} is required")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private_class_method def self.coerce_provider_profile_value(value, attributes)
|
|
119
|
+
return value if value.nil?
|
|
120
|
+
return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
|
|
121
|
+
|
|
122
|
+
value
|
|
123
|
+
end
|
|
124
|
+
|
|
52
125
|
private_class_method def self.user_table(options, plugin_table)
|
|
53
126
|
table(
|
|
54
127
|
model_name: model_option(options, :user, :model_name) || "users",
|
|
55
128
|
order: 1,
|
|
129
|
+
disable_migration: plugin_table&.fetch(:disable_migration, false),
|
|
56
130
|
fields: id_field.merge(
|
|
57
131
|
"name" => field("string", required: true, sortable: true, field_name: mapped_field(options, :user, "name")),
|
|
58
132
|
"email" => field("string", required: true, unique: true, sortable: true, field_name: mapped_field(options, :user, "email")),
|
|
@@ -67,6 +141,7 @@ module BetterAuth
|
|
|
67
141
|
table(
|
|
68
142
|
model_name: model_option(options, :session, :model_name) || "sessions",
|
|
69
143
|
order: 2,
|
|
144
|
+
disable_migration: plugin_table&.fetch(:disable_migration, false),
|
|
70
145
|
fields: base_fields.merge(
|
|
71
146
|
"expiresAt" => field("date", required: true, field_name: mapped_field(options, :session, "expiresAt")),
|
|
72
147
|
"token" => field("string", required: true, unique: true, field_name: mapped_field(options, :session, "token")),
|
|
@@ -82,6 +157,7 @@ module BetterAuth
|
|
|
82
157
|
table(
|
|
83
158
|
model_name: model_option(options, :account, :model_name) || "accounts",
|
|
84
159
|
order: 3,
|
|
160
|
+
disable_migration: plugin_table&.fetch(:disable_migration, false),
|
|
85
161
|
fields: base_fields.merge(
|
|
86
162
|
"accountId" => field("string", required: true, field_name: mapped_field(options, :account, "accountId")),
|
|
87
163
|
"providerId" => field("string", required: true, field_name: mapped_field(options, :account, "providerId")),
|
|
@@ -102,6 +178,7 @@ module BetterAuth
|
|
|
102
178
|
table(
|
|
103
179
|
model_name: model_option(options, :verification, :model_name) || "verifications",
|
|
104
180
|
order: 4,
|
|
181
|
+
disable_migration: plugin_table&.fetch(:disable_migration, false),
|
|
105
182
|
fields: base_fields.merge(
|
|
106
183
|
"identifier" => field("string", required: true, index: true, field_name: mapped_field(options, :verification, "identifier")),
|
|
107
184
|
"value" => field("string", required: true, field_name: mapped_field(options, :verification, "value")),
|
|
@@ -131,6 +208,49 @@ module BetterAuth
|
|
|
131
208
|
end
|
|
132
209
|
end
|
|
133
210
|
|
|
211
|
+
private_class_method def self.physical_field_attributes(logical_field, attributes, tables)
|
|
212
|
+
column = attributes[:field_name] || physical_name(logical_field)
|
|
213
|
+
physical_attributes = attributes.merge(field_name: column, logical_name: logical_field.to_s)
|
|
214
|
+
reference = attributes[:references]
|
|
215
|
+
return physical_attributes unless reference
|
|
216
|
+
|
|
217
|
+
physical_attributes.merge(references: physical_reference(reference, tables))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private_class_method def self.physical_reference(reference, tables)
|
|
221
|
+
model = reference.fetch(:model).to_s
|
|
222
|
+
target_table = tables.fetch(model, nil) || tables.each_value.find { |table| table.fetch(:model_name).to_s == model }
|
|
223
|
+
return reference unless target_table
|
|
224
|
+
|
|
225
|
+
field = reference.fetch(:field).to_s
|
|
226
|
+
target_field = target_table.fetch(:fields).fetch(field, nil)
|
|
227
|
+
physical_field = if target_field
|
|
228
|
+
target_field[:field_name] || physical_name(field)
|
|
229
|
+
elsif target_table.fetch(:fields).each_value.any? { |attributes| attributes[:field_name].to_s == field }
|
|
230
|
+
field
|
|
231
|
+
else
|
|
232
|
+
physical_name(field)
|
|
233
|
+
end
|
|
234
|
+
reference.merge(model: target_table.fetch(:model_name), field: physical_field)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
private_class_method def self.merge_physical_field!(table, column, attributes)
|
|
238
|
+
existing = table.fetch(:fields)[column]
|
|
239
|
+
unless existing
|
|
240
|
+
table.fetch(:fields)[column] = attributes
|
|
241
|
+
return
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
return if compatible_physical_field?(existing, attributes)
|
|
245
|
+
|
|
246
|
+
raise BetterAuth::Error, "Conflicting schema metadata for #{table.fetch(:model_name)}.#{column}"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private_class_method def self.compatible_physical_field?(left, right)
|
|
250
|
+
keys = [:type, :required, :references, :index, :unique, :sortable, :bigint]
|
|
251
|
+
keys.all? { |key| left[key] == right[key] }
|
|
252
|
+
end
|
|
253
|
+
|
|
134
254
|
private_class_method def self.base_fields
|
|
135
255
|
id_field.merge(timestamp_fields)
|
|
136
256
|
end
|
|
@@ -148,12 +268,12 @@ module BetterAuth
|
|
|
148
268
|
}
|
|
149
269
|
end
|
|
150
270
|
|
|
151
|
-
private_class_method def self.table(model_name:, fields:, extra_fields:, order:)
|
|
271
|
+
private_class_method def self.table(model_name:, fields:, extra_fields:, order:, disable_migration: false)
|
|
152
272
|
{
|
|
153
273
|
model_name: model_name,
|
|
154
274
|
fields: merge_fields(fields, *extra_fields),
|
|
155
275
|
order: order
|
|
156
|
-
}
|
|
276
|
+
}.tap { |data| data[:disable_migration] = true if disable_migration == true }
|
|
157
277
|
end
|
|
158
278
|
|
|
159
279
|
private_class_method def self.merge_fields(base, *extras)
|
|
@@ -175,6 +295,7 @@ module BetterAuth
|
|
|
175
295
|
existing = tables[key] || {model_name: table_data[:model_name] || physical_table_name(key), fields: {}}
|
|
176
296
|
existing[:model_name] = table_data[:model_name] || existing[:model_name] || physical_table_name(key)
|
|
177
297
|
existing[:fields] = existing[:fields].merge(normalize_fields(table_data[:fields] || {}))
|
|
298
|
+
existing[:disable_migration] = true if table_data[:disable_migration] == true
|
|
178
299
|
existing[:fields] = id_field.merge(existing[:fields]) unless core_table?(key) || existing[:fields].key?("id")
|
|
179
300
|
tables[key] = existing
|
|
180
301
|
end
|
|
@@ -319,6 +440,6 @@ module BetterAuth
|
|
|
319
440
|
(Time.now.to_f * 1000).to_i
|
|
320
441
|
end
|
|
321
442
|
|
|
322
|
-
public_class_method :storage_key
|
|
443
|
+
public_class_method :physical_name, :storage_key
|
|
323
444
|
end
|
|
324
445
|
end
|
data/lib/better_auth/session.rb
CHANGED
|
@@ -7,15 +7,21 @@ module BetterAuth
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
9
|
def find_current(ctx, disable_cookie_cache: false, disable_refresh: false, sensitive: false)
|
|
10
|
-
|
|
10
|
+
authoritative = sensitive && stateful?(ctx)
|
|
11
|
+
if ctx.context.current_session && !authoritative
|
|
11
12
|
return ctx.context.current_session
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
token_cookie = ctx.context.auth_cookies[:session_token]
|
|
15
16
|
token = ctx.get_signed_cookie(token_cookie.name, ctx.context.secret)
|
|
16
|
-
|
|
17
|
+
unless token
|
|
18
|
+
present = ctx.get_cookie(token_cookie.name)
|
|
19
|
+
return missing_session(ctx) if present && !present.empty?
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
return nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
cached = cached_session(ctx, token, disable_cookie_cache: disable_cookie_cache, sensitive: authoritative)
|
|
19
25
|
if cached
|
|
20
26
|
ctx.context.set_current_session(cached) if ctx.context.respond_to?(:set_current_session)
|
|
21
27
|
return cached
|
|
@@ -57,10 +63,16 @@ module BetterAuth
|
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
def missing_session(ctx)
|
|
66
|
+
ctx.context.set_current_session(nil) if ctx.context.respond_to?(:set_current_session)
|
|
60
67
|
Cookies.delete_session_cookie(ctx)
|
|
61
68
|
nil
|
|
62
69
|
end
|
|
63
70
|
|
|
71
|
+
def stateful?(ctx)
|
|
72
|
+
options = ctx.context.options
|
|
73
|
+
!options.database.nil? || !options.secondary_storage.nil?
|
|
74
|
+
end
|
|
75
|
+
|
|
64
76
|
def expired?(session)
|
|
65
77
|
expires_at = normalize_time(session["expiresAt"])
|
|
66
78
|
expires_at && expires_at <= Time.now
|
|
@@ -84,7 +96,12 @@ module BetterAuth
|
|
|
84
96
|
"expiresAt" => expires_at,
|
|
85
97
|
"updatedAt" => now
|
|
86
98
|
)
|
|
87
|
-
|
|
99
|
+
unless updated
|
|
100
|
+
missing_session(ctx)
|
|
101
|
+
raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["FAILED_TO_GET_SESSION"])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
session = stringify_keys(updated)
|
|
88
105
|
refreshed = {session: session, user: result[:user]}
|
|
89
106
|
Cookies.set_session_cookie(ctx, refreshed, Cookies.dont_remember?(ctx))
|
|
90
107
|
refreshed
|
|
@@ -92,12 +109,13 @@ module BetterAuth
|
|
|
92
109
|
|
|
93
110
|
def refresh_cached_session(ctx, result)
|
|
94
111
|
now = Time.now
|
|
95
|
-
session = stringify_keys(result[:session])
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
session = stringify_keys(result[:session])
|
|
113
|
+
expires_at = normalize_time(session["expiresAt"])
|
|
114
|
+
return missing_session(ctx) if expires_at && expires_at <= now
|
|
115
|
+
|
|
116
|
+
remaining = expires_at && [(expires_at - now).floor, 0].max
|
|
99
117
|
refreshed = {session: session, user: result[:user]}
|
|
100
|
-
Cookies.set_session_cookie(ctx, refreshed, Cookies.dont_remember?(ctx))
|
|
118
|
+
Cookies.set_session_cookie(ctx, refreshed, Cookies.dont_remember?(ctx), max_age: remaining)
|
|
101
119
|
refreshed
|
|
102
120
|
end
|
|
103
121
|
|
|
@@ -6,7 +6,7 @@ module BetterAuth
|
|
|
6
6
|
ESTIMATED_EMPTY_COOKIE_SIZE = 200
|
|
7
7
|
CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE
|
|
8
8
|
|
|
9
|
-
CookieValue = Struct.new(:name, :value, :attributes
|
|
9
|
+
CookieValue = Struct.new(:name, :value, :attributes)
|
|
10
10
|
|
|
11
11
|
attr_reader :cookie_name, :cookie_options, :context, :chunks
|
|
12
12
|
|
|
@@ -45,8 +45,13 @@ module BetterAuth
|
|
|
45
45
|
algorithms: ["RS256"],
|
|
46
46
|
issuers: "https://appleid.apple.com",
|
|
47
47
|
audience: audiences,
|
|
48
|
-
nonce:
|
|
48
|
+
nonce: nil
|
|
49
49
|
)
|
|
50
|
+
if nonce
|
|
51
|
+
token_nonce = profile&.fetch("nonce", nil)
|
|
52
|
+
expected_nonces = [nonce, OpenSSL::Digest::SHA256.hexdigest(nonce.to_s)]
|
|
53
|
+
return false unless token_nonce.is_a?(String) && expected_nonces.include?(token_nonce)
|
|
54
|
+
end
|
|
50
55
|
!!profile&.fetch("sub", nil)
|
|
51
56
|
end,
|
|
52
57
|
get_user_info: lambda do |tokens|
|
|
@@ -26,8 +26,9 @@ module BetterAuth
|
|
|
26
26
|
uri.to_s
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def oauth_provider(id:, name:, client_id:, authorization_endpoint:, token_endpoint:, profile_map:, client_secret: nil, user_info_endpoint: nil, scopes: [], scope_separator: " ", pkce: false, auth_params: {}, token_params: {}, user_info_method: :get, user_info_headers: {}, user_info_body: nil, **options)
|
|
29
|
+
def oauth_provider(id:, name:, client_id:, authorization_endpoint:, token_endpoint:, profile_map:, client_secret: nil, user_info_endpoint: nil, scopes: [], scope_separator: " ", pkce: false, require_code_verifier: false, auth_params: {}, token_params: {}, token_authentication: :post, user_info_method: :get, user_info_headers: {}, user_info_body: nil, **options)
|
|
30
30
|
opts = normalize_options(options.merge(client_id: client_id, client_secret: client_secret))
|
|
31
|
+
primary_id = primary_client_id(client_id)
|
|
31
32
|
{
|
|
32
33
|
id: id,
|
|
33
34
|
name: name,
|
|
@@ -36,9 +37,11 @@ module BetterAuth
|
|
|
36
37
|
options: opts,
|
|
37
38
|
create_authorization_url: lambda do |data|
|
|
38
39
|
verifier = value(data, :code_verifier, :codeVerifier)
|
|
40
|
+
raise Error, "codeVerifier is required for #{name}" if require_code_verifier && verifier.to_s.empty?
|
|
41
|
+
|
|
39
42
|
selected_scopes = selected_scopes(scopes, opts, data)
|
|
40
43
|
params = {
|
|
41
|
-
client_id:
|
|
44
|
+
client_id: primary_id,
|
|
42
45
|
redirect_uri: opts[:redirect_uri] || value(data, :redirect_uri, :redirectURI),
|
|
43
46
|
response_type: "code",
|
|
44
47
|
scope: selected_scopes.empty? ? nil : selected_scopes.join(scope_separator),
|
|
@@ -51,21 +54,27 @@ module BetterAuth
|
|
|
51
54
|
authorization_url(option(opts, :authorization_endpoint, :authorizationEndpoint) || authorization_endpoint, params)
|
|
52
55
|
end,
|
|
53
56
|
validate_authorization_code: lambda do |data|
|
|
54
|
-
|
|
55
|
-
client_id: primary_client_id(client_id),
|
|
56
|
-
client_secret: client_secret,
|
|
57
|
+
token_form = {
|
|
57
58
|
code: value(data, :code),
|
|
58
59
|
code_verifier: value(data, :code_verifier, :codeVerifier),
|
|
59
60
|
grant_type: "authorization_code",
|
|
60
61
|
redirect_uri: opts[:redirect_uri] || value(data, :redirect_uri, :redirectURI)
|
|
61
|
-
}.merge(resolve_hash(token_params, data, opts))
|
|
62
|
+
}.merge(resolve_hash(token_params, data, opts))
|
|
63
|
+
post_token_form(
|
|
64
|
+
option(opts, :token_endpoint, :tokenEndpoint) || token_endpoint,
|
|
65
|
+
token_form,
|
|
66
|
+
client_id: primary_id,
|
|
67
|
+
client_secret: client_secret,
|
|
68
|
+
authentication: token_authentication
|
|
69
|
+
)
|
|
62
70
|
end,
|
|
63
71
|
refresh_access_token: opts[:refresh_access_token] || lambda do |refresh_token|
|
|
64
72
|
refresh_access_token(
|
|
65
73
|
option(opts, :token_endpoint, :tokenEndpoint) || token_endpoint,
|
|
66
74
|
refresh_token,
|
|
67
|
-
client_id:
|
|
68
|
-
client_secret: client_secret
|
|
75
|
+
client_id: primary_id,
|
|
76
|
+
client_secret: client_secret,
|
|
77
|
+
authentication: token_authentication
|
|
69
78
|
)
|
|
70
79
|
end,
|
|
71
80
|
verify_id_token: opts[:verify_id_token],
|
|
@@ -138,13 +147,27 @@ module BetterAuth
|
|
|
138
147
|
(method == :post) ? post_json(endpoint, resolved_body, resolved_headers) : get_json(endpoint, resolved_headers)
|
|
139
148
|
end
|
|
140
149
|
|
|
141
|
-
def refresh_access_token(token_endpoint, refresh_token, client_id:, client_secret: nil, extra_params: {})
|
|
142
|
-
normalize_tokens(
|
|
143
|
-
client_id: client_id,
|
|
144
|
-
client_secret: client_secret,
|
|
150
|
+
def refresh_access_token(token_endpoint, refresh_token, client_id:, client_secret: nil, extra_params: {}, authentication: :post)
|
|
151
|
+
normalize_tokens(post_token_form(token_endpoint, {
|
|
145
152
|
grant_type: "refresh_token",
|
|
146
153
|
refresh_token: refresh_token
|
|
147
|
-
}.merge(extra_params || {})))
|
|
154
|
+
}.merge(extra_params || {}), client_id: client_id, client_secret: client_secret, authentication: authentication))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def post_token_form(token_endpoint, form, client_id:, client_secret: nil, authentication: :post, headers: {})
|
|
158
|
+
case authentication&.to_sym
|
|
159
|
+
when :basic
|
|
160
|
+
post_form_json(token_endpoint, form.compact, basic_auth_headers(client_id, client_secret).merge(headers))
|
|
161
|
+
else
|
|
162
|
+
post_form_json(token_endpoint, {
|
|
163
|
+
client_id: client_id,
|
|
164
|
+
client_secret: client_secret
|
|
165
|
+
}.merge(form), headers)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def basic_auth_headers(client_id, client_secret)
|
|
170
|
+
{"Authorization" => "Basic #{Base64.strict_encode64("#{client_id}:#{client_secret}")}"}
|
|
148
171
|
end
|
|
149
172
|
|
|
150
173
|
def normalize_tokens(tokens, now: Time.now)
|
|
@@ -211,7 +234,11 @@ module BetterAuth
|
|
|
211
234
|
appBundleIdentifier: :app_bundle_identifier,
|
|
212
235
|
profilePhotoSize: :profile_photo_size,
|
|
213
236
|
disableProfilePhoto: :disable_profile_photo,
|
|
214
|
-
tenantId: :tenant_id
|
|
237
|
+
tenantId: :tenant_id,
|
|
238
|
+
userPoolId: :user_pool_id,
|
|
239
|
+
requireClientSecret: :require_client_secret,
|
|
240
|
+
configId: :config_id,
|
|
241
|
+
loginUrl: :login_url
|
|
215
242
|
}.each do |camel, snake|
|
|
216
243
|
normalized[snake] = normalized[camel] if normalized.key?(camel) && !normalized.key?(snake)
|
|
217
244
|
end
|
|
@@ -5,15 +5,25 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def cognito(client_id:, client_secret: nil, scopes: ["openid", "profile", "email"], **options)
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
normalized = Base.normalize_options(options)
|
|
9
|
+
domain = normalized[:domain]
|
|
10
|
+
region = normalized[:region]
|
|
11
|
+
user_pool_id = normalized[:user_pool_id]
|
|
12
|
+
raise Error, "DOMAIN_AND_REGION_REQUIRED" if domain.to_s.empty? || region.to_s.empty? || user_pool_id.to_s.empty?
|
|
13
|
+
|
|
14
|
+
if normalized[:require_client_secret] && client_secret.to_s.empty?
|
|
15
|
+
raise Error, "CLIENT_SECRET_REQUIRED"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
clean_domain = domain.to_s.sub(%r{\Ahttps?://}, "").sub(%r{/+\z}, "")
|
|
19
|
+
provider = Base.oauth_provider(
|
|
10
20
|
id: "cognito",
|
|
11
21
|
name: "Cognito",
|
|
12
22
|
client_id: client_id,
|
|
13
23
|
client_secret: client_secret,
|
|
14
|
-
authorization_endpoint: "
|
|
15
|
-
token_endpoint: "
|
|
16
|
-
user_info_endpoint: "
|
|
24
|
+
authorization_endpoint: "https://#{clean_domain}/oauth2/authorize",
|
|
25
|
+
token_endpoint: "https://#{clean_domain}/oauth2/token",
|
|
26
|
+
user_info_endpoint: "https://#{clean_domain}/oauth2/userinfo",
|
|
17
27
|
scopes: scopes,
|
|
18
28
|
pkce: true,
|
|
19
29
|
profile_map: ->(profile) {
|
|
@@ -27,6 +37,66 @@ module BetterAuth
|
|
|
27
37
|
},
|
|
28
38
|
**options
|
|
29
39
|
)
|
|
40
|
+
create_authorization_url = provider.fetch(:create_authorization_url)
|
|
41
|
+
provider[:create_authorization_url] = lambda do |data|
|
|
42
|
+
create_authorization_url.call(data).sub(/scope=([^&]+)/) { "scope=#{$1.gsub("+", "%20")}" }
|
|
43
|
+
end
|
|
44
|
+
provider[:verify_id_token] ||= lambda do |token, nonce = nil|
|
|
45
|
+
return false if normalized[:disable_id_token_sign_in]
|
|
46
|
+
|
|
47
|
+
profile = Base.verify_jwt_with_jwks(
|
|
48
|
+
token,
|
|
49
|
+
jwks: normalized[:jwks],
|
|
50
|
+
jwks_endpoint: normalized[:jwks_endpoint] || "https://cognito-idp.#{region}.amazonaws.com/#{user_pool_id}/.well-known/jwks.json",
|
|
51
|
+
algorithms: ["RS256"],
|
|
52
|
+
issuers: "https://cognito-idp.#{region}.amazonaws.com/#{user_pool_id}",
|
|
53
|
+
audience: Array(client_id),
|
|
54
|
+
nonce: nonce,
|
|
55
|
+
max_age: 3600
|
|
56
|
+
)
|
|
57
|
+
!!profile&.fetch("sub", nil)
|
|
58
|
+
end
|
|
59
|
+
provider[:get_user_info] = lambda do |tokens|
|
|
60
|
+
custom = normalized[:get_user_info]
|
|
61
|
+
next custom.call(tokens) if custom
|
|
62
|
+
|
|
63
|
+
if Base.id_token(tokens)
|
|
64
|
+
profile = Base.decode_jwt_payload(Base.id_token(tokens))
|
|
65
|
+
unless profile.empty?
|
|
66
|
+
name = profile["name"] || profile["given_name"] || profile["username"] || ""
|
|
67
|
+
enriched = profile.merge("name" => name)
|
|
68
|
+
user = Base.apply_profile_mapping(
|
|
69
|
+
{
|
|
70
|
+
id: profile["sub"],
|
|
71
|
+
name: name,
|
|
72
|
+
email: profile["email"],
|
|
73
|
+
image: profile["picture"],
|
|
74
|
+
emailVerified: !!profile["email_verified"]
|
|
75
|
+
},
|
|
76
|
+
enriched,
|
|
77
|
+
normalized
|
|
78
|
+
)
|
|
79
|
+
next({user: user, data: enriched})
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
profile = Base.fetch_user_info("https://#{clean_domain}/oauth2/userinfo", tokens)
|
|
84
|
+
next nil unless profile
|
|
85
|
+
|
|
86
|
+
user = Base.apply_profile_mapping(
|
|
87
|
+
{
|
|
88
|
+
id: profile["sub"],
|
|
89
|
+
name: profile["name"] || profile["given_name"] || profile["username"] || "",
|
|
90
|
+
email: profile["email"],
|
|
91
|
+
image: profile["picture"],
|
|
92
|
+
emailVerified: !!profile["email_verified"]
|
|
93
|
+
},
|
|
94
|
+
profile,
|
|
95
|
+
normalized
|
|
96
|
+
)
|
|
97
|
+
{user: user, data: profile}
|
|
98
|
+
end
|
|
99
|
+
provider
|
|
30
100
|
end
|
|
31
101
|
end
|
|
32
102
|
end
|
|
@@ -5,6 +5,7 @@ module BetterAuth
|
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
7
|
def facebook(client_id:, client_secret:, scopes: ["email", "public_profile"], **options)
|
|
8
|
+
normalized = Base.normalize_options(options)
|
|
8
9
|
fields = Array(options[:fields] || %w[id name email picture email_verified]).join(",")
|
|
9
10
|
provider = Base.oauth_provider(
|
|
10
11
|
id: "facebook",
|
|
@@ -28,7 +29,100 @@ module BetterAuth
|
|
|
28
29
|
},
|
|
29
30
|
**options
|
|
30
31
|
)
|
|
31
|
-
|
|
32
|
+
verify_access_token = lambda do |access_token|
|
|
33
|
+
primary_client_id = Base.primary_client_id(client_id)
|
|
34
|
+
next nil if access_token.to_s.empty? || client_secret.to_s.empty?
|
|
35
|
+
|
|
36
|
+
response = Base.get_json(Base.authorization_url("https://graph.facebook.com/debug_token", {
|
|
37
|
+
input_token: access_token,
|
|
38
|
+
access_token: "#{primary_client_id}|#{client_secret}"
|
|
39
|
+
}))
|
|
40
|
+
data = response.is_a?(Hash) ? response["data"] : nil
|
|
41
|
+
next nil unless data.is_a?(Hash)
|
|
42
|
+
next nil unless data["is_valid"] == true
|
|
43
|
+
next nil unless Array(client_id).map(&:to_s).include?(data["app_id"].to_s)
|
|
44
|
+
next nil if data["app_id"].to_s.empty? || data["user_id"].to_s.empty?
|
|
45
|
+
|
|
46
|
+
data["user_id"]
|
|
47
|
+
rescue
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
provider[:verify_id_token] = lambda do |token, nonce = nil|
|
|
51
|
+
return false if normalized[:disable_id_token_sign_in]
|
|
52
|
+
|
|
53
|
+
custom = normalized[:verify_id_token]
|
|
54
|
+
next custom.call(token, nonce) if custom
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
unless token.to_s.split(".").length == 3
|
|
58
|
+
next !!verify_access_token.call(token)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
profile = Base.verify_jwt_with_jwks(
|
|
62
|
+
token,
|
|
63
|
+
jwks: normalized[:jwks],
|
|
64
|
+
jwks_endpoint: normalized[:jwks_endpoint] || "https://limited.facebook.com/.well-known/oauth/openid/jwks/",
|
|
65
|
+
algorithms: ["RS256"],
|
|
66
|
+
issuers: "https://www.facebook.com",
|
|
67
|
+
audience: Array(client_id),
|
|
68
|
+
nonce: nonce
|
|
69
|
+
)
|
|
70
|
+
!!profile
|
|
71
|
+
rescue
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
provider[:get_user_info] = lambda do |tokens|
|
|
76
|
+
custom = normalized[:get_user_info]
|
|
77
|
+
next custom.call(tokens) if custom
|
|
78
|
+
|
|
79
|
+
begin
|
|
80
|
+
token = Base.id_token(tokens)
|
|
81
|
+
if token && token.split(".").length == 3
|
|
82
|
+
profile = Base.decode_jwt_payload(token)
|
|
83
|
+
next nil if profile["sub"].to_s.empty?
|
|
84
|
+
|
|
85
|
+
user = Base.apply_profile_mapping(
|
|
86
|
+
{
|
|
87
|
+
id: profile["sub"],
|
|
88
|
+
name: profile["name"],
|
|
89
|
+
email: profile["email"],
|
|
90
|
+
image: profile["picture"],
|
|
91
|
+
emailVerified: false
|
|
92
|
+
},
|
|
93
|
+
profile.merge("email_verified" => false),
|
|
94
|
+
normalized
|
|
95
|
+
)
|
|
96
|
+
next({user: user, data: profile})
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
access_token = Base.access_token(tokens)
|
|
100
|
+
next nil if access_token.to_s.empty?
|
|
101
|
+
|
|
102
|
+
token_user_id = verify_access_token.call(access_token)
|
|
103
|
+
next nil unless token_user_id
|
|
104
|
+
|
|
105
|
+
profile = Base.fetch_user_info("https://graph.facebook.com/me?fields=#{URI.encode_www_form_component(fields)}", tokens)
|
|
106
|
+
next nil unless profile.is_a?(Hash)
|
|
107
|
+
next nil unless profile["id"] == token_user_id
|
|
108
|
+
|
|
109
|
+
picture = profile.dig("picture", "data", "url") || profile["picture"]
|
|
110
|
+
user = Base.apply_profile_mapping(
|
|
111
|
+
{
|
|
112
|
+
id: profile["id"] || profile["sub"],
|
|
113
|
+
name: profile["name"],
|
|
114
|
+
email: profile["email"],
|
|
115
|
+
image: picture,
|
|
116
|
+
emailVerified: !!profile["email_verified"]
|
|
117
|
+
},
|
|
118
|
+
profile,
|
|
119
|
+
normalized
|
|
120
|
+
)
|
|
121
|
+
{user: user, data: profile}
|
|
122
|
+
rescue
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
end
|
|
32
126
|
provider
|
|
33
127
|
end
|
|
34
128
|
end
|
|
@@ -4,6 +4,14 @@ module BetterAuth
|
|
|
4
4
|
module SocialProviders
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
+
def google_hosted_domain_allowed?(configured_hosted_domain, token_hosted_domain)
|
|
8
|
+
return true if configured_hosted_domain.nil? || configured_hosted_domain.to_s.empty?
|
|
9
|
+
return false unless token_hosted_domain.is_a?(String) && !token_hosted_domain.strip.empty?
|
|
10
|
+
return true if configured_hosted_domain == "*"
|
|
11
|
+
|
|
12
|
+
token_hosted_domain == configured_hosted_domain
|
|
13
|
+
end
|
|
14
|
+
|
|
7
15
|
def google(client_id:, client_secret:, scopes: ["openid", "email", "profile"], **options)
|
|
8
16
|
normalized = Base.normalize_options(options)
|
|
9
17
|
primary_client_id = Base.primary_client_id(client_id)
|
|
@@ -12,6 +20,7 @@ module BetterAuth
|
|
|
12
20
|
name: "Google",
|
|
13
21
|
client_id: client_id,
|
|
14
22
|
client_secret: client_secret,
|
|
23
|
+
options: normalized,
|
|
15
24
|
create_authorization_url: lambda do |data|
|
|
16
25
|
verifier = data[:code_verifier] || data[:codeVerifier]
|
|
17
26
|
raise Error, "codeVerifier is required for Google" if verifier.to_s.empty?
|
|
@@ -57,7 +66,7 @@ module BetterAuth
|
|
|
57
66
|
audience: audiences,
|
|
58
67
|
nonce: nonce
|
|
59
68
|
)
|
|
60
|
-
!!profile&.fetch("sub", nil)
|
|
69
|
+
!!profile&.fetch("sub", nil) && google_hosted_domain_allowed?(normalized[:hd], profile["hd"])
|
|
61
70
|
end,
|
|
62
71
|
get_user_info: lambda do |tokens|
|
|
63
72
|
custom = normalized[:get_user_info]
|
|
@@ -65,6 +74,8 @@ module BetterAuth
|
|
|
65
74
|
next nil unless Base.id_token(tokens)
|
|
66
75
|
|
|
67
76
|
profile = Base.decode_jwt_payload(Base.id_token(tokens))
|
|
77
|
+
next nil unless google_hosted_domain_allowed?(normalized[:hd], profile["hd"])
|
|
78
|
+
|
|
68
79
|
user = Base.apply_profile_mapping(
|
|
69
80
|
{
|
|
70
81
|
id: profile["sub"],
|