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
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module BetterAuth
|
|
7
|
+
module Plugins
|
|
8
|
+
OAUTH_POPUP_MESSAGE_TYPE = "better-auth:oauth-popup"
|
|
9
|
+
OAUTH_POPUP_DATA_ELEMENT_ID = "better-auth-oauth-popup"
|
|
10
|
+
OAUTH_POPUP_MARKER_COOKIE = "oauth_popup"
|
|
11
|
+
OAUTH_POPUP_ERROR_CODES = {
|
|
12
|
+
"POPUP_SIGN_IN_FAILED" => "Popup sign-in failed",
|
|
13
|
+
"POPUP_BLOCKED" => "Sign-in popup was blocked by the browser",
|
|
14
|
+
"POPUP_CLOSED" => "Sign-in popup was closed before completing",
|
|
15
|
+
"POPUP_TIMEOUT" => "Sign-in popup timed out"
|
|
16
|
+
}.freeze
|
|
17
|
+
OAUTH_POPUP_INTERNAL_STATE_KEYS = OAuthState::INTERNAL_KEYS
|
|
18
|
+
OAUTH_POPUP_COMPLETE_SCRIPT = [
|
|
19
|
+
"(function () {",
|
|
20
|
+
"\tvar el = document.getElementById(\"better-auth-oauth-popup\");",
|
|
21
|
+
"\tif (!el) return;",
|
|
22
|
+
"\tvar payload;",
|
|
23
|
+
"\ttry {",
|
|
24
|
+
"\t\tpayload = JSON.parse(el.textContent || \"\");",
|
|
25
|
+
"\t} catch (e) {",
|
|
26
|
+
"\t\treturn;",
|
|
27
|
+
"\t}",
|
|
28
|
+
"\tvar target = window.opener || window.parent;",
|
|
29
|
+
"\tif (target && target !== window) {",
|
|
30
|
+
"\t\ttry {",
|
|
31
|
+
"\t\t\ttarget.postMessage(",
|
|
32
|
+
"\t\t\t\t{",
|
|
33
|
+
"\t\t\t\t\ttype: payload.type,",
|
|
34
|
+
"\t\t\t\t\tnonce: payload.nonce,",
|
|
35
|
+
"\t\t\t\t\ttoken: payload.token,",
|
|
36
|
+
"\t\t\t\t\tredirectTo: payload.redirectTo,",
|
|
37
|
+
"\t\t\t\t\terror: payload.error,",
|
|
38
|
+
"\t\t\t\t},",
|
|
39
|
+
"\t\t\t\tpayload.targetOrigin,",
|
|
40
|
+
"\t\t\t);",
|
|
41
|
+
"\t\t} catch (e) {}",
|
|
42
|
+
"\t}",
|
|
43
|
+
"\twindow.close();",
|
|
44
|
+
"})();",
|
|
45
|
+
""
|
|
46
|
+
].join("\n")
|
|
47
|
+
OAUTH_POPUP_SCRIPT_CSP_HASH = "sha256-tIo2K8VBC9SnhvdZ+9GsGkQoZm+jm/JcxL+d+i8b8KQ="
|
|
48
|
+
|
|
49
|
+
module_function
|
|
50
|
+
|
|
51
|
+
def oauth_popup
|
|
52
|
+
Plugin.new(
|
|
53
|
+
id: "oauth-popup",
|
|
54
|
+
endpoints: {
|
|
55
|
+
oauth_popup_start: oauth_popup_start_endpoint
|
|
56
|
+
},
|
|
57
|
+
hooks: {
|
|
58
|
+
after: [
|
|
59
|
+
{
|
|
60
|
+
matcher: ->(ctx) { oauth_popup_callback_path?(ctx.path) },
|
|
61
|
+
handler: ->(ctx) { oauth_popup_after_callback(ctx) }
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
error_codes: OAUTH_POPUP_ERROR_CODES
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def oauth_popup_start_endpoint
|
|
70
|
+
Endpoint.new(
|
|
71
|
+
path: "/oauth-popup/start",
|
|
72
|
+
method: "GET",
|
|
73
|
+
query_schema: Routes.request_query_schema(
|
|
74
|
+
required_strings: %w[provider popupOrigin],
|
|
75
|
+
optional_strings: %w[popupNonce callbackURL errorCallbackURL newUserCallbackURL scopes requestSignUp additionalData]
|
|
76
|
+
),
|
|
77
|
+
metadata: {
|
|
78
|
+
hide: true,
|
|
79
|
+
openapi: {
|
|
80
|
+
parameters: [
|
|
81
|
+
{name: "provider", in: "query", required: true, schema: {type: "string"}},
|
|
82
|
+
{name: "popupOrigin", in: "query", required: true, schema: {type: "string"}},
|
|
83
|
+
{name: "popupNonce", in: "query", required: false, schema: {type: "string"}},
|
|
84
|
+
{name: "callbackURL", in: "query", required: false, schema: {type: "string"}},
|
|
85
|
+
{name: "errorCallbackURL", in: "query", required: false, schema: {type: "string"}},
|
|
86
|
+
{name: "newUserCallbackURL", in: "query", required: false, schema: {type: "string"}},
|
|
87
|
+
{name: "scopes", in: "query", required: false, schema: {type: "string"}},
|
|
88
|
+
{name: "requestSignUp", in: "query", required: false, schema: {type: "string"}},
|
|
89
|
+
{name: "additionalData", in: "query", required: false, schema: {type: "string"}}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
) do |ctx|
|
|
94
|
+
query = normalize_hash(ctx.query)
|
|
95
|
+
popup_origin = oauth_popup_validate_origin!(ctx, query[:popup_origin].to_s)
|
|
96
|
+
|
|
97
|
+
nonce = query[:popup_nonce].to_s
|
|
98
|
+
invalid_redirect = oauth_popup_invalid_redirect(ctx, query, popup_origin, nonce)
|
|
99
|
+
next invalid_redirect if invalid_redirect
|
|
100
|
+
|
|
101
|
+
provider_id = query[:provider].to_s
|
|
102
|
+
provider = Routes.social_provider(ctx.context, provider_id)
|
|
103
|
+
unless provider
|
|
104
|
+
next oauth_popup_completion(ctx, popup_origin, nonce: nonce, error: {
|
|
105
|
+
code: "provider_not_found",
|
|
106
|
+
description: "Unknown provider: #{provider_id}"
|
|
107
|
+
})
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
code_verifier = Crypto.random_string(128)
|
|
111
|
+
scopes = query[:scopes]&.to_s&.split(",")
|
|
112
|
+
additional_data = oauth_popup_additional_data(query[:additional_data])
|
|
113
|
+
callback_url = query[:callback_url].to_s
|
|
114
|
+
callback_url = ctx.context.base_url if callback_url.empty?
|
|
115
|
+
request_sign_up = (query[:request_sign_up].to_s == "true") ? true : nil
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
authorization_url = if (start = fetch_value(provider, "oauthPopupAuthorizationUrl")).respond_to?(:call)
|
|
119
|
+
start.call(
|
|
120
|
+
ctx,
|
|
121
|
+
callbackURL: callback_url,
|
|
122
|
+
errorCallbackURL: query[:error_callback_url],
|
|
123
|
+
newUserCallbackURL: query[:new_user_callback_url],
|
|
124
|
+
requestSignUp: request_sign_up,
|
|
125
|
+
scopes: scopes,
|
|
126
|
+
additionalData: additional_data,
|
|
127
|
+
codeVerifier: code_verifier
|
|
128
|
+
)
|
|
129
|
+
else
|
|
130
|
+
state_data = additional_data.merge(
|
|
131
|
+
"callbackURL" => callback_url,
|
|
132
|
+
"errorURL" => query[:error_callback_url],
|
|
133
|
+
"newUserURL" => query[:new_user_callback_url],
|
|
134
|
+
"requestSignUp" => request_sign_up,
|
|
135
|
+
"codeVerifier" => code_verifier,
|
|
136
|
+
"expiresAt" => Time.now.to_i + 600
|
|
137
|
+
).compact
|
|
138
|
+
state = OAuthState.generate(ctx, state_data)
|
|
139
|
+
Routes.call_provider(provider, :create_authorization_url, {
|
|
140
|
+
state: state,
|
|
141
|
+
codeVerifier: code_verifier,
|
|
142
|
+
code_verifier: code_verifier,
|
|
143
|
+
redirectURI: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
144
|
+
redirect_uri: "#{ctx.context.canonical_base_url}/callback/#{provider_id}",
|
|
145
|
+
scopes: scopes
|
|
146
|
+
})
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
oauth_popup_set_marker(ctx, popup_origin, nonce)
|
|
150
|
+
rescue => error
|
|
151
|
+
oauth_popup_log(ctx.context, :error, "OAuth popup failed to start", error)
|
|
152
|
+
next oauth_popup_completion(ctx, popup_origin, nonce: nonce, error: {
|
|
153
|
+
code: "popup_sign_in_failed",
|
|
154
|
+
description: "Failed to start the OAuth flow."
|
|
155
|
+
})
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
raise ctx.redirect(authorization_url.to_s)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def oauth_popup_validate_origin!(ctx, popup_origin)
|
|
163
|
+
uri = URI.parse(popup_origin)
|
|
164
|
+
valid_scheme = %w[http https].include?(uri.scheme)
|
|
165
|
+
valid_path = uri.path.to_s.empty? || uri.path == "/"
|
|
166
|
+
absolute = valid_scheme && !uri.host.to_s.empty?
|
|
167
|
+
canonical = Configuration.origin_for(uri) if absolute
|
|
168
|
+
valid = absolute &&
|
|
169
|
+
uri.userinfo.nil? &&
|
|
170
|
+
valid_path &&
|
|
171
|
+
uri.query.nil? &&
|
|
172
|
+
uri.fragment.nil? &&
|
|
173
|
+
!popup_origin.match?(/[?*]/) &&
|
|
174
|
+
ctx.context.trusted_origin?(canonical, allow_relative_paths: false)
|
|
175
|
+
return canonical if valid
|
|
176
|
+
|
|
177
|
+
oauth_popup_log(ctx.context, :error, "OAuth popup origin is not trusted")
|
|
178
|
+
raise APIError.new("FORBIDDEN", code: "INVALID_ORIGIN", message: BASE_ERROR_CODES["INVALID_ORIGIN"])
|
|
179
|
+
rescue URI::InvalidURIError
|
|
180
|
+
raise APIError.new("FORBIDDEN", code: "INVALID_ORIGIN", message: BASE_ERROR_CODES["INVALID_ORIGIN"])
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def oauth_popup_invalid_redirect(ctx, query, popup_origin, nonce)
|
|
184
|
+
{
|
|
185
|
+
callback_url: "invalid_callback_url",
|
|
186
|
+
error_callback_url: "invalid_error_callback_url",
|
|
187
|
+
new_user_callback_url: "invalid_new_user_callback_url"
|
|
188
|
+
}.each do |key, code|
|
|
189
|
+
value = query[key]
|
|
190
|
+
next if value.to_s.empty?
|
|
191
|
+
next if ctx.context.trusted_origin?(value.to_s, allow_relative_paths: true)
|
|
192
|
+
|
|
193
|
+
oauth_popup_log(ctx.context, :error, "OAuth popup redirect URL is not trusted")
|
|
194
|
+
return oauth_popup_completion(ctx, popup_origin, nonce: nonce, error: {
|
|
195
|
+
code: code,
|
|
196
|
+
description: "Untrusted redirect URL"
|
|
197
|
+
})
|
|
198
|
+
end
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def oauth_popup_additional_data(value)
|
|
203
|
+
parsed = value.is_a?(Hash) ? value : JSON.parse(value.to_s)
|
|
204
|
+
return {} unless parsed.is_a?(Hash)
|
|
205
|
+
|
|
206
|
+
parsed.reject { |key, _entry| OAUTH_POPUP_INTERNAL_STATE_KEYS.include?(key.to_s) }
|
|
207
|
+
rescue JSON::ParserError
|
|
208
|
+
{}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def oauth_popup_set_marker(ctx, popup_origin, nonce)
|
|
212
|
+
cookie = ctx.context.create_auth_cookie(OAUTH_POPUP_MARKER_COOKIE, max_age: 600)
|
|
213
|
+
ctx.set_signed_cookie(
|
|
214
|
+
cookie.name,
|
|
215
|
+
JSON.generate({popupOrigin: popup_origin, popupNonce: nonce}),
|
|
216
|
+
ctx.context.secret,
|
|
217
|
+
cookie.attributes
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def oauth_popup_callback_path?(path)
|
|
222
|
+
path.to_s.start_with?("/callback/", "/oauth2/callback/")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def oauth_popup_after_callback(ctx)
|
|
226
|
+
location = ctx.response_headers["location"].to_s
|
|
227
|
+
return if location.empty?
|
|
228
|
+
|
|
229
|
+
marker_cookie = ctx.context.create_auth_cookie(OAUTH_POPUP_MARKER_COOKIE)
|
|
230
|
+
marker = ctx.get_signed_cookie(marker_cookie.name, ctx.context.secret)
|
|
231
|
+
return unless marker
|
|
232
|
+
|
|
233
|
+
Cookies.expire_cookie(ctx, marker_cookie)
|
|
234
|
+
data = JSON.parse(marker)
|
|
235
|
+
popup_origin = data.fetch("popupOrigin").to_s
|
|
236
|
+
nonce = data["popupNonce"].to_s
|
|
237
|
+
return if popup_origin.empty?
|
|
238
|
+
|
|
239
|
+
token = oauth_popup_session_token(ctx)
|
|
240
|
+
message = if token
|
|
241
|
+
{nonce: nonce, token: token, redirectTo: location}
|
|
242
|
+
else
|
|
243
|
+
error = oauth_popup_redirect_error(location)
|
|
244
|
+
return unless error
|
|
245
|
+
|
|
246
|
+
{nonce: nonce, error: error}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
oauth_popup_completion(ctx, popup_origin, message)
|
|
250
|
+
rescue JSON::ParserError, KeyError
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def oauth_popup_session_token(ctx)
|
|
255
|
+
name = ctx.context.auth_cookies[:session_token].name
|
|
256
|
+
Cookies.split_set_cookie_header(ctx.response_headers["set-cookie"]).each do |line|
|
|
257
|
+
cookie = Cookies.parse_set_cookie(line)
|
|
258
|
+
next unless cookie && cookie[:name] == name
|
|
259
|
+
max_age = cookie.dig(:attributes, "max-age")
|
|
260
|
+
next if cookie[:value].empty? || (max_age.to_s.strip.match?(/\A[+-]?\d+\z/) && max_age.to_i == 0)
|
|
261
|
+
|
|
262
|
+
return URI.decode_uri_component(cookie[:value])
|
|
263
|
+
rescue ArgumentError
|
|
264
|
+
return cookie[:value]
|
|
265
|
+
end
|
|
266
|
+
nil
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def oauth_popup_redirect_error(location)
|
|
270
|
+
uri = URI.parse(location)
|
|
271
|
+
params = URI.decode_www_form(uri.query.to_s).to_h
|
|
272
|
+
return if params["error"].to_s.empty?
|
|
273
|
+
|
|
274
|
+
{code: params["error"], description: params["error_description"]}.compact
|
|
275
|
+
rescue URI::InvalidURIError
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def oauth_popup_completion(ctx, popup_origin, message)
|
|
280
|
+
oauth_popup_warn_missing_bearer(ctx) if message[:token]
|
|
281
|
+
payload = {
|
|
282
|
+
type: OAUTH_POPUP_MESSAGE_TYPE,
|
|
283
|
+
targetOrigin: popup_origin
|
|
284
|
+
}.merge(message)
|
|
285
|
+
serialized = JSON.generate(payload)
|
|
286
|
+
.gsub("<", "\\u003c")
|
|
287
|
+
.gsub("\u2028", "\\u2028")
|
|
288
|
+
.gsub("\u2029", "\\u2029")
|
|
289
|
+
html = <<~HTML
|
|
290
|
+
<!doctype html>
|
|
291
|
+
<html>
|
|
292
|
+
<head><meta charset="utf-8"><title>Completing sign-in</title></head>
|
|
293
|
+
<body>
|
|
294
|
+
<script type="application/json" id="#{OAUTH_POPUP_DATA_ELEMENT_ID}">#{serialized}</script>
|
|
295
|
+
<script>#{OAUTH_POPUP_COMPLETE_SCRIPT}</script>
|
|
296
|
+
</body>
|
|
297
|
+
</html>
|
|
298
|
+
HTML
|
|
299
|
+
headers = ctx.response_headers.merge(
|
|
300
|
+
"content-type" => "text/html; charset=utf-8",
|
|
301
|
+
"content-security-policy" => "default-src 'none'; script-src '#{OAUTH_POPUP_SCRIPT_CSP_HASH}'; base-uri 'none'",
|
|
302
|
+
"cache-control" => "no-store",
|
|
303
|
+
"pragma" => "no-cache"
|
|
304
|
+
)
|
|
305
|
+
headers.delete("location")
|
|
306
|
+
Endpoint::Result.new(response: html, status: 200, headers: headers)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def oauth_popup_warn_missing_bearer(ctx)
|
|
310
|
+
return if ctx.context.options.plugins.any? { |plugin| plugin.id == "bearer" }
|
|
311
|
+
return if @oauth_popup_warned_missing_bearer
|
|
312
|
+
|
|
313
|
+
@oauth_popup_warned_missing_bearer = true
|
|
314
|
+
oauth_popup_log(
|
|
315
|
+
ctx.context,
|
|
316
|
+
:warn,
|
|
317
|
+
"OAuth popup hands the session token back via postMessage, but the `bearer` plugin is not registered. Add bearer() for embedded cross-site authentication."
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def oauth_popup_log(context, level, message, *details)
|
|
322
|
+
logger = context.logger
|
|
323
|
+
if logger.respond_to?(:call)
|
|
324
|
+
logger.call(level, message, *details)
|
|
325
|
+
elsif logger.respond_to?(level)
|
|
326
|
+
logger.public_send(level, message, *details)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "base64"
|
|
4
|
+
require "json"
|
|
4
5
|
require "jwt"
|
|
5
6
|
require "openssl"
|
|
6
7
|
require "time"
|
|
@@ -36,11 +37,11 @@ module BetterAuth
|
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def issuer(ctx)
|
|
39
|
-
|
|
40
|
+
origin_for(ctx.context.canonical_base_url)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def endpoint_base(ctx)
|
|
43
|
-
ctx.context.
|
|
44
|
+
ctx.context.canonical_base_url
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def origin_for(url)
|
|
@@ -59,6 +60,7 @@ module BetterAuth
|
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def validate_redirect_uri!(client, redirect_uri)
|
|
63
|
+
validate_safe_url!(redirect_uri, field: "redirect_uri")
|
|
62
64
|
redirects = client_redirect_uris(client)
|
|
63
65
|
return if redirects.include?(redirect_uri.to_s)
|
|
64
66
|
return if loopback_redirect_match?(redirects, redirect_uri)
|
|
@@ -105,15 +107,19 @@ module BetterAuth
|
|
|
105
107
|
value.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
106
108
|
end
|
|
107
109
|
|
|
108
|
-
def create_client(ctx, model:, body:, owner_session: nil, default_auth_method: "client_secret_basic", store_client_secret: "plain", unauthenticated: false, default_scopes: nil, allowed_scopes: nil, prefix: {}, dynamic_registration: false, admin: false, pairwise_secret: nil, strip_client_metadata: false, reference_id: nil)
|
|
110
|
+
def create_client(ctx, model:, body:, owner_session: nil, default_auth_method: "client_secret_basic", store_client_secret: "plain", unauthenticated: false, default_scopes: nil, allowed_scopes: nil, prefix: {}, dynamic_registration: false, admin: false, pairwise_secret: nil, strip_client_metadata: false, reference_id: nil, generate_client_id: nil, generate_client_secret: nil, client_registration_client_secret_expiration: nil)
|
|
109
111
|
body = request_body!(body || {})
|
|
110
112
|
requested_auth_method = body["token_endpoint_auth_method"] || default_auth_method
|
|
111
113
|
validate_client_metadata_enums!(requested_auth_method, body)
|
|
112
114
|
validate_admin_only_fields!(body, admin: admin)
|
|
113
115
|
auth_method = unauthenticated ? "none" : requested_auth_method
|
|
114
116
|
public_client = auth_method == "none"
|
|
115
|
-
client_id = Crypto.random_string(32)
|
|
116
|
-
client_secret = public_client
|
|
117
|
+
client_id = generate_client_id.respond_to?(:call) ? generate_client_id.call : Crypto.random_string(32)
|
|
118
|
+
client_secret = if public_client
|
|
119
|
+
nil
|
|
120
|
+
else
|
|
121
|
+
generate_client_secret.respond_to?(:call) ? generate_client_secret.call : Crypto.random_string(32)
|
|
122
|
+
end
|
|
117
123
|
redirects = Array(body["redirect_uris"]).map(&:to_s)
|
|
118
124
|
raise APIError.new("BAD_REQUEST", message: "redirect_uris is required") if redirects.empty?
|
|
119
125
|
redirects.each { |uri| validate_safe_url!(uri, field: "redirect_uris") }
|
|
@@ -146,6 +152,12 @@ module BetterAuth
|
|
|
146
152
|
else
|
|
147
153
|
body["type"] || (public_client ? nil : "web")
|
|
148
154
|
end
|
|
155
|
+
issued_at = Time.now.to_i
|
|
156
|
+
secret_expires_at = if client_secret && dynamic_registration && !client_registration_client_secret_expiration.nil?
|
|
157
|
+
resolve_client_secret_expires_at(client_registration_client_secret_expiration, issued_at)
|
|
158
|
+
elsif admin
|
|
159
|
+
body["client_secret_expires_at"] || 0
|
|
160
|
+
end
|
|
149
161
|
data = {
|
|
150
162
|
"clientId" => client_id,
|
|
151
163
|
"clientSecret" => client_secret ? store_client_secret_value(ctx, client_secret, store_client_secret) : nil,
|
|
@@ -163,7 +175,7 @@ module BetterAuth
|
|
|
163
175
|
"redirectUris" => redirects,
|
|
164
176
|
"redirectUrls" => redirects.join(","),
|
|
165
177
|
"postLogoutRedirectUris" => Array(body["post_logout_redirect_uris"]).map(&:to_s),
|
|
166
|
-
"clientSecretExpiresAt" =>
|
|
178
|
+
"clientSecretExpiresAt" => secret_expires_at,
|
|
167
179
|
"tokenEndpointAuthMethod" => auth_method,
|
|
168
180
|
"grantTypes" => grant_types,
|
|
169
181
|
"responseTypes" => response_types,
|
|
@@ -183,7 +195,10 @@ module BetterAuth
|
|
|
183
195
|
client_id_issued_at: Time.now.to_i
|
|
184
196
|
).compact
|
|
185
197
|
response[:require_pkce] = require_pkce unless require_pkce.nil?
|
|
186
|
-
|
|
198
|
+
if client_secret
|
|
199
|
+
expires = stringify_keys(created || {})["clientSecretExpiresAt"] || secret_expires_at
|
|
200
|
+
response[:client_secret_expires_at] = timestamp_seconds(expires) || 0
|
|
201
|
+
end
|
|
187
202
|
response
|
|
188
203
|
end
|
|
189
204
|
|
|
@@ -265,7 +280,12 @@ module BetterAuth
|
|
|
265
280
|
raise APIError.new("BAD_REQUEST", message: "pairwise subject_type requires pairwise_secret") if pairwise_secret.to_s.empty?
|
|
266
281
|
|
|
267
282
|
hosts = redirects.map { |uri| URI.parse(uri).host }.uniq
|
|
268
|
-
|
|
283
|
+
if hosts.length > 1
|
|
284
|
+
raise APIError.new(
|
|
285
|
+
"BAD_REQUEST",
|
|
286
|
+
message: "pairwise clients with redirect_uris on different hosts require a sector_identifier_uri, which is not yet supported. All redirect_uris must share the same host."
|
|
287
|
+
)
|
|
288
|
+
end
|
|
269
289
|
rescue URI::InvalidURIError
|
|
270
290
|
raise APIError.new("BAD_REQUEST", message: "invalid redirect_uris")
|
|
271
291
|
end
|
|
@@ -334,8 +354,17 @@ module BetterAuth
|
|
|
334
354
|
end
|
|
335
355
|
end
|
|
336
356
|
|
|
337
|
-
def find_client(ctx, model, client_id)
|
|
338
|
-
|
|
357
|
+
def find_client(ctx, model, client_id, cached_trusted_clients: nil)
|
|
358
|
+
client_id = client_id.to_s
|
|
359
|
+
if (cached = trusted_client_cache[client_id])
|
|
360
|
+
return cached
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
client = ctx.context.adapter.find_one(model: model, where: [{field: "clientId", value: client_id}])
|
|
364
|
+
if client && cached_trusted_clients&.include?(client_id)
|
|
365
|
+
trusted_client_cache[client_id] = client
|
|
366
|
+
end
|
|
367
|
+
client
|
|
339
368
|
end
|
|
340
369
|
|
|
341
370
|
def authenticate_client!(ctx, model, store_client_secret: "plain", prefix: {}, require_confidential: false)
|
|
@@ -390,9 +419,10 @@ module BetterAuth
|
|
|
390
419
|
seconds && seconds <= Time.now.to_i
|
|
391
420
|
end
|
|
392
421
|
|
|
393
|
-
def store_code(
|
|
422
|
+
def store_code(internal_adapter, 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, store_tokens: "hashed")
|
|
394
423
|
stored_code = get_stored_token(store_tokens, code, "authorization_code")
|
|
395
|
-
|
|
424
|
+
expires_at = Time.now + expires_in.to_i
|
|
425
|
+
data = {
|
|
396
426
|
client_id: client_id,
|
|
397
427
|
redirect_uri: redirect_uri,
|
|
398
428
|
session: session,
|
|
@@ -402,15 +432,19 @@ module BetterAuth
|
|
|
402
432
|
nonce: nonce,
|
|
403
433
|
reference_id: reference_id,
|
|
404
434
|
auth_time: auth_time || session_auth_time(session),
|
|
405
|
-
expires_at:
|
|
435
|
+
expires_at: expires_at.iso8601(6)
|
|
406
436
|
}
|
|
437
|
+
internal_adapter.create_verification_value(identifier: stored_code, value: JSON.generate(data), expiresAt: expires_at)
|
|
407
438
|
end
|
|
408
439
|
|
|
409
|
-
def consume_code!(
|
|
440
|
+
def consume_code!(internal_adapter, code, client_id:, redirect_uri:, code_verifier: nil, store_tokens: "hashed")
|
|
410
441
|
stored_code = get_stored_token(store_tokens, code.to_s, "authorization_code")
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
442
|
+
verification = internal_adapter.consume_verification_value(stored_code)
|
|
443
|
+
verification ||= internal_adapter.consume_verification_value(code.to_s) if stored_code != code.to_s
|
|
444
|
+
data = verification && JSON.parse(verification.fetch("value"), symbolize_names: true)
|
|
445
|
+
unless data.is_a?(Hash) && !data[:client_id].to_s.empty? && !data[:redirect_uri].to_s.empty? && data[:session].is_a?(Hash) && data[:scopes].is_a?(Array)
|
|
446
|
+
raise APIError.new("BAD_REQUEST", message: "invalid_grant")
|
|
447
|
+
end
|
|
414
448
|
raise APIError.new("BAD_REQUEST", message: "invalid_grant") unless data[:client_id] == client_id.to_s
|
|
415
449
|
raise APIError.new("BAD_REQUEST", message: "invalid_grant") unless data[:redirect_uri] == redirect_uri.to_s
|
|
416
450
|
if data[:code_challenge]
|
|
@@ -420,6 +454,8 @@ module BetterAuth
|
|
|
420
454
|
end
|
|
421
455
|
|
|
422
456
|
data
|
|
457
|
+
rescue JSON::ParserError, KeyError, TypeError
|
|
458
|
+
raise APIError.new("BAD_REQUEST", message: "invalid_grant")
|
|
423
459
|
end
|
|
424
460
|
|
|
425
461
|
def verify_pkce!(code_data, verifier)
|
|
@@ -452,7 +488,7 @@ module BetterAuth
|
|
|
452
488
|
true
|
|
453
489
|
end
|
|
454
490
|
|
|
455
|
-
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, store_tokens: "hashed")
|
|
491
|
+
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, store_tokens: "hashed", generate_opaque_access_token: nil, generate_refresh_token: nil, format_refresh_token: nil)
|
|
456
492
|
data = stringify_keys(session || {})
|
|
457
493
|
user = stringify_keys(data["user"] || data[:user] || {})
|
|
458
494
|
session_data = stringify_keys(data["session"] || data[:session] || {})
|
|
@@ -460,9 +496,11 @@ module BetterAuth
|
|
|
460
496
|
subject = subject_identifier(user["id"], client_data, pairwise_secret)
|
|
461
497
|
token_auth_time = auth_time || session_auth_time({"session" => session_data})
|
|
462
498
|
token_reference_id = reference_id || client_data["referenceId"]
|
|
463
|
-
access_token_value = Crypto.random_string(32)
|
|
464
|
-
refresh_token_value = include_refresh
|
|
465
|
-
|
|
499
|
+
access_token_value = generate_opaque_access_token.respond_to?(:call) ? generate_opaque_access_token.call : Crypto.random_string(32)
|
|
500
|
+
refresh_token_value = if include_refresh
|
|
501
|
+
generate_refresh_token.respond_to?(:call) ? generate_refresh_token.call : Crypto.random_string(32)
|
|
502
|
+
end
|
|
503
|
+
refresh_token = refresh_token_value ? encode_refresh_token(refresh_token_value, prefix: prefix, format_refresh_token: format_refresh_token, session_id: session_data["id"]) : nil
|
|
466
504
|
scope = scope_string(scopes)
|
|
467
505
|
expires_at = Time.now + access_token_expires_in.to_i
|
|
468
506
|
access_token = if jwt_access_token && audience
|
|
@@ -536,9 +574,10 @@ module BetterAuth
|
|
|
536
574
|
response
|
|
537
575
|
end
|
|
538
576
|
|
|
539
|
-
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, store_tokens: "hashed")
|
|
540
|
-
refresh_token_value =
|
|
541
|
-
|
|
577
|
+
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, store_tokens: "hashed", generate_opaque_access_token: nil, generate_refresh_token: nil, format_refresh_token: nil)
|
|
578
|
+
refresh_token_value = decode_refresh_token(refresh_token, prefix: prefix, format_refresh_token: format_refresh_token)
|
|
579
|
+
raise APIError.new("BAD_REQUEST", message: "invalid_grant") if refresh_token_value.to_s.empty?
|
|
580
|
+
data = store[:refresh_tokens][refresh_token_value]
|
|
542
581
|
raise APIError.new("BAD_REQUEST", message: "invalid_grant") unless data
|
|
543
582
|
if data["revoked"]
|
|
544
583
|
revoke_refresh_family!(ctx, store, data)
|
|
@@ -583,7 +622,10 @@ module BetterAuth
|
|
|
583
622
|
auth_time: data["authTime"],
|
|
584
623
|
reference_id: data["referenceId"],
|
|
585
624
|
filter_id_token_claims_by_scope: filter_id_token_claims_by_scope,
|
|
586
|
-
store_tokens: store_tokens
|
|
625
|
+
store_tokens: store_tokens,
|
|
626
|
+
generate_opaque_access_token: generate_opaque_access_token,
|
|
627
|
+
generate_refresh_token: generate_refresh_token,
|
|
628
|
+
format_refresh_token: format_refresh_token
|
|
587
629
|
)
|
|
588
630
|
end
|
|
589
631
|
|
|
@@ -616,8 +658,7 @@ module BetterAuth
|
|
|
616
658
|
"exp" => expires_at.to_i
|
|
617
659
|
).compact
|
|
618
660
|
if use_jwt_plugin
|
|
619
|
-
|
|
620
|
-
return signed if signed
|
|
661
|
+
return sign_oauth_jwt!(ctx, payload, issuer: issuer_value, audience: audience)
|
|
621
662
|
end
|
|
622
663
|
|
|
623
664
|
::JWT.encode(payload, ctx.context.secret, "HS256")
|
|
@@ -694,9 +735,12 @@ module BetterAuth
|
|
|
694
735
|
)
|
|
695
736
|
end
|
|
696
737
|
|
|
697
|
-
def find_token_by_hint(store, token, hint, prefix: {})
|
|
738
|
+
def find_token_by_hint(store, token, hint, prefix: {}, format_refresh_token: nil)
|
|
698
739
|
access = -> { (value = strip_prefix(token, prefix, :access_token)) && store[:tokens][value] }
|
|
699
|
-
refresh =
|
|
740
|
+
refresh = lambda {
|
|
741
|
+
value = decode_refresh_token(token, prefix: prefix, format_refresh_token: format_refresh_token)
|
|
742
|
+
value && store[:refresh_tokens][value]
|
|
743
|
+
}
|
|
700
744
|
|
|
701
745
|
case hint.to_s
|
|
702
746
|
when "access_token"
|
|
@@ -746,6 +790,65 @@ module BetterAuth
|
|
|
746
790
|
"#{token_prefix(prefix, kind)}#{value}"
|
|
747
791
|
end
|
|
748
792
|
|
|
793
|
+
def encode_refresh_token(token_value, prefix:, format_refresh_token: nil, session_id: nil)
|
|
794
|
+
formatted = if format_refresh_token.is_a?(Hash) && format_refresh_token[:encrypt].respond_to?(:call)
|
|
795
|
+
format_refresh_token[:encrypt].call(token_value, session_id)
|
|
796
|
+
else
|
|
797
|
+
token_value
|
|
798
|
+
end
|
|
799
|
+
apply_prefix(formatted, prefix, :refresh_token)
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def decode_refresh_token(token, prefix:, format_refresh_token: nil)
|
|
803
|
+
value = strip_prefix(token, prefix, :refresh_token)
|
|
804
|
+
return nil if value.nil?
|
|
805
|
+
|
|
806
|
+
if format_refresh_token.is_a?(Hash) && format_refresh_token[:decrypt].respond_to?(:call)
|
|
807
|
+
decrypted = format_refresh_token[:decrypt].call(value)
|
|
808
|
+
if decrypted.is_a?(Hash)
|
|
809
|
+
decrypted[:token] || decrypted["token"]
|
|
810
|
+
else
|
|
811
|
+
decrypted.to_s
|
|
812
|
+
end
|
|
813
|
+
else
|
|
814
|
+
value
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def trusted_client_cache
|
|
819
|
+
@trusted_client_cache ||= {}
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def resolve_client_secret_expires_at(value, issued_at)
|
|
823
|
+
return value.to_i if value.is_a?(Numeric)
|
|
824
|
+
return value.to_i if value.is_a?(Time)
|
|
825
|
+
|
|
826
|
+
seconds = timestamp_seconds(value)
|
|
827
|
+
return seconds if seconds && !value.is_a?(String)
|
|
828
|
+
|
|
829
|
+
issued_at.to_i + parse_duration(value.to_s)
|
|
830
|
+
rescue TypeError
|
|
831
|
+
0
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def parse_duration(value)
|
|
835
|
+
match = value.strip.match(/\A(-?\d+)\s*(s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|week|weeks|y|yr|yrs|year|years)(?:\s+from now|\s+ago)?\z/i)
|
|
836
|
+
raise TypeError, "Invalid time string" unless match
|
|
837
|
+
|
|
838
|
+
amount = match[1].to_i
|
|
839
|
+
amount = -amount if value.include?("ago")
|
|
840
|
+
unit = match[2].downcase
|
|
841
|
+
multiplier = case unit
|
|
842
|
+
when "s", "sec", "secs", "second", "seconds" then 1
|
|
843
|
+
when "m", "min", "mins", "minute", "minutes" then 60
|
|
844
|
+
when "h", "hr", "hrs", "hour", "hours" then 3600
|
|
845
|
+
when "d", "day", "days" then 86_400
|
|
846
|
+
when "w", "week", "weeks" then 604_800
|
|
847
|
+
else 31_557_600
|
|
848
|
+
end
|
|
849
|
+
amount * multiplier
|
|
850
|
+
end
|
|
851
|
+
|
|
749
852
|
def strip_prefix(value, prefix, kind)
|
|
750
853
|
token = value.to_s
|
|
751
854
|
expected = token_prefix(prefix, kind)
|
|
@@ -796,8 +899,7 @@ module BetterAuth
|
|
|
796
899
|
return signer.call(ctx, payload) if signer.respond_to?(:call)
|
|
797
900
|
|
|
798
901
|
if use_jwt_plugin && ctx
|
|
799
|
-
|
|
800
|
-
return signed if signed
|
|
902
|
+
return sign_oauth_jwt!(ctx, payload, issuer: issuer_value, audience: audience)
|
|
801
903
|
end
|
|
802
904
|
|
|
803
905
|
Crypto.sign_jwt(
|
|
@@ -838,6 +940,15 @@ module BetterAuth
|
|
|
838
940
|
BetterAuth::Plugins.sign_jwt_payload(ctx, stringify_keys(payload), config)
|
|
839
941
|
end
|
|
840
942
|
|
|
943
|
+
def sign_oauth_jwt!(ctx, payload, issuer:, audience:)
|
|
944
|
+
raise BetterAuth::Error, "jwt_config" unless jwt_plugin_options(ctx)
|
|
945
|
+
|
|
946
|
+
signed = sign_oauth_jwt(ctx, payload, issuer: issuer, audience: audience)
|
|
947
|
+
raise BetterAuth::Error, "jwt_config" unless signed
|
|
948
|
+
|
|
949
|
+
signed
|
|
950
|
+
end
|
|
951
|
+
|
|
841
952
|
def verify_oauth_jwt(ctx, token, issuer:, hs256_secret:)
|
|
842
953
|
payload = ::JWT.decode(token.to_s, nil, false).first
|
|
843
954
|
audience = payload["aud"]
|
|
@@ -955,7 +1066,6 @@ module BetterAuth
|
|
|
955
1066
|
|
|
956
1067
|
def stores
|
|
957
1068
|
{
|
|
958
|
-
codes: {},
|
|
959
1069
|
tokens: {},
|
|
960
1070
|
refresh_tokens: {},
|
|
961
1071
|
consents: {}
|