better_auth 0.6.0 → 0.7.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 +16 -0
- data/README.md +12 -0
- data/lib/better_auth/adapters/join_support.rb +63 -0
- data/lib/better_auth/adapters/memory.rb +4 -4
- data/lib/better_auth/adapters/sql.rb +15 -52
- data/lib/better_auth/endpoint.rb +7 -2
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +9 -5
- data/lib/better_auth/plugins/mcp.rb +28 -3
- data/lib/better_auth/plugins/oauth_protocol.rb +78 -19
- data/lib/better_auth/plugins/oidc_provider.rb +10 -1
- data/lib/better_auth/plugins/open_api.rb +70 -4
- data/lib/better_auth/router.rb +19 -9
- data/lib/better_auth/routes/account.rb +23 -1
- data/lib/better_auth/routes/error.rb +20 -1
- data/lib/better_auth/routes/ok.rb +20 -1
- data/lib/better_auth/routes/session.rb +1 -1
- data/lib/better_auth/routes/user.rb +1 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b62f1d0f1e0fc4c774ab2d4907f9229f4cbcae46599cf8b72c03a4d5984aafcf
|
|
4
|
+
data.tar.gz: a02c36229e3d829a1d3c24d64e9e560fc9f592e9ad4ad915bcd6a962fd0f633a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50d18b3901b1e7292b3b4089e1eb388dadb16836d88a6d8af35ecb8061005dc062712010865b0f131b768538fbc2e64042edfb5b652feaad979f93d03f6ca7fa
|
|
7
|
+
data.tar.gz: ce679e88e9e12dd1d36aab49a20bb6d2a8e55e95de6eeccbdc3b4bd3736924d9496fb5148e874132f6f07efb914035ed6ce0cd200216355390bfe86b016c6ff8
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-05-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Completed OpenAPI support with upstream v1.6.9 base-route schema parity, `/ok` and `/error` documentation, richer helper-generated schemas, plugin endpoint metadata coverage, and Scalar reference configuration parity.
|
|
15
|
+
- Added shared join query handling for adapter-backed relation loading.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
10
19
|
- Modernized the MCP plugin to use OAuth Provider-style client, token, metadata, and protected-resource behavior while keeping legacy MCP routes as aliases.
|
|
20
|
+
- Changed OAuth HS256 ID token signing to use non-public key material; existing ID tokens signed only with the public client id will no longer validate.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed OAuth refresh token rotation to reject refresh tokens presented by a different authenticated client.
|
|
25
|
+
- Fixed OAuth client-secret verification to use constant-time comparison for encrypted and custom-hashed storage modes.
|
|
26
|
+
- Hardened router and OAuth protocol behavior around path handling, issuer metadata, and public route coverage.
|
|
11
27
|
|
|
12
28
|
## [0.4.0] - 2026-04-30
|
|
13
29
|
|
data/README.md
CHANGED
|
@@ -378,6 +378,18 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
378
378
|
|
|
379
379
|
## Security
|
|
380
380
|
|
|
381
|
+
### Trusted origins
|
|
382
|
+
|
|
383
|
+
`trusted_origins` is merged with the resolved application origin and deployment
|
|
384
|
+
configuration rather than acting as an adapter-local CORS switch. Upstream
|
|
385
|
+
Better Auth also incorporates environment-driven origins and dynamic
|
|
386
|
+
configuration, so an explicit empty array should not be assumed to mean "reject
|
|
387
|
+
every browser origin" unless the deployed core configuration documents that full
|
|
388
|
+
merge contract. Configure concrete origins for each environment and keep browser
|
|
389
|
+
CORS headers in the host Rack stack or reverse proxy. See
|
|
390
|
+
[`host-app-responsibilities.md`](../../.docs/features/host-app-responsibilities.md)
|
|
391
|
+
for the boundary between origin validation, CORS, and CSRF ownership.
|
|
392
|
+
|
|
381
393
|
If you discover a security vulnerability within Better Auth Ruby, please send an e-mail to [security@openparcel.dev](mailto:security@openparcel.dev).
|
|
382
394
|
|
|
383
395
|
All reports will be promptly addressed, and you'll be credited accordingly.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Adapters
|
|
5
|
+
module JoinSupport
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def normalized_join(model, join)
|
|
9
|
+
return {} unless join
|
|
10
|
+
|
|
11
|
+
join.each_with_object({}) do |(join_model, config), result|
|
|
12
|
+
join_model = join_model.to_s
|
|
13
|
+
result[join_model] = normalize_join_config(model.to_s, join_model, config)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def normalize_join_config(model, join_model, config)
|
|
18
|
+
if config.is_a?(Hash) && (config.key?(:on) || config.key?("on"))
|
|
19
|
+
on = config[:on] || config["on"]
|
|
20
|
+
from = storage_key(fetch_key(on, :from))
|
|
21
|
+
to = storage_key(fetch_key(on, :to))
|
|
22
|
+
relation = config[:relation] || config["relation"]
|
|
23
|
+
limit = config[:limit] || config["limit"]
|
|
24
|
+
return {from: from, to: to, relation: relation, limit: limit, unique: unique_join_field?(join_model, to)}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
inferred = inferred_join_config(model, join_model)
|
|
28
|
+
if config.is_a?(Hash)
|
|
29
|
+
relation = config[:relation] || config["relation"]
|
|
30
|
+
limit = config[:limit] || config["limit"]
|
|
31
|
+
inferred = inferred.merge(relation: relation) if relation
|
|
32
|
+
inferred = inferred.merge(limit: limit) if limit
|
|
33
|
+
end
|
|
34
|
+
inferred
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reference_model_matches?(attributes, model)
|
|
38
|
+
reference = attributes[:references]
|
|
39
|
+
return false unless reference
|
|
40
|
+
|
|
41
|
+
reference_model = reference[:model] || reference["model"]
|
|
42
|
+
reference_model.to_s == model.to_s || reference_model.to_s == table_for(model)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def unique_join_field?(model, field)
|
|
46
|
+
field = storage_key(field)
|
|
47
|
+
field == "id" || schema_for(model).fetch(:fields).dig(field, :unique) == true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collection_join?(model, join)
|
|
51
|
+
normalized_join(model, join).any? do |_join_model, config|
|
|
52
|
+
if config.key?(:relation)
|
|
53
|
+
config[:relation] != "one-to-one" && config[:unique] != true
|
|
54
|
+
elsif config.key?(:collection)
|
|
55
|
+
config[:collection] == true
|
|
56
|
+
else
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -172,13 +172,13 @@ module BetterAuth
|
|
|
172
172
|
when "ne"
|
|
173
173
|
current != comparable
|
|
174
174
|
when "gt"
|
|
175
|
-
!comparable.nil? && current > comparable
|
|
175
|
+
!current.nil? && !comparable.nil? && current > comparable
|
|
176
176
|
when "gte"
|
|
177
|
-
!comparable.nil? && current >= comparable
|
|
177
|
+
!current.nil? && !comparable.nil? && current >= comparable
|
|
178
178
|
when "lt"
|
|
179
|
-
!comparable.nil? && current < comparable
|
|
179
|
+
!current.nil? && !comparable.nil? && current < comparable
|
|
180
180
|
when "lte"
|
|
181
|
-
!comparable.nil? && current <= comparable
|
|
181
|
+
!current.nil? && !comparable.nil? && current <= comparable
|
|
182
182
|
else
|
|
183
183
|
current == comparable
|
|
184
184
|
end
|
|
@@ -7,6 +7,8 @@ require "time"
|
|
|
7
7
|
module BetterAuth
|
|
8
8
|
module Adapters
|
|
9
9
|
class SQL < Base
|
|
10
|
+
include JoinSupport
|
|
11
|
+
|
|
10
12
|
attr_reader :connection, :dialect
|
|
11
13
|
|
|
12
14
|
def initialize(options, connection:, dialect:)
|
|
@@ -191,35 +193,6 @@ module BetterAuth
|
|
|
191
193
|
end.join
|
|
192
194
|
end
|
|
193
195
|
|
|
194
|
-
def normalized_join(model, join)
|
|
195
|
-
return {} unless join
|
|
196
|
-
|
|
197
|
-
join.each_with_object({}) do |(join_model, config), result|
|
|
198
|
-
join_model = join_model.to_s
|
|
199
|
-
result[join_model] = normalize_join_config(model.to_s, join_model, config)
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def normalize_join_config(model, join_model, config)
|
|
204
|
-
if config.is_a?(Hash) && (config.key?(:on) || config.key?("on"))
|
|
205
|
-
on = config[:on] || config["on"]
|
|
206
|
-
from = storage_key(fetch_key(on, :from))
|
|
207
|
-
to = storage_key(fetch_key(on, :to))
|
|
208
|
-
relation = config[:relation] || config["relation"]
|
|
209
|
-
limit = config[:limit] || config["limit"]
|
|
210
|
-
return {from: from, to: to, relation: relation, limit: limit, unique: unique_join_field?(join_model, to)}
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
inferred = inferred_join_config(model, join_model)
|
|
214
|
-
if config.is_a?(Hash)
|
|
215
|
-
relation = config[:relation] || config["relation"]
|
|
216
|
-
limit = config[:limit] || config["limit"]
|
|
217
|
-
inferred = inferred.merge(relation: relation) if relation
|
|
218
|
-
inferred = inferred.merge(limit: limit) if limit
|
|
219
|
-
end
|
|
220
|
-
inferred
|
|
221
|
-
end
|
|
222
|
-
|
|
223
196
|
def inferred_join_config(model, join_model)
|
|
224
197
|
foreign_keys = schema_for(join_model).fetch(:fields).select do |_field, attributes|
|
|
225
198
|
reference_model_matches?(attributes, model)
|
|
@@ -246,19 +219,6 @@ module BetterAuth
|
|
|
246
219
|
end
|
|
247
220
|
end
|
|
248
221
|
|
|
249
|
-
def reference_model_matches?(attributes, model)
|
|
250
|
-
reference = attributes[:references]
|
|
251
|
-
return false unless reference
|
|
252
|
-
|
|
253
|
-
reference_model = reference[:model] || reference["model"]
|
|
254
|
-
reference_model.to_s == model.to_s || reference_model.to_s == table_for(model)
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def unique_join_field?(model, field)
|
|
258
|
-
field = storage_key(field)
|
|
259
|
-
field == "id" || schema_for(model).fetch(:fields).dig(field, :unique) == true
|
|
260
|
-
end
|
|
261
|
-
|
|
262
222
|
def build_where(model, where, params)
|
|
263
223
|
Array(where).each_with_index.map do |clause, index|
|
|
264
224
|
field = storage_key(fetch_key(clause, :field))
|
|
@@ -277,13 +237,14 @@ module BetterAuth
|
|
|
277
237
|
sql_operator = (operator == "not_in") ? "NOT IN" : "IN"
|
|
278
238
|
"#{column} #{sql_operator} (#{placeholders})"
|
|
279
239
|
when "contains", "starts_with", "ends_with"
|
|
240
|
+
escaped = escape_like(value)
|
|
280
241
|
pattern = case operator
|
|
281
|
-
when "starts_with" then "#{
|
|
282
|
-
when "ends_with" then "%#{
|
|
283
|
-
else "%#{
|
|
242
|
+
when "starts_with" then "#{escaped}%"
|
|
243
|
+
when "ends_with" then "%#{escaped}"
|
|
244
|
+
else "%#{escaped}%"
|
|
284
245
|
end
|
|
285
246
|
params << pattern
|
|
286
|
-
"#{column} LIKE #{placeholder(params.length)}"
|
|
247
|
+
"#{column} LIKE #{placeholder(params.length)} ESCAPE #{escape_literal}"
|
|
287
248
|
else
|
|
288
249
|
params << coerce_where_value(value, attributes)
|
|
289
250
|
"#{column} #{sql_operator(operator)} #{placeholder(params.length)}"
|
|
@@ -379,12 +340,6 @@ module BetterAuth
|
|
|
379
340
|
end
|
|
380
341
|
end
|
|
381
342
|
|
|
382
|
-
def collection_join?(model, join)
|
|
383
|
-
normalized_join(model, join).any? do |_join_model, config|
|
|
384
|
-
config[:relation] != "one-to-one" && config[:unique] != true
|
|
385
|
-
end
|
|
386
|
-
end
|
|
387
|
-
|
|
388
343
|
def aggregate_collection_joins(model, records, join)
|
|
389
344
|
join_config = normalized_join(model, join)
|
|
390
345
|
grouped = {}
|
|
@@ -450,6 +405,14 @@ module BetterAuth
|
|
|
450
405
|
SecureRandom.hex(16)
|
|
451
406
|
end
|
|
452
407
|
|
|
408
|
+
def escape_like(value)
|
|
409
|
+
value.to_s.gsub(/[\\%_]/) { |match| "\\#{match}" }
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def escape_literal
|
|
413
|
+
(dialect == :postgres) ? "'\\\\'" : "'\\'"
|
|
414
|
+
end
|
|
415
|
+
|
|
453
416
|
def resolve_default(default)
|
|
454
417
|
default.respond_to?(:call) ? default.call : default
|
|
455
418
|
end
|
data/lib/better_auth/endpoint.rb
CHANGED
|
@@ -12,9 +12,10 @@ module BetterAuth
|
|
|
12
12
|
:metadata,
|
|
13
13
|
:options,
|
|
14
14
|
:use,
|
|
15
|
+
:disable_body,
|
|
15
16
|
:handler
|
|
16
17
|
|
|
17
|
-
def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, params_schema: nil, headers_schema: nil, metadata: {}, use: [], &handler)
|
|
18
|
+
def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, params_schema: nil, headers_schema: nil, metadata: {}, use: [], disable_body: false, &handler)
|
|
18
19
|
@path = path
|
|
19
20
|
@methods = Array(method || "*").map { |value| value.to_s.upcase }
|
|
20
21
|
@body_schema = body_schema
|
|
@@ -26,6 +27,7 @@ module BetterAuth
|
|
|
26
27
|
apply_open_api_schemas!
|
|
27
28
|
@options = endpoint_options
|
|
28
29
|
@use = Array(use)
|
|
30
|
+
@disable_body = !!disable_body
|
|
29
31
|
@handler = handler || ->(_ctx) {}
|
|
30
32
|
end
|
|
31
33
|
|
|
@@ -57,6 +59,7 @@ module BetterAuth
|
|
|
57
59
|
query: query_schema,
|
|
58
60
|
params: params_schema,
|
|
59
61
|
headers: headers_schema,
|
|
62
|
+
disableBody: disable_body,
|
|
60
63
|
metadata: metadata
|
|
61
64
|
}.compact
|
|
62
65
|
end
|
|
@@ -249,19 +252,21 @@ module BetterAuth
|
|
|
249
252
|
:body,
|
|
250
253
|
:params,
|
|
251
254
|
:headers,
|
|
255
|
+
:raw_body,
|
|
252
256
|
:context,
|
|
253
257
|
:request,
|
|
254
258
|
:status,
|
|
255
259
|
:returned,
|
|
256
260
|
:response_headers
|
|
257
261
|
|
|
258
|
-
def initialize(path:, method:, query:, body:, params:, headers:, context:, request: nil)
|
|
262
|
+
def initialize(path:, method:, query:, body:, params:, headers:, context:, request: nil, raw_body: nil)
|
|
259
263
|
@path = path
|
|
260
264
|
@method = method.to_s.upcase
|
|
261
265
|
@query = query || {}
|
|
262
266
|
@body = body || {}
|
|
263
267
|
@params = params || {}
|
|
264
268
|
@headers = normalize_headers(headers || {})
|
|
269
|
+
@raw_body = raw_body
|
|
265
270
|
@context = context
|
|
266
271
|
@request = request
|
|
267
272
|
@status = 200
|
|
@@ -6,31 +6,35 @@ module BetterAuth
|
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
8
|
def legacy_register_endpoint(config)
|
|
9
|
-
Endpoint.new(path: "/mcp/register", method: "POST") do |ctx|
|
|
9
|
+
Endpoint.new(path: "/mcp/register", method: "POST", metadata: BetterAuth::Plugins.mcp_openapi("legacyRegisterMcpClient", "Register an OAuth2 application using the legacy MCP path", "OAuth2 application registered successfully", BetterAuth::Plugins.mcp_client_schema)) do |ctx|
|
|
10
10
|
ctx.json(register_client(ctx, config), status: 201, headers: no_store_headers)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def legacy_authorize_endpoint(config)
|
|
15
|
-
Endpoint.new(path: "/mcp/authorize", method: "GET") do |ctx|
|
|
15
|
+
Endpoint.new(path: "/mcp/authorize", method: "GET", metadata: BetterAuth::Plugins.mcp_openapi("legacyMcpOAuthAuthorize", "Authorize an OAuth2 request using the legacy MCP path", "Authorization response generated successfully", {type: "object", additionalProperties: true})) do |ctx|
|
|
16
16
|
authorize(ctx, config)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def legacy_token_endpoint(config)
|
|
21
|
-
Endpoint.new(
|
|
21
|
+
Endpoint.new(
|
|
22
|
+
path: "/mcp/token",
|
|
23
|
+
method: "POST",
|
|
24
|
+
metadata: BetterAuth::Plugins.mcp_openapi("legacyMcpOAuthToken", "Exchange OAuth2 code for MCP tokens using the legacy path", "OAuth2 tokens issued successfully", BetterAuth::Plugins.mcp_token_response_schema).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
|
|
25
|
+
) do |ctx|
|
|
22
26
|
ctx.json(token(ctx, config), headers: no_store_headers)
|
|
23
27
|
end
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
def legacy_userinfo_endpoint(config)
|
|
27
|
-
Endpoint.new(path: "/mcp/userinfo", method: "GET") do |ctx|
|
|
31
|
+
Endpoint.new(path: "/mcp/userinfo", method: "GET", metadata: BetterAuth::Plugins.mcp_openapi("legacyMcpOAuthUserinfo", "Get MCP OAuth2 user information using the legacy path", "User information retrieved successfully", BetterAuth::Plugins.mcp_userinfo_schema)) do |ctx|
|
|
28
32
|
ctx.json(userinfo(ctx, config))
|
|
29
33
|
end
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def legacy_jwks_endpoint(config)
|
|
33
|
-
Endpoint.new(path: "/mcp/jwks", method: "GET") do |ctx|
|
|
37
|
+
Endpoint.new(path: "/mcp/jwks", method: "GET", metadata: BetterAuth::Plugins.mcp_openapi("legacyMcpJSONWebKeySet", "Get the MCP JSON Web Key Set using the legacy path", "JSON Web Key Set retrieved successfully", BetterAuth::Plugins.mcp_jwks_response_schema)) do |ctx|
|
|
34
38
|
ctx.json(jwks(ctx, config))
|
|
35
39
|
end
|
|
36
40
|
end
|
|
@@ -86,7 +86,7 @@ module BetterAuth
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def mcp_consent_endpoint(config)
|
|
89
|
-
Endpoint.new(path: "/oauth2/consent", method: "POST") do |ctx|
|
|
89
|
+
Endpoint.new(path: "/oauth2/consent", method: "POST", metadata: mcp_openapi("mcpOAuthConsent", "Handle MCP OAuth2 consent", "OAuth2 consent handled successfully", {type: "object", additionalProperties: true})) do |ctx|
|
|
90
90
|
ctx.json(MCP.consent(ctx, config))
|
|
91
91
|
end
|
|
92
92
|
end
|
|
@@ -120,13 +120,21 @@ module BetterAuth
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
def mcp_introspect_endpoint(config)
|
|
123
|
-
Endpoint.new(
|
|
123
|
+
Endpoint.new(
|
|
124
|
+
path: "/oauth2/introspect",
|
|
125
|
+
method: "POST",
|
|
126
|
+
metadata: mcp_openapi("mcpOAuthIntrospect", "Introspect an MCP OAuth2 token", "OAuth2 token introspection result", mcp_introspection_schema).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
|
|
127
|
+
) do |ctx|
|
|
124
128
|
ctx.json(MCP.introspect(ctx, config))
|
|
125
129
|
end
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
def mcp_revoke_endpoint(config)
|
|
129
|
-
Endpoint.new(
|
|
133
|
+
Endpoint.new(
|
|
134
|
+
path: "/oauth2/revoke",
|
|
135
|
+
method: "POST",
|
|
136
|
+
metadata: mcp_openapi("mcpOAuthRevoke", "Revoke an MCP OAuth2 token", "OAuth2 token revoked successfully", OpenAPI.object_schema({revoked: {type: "boolean"}}, required: ["revoked"])).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
|
|
137
|
+
) do |ctx|
|
|
130
138
|
ctx.json(MCP.revoke(ctx, config))
|
|
131
139
|
end
|
|
132
140
|
end
|
|
@@ -186,5 +194,22 @@ module BetterAuth
|
|
|
186
194
|
required: ["keys"]
|
|
187
195
|
)
|
|
188
196
|
end
|
|
197
|
+
|
|
198
|
+
def mcp_introspection_schema
|
|
199
|
+
OpenAPI.object_schema(
|
|
200
|
+
{
|
|
201
|
+
active: {type: "boolean"},
|
|
202
|
+
client_id: {type: ["string", "null"]},
|
|
203
|
+
scope: {type: ["string", "null"]},
|
|
204
|
+
sub: {type: ["string", "null"]},
|
|
205
|
+
iss: {type: ["string", "null"]},
|
|
206
|
+
iat: {type: ["number", "null"]},
|
|
207
|
+
exp: {type: ["number", "null"]},
|
|
208
|
+
sid: {type: ["string", "null"]},
|
|
209
|
+
aud: {type: ["string", "array", "null"], items: {type: "string"}}
|
|
210
|
+
},
|
|
211
|
+
required: ["active"]
|
|
212
|
+
)
|
|
213
|
+
end
|
|
189
214
|
end
|
|
190
215
|
end
|
|
@@ -344,7 +344,7 @@ module BetterAuth
|
|
|
344
344
|
raise APIError.new("UNAUTHORIZED", message: "invalid_client")
|
|
345
345
|
end
|
|
346
346
|
|
|
347
|
-
def store_code(store, code:, client_id:, redirect_uri:, session:, scopes:, code_challenge: nil, code_challenge_method: nil, nonce: nil, reference_id: nil, auth_time: nil)
|
|
347
|
+
def store_code(store, code:, client_id:, redirect_uri:, session:, scopes:, code_challenge: nil, code_challenge_method: nil, nonce: nil, reference_id: nil, auth_time: nil, expires_in: 600)
|
|
348
348
|
store[:codes][code] = {
|
|
349
349
|
client_id: client_id,
|
|
350
350
|
redirect_uri: redirect_uri,
|
|
@@ -355,7 +355,7 @@ module BetterAuth
|
|
|
355
355
|
nonce: nonce,
|
|
356
356
|
reference_id: reference_id,
|
|
357
357
|
auth_time: auth_time || session_auth_time(session),
|
|
358
|
-
expires_at: Time.now +
|
|
358
|
+
expires_at: Time.now + expires_in.to_i
|
|
359
359
|
}
|
|
360
360
|
end
|
|
361
361
|
|
|
@@ -403,7 +403,7 @@ module BetterAuth
|
|
|
403
403
|
true
|
|
404
404
|
end
|
|
405
405
|
|
|
406
|
-
def issue_tokens(ctx, store, model:, client:, session:, scopes:, include_refresh: false, issuer: nil, jwt_audience: nil, access_token_expires_in: 3600, refresh_token_expires_in: 2_592_000, id_token_signer: nil, prefix: {}, audience: nil, grant_type: nil, custom_token_response_fields: nil, custom_access_token_claims: nil, custom_id_token_claims: nil, jwt_access_token: false, pairwise_secret: nil, nonce: nil, auth_time: nil, reference_id: nil, filter_id_token_claims_by_scope: false)
|
|
406
|
+
def issue_tokens(ctx, store, model:, client:, session:, scopes:, include_refresh: false, issuer: nil, jwt_audience: nil, access_token_expires_in: 3600, refresh_token_expires_in: 2_592_000, id_token_expires_in: 3600, id_token_signer: nil, prefix: {}, audience: nil, grant_type: nil, custom_token_response_fields: nil, custom_access_token_claims: nil, custom_id_token_claims: nil, jwt_access_token: false, use_jwt_plugin: false, pairwise_secret: nil, nonce: nil, auth_time: nil, reference_id: nil, filter_id_token_claims_by_scope: false)
|
|
407
407
|
data = stringify_keys(session || {})
|
|
408
408
|
user = stringify_keys(data["user"] || data[:user] || {})
|
|
409
409
|
session_data = stringify_keys(data["session"] || data[:session] || {})
|
|
@@ -417,7 +417,7 @@ module BetterAuth
|
|
|
417
417
|
scope = scope_string(scopes)
|
|
418
418
|
expires_at = Time.now + access_token_expires_in.to_i
|
|
419
419
|
access_token = if jwt_access_token && audience
|
|
420
|
-
build_jwt_access_token(ctx, client_data, user, session_data, scope, audience, issuer || issuer(ctx), expires_at, custom_access_token_claims, reference_id: token_reference_id)
|
|
420
|
+
build_jwt_access_token(ctx, client_data, user, session_data, scope, audience, issuer || issuer(ctx), expires_at, custom_access_token_claims, reference_id: token_reference_id, use_jwt_plugin: use_jwt_plugin)
|
|
421
421
|
else
|
|
422
422
|
apply_prefix(access_token_value, prefix, :access_token)
|
|
423
423
|
end
|
|
@@ -461,7 +461,9 @@ module BetterAuth
|
|
|
461
461
|
"issuer" => issuer || issuer(ctx),
|
|
462
462
|
"issuedAt" => Time.now
|
|
463
463
|
}
|
|
464
|
-
ctx.context.adapter.create(model: model, data: record)
|
|
464
|
+
created_access = ctx.context.adapter.create(model: model, data: record)
|
|
465
|
+
created = stringify_keys(created_access || {})
|
|
466
|
+
record = record.merge("id" => created["id"]) if created["id"]
|
|
465
467
|
stored_record = record.merge("user" => user, "session" => session_data, "client" => client_data)
|
|
466
468
|
store[:tokens][access_token_value] = stored_record
|
|
467
469
|
store[:tokens][access_token] = stored_record
|
|
@@ -476,7 +478,7 @@ module BetterAuth
|
|
|
476
478
|
}
|
|
477
479
|
response[:audience] = audience if audience
|
|
478
480
|
response[:refresh_token] = refresh_token if refresh_token
|
|
479
|
-
response[:id_token] = id_token(user.merge("id" => subject), client_data["clientId"], issuer || issuer(ctx), jwt_audience || client_data["clientId"], ctx: ctx, signer: id_token_signer, session_id: session_data["id"], include_sid: !!client_data["enableEndSession"], nonce: nonce, auth_time: token_auth_time, custom_claims: custom_id_token_claims, scopes: parse_scopes(scope), client: client_data, filter_claims_by_scope: filter_id_token_claims_by_scope) if parse_scopes(scope).include?("openid")
|
|
481
|
+
response[:id_token] = id_token(user.merge("id" => subject), client_data["clientId"], issuer || issuer(ctx), jwt_audience || client_data["clientId"], ctx: ctx, signer: id_token_signer, session_id: session_data["id"], include_sid: !!client_data["enableEndSession"], nonce: nonce, auth_time: token_auth_time, custom_claims: custom_id_token_claims, scopes: parse_scopes(scope), client: client_data, filter_claims_by_scope: filter_id_token_claims_by_scope, expires_in: id_token_expires_in, use_jwt_plugin: use_jwt_plugin) if parse_scopes(scope).include?("openid")
|
|
480
482
|
if custom_token_response_fields.respond_to?(:call)
|
|
481
483
|
extra = custom_token_response_fields.call({grant_type: grant_type, user: user.empty? ? nil : user, scopes: parse_scopes(scope), metadata: stringify_keys(client_data["metadata"] || {})})
|
|
482
484
|
response.merge!(stringify_keys(extra).reject { |key, _value| standard_token_response_field?(key) }.transform_keys(&:to_sym)) if extra.is_a?(Hash)
|
|
@@ -484,7 +486,7 @@ module BetterAuth
|
|
|
484
486
|
response
|
|
485
487
|
end
|
|
486
488
|
|
|
487
|
-
def refresh_tokens(ctx, store, model:, client:, refresh_token:, scopes: nil, issuer: nil, access_token_expires_in: 3600, refresh_token_expires_in: 2_592_000, id_token_signer: nil, prefix: {}, audience: nil, custom_token_response_fields: nil, custom_access_token_claims: nil, custom_id_token_claims: nil, jwt_access_token: false, pairwise_secret: nil, filter_id_token_claims_by_scope: false)
|
|
489
|
+
def refresh_tokens(ctx, store, model:, client:, refresh_token:, scopes: nil, issuer: nil, access_token_expires_in: 3600, refresh_token_expires_in: 2_592_000, id_token_expires_in: 3600, id_token_signer: nil, prefix: {}, audience: nil, custom_token_response_fields: nil, custom_access_token_claims: nil, custom_id_token_claims: nil, jwt_access_token: false, use_jwt_plugin: false, pairwise_secret: nil, filter_id_token_claims_by_scope: false)
|
|
488
490
|
refresh_token_value = strip_prefix(refresh_token, prefix, :refresh_token)
|
|
489
491
|
data = refresh_token_value ? store[:refresh_tokens][refresh_token_value] : nil
|
|
490
492
|
raise APIError.new("BAD_REQUEST", message: "invalid_grant") unless data
|
|
@@ -494,6 +496,11 @@ module BetterAuth
|
|
|
494
496
|
end
|
|
495
497
|
raise APIError.new("BAD_REQUEST", message: "invalid_grant") if data["expiresAt"] && data["expiresAt"] <= Time.now
|
|
496
498
|
|
|
499
|
+
client_data = stringify_keys(client)
|
|
500
|
+
unless data["clientId"].to_s == client_data["clientId"].to_s
|
|
501
|
+
raise APIError.new("BAD_REQUEST", message: "invalid_grant")
|
|
502
|
+
end
|
|
503
|
+
|
|
497
504
|
requested = scopes ? parse_scopes(scopes) : data["scopes"]
|
|
498
505
|
unless requested.all? { |scope| data["scopes"].include?(scope) }
|
|
499
506
|
raise APIError.new("BAD_REQUEST", message: "invalid_scope")
|
|
@@ -520,7 +527,9 @@ module BetterAuth
|
|
|
520
527
|
custom_access_token_claims: custom_access_token_claims,
|
|
521
528
|
custom_id_token_claims: custom_id_token_claims,
|
|
522
529
|
jwt_access_token: jwt_access_token,
|
|
530
|
+
use_jwt_plugin: use_jwt_plugin,
|
|
523
531
|
pairwise_secret: pairwise_secret,
|
|
532
|
+
id_token_expires_in: id_token_expires_in,
|
|
524
533
|
auth_time: data["authTime"],
|
|
525
534
|
reference_id: data["referenceId"],
|
|
526
535
|
filter_id_token_claims_by_scope: filter_id_token_claims_by_scope
|
|
@@ -537,13 +546,13 @@ module BetterAuth
|
|
|
537
546
|
data
|
|
538
547
|
end
|
|
539
548
|
|
|
540
|
-
def build_jwt_access_token(ctx, client, user, session, scope, audience, issuer_value, expires_at, custom_claims, reference_id: nil)
|
|
549
|
+
def build_jwt_access_token(ctx, client, user, session, scope, audience, issuer_value, expires_at, custom_claims, reference_id: nil, use_jwt_plugin: false)
|
|
541
550
|
scopes = parse_scopes(scope)
|
|
542
551
|
extra = if custom_claims.respond_to?(:call)
|
|
543
552
|
custom_claims.call({user: user.empty? ? nil : user, scopes: scopes, resource: audience, reference_id: reference_id, metadata: stringify_keys(client["metadata"] || {})})
|
|
544
553
|
end
|
|
545
554
|
payload = (extra.is_a?(Hash) ? stringify_keys(extra) : {}).merge(
|
|
546
|
-
"sub" => user["id"],
|
|
555
|
+
"sub" => user["id"] || client["clientId"],
|
|
547
556
|
"aud" => audience,
|
|
548
557
|
"azp" => client["clientId"],
|
|
549
558
|
"scope" => scope,
|
|
@@ -555,10 +564,15 @@ module BetterAuth
|
|
|
555
564
|
"iat" => Time.now.to_i,
|
|
556
565
|
"exp" => expires_at.to_i
|
|
557
566
|
).compact
|
|
567
|
+
if use_jwt_plugin
|
|
568
|
+
signed = sign_oauth_jwt(ctx, payload, issuer: issuer_value, audience: audience)
|
|
569
|
+
return signed if signed
|
|
570
|
+
end
|
|
571
|
+
|
|
558
572
|
::JWT.encode(payload, ctx.context.secret, "HS256")
|
|
559
573
|
end
|
|
560
574
|
|
|
561
|
-
def userinfo(store, authorization, additional_claim: nil, prefix: {}, jwt_secret: nil)
|
|
575
|
+
def userinfo(store, authorization, additional_claim: nil, prefix: {}, jwt_secret: nil, ctx: nil, issuer: nil)
|
|
562
576
|
if authorization.to_s.strip.empty?
|
|
563
577
|
raise APIError.new(
|
|
564
578
|
"UNAUTHORIZED",
|
|
@@ -568,7 +582,7 @@ module BetterAuth
|
|
|
568
582
|
end
|
|
569
583
|
token = authorization.to_s.delete_prefix("Bearer ").strip
|
|
570
584
|
record = token_record(store, token, prefix: prefix)
|
|
571
|
-
return jwt_userinfo(token, jwt_secret, additional_claim: additional_claim) unless record
|
|
585
|
+
return jwt_userinfo(token, jwt_secret, additional_claim: additional_claim, ctx: ctx, issuer: issuer) unless record
|
|
572
586
|
user = stringify_keys(record["user"])
|
|
573
587
|
scopes = parse_scopes(record["scopes"])
|
|
574
588
|
raise userinfo_openid_scope_error unless scopes.include?("openid")
|
|
@@ -593,8 +607,12 @@ module BetterAuth
|
|
|
593
607
|
response
|
|
594
608
|
end
|
|
595
609
|
|
|
596
|
-
def jwt_userinfo(token, jwt_secret, additional_claim: nil)
|
|
597
|
-
payload =
|
|
610
|
+
def jwt_userinfo(token, jwt_secret, additional_claim: nil, ctx: nil, issuer: nil)
|
|
611
|
+
payload = if ctx
|
|
612
|
+
verify_oauth_jwt(ctx, token, issuer: issuer || issuer(ctx), hs256_secret: jwt_secret)
|
|
613
|
+
else
|
|
614
|
+
::JWT.decode(token, jwt_secret.to_s, true, algorithm: "HS256").first
|
|
615
|
+
end
|
|
598
616
|
scopes = parse_scopes(payload["scope"])
|
|
599
617
|
raise userinfo_openid_scope_error unless scopes.include?("openid")
|
|
600
618
|
|
|
@@ -700,7 +718,7 @@ module BetterAuth
|
|
|
700
718
|
end
|
|
701
719
|
end
|
|
702
720
|
|
|
703
|
-
def id_token(user, client_id, issuer_value, audience, ctx: nil, signer: nil, session_id: nil, include_sid: false, nonce: nil, auth_time: nil, custom_claims: nil, scopes: [], client: {}, filter_claims_by_scope: false)
|
|
721
|
+
def id_token(user, client_id, issuer_value, audience, ctx: nil, signer: nil, session_id: nil, include_sid: false, nonce: nil, auth_time: nil, custom_claims: nil, scopes: [], client: {}, filter_claims_by_scope: false, expires_in: 3600, use_jwt_plugin: false)
|
|
704
722
|
requested_scopes = parse_scopes(scopes)
|
|
705
723
|
payload = {
|
|
706
724
|
sub: user["id"],
|
|
@@ -726,13 +744,54 @@ module BetterAuth
|
|
|
726
744
|
end
|
|
727
745
|
return signer.call(ctx, payload) if signer.respond_to?(:call)
|
|
728
746
|
|
|
747
|
+
if use_jwt_plugin && ctx
|
|
748
|
+
signed = sign_oauth_jwt(ctx, payload, issuer: issuer_value, audience: audience)
|
|
749
|
+
return signed if signed
|
|
750
|
+
end
|
|
751
|
+
|
|
729
752
|
Crypto.sign_jwt(
|
|
730
753
|
payload,
|
|
731
|
-
client_id
|
|
732
|
-
expires_in:
|
|
754
|
+
id_token_hs256_key(ctx, client_id, stringify_keys(client)["clientSecret"] || stringify_keys(client)["client_secret"]),
|
|
755
|
+
expires_in: expires_in
|
|
733
756
|
)
|
|
734
757
|
end
|
|
735
758
|
|
|
759
|
+
def id_token_hs256_key(ctx, client_id, client_secret = nil)
|
|
760
|
+
return client_secret.to_s unless client_secret.to_s.empty?
|
|
761
|
+
|
|
762
|
+
label = client_id.to_s.empty? ? "better-auth" : client_id.to_s
|
|
763
|
+
OpenSSL::HMAC.hexdigest("SHA256", ctx.context.secret.to_s, "oidc.id_token.#{label}")
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def jwt_plugin_options(ctx)
|
|
767
|
+
plugin = ctx.context.options.plugins.find { |entry| entry.id == "jwt" }
|
|
768
|
+
plugin&.options
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def oauth_jwt_config(ctx, issuer:, audience:)
|
|
772
|
+
options = jwt_plugin_options(ctx)
|
|
773
|
+
return nil unless options
|
|
774
|
+
|
|
775
|
+
BetterAuth::Plugins.deep_merge(options, jwt: {issuer: issuer, audience: audience})
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def sign_oauth_jwt(ctx, payload, issuer:, audience:)
|
|
779
|
+
config = oauth_jwt_config(ctx, issuer: issuer, audience: audience)
|
|
780
|
+
return nil unless config
|
|
781
|
+
|
|
782
|
+
BetterAuth::Plugins.sign_jwt_payload(ctx, stringify_keys(payload), config)
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def verify_oauth_jwt(ctx, token, issuer:, hs256_secret:)
|
|
786
|
+
payload = ::JWT.decode(token.to_s, nil, false).first
|
|
787
|
+
audience = payload["aud"]
|
|
788
|
+
config = oauth_jwt_config(ctx, issuer: issuer, audience: audience.is_a?(Array) ? audience.first : audience)
|
|
789
|
+
verified = BetterAuth::Plugins.verify_jwt_token(ctx, token, config) if config
|
|
790
|
+
return verified if verified
|
|
791
|
+
|
|
792
|
+
::JWT.decode(token, hs256_secret.to_s, true, algorithm: "HS256").first
|
|
793
|
+
end
|
|
794
|
+
|
|
736
795
|
def standard_token_response_field?(key)
|
|
737
796
|
%w[access_token token_type expires_in scope refresh_token id_token audience].include?(key.to_s)
|
|
738
797
|
end
|
|
@@ -801,11 +860,11 @@ module BetterAuth
|
|
|
801
860
|
def verify_client_secret(ctx, stored_secret, provided_secret, mode)
|
|
802
861
|
mode = normalize_secret_storage_mode(mode)
|
|
803
862
|
return Crypto.constant_time_compare(Crypto.sha256(provided_secret, encoding: :base64url), stored_secret.to_s) if mode == "hashed"
|
|
804
|
-
return Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_secret)
|
|
863
|
+
return Crypto.constant_time_compare(Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_secret).to_s, provided_secret.to_s) if mode == "encrypted"
|
|
805
864
|
|
|
806
865
|
if mode.is_a?(Hash)
|
|
807
|
-
return mode[:hash].call(provided_secret).to_s
|
|
808
|
-
return mode[:decrypt].call(stored_secret).to_s
|
|
866
|
+
return Crypto.constant_time_compare(mode[:hash].call(provided_secret).to_s, stored_secret.to_s) if mode[:hash].respond_to?(:call)
|
|
867
|
+
return Crypto.constant_time_compare(mode[:decrypt].call(stored_secret).to_s, provided_secret.to_s) if mode[:decrypt].respond_to?(:call)
|
|
809
868
|
end
|
|
810
869
|
|
|
811
870
|
Crypto.constant_time_compare(stored_secret.to_s, provided_secret.to_s)
|
|
@@ -437,7 +437,16 @@ module BetterAuth
|
|
|
437
437
|
Endpoint.new(
|
|
438
438
|
path: "/oauth2/endsession",
|
|
439
439
|
method: ["GET", "POST"],
|
|
440
|
-
metadata:
|
|
440
|
+
metadata: {
|
|
441
|
+
openapi: {
|
|
442
|
+
operationId: "oauth2EndSession",
|
|
443
|
+
description: "RP-Initiated Logout endpoint",
|
|
444
|
+
responses: {
|
|
445
|
+
"302" => {description: "Redirects after clearing the session cookie"}
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]
|
|
449
|
+
}
|
|
441
450
|
) do |ctx|
|
|
442
451
|
input_source = (ctx.method == "GET") ? ctx.query : ctx.body
|
|
443
452
|
input = OAuthProtocol.stringify_keys(input_source)
|
|
@@ -90,6 +90,31 @@ module BetterAuth
|
|
|
90
90
|
)
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
def ref_schema(name, type: "object")
|
|
94
|
+
{type: type, "$ref": "#/components/schemas/#{name}"}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def array_schema(items)
|
|
98
|
+
{type: "array", items: items}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def nullable(type)
|
|
102
|
+
types = Array(type)
|
|
103
|
+
(types.include?("null") ? types : types + ["null"])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def query_parameter(name, required: false, schema: {type: "string"}, description: nil)
|
|
107
|
+
parameter = {name: name, in: "query", required: required, schema: schema}
|
|
108
|
+
parameter[:description] = description if description
|
|
109
|
+
parameter
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def path_parameter(name, schema: {type: "string"}, description: nil)
|
|
113
|
+
parameter = {name: name, in: "path", required: true, schema: schema}
|
|
114
|
+
parameter[:description] = description if description
|
|
115
|
+
parameter
|
|
116
|
+
end
|
|
117
|
+
|
|
93
118
|
def empty_request_body
|
|
94
119
|
{
|
|
95
120
|
content: {
|
|
@@ -243,10 +268,12 @@ module BetterAuth
|
|
|
243
268
|
|
|
244
269
|
def open_api_paths(endpoints, options)
|
|
245
270
|
disabled_paths = Array(options.disabled_paths).map(&:to_s)
|
|
246
|
-
endpoints.each_with_object({}) do |(
|
|
271
|
+
endpoints.each_with_object({}) do |(key, endpoint, tag), paths|
|
|
247
272
|
next unless endpoint.path
|
|
248
|
-
next if endpoint.metadata[:
|
|
273
|
+
next if endpoint.metadata[:exclude_from_openapi] || endpoint.metadata[:SERVER_ONLY] || endpoint.metadata[:server_only]
|
|
274
|
+
next if endpoint.metadata[:hide] && !open_api_documented_hidden_endpoint?(key, endpoint, tag)
|
|
249
275
|
next if disabled_paths.include?(endpoint.path)
|
|
276
|
+
next if key == :set_password
|
|
250
277
|
|
|
251
278
|
path = open_api_path(endpoint.path)
|
|
252
279
|
paths[path] ||= {}
|
|
@@ -260,6 +287,10 @@ module BetterAuth
|
|
|
260
287
|
path.split("/").map { |part| part.start_with?(":") ? "{#{part.delete_prefix(":")}}" : part }.join("/")
|
|
261
288
|
end
|
|
262
289
|
|
|
290
|
+
def open_api_documented_hidden_endpoint?(key, endpoint, tag)
|
|
291
|
+
tag == "Default" && [:ok, :error].include?(key) && endpoint.metadata[:openapi]
|
|
292
|
+
end
|
|
293
|
+
|
|
263
294
|
def open_api_operation(endpoint, method, tag)
|
|
264
295
|
metadata = endpoint.metadata[:openapi] || {}
|
|
265
296
|
operation = {
|
|
@@ -314,6 +345,8 @@ module BetterAuth
|
|
|
314
345
|
|
|
315
346
|
def open_api_html(schema, config)
|
|
316
347
|
nonce = config[:nonce] ? " nonce=\"#{config[:nonce]}\"" : ""
|
|
348
|
+
nonce_attr = config[:nonce] ? "nonce=\"#{config[:nonce]}\"" : ""
|
|
349
|
+
encoded_logo = open_api_encode_uri_component(open_api_logo)
|
|
317
350
|
<<~HTML
|
|
318
351
|
<!doctype html>
|
|
319
352
|
<html>
|
|
@@ -323,12 +356,45 @@ module BetterAuth
|
|
|
323
356
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
324
357
|
</head>
|
|
325
358
|
<body>
|
|
326
|
-
<script
|
|
327
|
-
|
|
359
|
+
<script
|
|
360
|
+
id="api-reference"
|
|
361
|
+
type="application/json">
|
|
362
|
+
#{JSON.generate(schema)}
|
|
363
|
+
</script>
|
|
364
|
+
<script #{nonce_attr}>
|
|
365
|
+
var configuration = {
|
|
366
|
+
favicon: "data:image/svg+xml;utf8,#{encoded_logo}",
|
|
367
|
+
theme: "#{config[:theme] || "default"}",
|
|
368
|
+
metaData: {
|
|
369
|
+
title: "Better Auth API",
|
|
370
|
+
description: "API Reference for your Better Auth Instance",
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
document.getElementById('api-reference').dataset.configuration =
|
|
375
|
+
JSON.stringify(configuration)
|
|
376
|
+
</script>
|
|
328
377
|
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"#{nonce}></script>
|
|
329
378
|
</body>
|
|
330
379
|
</html>
|
|
331
380
|
HTML
|
|
332
381
|
end
|
|
382
|
+
|
|
383
|
+
def open_api_logo
|
|
384
|
+
<<~SVG
|
|
385
|
+
<svg width="75" height="75" viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
386
|
+
<rect width="75" height="75" rx="14" fill="#050505"/>
|
|
387
|
+
<path d="M19 50V25h15.5c5.5 0 9 2.6 9 6.6 0 2.7-1.6 4.7-4.1 5.7 3.2.9 5.2 3 5.2 6.1 0 4.2-3.7 6.6-9.6 6.6H19Zm7.1-14.8h7.2c2.1 0 3.2-.9 3.2-2.4s-1.1-2.4-3.2-2.4h-7.2v4.8Zm0 9.5h7.9c2.2 0 3.5-.9 3.5-2.5s-1.3-2.6-3.5-2.6h-7.9v5.1Z" fill="white"/>
|
|
388
|
+
<path d="M47 50V25h7.1v25H47Z" fill="white"/>
|
|
389
|
+
</svg>
|
|
390
|
+
SVG
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def open_api_encode_uri_component(value)
|
|
394
|
+
value.to_s.bytes.map do |byte|
|
|
395
|
+
char = byte.chr
|
|
396
|
+
char.match?(/[A-Za-z0-9\-_.!~*'()]/) ? char : "%%%02X" % byte
|
|
397
|
+
end.join
|
|
398
|
+
end
|
|
333
399
|
end
|
|
334
400
|
end
|
data/lib/better_auth/router.rb
CHANGED
|
@@ -62,8 +62,9 @@ module BetterAuth
|
|
|
62
62
|
return run_on_response_chain(method_not_allowed(allowed_methods)) unless endpoint.matches_method?(request.request_method)
|
|
63
63
|
return run_on_response_chain(unsupported_media_type) unless allowed_media_type?(request, endpoint)
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
raw_body = read_raw_body(request)
|
|
66
|
+
body = parse_body(request, raw_body: raw_body, disable_body: endpoint.disable_body)
|
|
67
|
+
endpoint_context = build_endpoint_context(request, route_path, query, body, params, raw_body)
|
|
67
68
|
return run_on_response_chain(forbidden) if server_only?(endpoint)
|
|
68
69
|
|
|
69
70
|
response = @origin_check.call(endpoint_context)
|
|
@@ -80,7 +81,7 @@ module BetterAuth
|
|
|
80
81
|
response = rate_limiter.call(request, context, route_path)
|
|
81
82
|
return run_on_response_chain(response) if response
|
|
82
83
|
|
|
83
|
-
endpoint_context = rebuild_endpoint_context(endpoint_context, request, route_path, params)
|
|
84
|
+
endpoint_context = rebuild_endpoint_context(endpoint_context, request, route_path, params, endpoint)
|
|
84
85
|
result = API.new(context, endpoints).execute(endpoint, endpoint_context)
|
|
85
86
|
response = result.response.is_a?(APIError) ? error_response(result.response, headers: result.headers) : result.to_rack_response
|
|
86
87
|
run_on_response_chain(response)
|
|
@@ -161,12 +162,19 @@ module BetterAuth
|
|
|
161
162
|
path.empty? ? "/" : path
|
|
162
163
|
end
|
|
163
164
|
|
|
164
|
-
def
|
|
165
|
-
return
|
|
165
|
+
def read_raw_body(request)
|
|
166
|
+
return "" unless request.body
|
|
166
167
|
|
|
167
168
|
request.body.rewind
|
|
168
169
|
raw = request.body.read.to_s
|
|
169
170
|
request.body.rewind
|
|
171
|
+
raw
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def parse_body(request, raw_body:, disable_body: false)
|
|
175
|
+
return {} if disable_body
|
|
176
|
+
|
|
177
|
+
raw = raw_body.to_s
|
|
170
178
|
return {} if raw.empty?
|
|
171
179
|
|
|
172
180
|
if json_media_type?(request.media_type)
|
|
@@ -203,7 +211,7 @@ module BetterAuth
|
|
|
203
211
|
request.GET
|
|
204
212
|
end
|
|
205
213
|
|
|
206
|
-
def build_endpoint_context(request, path, query, body, params)
|
|
214
|
+
def build_endpoint_context(request, path, query, body, params, raw_body)
|
|
207
215
|
Endpoint::Context.new(
|
|
208
216
|
path: path,
|
|
209
217
|
method: request.request_method,
|
|
@@ -212,12 +220,14 @@ module BetterAuth
|
|
|
212
220
|
params: params,
|
|
213
221
|
headers: headers_from(request.env),
|
|
214
222
|
context: context,
|
|
215
|
-
request: request
|
|
223
|
+
request: request,
|
|
224
|
+
raw_body: raw_body
|
|
216
225
|
)
|
|
217
226
|
end
|
|
218
227
|
|
|
219
|
-
def rebuild_endpoint_context(previous_context, request, route_path, params)
|
|
220
|
-
|
|
228
|
+
def rebuild_endpoint_context(previous_context, request, route_path, params, endpoint)
|
|
229
|
+
raw_body = read_raw_body(request)
|
|
230
|
+
fresh_context = build_endpoint_context(request, route_path, parse_query(request), parse_body(request, raw_body: raw_body, disable_body: endpoint.disable_body), params, raw_body)
|
|
221
231
|
fresh_context.headers = merge_hashes(previous_context.headers, fresh_context.headers)
|
|
222
232
|
fresh_context.query = merge_hashes(previous_context.query, fresh_context.query)
|
|
223
233
|
fresh_context.body = merge_hashes(previous_context.body, fresh_context.body)
|
|
@@ -229,7 +229,29 @@ module BetterAuth
|
|
|
229
229
|
}
|
|
230
230
|
],
|
|
231
231
|
responses: {
|
|
232
|
-
"200" => OpenAPI.json_response(
|
|
232
|
+
"200" => OpenAPI.json_response(
|
|
233
|
+
"Success",
|
|
234
|
+
OpenAPI.object_schema(
|
|
235
|
+
{
|
|
236
|
+
user: OpenAPI.object_schema(
|
|
237
|
+
{
|
|
238
|
+
id: {type: "string"},
|
|
239
|
+
name: {type: "string"},
|
|
240
|
+
email: {type: "string"},
|
|
241
|
+
image: {type: "string"},
|
|
242
|
+
emailVerified: {type: "boolean"}
|
|
243
|
+
},
|
|
244
|
+
required: ["id", "emailVerified"]
|
|
245
|
+
),
|
|
246
|
+
data: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {},
|
|
249
|
+
additionalProperties: true
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
required: ["user", "data"]
|
|
253
|
+
).merge(additionalProperties: false)
|
|
254
|
+
)
|
|
233
255
|
}
|
|
234
256
|
}
|
|
235
257
|
}
|
|
@@ -8,7 +8,26 @@ module BetterAuth
|
|
|
8
8
|
Endpoint.new(
|
|
9
9
|
path: "/error",
|
|
10
10
|
method: "GET",
|
|
11
|
-
metadata: {
|
|
11
|
+
metadata: {
|
|
12
|
+
hide: true,
|
|
13
|
+
openapi: {
|
|
14
|
+
description: "Displays an error page",
|
|
15
|
+
responses: {
|
|
16
|
+
"200" => OpenAPI.json_response(
|
|
17
|
+
"Success",
|
|
18
|
+
OpenAPI.object_schema(
|
|
19
|
+
{
|
|
20
|
+
html: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "The HTML content of the error page"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
required: ["html"]
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
) do |ctx|
|
|
13
32
|
query = ctx.query || {}
|
|
14
33
|
raw_code = query["error"] || query[:error] || "UNKNOWN"
|
|
@@ -6,7 +6,26 @@ module BetterAuth
|
|
|
6
6
|
Endpoint.new(
|
|
7
7
|
path: "/ok",
|
|
8
8
|
method: "GET",
|
|
9
|
-
metadata: {
|
|
9
|
+
metadata: {
|
|
10
|
+
hide: true,
|
|
11
|
+
openapi: {
|
|
12
|
+
description: "Check if the API is working",
|
|
13
|
+
responses: {
|
|
14
|
+
"200" => OpenAPI.json_response(
|
|
15
|
+
"API is working",
|
|
16
|
+
OpenAPI.object_schema(
|
|
17
|
+
{
|
|
18
|
+
ok: {
|
|
19
|
+
type: "boolean",
|
|
20
|
+
description: "Indicates if the API is working"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
required: ["ok"]
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
10
29
|
) do |ctx|
|
|
11
30
|
ctx.json({ok: true})
|
|
12
31
|
end
|
|
@@ -11,7 +11,7 @@ module BetterAuth
|
|
|
11
11
|
operationId: "getSession",
|
|
12
12
|
description: "Get the current session",
|
|
13
13
|
responses: {
|
|
14
|
-
"200" => OpenAPI.json_response("Current session or null", OpenAPI.session_response_schema_pair.merge(
|
|
14
|
+
"200" => OpenAPI.json_response("Current session or null", OpenAPI.session_response_schema_pair.merge(type: ["object", "null"]))
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
}
|
data/lib/better_auth/version.rb
CHANGED
data/lib/better_auth.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "better_auth/configuration"
|
|
|
21
21
|
require_relative "better_auth/schema"
|
|
22
22
|
require_relative "better_auth/schema/sql"
|
|
23
23
|
require_relative "better_auth/adapters/base"
|
|
24
|
+
require_relative "better_auth/adapters/join_support"
|
|
24
25
|
require_relative "better_auth/adapters/memory"
|
|
25
26
|
require_relative "better_auth/adapters/sql"
|
|
26
27
|
require_relative "better_auth/adapters/postgres"
|
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.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Sala
|
|
@@ -259,6 +259,7 @@ files:
|
|
|
259
259
|
- lib/better_auth.rb
|
|
260
260
|
- lib/better_auth/adapters/base.rb
|
|
261
261
|
- lib/better_auth/adapters/internal_adapter.rb
|
|
262
|
+
- lib/better_auth/adapters/join_support.rb
|
|
262
263
|
- lib/better_auth/adapters/memory.rb
|
|
263
264
|
- lib/better_auth/adapters/mongodb.rb
|
|
264
265
|
- lib/better_auth/adapters/mssql.rb
|