better_auth 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -0
- data/README.md +24 -0
- data/lib/better_auth/adapters/internal_adapter.rb +5 -5
- data/lib/better_auth/adapters/sql.rb +96 -18
- data/lib/better_auth/api.rb +113 -13
- data/lib/better_auth/configuration.rb +97 -7
- data/lib/better_auth/context.rb +165 -12
- data/lib/better_auth/cookies.rb +6 -4
- data/lib/better_auth/core.rb +2 -0
- data/lib/better_auth/crypto/jwe.rb +27 -5
- data/lib/better_auth/crypto.rb +32 -0
- data/lib/better_auth/database_hooks.rb +5 -5
- data/lib/better_auth/endpoint.rb +87 -3
- data/lib/better_auth/error.rb +8 -1
- data/lib/better_auth/plugins/admin/schema.rb +2 -2
- data/lib/better_auth/plugins/admin.rb +344 -16
- data/lib/better_auth/plugins/anonymous.rb +37 -3
- data/lib/better_auth/plugins/device_authorization.rb +102 -5
- data/lib/better_auth/plugins/dub.rb +148 -0
- data/lib/better_auth/plugins/email_otp.rb +246 -15
- data/lib/better_auth/plugins/expo.rb +17 -1
- data/lib/better_auth/plugins/generic_oauth.rb +53 -7
- data/lib/better_auth/plugins/jwt.rb +37 -4
- data/lib/better_auth/plugins/last_login_method.rb +2 -2
- data/lib/better_auth/plugins/magic_link.rb +66 -3
- data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
- data/lib/better_auth/plugins/mcp/config.rb +51 -0
- data/lib/better_auth/plugins/mcp/consent.rb +31 -0
- data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
- data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
- data/lib/better_auth/plugins/mcp/registration.rb +31 -0
- data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
- data/lib/better_auth/plugins/mcp/schema.rb +91 -0
- data/lib/better_auth/plugins/mcp/token.rb +108 -0
- data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
- data/lib/better_auth/plugins/mcp.rb +111 -263
- data/lib/better_auth/plugins/multi_session.rb +61 -3
- data/lib/better_auth/plugins/oauth_protocol.rb +2 -2
- data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
- data/lib/better_auth/plugins/oidc_provider.rb +118 -14
- data/lib/better_auth/plugins/one_tap.rb +7 -2
- data/lib/better_auth/plugins/one_time_token.rb +42 -2
- data/lib/better_auth/plugins/open_api.rb +163 -318
- data/lib/better_auth/plugins/organization.rb +135 -36
- data/lib/better_auth/plugins/phone_number.rb +141 -6
- data/lib/better_auth/plugins/siwe.rb +69 -3
- data/lib/better_auth/plugins/two_factor.rb +65 -23
- data/lib/better_auth/plugins/username.rb +57 -2
- data/lib/better_auth/rate_limiter.rb +20 -0
- data/lib/better_auth/response.rb +42 -0
- data/lib/better_auth/router.rb +7 -1
- data/lib/better_auth/routes/account.rb +204 -38
- data/lib/better_auth/routes/email_verification.rb +98 -14
- data/lib/better_auth/routes/password.rb +125 -8
- data/lib/better_auth/routes/session.rb +128 -13
- data/lib/better_auth/routes/sign_in.rb +24 -2
- data/lib/better_auth/routes/sign_out.rb +13 -1
- data/lib/better_auth/routes/sign_up.rb +62 -4
- data/lib/better_auth/routes/social.rb +102 -7
- data/lib/better_auth/routes/user.rb +222 -20
- data/lib/better_auth/routes/validation.rb +50 -0
- data/lib/better_auth/secret_config.rb +115 -0
- data/lib/better_auth/session.rb +1 -1
- data/lib/better_auth/url_helpers.rb +12 -1
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +4 -0
- metadata +15 -1
|
@@ -36,7 +36,25 @@ module BetterAuth
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def list_device_sessions_endpoint
|
|
39
|
-
Endpoint.new(
|
|
39
|
+
Endpoint.new(
|
|
40
|
+
path: "/multi-session/list-device-sessions",
|
|
41
|
+
method: "GET",
|
|
42
|
+
metadata: {
|
|
43
|
+
openapi: {
|
|
44
|
+
operationId: "listDeviceSessions",
|
|
45
|
+
description: "List device sessions",
|
|
46
|
+
responses: {
|
|
47
|
+
"200" => OpenAPI.json_response(
|
|
48
|
+
"Device sessions",
|
|
49
|
+
{
|
|
50
|
+
type: "array",
|
|
51
|
+
items: OpenAPI.session_response_schema_pair
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
) do |ctx|
|
|
40
58
|
tokens = verified_multi_session_tokens(ctx)
|
|
41
59
|
sessions = ctx.context.internal_adapter.find_sessions(tokens)
|
|
42
60
|
.reject { |entry| entry[:session]["expiresAt"] && entry[:session]["expiresAt"] <= Time.now }
|
|
@@ -47,7 +65,27 @@ module BetterAuth
|
|
|
47
65
|
end
|
|
48
66
|
|
|
49
67
|
def set_active_session_endpoint
|
|
50
|
-
Endpoint.new(
|
|
68
|
+
Endpoint.new(
|
|
69
|
+
path: "/multi-session/set-active",
|
|
70
|
+
method: "POST",
|
|
71
|
+
metadata: {
|
|
72
|
+
openapi: {
|
|
73
|
+
operationId: "setActiveSession",
|
|
74
|
+
description: "Set the active session",
|
|
75
|
+
requestBody: OpenAPI.json_request_body(
|
|
76
|
+
OpenAPI.object_schema(
|
|
77
|
+
{
|
|
78
|
+
sessionToken: {type: "string", description: "The session token"}
|
|
79
|
+
},
|
|
80
|
+
required: ["sessionToken"]
|
|
81
|
+
)
|
|
82
|
+
),
|
|
83
|
+
responses: {
|
|
84
|
+
"200" => OpenAPI.json_response("Active session", OpenAPI.session_response_schema_pair)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
) do |ctx|
|
|
51
89
|
token = fetch_value(ctx.body, "sessionToken").to_s
|
|
52
90
|
cookie_name = multi_session_cookie_name(ctx, token)
|
|
53
91
|
unless !token.empty? && ctx.get_signed_cookie(cookie_name, ctx.context.secret)
|
|
@@ -66,7 +104,27 @@ module BetterAuth
|
|
|
66
104
|
end
|
|
67
105
|
|
|
68
106
|
def revoke_device_session_endpoint
|
|
69
|
-
Endpoint.new(
|
|
107
|
+
Endpoint.new(
|
|
108
|
+
path: "/multi-session/revoke",
|
|
109
|
+
method: "POST",
|
|
110
|
+
metadata: {
|
|
111
|
+
openapi: {
|
|
112
|
+
operationId: "revokeDeviceSession",
|
|
113
|
+
description: "Revoke a device session",
|
|
114
|
+
requestBody: OpenAPI.json_request_body(
|
|
115
|
+
OpenAPI.object_schema(
|
|
116
|
+
{
|
|
117
|
+
sessionToken: {type: "string", description: "The session token"}
|
|
118
|
+
},
|
|
119
|
+
required: ["sessionToken"]
|
|
120
|
+
)
|
|
121
|
+
),
|
|
122
|
+
responses: {
|
|
123
|
+
"200" => OpenAPI.json_response("Device session revoked", OpenAPI.status_response_schema)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
) do |ctx|
|
|
70
128
|
current = Routes.current_session(ctx)
|
|
71
129
|
token = fetch_value(ctx.body, "sessionToken").to_s
|
|
72
130
|
cookie_name = multi_session_cookie_name(ctx, token)
|
|
@@ -788,7 +788,7 @@ module BetterAuth
|
|
|
788
788
|
def store_client_secret_value(ctx, secret, mode)
|
|
789
789
|
mode = normalize_secret_storage_mode(mode)
|
|
790
790
|
return Crypto.sha256(secret, encoding: :base64url) if mode == "hashed"
|
|
791
|
-
return Crypto.symmetric_encrypt(key: ctx.context.
|
|
791
|
+
return Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: secret) if mode == "encrypted"
|
|
792
792
|
|
|
793
793
|
if mode.is_a?(Hash)
|
|
794
794
|
return mode[:hash].call(secret) if mode[:hash].respond_to?(:call)
|
|
@@ -801,7 +801,7 @@ module BetterAuth
|
|
|
801
801
|
def verify_client_secret(ctx, stored_secret, provided_secret, mode)
|
|
802
802
|
mode = normalize_secret_storage_mode(mode)
|
|
803
803
|
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.
|
|
804
|
+
return Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored_secret) == provided_secret.to_s if mode == "encrypted"
|
|
805
805
|
|
|
806
806
|
if mode.is_a?(Hash)
|
|
807
807
|
return mode[:hash].call(provided_secret).to_s == stored_secret.to_s if mode[:hash].respond_to?(:call)
|
|
@@ -43,12 +43,28 @@ module BetterAuth
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def oauth_proxy_endpoint(config)
|
|
46
|
-
Endpoint.new(
|
|
46
|
+
Endpoint.new(
|
|
47
|
+
path: "/oauth-proxy-callback",
|
|
48
|
+
method: "GET",
|
|
49
|
+
metadata: {
|
|
50
|
+
openapi: {
|
|
51
|
+
operationId: "oauthProxyCallback",
|
|
52
|
+
description: "OAuth Proxy Callback",
|
|
53
|
+
parameters: [
|
|
54
|
+
{in: "query", name: "callbackURL", required: true, schema: {type: "string", format: "uri"}},
|
|
55
|
+
{in: "query", name: "cookies", required: true, schema: {type: "string"}}
|
|
56
|
+
],
|
|
57
|
+
responses: {
|
|
58
|
+
"302" => {description: "Redirects to the callback URL"}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
) do |ctx|
|
|
47
63
|
query = normalize_hash(ctx.query)
|
|
48
64
|
callback_url = query[:callback_url] || "/"
|
|
49
65
|
oauth_proxy_validate_callback!(ctx, callback_url)
|
|
50
66
|
|
|
51
|
-
decrypted = Crypto.symmetric_decrypt(key: ctx
|
|
67
|
+
decrypted = Crypto.symmetric_decrypt(key: oauth_proxy_secret(ctx, config), data: query[:cookies].to_s)
|
|
52
68
|
raise ctx.redirect(oauth_proxy_error_url(ctx, "OAuthProxy - Invalid cookies or secret")) unless decrypted
|
|
53
69
|
|
|
54
70
|
payload = JSON.parse(decrypted)
|
|
@@ -83,11 +99,11 @@ module BetterAuth
|
|
|
83
99
|
nil
|
|
84
100
|
end
|
|
85
101
|
|
|
86
|
-
def oauth_proxy_restore_state_package(ctx,
|
|
102
|
+
def oauth_proxy_restore_state_package(ctx, config)
|
|
87
103
|
state = fetch_value(ctx.query, "state") || fetch_value(ctx.body, "state")
|
|
88
104
|
return if state.to_s.empty?
|
|
89
105
|
|
|
90
|
-
decrypted = Crypto.symmetric_decrypt(key: ctx
|
|
106
|
+
decrypted = Crypto.symmetric_decrypt(key: oauth_proxy_secret(ctx, config), data: state.to_s)
|
|
91
107
|
return unless decrypted
|
|
92
108
|
|
|
93
109
|
package = JSON.parse(decrypted)
|
|
@@ -121,7 +137,7 @@ module BetterAuth
|
|
|
121
137
|
return if state_cookie.to_s.empty?
|
|
122
138
|
|
|
123
139
|
encrypted_package = Crypto.symmetric_encrypt(
|
|
124
|
-
key: ctx
|
|
140
|
+
key: oauth_proxy_secret(ctx, config),
|
|
125
141
|
data: JSON.generate({
|
|
126
142
|
state: original_state,
|
|
127
143
|
stateCookie: state_cookie,
|
|
@@ -160,7 +176,7 @@ module BetterAuth
|
|
|
160
176
|
return if set_cookie.to_s.empty?
|
|
161
177
|
|
|
162
178
|
encrypted = Crypto.symmetric_encrypt(
|
|
163
|
-
key: ctx
|
|
179
|
+
key: oauth_proxy_secret(ctx, config),
|
|
164
180
|
data: JSON.generate({
|
|
165
181
|
cookies: set_cookie,
|
|
166
182
|
timestamp: (Time.now.to_f * 1000).to_i
|
|
@@ -180,6 +196,10 @@ module BetterAuth
|
|
|
180
196
|
exact && exact[:value]
|
|
181
197
|
end
|
|
182
198
|
|
|
199
|
+
def oauth_proxy_secret(ctx, config)
|
|
200
|
+
config[:secret] || ctx.context.secret_config
|
|
201
|
+
end
|
|
202
|
+
|
|
183
203
|
def oauth_proxy_sign_in_path?(path)
|
|
184
204
|
path.to_s.start_with?("/sign-in/social", "/sign-in/oauth2")
|
|
185
205
|
end
|
|
@@ -6,9 +6,21 @@ module BetterAuth
|
|
|
6
6
|
module Plugins
|
|
7
7
|
module OIDCProvider
|
|
8
8
|
VALID_PROMPTS = %w[none login consent create select_account].freeze
|
|
9
|
+
DEPRECATION_MESSAGE = 'The "oidc-provider" plugin is deprecated and will be removed in the next major version. Migrate to better_auth-oauth-provider. See: https://www.better-auth.com/docs/plugins/oauth-provider'
|
|
9
10
|
|
|
10
11
|
module_function
|
|
11
12
|
|
|
13
|
+
def warn_deprecation!(logger = nil)
|
|
14
|
+
return if @deprecation_warned
|
|
15
|
+
|
|
16
|
+
Deprecate.warn_once("[Deprecation] #{DEPRECATION_MESSAGE}", logger)
|
|
17
|
+
@deprecation_warned = true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset_deprecation_warning!
|
|
21
|
+
@deprecation_warned = false
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
def normalize_issuer(value)
|
|
13
25
|
uri = URI.parse(value.to_s)
|
|
14
26
|
uri.query = nil
|
|
@@ -34,6 +46,9 @@ module BetterAuth
|
|
|
34
46
|
module_function
|
|
35
47
|
|
|
36
48
|
def oidc_provider(options = {})
|
|
49
|
+
raw_options = normalize_hash(options)
|
|
50
|
+
OIDCProvider.warn_deprecation!(raw_options[:logger]) unless raw_options[:__skip_deprecation_warning]
|
|
51
|
+
|
|
37
52
|
config = {
|
|
38
53
|
code_expires_in: 600,
|
|
39
54
|
consent_page: "/oauth2/authorize",
|
|
@@ -45,7 +60,7 @@ module BetterAuth
|
|
|
45
60
|
store_client_secret: "plain",
|
|
46
61
|
scopes: %w[openid profile email offline_access],
|
|
47
62
|
store: OAuthProtocol.stores
|
|
48
|
-
}.merge(
|
|
63
|
+
}.merge(raw_options.except(:logger, :__skip_deprecation_warning))
|
|
49
64
|
|
|
50
65
|
Plugin.new(
|
|
51
66
|
id: "oidc-provider",
|
|
@@ -113,7 +128,7 @@ module BetterAuth
|
|
|
113
128
|
end
|
|
114
129
|
|
|
115
130
|
def oidc_register_endpoint(config)
|
|
116
|
-
Endpoint.new(path: "/oauth2/register", method: "POST") do |ctx|
|
|
131
|
+
Endpoint.new(path: "/oauth2/register", method: "POST", metadata: oidc_openapi("registerOAuthApplication", "Register an OAuth2 application", "OAuth2 application registered successfully", oidc_client_schema)) do |ctx|
|
|
117
132
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
118
133
|
unless session || config[:allow_dynamic_client_registration]
|
|
119
134
|
raise APIError.new("UNAUTHORIZED", message: "invalid_token")
|
|
@@ -146,7 +161,7 @@ module BetterAuth
|
|
|
146
161
|
end
|
|
147
162
|
|
|
148
163
|
def oidc_get_client_endpoint
|
|
149
|
-
Endpoint.new(path: "/oauth2/client/:id", method: "GET") do |ctx|
|
|
164
|
+
Endpoint.new(path: "/oauth2/client/:id", method: "GET", metadata: oidc_openapi("getOAuthClient", "Get OAuth2 client details", "OAuth2 client retrieved successfully", oidc_client_schema)) do |ctx|
|
|
150
165
|
client = OAuthProtocol.find_client(ctx, "oauthApplication", ctx.params["id"] || ctx.params[:id])
|
|
151
166
|
raise APIError.new("NOT_FOUND", message: "client not found") unless client
|
|
152
167
|
|
|
@@ -155,7 +170,7 @@ module BetterAuth
|
|
|
155
170
|
end
|
|
156
171
|
|
|
157
172
|
def oidc_list_clients_endpoint
|
|
158
|
-
Endpoint.new(path: "/oauth2/clients", method: "GET") do |ctx|
|
|
173
|
+
Endpoint.new(path: "/oauth2/clients", method: "GET", metadata: oidc_openapi("listOAuthApplications", "List OAuth2 applications", "OAuth2 applications retrieved successfully", {type: "array", items: oidc_client_schema})) do |ctx|
|
|
159
174
|
session = Routes.current_session(ctx)
|
|
160
175
|
clients = ctx.context.adapter.find_many(model: "oauthApplication", where: [{field: "userId", value: session[:user]["id"]}])
|
|
161
176
|
ctx.json(clients.map { |client| OAuthProtocol.client_response(client, include_secret: false) })
|
|
@@ -163,7 +178,7 @@ module BetterAuth
|
|
|
163
178
|
end
|
|
164
179
|
|
|
165
180
|
def oidc_update_client_endpoint
|
|
166
|
-
Endpoint.new(path: "/oauth2/client/:id", method: "PATCH") do |ctx|
|
|
181
|
+
Endpoint.new(path: "/oauth2/client/:id", method: "PATCH", metadata: oidc_openapi("updateOAuthApplication", "Update an OAuth2 application", "OAuth2 application updated successfully", oidc_client_schema)) do |ctx|
|
|
167
182
|
session = Routes.current_session(ctx)
|
|
168
183
|
client = oidc_find_owned_client!(ctx, session)
|
|
169
184
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
@@ -191,7 +206,7 @@ module BetterAuth
|
|
|
191
206
|
end
|
|
192
207
|
|
|
193
208
|
def oidc_rotate_client_secret_endpoint(config)
|
|
194
|
-
Endpoint.new(path: "/oauth2/client/:id/rotate-secret", method: "POST") do |ctx|
|
|
209
|
+
Endpoint.new(path: "/oauth2/client/:id/rotate-secret", method: "POST", metadata: oidc_openapi("rotateOAuthApplicationSecret", "Rotate an OAuth2 application secret", "OAuth2 application secret rotated successfully", oidc_client_schema)) do |ctx|
|
|
195
210
|
session = Routes.current_session(ctx)
|
|
196
211
|
client = oidc_find_owned_client!(ctx, session)
|
|
197
212
|
if OAuthProtocol.stringify_keys(client)["tokenEndpointAuthMethod"] == "none"
|
|
@@ -209,7 +224,7 @@ module BetterAuth
|
|
|
209
224
|
end
|
|
210
225
|
|
|
211
226
|
def oidc_delete_client_endpoint
|
|
212
|
-
Endpoint.new(path: "/oauth2/client/:id", method: "DELETE") do |ctx|
|
|
227
|
+
Endpoint.new(path: "/oauth2/client/:id", method: "DELETE", metadata: oidc_openapi("deleteOAuthApplication", "Delete an OAuth2 application", "OAuth2 application deleted successfully", OpenAPI.success_response_schema)) do |ctx|
|
|
213
228
|
session = Routes.current_session(ctx)
|
|
214
229
|
client = oidc_find_owned_client!(ctx, session)
|
|
215
230
|
ctx.context.adapter.delete(model: "oauthApplication", where: [{field: "id", value: client.fetch("id")}])
|
|
@@ -218,7 +233,7 @@ module BetterAuth
|
|
|
218
233
|
end
|
|
219
234
|
|
|
220
235
|
def oidc_authorize_endpoint(config)
|
|
221
|
-
Endpoint.new(path: "/oauth2/authorize", method: "GET") do |ctx|
|
|
236
|
+
Endpoint.new(path: "/oauth2/authorize", method: "GET", metadata: oidc_openapi("oauth2Authorize", "Authorize an OAuth2 request", "Authorization response generated successfully", {type: "object", additionalProperties: true})) do |ctx|
|
|
222
237
|
query = OAuthProtocol.stringify_keys(ctx.query)
|
|
223
238
|
prompts = OIDCProvider.parse_prompt(query["prompt"])
|
|
224
239
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
@@ -308,7 +323,7 @@ module BetterAuth
|
|
|
308
323
|
end
|
|
309
324
|
|
|
310
325
|
def oidc_consent_endpoint(config)
|
|
311
|
-
Endpoint.new(path: "/oauth2/consent", method: "POST") do |ctx|
|
|
326
|
+
Endpoint.new(path: "/oauth2/consent", method: "POST", metadata: oidc_openapi("oauth2Consent", "Handle OAuth2 consent", "OAuth2 consent handled successfully", oidc_redirect_response_schema)) do |ctx|
|
|
312
327
|
Routes.current_session(ctx)
|
|
313
328
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
314
329
|
consent = config[:store][:consents].delete(body["consent_code"].to_s)
|
|
@@ -338,7 +353,11 @@ module BetterAuth
|
|
|
338
353
|
end
|
|
339
354
|
|
|
340
355
|
def oidc_token_endpoint(config)
|
|
341
|
-
Endpoint.new(
|
|
356
|
+
Endpoint.new(
|
|
357
|
+
path: "/oauth2/token",
|
|
358
|
+
method: "POST",
|
|
359
|
+
metadata: oidc_openapi("oauth2Token", "Exchange OAuth2 code for tokens", "OAuth2 tokens issued successfully", oidc_token_response_schema).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
|
|
360
|
+
) do |ctx|
|
|
342
361
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
343
362
|
client = OAuthProtocol.authenticate_client!(ctx, "oauthApplication", store_client_secret: config[:store_client_secret])
|
|
344
363
|
raise APIError.new("UNAUTHORIZED", message: "invalid_client") unless client
|
|
@@ -374,13 +393,17 @@ module BetterAuth
|
|
|
374
393
|
end
|
|
375
394
|
|
|
376
395
|
def oidc_userinfo_endpoint(config)
|
|
377
|
-
Endpoint.new(path: "/oauth2/userinfo", method: "GET") do |ctx|
|
|
396
|
+
Endpoint.new(path: "/oauth2/userinfo", method: "GET", metadata: oidc_openapi("oauth2Userinfo", "Get OAuth2 user information", "User information retrieved successfully", oidc_userinfo_schema)) do |ctx|
|
|
378
397
|
ctx.json(OAuthProtocol.userinfo(config[:store], ctx.headers["authorization"], additional_claim: config[:get_additional_user_info_claim]))
|
|
379
398
|
end
|
|
380
399
|
end
|
|
381
400
|
|
|
382
401
|
def oidc_introspect_endpoint(config)
|
|
383
|
-
Endpoint.new(
|
|
402
|
+
Endpoint.new(
|
|
403
|
+
path: "/oauth2/introspect",
|
|
404
|
+
method: "POST",
|
|
405
|
+
metadata: oidc_openapi("oauth2Introspect", "Introspect an OAuth2 token", "OAuth2 token introspection result", oidc_introspection_schema).merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
|
|
406
|
+
) do |ctx|
|
|
384
407
|
OAuthProtocol.authenticate_client!(ctx, "oauthApplication", store_client_secret: config[:store_client_secret])
|
|
385
408
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
386
409
|
token = config[:store][:tokens][body["token"].to_s] || config[:store][:refresh_tokens][body["token"].to_s]
|
|
@@ -396,7 +419,11 @@ module BetterAuth
|
|
|
396
419
|
end
|
|
397
420
|
|
|
398
421
|
def oidc_revoke_endpoint(config)
|
|
399
|
-
Endpoint.new(
|
|
422
|
+
Endpoint.new(
|
|
423
|
+
path: "/oauth2/revoke",
|
|
424
|
+
method: "POST",
|
|
425
|
+
metadata: oidc_openapi("oauth2Revoke", "Revoke an 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"])
|
|
426
|
+
) do |ctx|
|
|
400
427
|
OAuthProtocol.authenticate_client!(ctx, "oauthApplication", store_client_secret: config[:store_client_secret])
|
|
401
428
|
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
402
429
|
if (token = config[:store][:tokens][body["token"].to_s] || config[:store][:refresh_tokens][body["token"].to_s])
|
|
@@ -407,7 +434,11 @@ module BetterAuth
|
|
|
407
434
|
end
|
|
408
435
|
|
|
409
436
|
def oidc_end_session_endpoint
|
|
410
|
-
Endpoint.new(
|
|
437
|
+
Endpoint.new(
|
|
438
|
+
path: "/oauth2/endsession",
|
|
439
|
+
method: ["GET", "POST"],
|
|
440
|
+
metadata: oidc_openapi("oauth2EndSession", "RP-Initiated Logout endpoint", "Logout request handled").merge(allowed_media_types: ["application/x-www-form-urlencoded", "application/json"])
|
|
441
|
+
) do |ctx|
|
|
411
442
|
input_source = (ctx.method == "GET") ? ctx.query : ctx.body
|
|
412
443
|
input = OAuthProtocol.stringify_keys(input_source)
|
|
413
444
|
if input["post_logout_redirect_uri"]
|
|
@@ -425,6 +456,79 @@ module BetterAuth
|
|
|
425
456
|
end
|
|
426
457
|
end
|
|
427
458
|
|
|
459
|
+
def oidc_openapi(operation_id, description, response_description = "Success", response_schema = {type: "object"})
|
|
460
|
+
{
|
|
461
|
+
openapi: {
|
|
462
|
+
operationId: operation_id,
|
|
463
|
+
description: description,
|
|
464
|
+
responses: {
|
|
465
|
+
"200" => OpenAPI.json_response(response_description, response_schema)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def oidc_client_schema
|
|
472
|
+
OpenAPI.object_schema(
|
|
473
|
+
{
|
|
474
|
+
clientId: {type: "string"},
|
|
475
|
+
clientSecret: {type: ["string", "null"]},
|
|
476
|
+
name: {type: "string"},
|
|
477
|
+
redirectUris: {type: "array", items: {type: "string"}},
|
|
478
|
+
grantTypes: {type: "array", items: {type: "string"}},
|
|
479
|
+
responseTypes: {type: "array", items: {type: "string"}}
|
|
480
|
+
},
|
|
481
|
+
required: ["clientId", "name"]
|
|
482
|
+
)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def oidc_redirect_response_schema
|
|
486
|
+
OpenAPI.object_schema(
|
|
487
|
+
{redirectURI: {type: "string", format: "uri"}},
|
|
488
|
+
required: ["redirectURI"]
|
|
489
|
+
)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def oidc_token_response_schema
|
|
493
|
+
OpenAPI.object_schema(
|
|
494
|
+
{
|
|
495
|
+
access_token: {type: "string"},
|
|
496
|
+
token_type: {type: "string"},
|
|
497
|
+
expires_in: {type: "number"},
|
|
498
|
+
refresh_token: {type: ["string", "null"]},
|
|
499
|
+
id_token: {type: ["string", "null"]},
|
|
500
|
+
scope: {type: ["string", "null"]}
|
|
501
|
+
},
|
|
502
|
+
required: ["access_token", "token_type", "expires_in"]
|
|
503
|
+
)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def oidc_userinfo_schema
|
|
507
|
+
OpenAPI.object_schema(
|
|
508
|
+
{
|
|
509
|
+
sub: {type: "string"},
|
|
510
|
+
email: {type: ["string", "null"]},
|
|
511
|
+
email_verified: {type: ["boolean", "null"]},
|
|
512
|
+
name: {type: ["string", "null"]},
|
|
513
|
+
picture: {type: ["string", "null"]}
|
|
514
|
+
},
|
|
515
|
+
required: ["sub"]
|
|
516
|
+
)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def oidc_introspection_schema
|
|
520
|
+
OpenAPI.object_schema(
|
|
521
|
+
{
|
|
522
|
+
active: {type: "boolean"},
|
|
523
|
+
client_id: {type: ["string", "null"]},
|
|
524
|
+
scope: {type: ["string", "null"]},
|
|
525
|
+
sub: {type: ["string", "null"]},
|
|
526
|
+
exp: {type: ["number", "null"]}
|
|
527
|
+
},
|
|
528
|
+
required: ["active"]
|
|
529
|
+
)
|
|
530
|
+
end
|
|
531
|
+
|
|
428
532
|
def oidc_provider_schema
|
|
429
533
|
{
|
|
430
534
|
oauthApplication: {
|
|
@@ -29,8 +29,12 @@ module BetterAuth
|
|
|
29
29
|
},
|
|
30
30
|
metadata: {
|
|
31
31
|
openapi: {
|
|
32
|
+
operationId: "oneTapCallback",
|
|
32
33
|
summary: "One tap callback",
|
|
33
|
-
description: "Use this endpoint to authenticate with Google One Tap"
|
|
34
|
+
description: "Use this endpoint to authenticate with Google One Tap",
|
|
35
|
+
responses: {
|
|
36
|
+
"200" => OpenAPI.json_response("Success", OpenAPI.session_response_schema_pair)
|
|
37
|
+
}
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
) do |ctx|
|
|
@@ -61,7 +65,8 @@ module BetterAuth
|
|
|
61
65
|
providerId: "google",
|
|
62
66
|
accountId: fetch_value(payload, "sub").to_s,
|
|
63
67
|
idToken: id_token
|
|
64
|
-
}
|
|
68
|
+
},
|
|
69
|
+
context: ctx
|
|
65
70
|
)
|
|
66
71
|
raise APIError.new("INTERNAL_SERVER_ERROR", message: "Could not create user") unless created
|
|
67
72
|
|
|
@@ -29,7 +29,27 @@ module BetterAuth
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def generate_one_time_token_endpoint(config)
|
|
32
|
-
Endpoint.new(
|
|
32
|
+
Endpoint.new(
|
|
33
|
+
path: "/one-time-token/generate",
|
|
34
|
+
method: "GET",
|
|
35
|
+
metadata: {
|
|
36
|
+
openapi: {
|
|
37
|
+
operationId: "generateOneTimeToken",
|
|
38
|
+
description: "Generate a one-time token for the current session",
|
|
39
|
+
responses: {
|
|
40
|
+
"200" => OpenAPI.json_response(
|
|
41
|
+
"One-time token",
|
|
42
|
+
OpenAPI.object_schema(
|
|
43
|
+
{
|
|
44
|
+
token: {type: "string"}
|
|
45
|
+
},
|
|
46
|
+
required: ["token"]
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
) do |ctx|
|
|
33
53
|
if config[:disable_client_request] && ctx.request
|
|
34
54
|
raise APIError.new("BAD_REQUEST", message: "Client requests are disabled")
|
|
35
55
|
end
|
|
@@ -41,7 +61,27 @@ module BetterAuth
|
|
|
41
61
|
end
|
|
42
62
|
|
|
43
63
|
def verify_one_time_token_endpoint(config)
|
|
44
|
-
Endpoint.new(
|
|
64
|
+
Endpoint.new(
|
|
65
|
+
path: "/one-time-token/verify",
|
|
66
|
+
method: "POST",
|
|
67
|
+
metadata: {
|
|
68
|
+
openapi: {
|
|
69
|
+
operationId: "verifyOneTimeToken",
|
|
70
|
+
description: "Verify a one-time token and restore its session",
|
|
71
|
+
requestBody: OpenAPI.json_request_body(
|
|
72
|
+
OpenAPI.object_schema(
|
|
73
|
+
{
|
|
74
|
+
token: {type: "string"}
|
|
75
|
+
},
|
|
76
|
+
required: ["token"]
|
|
77
|
+
)
|
|
78
|
+
),
|
|
79
|
+
responses: {
|
|
80
|
+
"200" => OpenAPI.json_response("Session restored", OpenAPI.session_response_schema_pair)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
) do |ctx|
|
|
45
85
|
body = normalize_hash(ctx.body)
|
|
46
86
|
token = body[:token].to_s
|
|
47
87
|
stored_token = one_time_token_stored_value(config, token)
|