better_auth 0.4.0 → 0.6.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
|
@@ -3,11 +3,34 @@
|
|
|
3
3
|
module BetterAuth
|
|
4
4
|
module Routes
|
|
5
5
|
def self.get_session
|
|
6
|
-
Endpoint.new(
|
|
7
|
-
session
|
|
6
|
+
Endpoint.new(
|
|
7
|
+
path: "/get-session",
|
|
8
|
+
method: ["GET", "POST"],
|
|
9
|
+
metadata: {
|
|
10
|
+
openapi: {
|
|
11
|
+
operationId: "getSession",
|
|
12
|
+
description: "Get the current session",
|
|
13
|
+
responses: {
|
|
14
|
+
"200" => OpenAPI.json_response("Current session or null", OpenAPI.session_response_schema_pair.merge(nullable: true))
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
) do |ctx|
|
|
19
|
+
defer_refresh = ctx.context.session_config[:defer_session_refresh]
|
|
20
|
+
if ctx.method == "POST" && !defer_refresh
|
|
21
|
+
raise APIError.new(
|
|
22
|
+
"METHOD_NOT_ALLOWED",
|
|
23
|
+
code: "METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED",
|
|
24
|
+
message: BASE_ERROR_CODES["METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED"]
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
session = current_session(ctx, allow_nil: true, disable_refresh: defer_refresh && ctx.method == "GET")
|
|
8
29
|
next ctx.json(nil) unless session
|
|
9
30
|
|
|
10
|
-
|
|
31
|
+
response = parsed_session_response(ctx, session)
|
|
32
|
+
response[:needsRefresh] = session_needs_refresh?(ctx, session[:session]) if defer_refresh && ctx.method == "GET"
|
|
33
|
+
ctx.json(response)
|
|
11
34
|
rescue APIError
|
|
12
35
|
raise
|
|
13
36
|
rescue => error
|
|
@@ -17,7 +40,22 @@ module BetterAuth
|
|
|
17
40
|
end
|
|
18
41
|
|
|
19
42
|
def self.list_sessions
|
|
20
|
-
Endpoint.new(
|
|
43
|
+
Endpoint.new(
|
|
44
|
+
path: "/list-sessions",
|
|
45
|
+
method: "GET",
|
|
46
|
+
metadata: {
|
|
47
|
+
openapi: {
|
|
48
|
+
operationId: "listSessions",
|
|
49
|
+
description: "List active sessions for the current user",
|
|
50
|
+
responses: {
|
|
51
|
+
"200" => OpenAPI.json_response(
|
|
52
|
+
"Active sessions",
|
|
53
|
+
{type: "array", items: {type: "object", "$ref": "#/components/schemas/Session"}}
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
) do |ctx|
|
|
21
59
|
session = current_session(ctx)
|
|
22
60
|
sessions = ctx.context.internal_adapter.list_sessions(session[:user]["id"])
|
|
23
61
|
active = sessions
|
|
@@ -29,8 +67,22 @@ module BetterAuth
|
|
|
29
67
|
end
|
|
30
68
|
|
|
31
69
|
def self.update_session
|
|
32
|
-
Endpoint.new(
|
|
33
|
-
session
|
|
70
|
+
Endpoint.new(
|
|
71
|
+
path: "/update-session",
|
|
72
|
+
method: "POST",
|
|
73
|
+
metadata: {
|
|
74
|
+
openapi: {
|
|
75
|
+
operationId: "updateSession",
|
|
76
|
+
description: "Update the current session",
|
|
77
|
+
responses: {
|
|
78
|
+
"200" => OpenAPI.json_response("Updated session", OpenAPI.session_response_schema_pair)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
) do |ctx|
|
|
83
|
+
session = current_session(ctx)
|
|
84
|
+
raise APIError.new("BAD_REQUEST", code: "BODY_MUST_BE_AN_OBJECT", message: BASE_ERROR_CODES["BODY_MUST_BE_AN_OBJECT"]) unless ctx.body.is_a?(Hash)
|
|
85
|
+
|
|
34
86
|
body = Routes.parse_declared_input(ctx, "session", ctx.body, allowed_base: [])
|
|
35
87
|
raise APIError.new("BAD_REQUEST", message: "No fields to update") if body.empty?
|
|
36
88
|
|
|
@@ -38,12 +90,32 @@ module BetterAuth
|
|
|
38
90
|
updated = ctx.context.internal_adapter.update_session(session[:session]["token"], update)
|
|
39
91
|
merged = session[:session].merge(updated || update)
|
|
40
92
|
Cookies.set_session_cookie(ctx, {session: merged, user: session[:user]}, Cookies.dont_remember?(ctx))
|
|
41
|
-
ctx.json(
|
|
93
|
+
ctx.json({session: Schema.parse_output(ctx.context.options, "session", merged)})
|
|
42
94
|
end
|
|
43
95
|
end
|
|
44
96
|
|
|
45
97
|
def self.revoke_session
|
|
46
|
-
Endpoint.new(
|
|
98
|
+
Endpoint.new(
|
|
99
|
+
path: "/revoke-session",
|
|
100
|
+
method: "POST",
|
|
101
|
+
metadata: {
|
|
102
|
+
openapi: {
|
|
103
|
+
operationId: "revokeSession",
|
|
104
|
+
description: "Revoke a session by token",
|
|
105
|
+
requestBody: OpenAPI.json_request_body(
|
|
106
|
+
OpenAPI.object_schema(
|
|
107
|
+
{
|
|
108
|
+
token: {type: "string", description: "The session token to revoke"}
|
|
109
|
+
},
|
|
110
|
+
required: ["token"]
|
|
111
|
+
)
|
|
112
|
+
),
|
|
113
|
+
responses: {
|
|
114
|
+
"200" => OpenAPI.json_response("Session revoked", OpenAPI.status_response_schema)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
) do |ctx|
|
|
47
119
|
session = current_session(ctx, sensitive: true)
|
|
48
120
|
body = normalize_hash(ctx.body)
|
|
49
121
|
token = body["token"].to_s
|
|
@@ -58,7 +130,19 @@ module BetterAuth
|
|
|
58
130
|
end
|
|
59
131
|
|
|
60
132
|
def self.revoke_sessions
|
|
61
|
-
Endpoint.new(
|
|
133
|
+
Endpoint.new(
|
|
134
|
+
path: "/revoke-sessions",
|
|
135
|
+
method: "POST",
|
|
136
|
+
metadata: {
|
|
137
|
+
openapi: {
|
|
138
|
+
operationId: "revokeSessions",
|
|
139
|
+
description: "Revoke all sessions for the current user",
|
|
140
|
+
responses: {
|
|
141
|
+
"200" => OpenAPI.json_response("Sessions revoked", OpenAPI.status_response_schema)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
) do |ctx|
|
|
62
146
|
session = current_session(ctx, sensitive: true)
|
|
63
147
|
ctx.context.internal_adapter.delete_sessions(session[:user]["id"])
|
|
64
148
|
Cookies.delete_session_cookie(ctx)
|
|
@@ -67,7 +151,19 @@ module BetterAuth
|
|
|
67
151
|
end
|
|
68
152
|
|
|
69
153
|
def self.revoke_other_sessions
|
|
70
|
-
Endpoint.new(
|
|
154
|
+
Endpoint.new(
|
|
155
|
+
path: "/revoke-other-sessions",
|
|
156
|
+
method: "POST",
|
|
157
|
+
metadata: {
|
|
158
|
+
openapi: {
|
|
159
|
+
operationId: "revokeOtherSessions",
|
|
160
|
+
description: "Revoke all sessions except the current one",
|
|
161
|
+
responses: {
|
|
162
|
+
"200" => OpenAPI.json_response("Other sessions revoked", OpenAPI.status_response_schema)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
) do |ctx|
|
|
71
167
|
session = current_session(ctx, sensitive: true)
|
|
72
168
|
current_token = session[:session]["token"]
|
|
73
169
|
sessions = ctx.context.internal_adapter.list_sessions(session[:user]["id"])
|
|
@@ -81,11 +177,11 @@ module BetterAuth
|
|
|
81
177
|
end
|
|
82
178
|
end
|
|
83
179
|
|
|
84
|
-
def self.current_session(ctx, allow_nil: false, sensitive: false)
|
|
180
|
+
def self.current_session(ctx, allow_nil: false, sensitive: false, fresh: false, disable_refresh: false)
|
|
85
181
|
data = Session.find_current(
|
|
86
182
|
ctx,
|
|
87
183
|
disable_cookie_cache: truthy_query?(ctx.query, "disableCookieCache"),
|
|
88
|
-
disable_refresh: truthy_query?(ctx.query, "disableRefresh"),
|
|
184
|
+
disable_refresh: disable_refresh || truthy_query?(ctx.query, "disableRefresh"),
|
|
89
185
|
sensitive: sensitive
|
|
90
186
|
)
|
|
91
187
|
return nil if allow_nil && data.nil?
|
|
@@ -93,7 +189,7 @@ module BetterAuth
|
|
|
93
189
|
raise APIError.new("UNAUTHORIZED") unless data
|
|
94
190
|
|
|
95
191
|
session = stringify_keys(data[:session] || data["session"])
|
|
96
|
-
ensure_fresh_session!(ctx, session) if
|
|
192
|
+
ensure_fresh_session!(ctx, session) if fresh
|
|
97
193
|
|
|
98
194
|
{
|
|
99
195
|
session: session,
|
|
@@ -101,6 +197,15 @@ module BetterAuth
|
|
|
101
197
|
}
|
|
102
198
|
end
|
|
103
199
|
|
|
200
|
+
def self.request_only_session(ctx)
|
|
201
|
+
session = current_session(ctx, allow_nil: true)
|
|
202
|
+
if !session && (ctx.request || !ctx.headers.empty?)
|
|
203
|
+
raise APIError.new("UNAUTHORIZED", code: "UNAUTHORIZED", message: "Unauthorized")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
session
|
|
207
|
+
end
|
|
208
|
+
|
|
104
209
|
def self.ensure_fresh_session!(ctx, session)
|
|
105
210
|
fresh_age = ctx.context.session_config[:fresh_age].to_i
|
|
106
211
|
return if fresh_age.zero?
|
|
@@ -111,6 +216,16 @@ module BetterAuth
|
|
|
111
216
|
raise APIError.new("FORBIDDEN", code: "SESSION_NOT_FRESH", message: BASE_ERROR_CODES.fetch("SESSION_NOT_FRESH"))
|
|
112
217
|
end
|
|
113
218
|
|
|
219
|
+
def self.session_needs_refresh?(ctx, session)
|
|
220
|
+
return false if truthy_query?(ctx.query, "disableRefresh") || ctx.context.session_config[:disable_session_refresh]
|
|
221
|
+
|
|
222
|
+
update_age = ctx.context.session_config[:update_age].to_i
|
|
223
|
+
return true if update_age.zero?
|
|
224
|
+
|
|
225
|
+
updated_at = normalize_time(session["updatedAt"])
|
|
226
|
+
updated_at && updated_at + update_age <= Time.now
|
|
227
|
+
end
|
|
228
|
+
|
|
114
229
|
def self.normalize_time(value)
|
|
115
230
|
return value if value.is_a?(Time)
|
|
116
231
|
return nil if value.nil? || value.to_s.empty?
|
|
@@ -8,17 +8,39 @@ module BetterAuth
|
|
|
8
8
|
Endpoint.new(
|
|
9
9
|
path: "/sign-in/email",
|
|
10
10
|
method: "POST",
|
|
11
|
+
body_schema: request_body_schema(required_strings: %w[email password]),
|
|
11
12
|
metadata: {
|
|
12
13
|
allowed_media_types: [
|
|
13
14
|
"application/x-www-form-urlencoded",
|
|
14
15
|
"application/json"
|
|
15
|
-
]
|
|
16
|
+
],
|
|
17
|
+
openapi: {
|
|
18
|
+
operationId: "signInEmail",
|
|
19
|
+
description: "Sign in with email and password",
|
|
20
|
+
requestBody: OpenAPI.json_request_body(
|
|
21
|
+
OpenAPI.object_schema(
|
|
22
|
+
{
|
|
23
|
+
email: {type: "string", description: "Email of the user"},
|
|
24
|
+
password: {type: "string", description: "Password of the user"},
|
|
25
|
+
callbackURL: {type: ["string", "null"], description: "Callback URL to use as a redirect for email verification"},
|
|
26
|
+
rememberMe: {type: ["boolean", "null"], default: true, description: "If this is false, the session will not be remembered. Default is `true`."}
|
|
27
|
+
},
|
|
28
|
+
required: ["email", "password"]
|
|
29
|
+
)
|
|
30
|
+
),
|
|
31
|
+
responses: {
|
|
32
|
+
"200" => OpenAPI.json_response(
|
|
33
|
+
"Success - Returns either session details or redirect URL",
|
|
34
|
+
OpenAPI.session_response_schema(description: "Session response when idToken is provided", nullable_url: true)
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
16
38
|
}
|
|
17
39
|
) do |ctx|
|
|
18
40
|
options = ctx.context.options
|
|
19
41
|
email_config = options.email_and_password
|
|
20
42
|
if email_config[:enabled] != true
|
|
21
|
-
raise APIError.new("BAD_REQUEST",
|
|
43
|
+
raise APIError.new("BAD_REQUEST", code: "EMAIL_PASSWORD_DISABLED", message: BASE_ERROR_CODES["EMAIL_PASSWORD_DISABLED"])
|
|
22
44
|
end
|
|
23
45
|
|
|
24
46
|
body = normalize_hash(ctx.body)
|
|
@@ -3,7 +3,19 @@
|
|
|
3
3
|
module BetterAuth
|
|
4
4
|
module Routes
|
|
5
5
|
def self.sign_out
|
|
6
|
-
Endpoint.new(
|
|
6
|
+
Endpoint.new(
|
|
7
|
+
path: "/sign-out",
|
|
8
|
+
method: "POST",
|
|
9
|
+
metadata: {
|
|
10
|
+
openapi: {
|
|
11
|
+
operationId: "signOut",
|
|
12
|
+
description: "Sign out the current session",
|
|
13
|
+
responses: {
|
|
14
|
+
"200" => OpenAPI.json_response("Successfully signed out", OpenAPI.success_response_schema)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
) do |ctx|
|
|
7
19
|
token_cookie = ctx.context.auth_cookies[:session_token]
|
|
8
20
|
token = ctx.get_signed_cookie(token_cookie.name, ctx.context.secret)
|
|
9
21
|
ctx.context.internal_adapter.delete_session(token) if token
|
|
@@ -11,17 +11,52 @@ module BetterAuth
|
|
|
11
11
|
Endpoint.new(
|
|
12
12
|
path: "/sign-up/email",
|
|
13
13
|
method: "POST",
|
|
14
|
+
body_schema: request_body_schema(
|
|
15
|
+
required_strings: %w[name email],
|
|
16
|
+
required_nonempty_strings: %w[password]
|
|
17
|
+
),
|
|
14
18
|
metadata: {
|
|
15
19
|
allowed_media_types: [
|
|
16
20
|
"application/x-www-form-urlencoded",
|
|
17
21
|
"application/json"
|
|
18
|
-
]
|
|
22
|
+
],
|
|
23
|
+
openapi: {
|
|
24
|
+
operationId: "signUpWithEmailAndPassword",
|
|
25
|
+
description: "Sign up a user using email and password",
|
|
26
|
+
requestBody: OpenAPI.json_request_body(
|
|
27
|
+
OpenAPI.object_schema(
|
|
28
|
+
{
|
|
29
|
+
name: {type: "string", description: "The name of the user"},
|
|
30
|
+
email: {type: "string", description: "The email of the user"},
|
|
31
|
+
password: {type: "string", description: "The password of the user"},
|
|
32
|
+
image: {type: "string", description: "The profile image URL of the user"},
|
|
33
|
+
callbackURL: {type: "string", description: "The URL to use for email verification callback"},
|
|
34
|
+
rememberMe: {type: "boolean", description: "If this is false, the session will not be remembered. Default is `true`."}
|
|
35
|
+
},
|
|
36
|
+
required: ["name", "email", "password"]
|
|
37
|
+
),
|
|
38
|
+
required: false
|
|
39
|
+
),
|
|
40
|
+
responses: {
|
|
41
|
+
"200" => OpenAPI.json_response(
|
|
42
|
+
"Successfully created user",
|
|
43
|
+
OpenAPI.object_schema(
|
|
44
|
+
{
|
|
45
|
+
token: {type: "string", nullable: true, description: "Authentication token for the session"},
|
|
46
|
+
user: {type: "object", "$ref": "#/components/schemas/User"}
|
|
47
|
+
},
|
|
48
|
+
required: ["user"]
|
|
49
|
+
)
|
|
50
|
+
),
|
|
51
|
+
"422" => OpenAPI.error_response("Unprocessable Entity. User already exists or failed to create user.")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
19
54
|
}
|
|
20
55
|
) do |ctx|
|
|
21
56
|
options = ctx.context.options
|
|
22
57
|
email_config = options.email_and_password
|
|
23
58
|
if email_config[:enabled] != true || email_config[:disable_sign_up]
|
|
24
|
-
raise APIError.new("BAD_REQUEST",
|
|
59
|
+
raise APIError.new("BAD_REQUEST", code: "EMAIL_PASSWORD_SIGN_UP_DISABLED", message: BASE_ERROR_CODES["EMAIL_PASSWORD_SIGN_UP_DISABLED"])
|
|
25
60
|
end
|
|
26
61
|
|
|
27
62
|
body = normalize_hash(ctx.body)
|
|
@@ -118,7 +153,8 @@ module BetterAuth
|
|
|
118
153
|
"name" => name,
|
|
119
154
|
"image" => image,
|
|
120
155
|
"emailVerified" => false
|
|
121
|
-
)
|
|
156
|
+
),
|
|
157
|
+
context: ctx
|
|
122
158
|
)
|
|
123
159
|
rescue APIError
|
|
124
160
|
raise
|
|
@@ -151,18 +187,40 @@ module BetterAuth
|
|
|
151
187
|
"updatedAt" => now
|
|
152
188
|
}
|
|
153
189
|
reserved = %w[email password name image callbackURL callbackUrl callback_url rememberMe remember_me]
|
|
154
|
-
additional =
|
|
190
|
+
additional = synthetic_additional_user_fields(ctx, body.except(*reserved))
|
|
155
191
|
custom = ctx.context.options.email_and_password[:custom_synthetic_user]
|
|
156
192
|
return core_fields.merge(additional) unless custom.respond_to?(:call)
|
|
157
193
|
|
|
158
194
|
value = {
|
|
159
195
|
core_fields: core_fields.except("id"),
|
|
196
|
+
coreFields: core_fields.except("id"),
|
|
160
197
|
additional_fields: additional,
|
|
198
|
+
additionalFields: additional,
|
|
161
199
|
id: core_fields["id"]
|
|
162
200
|
}
|
|
163
201
|
stringify_synthetic_user(custom.call(value))
|
|
164
202
|
end
|
|
165
203
|
|
|
204
|
+
def self.synthetic_additional_user_fields(ctx, data)
|
|
205
|
+
additional = parse_declared_input(ctx, "user", data, allowed_base: [])
|
|
206
|
+
configured = ctx.context.options.user[:additional_fields] || {}
|
|
207
|
+
configured.each do |field, attributes|
|
|
208
|
+
storage_field = Schema.storage_key(field)
|
|
209
|
+
next if additional.key?(storage_field)
|
|
210
|
+
|
|
211
|
+
field_attributes = normalize_hash(attributes || {})
|
|
212
|
+
next unless field_attributes.key?("defaultValue") || field_attributes.key?("default_value")
|
|
213
|
+
|
|
214
|
+
default = field_attributes.key?("defaultValue") ? field_attributes["defaultValue"] : field_attributes["default_value"]
|
|
215
|
+
additional[storage_field] = resolve_default(default)
|
|
216
|
+
end
|
|
217
|
+
additional
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.resolve_default(value)
|
|
221
|
+
value.respond_to?(:call) ? value.call : value
|
|
222
|
+
end
|
|
223
|
+
|
|
166
224
|
def self.stringify_synthetic_user(value)
|
|
167
225
|
return value.each_with_object({}) { |(key, object_value), result| result[Schema.storage_key(key)] = object_value } if value.is_a?(Hash)
|
|
168
226
|
|
|
@@ -7,7 +7,49 @@ require "securerandom"
|
|
|
7
7
|
module BetterAuth
|
|
8
8
|
module Routes
|
|
9
9
|
def self.sign_in_social
|
|
10
|
-
Endpoint.new(
|
|
10
|
+
Endpoint.new(
|
|
11
|
+
path: "/sign-in/social",
|
|
12
|
+
method: "POST",
|
|
13
|
+
metadata: {
|
|
14
|
+
openapi: {
|
|
15
|
+
description: "Sign in with a social provider",
|
|
16
|
+
operationId: "socialSignIn",
|
|
17
|
+
requestBody: OpenAPI.json_request_body(
|
|
18
|
+
OpenAPI.object_schema(
|
|
19
|
+
{
|
|
20
|
+
provider: {type: "string"},
|
|
21
|
+
callbackURL: {type: ["string", "null"], description: "Callback URL to redirect to after the user has signed in"},
|
|
22
|
+
errorCallbackURL: {type: ["string", "null"], description: "Callback URL to redirect to if an error happens"},
|
|
23
|
+
newUserCallbackURL: {type: ["string", "null"]},
|
|
24
|
+
disableRedirect: {type: ["boolean", "null"], description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself"},
|
|
25
|
+
requestSignUp: {type: ["boolean", "null"], description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider"},
|
|
26
|
+
loginHint: {type: ["string", "null"], description: "The login hint to use for the authorization code request"},
|
|
27
|
+
additionalData: {type: ["string", "null"]},
|
|
28
|
+
scopes: {type: ["array", "null"], description: "Array of scopes to request from the provider. This will override the default scopes passed."},
|
|
29
|
+
idToken: {
|
|
30
|
+
type: ["object", "null"],
|
|
31
|
+
properties: {
|
|
32
|
+
token: {type: "string", description: "ID token from the provider"},
|
|
33
|
+
accessToken: {type: ["string", "null"], description: "Access token from the provider"},
|
|
34
|
+
refreshToken: {type: ["string", "null"], description: "Refresh token from the provider"},
|
|
35
|
+
expiresAt: {type: ["number", "null"], description: "Expiry date of the token"},
|
|
36
|
+
nonce: {type: ["string", "null"], description: "Nonce used to generate the token"}
|
|
37
|
+
},
|
|
38
|
+
required: ["token"]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: ["provider"]
|
|
42
|
+
)
|
|
43
|
+
),
|
|
44
|
+
responses: {
|
|
45
|
+
"200" => OpenAPI.json_response(
|
|
46
|
+
"Success - Returns either session details or redirect URL",
|
|
47
|
+
OpenAPI.session_response_schema(description: "Session response when idToken is provided")
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
) do |ctx|
|
|
11
53
|
body = normalize_hash(ctx.body)
|
|
12
54
|
provider_id = body["provider"].to_s
|
|
13
55
|
provider = social_provider(ctx.context, provider_id)
|
|
@@ -66,21 +108,38 @@ module BetterAuth
|
|
|
66
108
|
|
|
67
109
|
def self.callback_oauth
|
|
68
110
|
Endpoint.new(
|
|
69
|
-
path: "/callback/:
|
|
111
|
+
path: "/callback/:id",
|
|
70
112
|
method: ["GET", "POST"],
|
|
71
|
-
metadata: {
|
|
113
|
+
metadata: {
|
|
114
|
+
allowed_media_types: ["application/x-www-form-urlencoded", "application/json"],
|
|
115
|
+
openapi: {
|
|
116
|
+
operationId: "callbackOAuth",
|
|
117
|
+
description: "Handle an OAuth provider callback",
|
|
118
|
+
parameters: [
|
|
119
|
+
{
|
|
120
|
+
name: "id",
|
|
121
|
+
in: "path",
|
|
122
|
+
required: true,
|
|
123
|
+
schema: {type: "string"}
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
responses: {
|
|
127
|
+
"302" => {description: "Redirects to the configured callback URL"}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
72
131
|
) do |ctx|
|
|
132
|
+
provider_id = (fetch_value(ctx.params, "id") || fetch_value(ctx.params, "providerId")).to_s
|
|
73
133
|
if ctx.method == "POST"
|
|
74
134
|
merged = normalize_hash(ctx.query).merge(normalize_hash(ctx.body))
|
|
75
135
|
query = URI.encode_www_form(merged.reject { |_key, value| value.nil? || value.to_s.empty? })
|
|
76
|
-
target = "#{ctx.context.base_url}/callback/#{
|
|
136
|
+
target = "#{ctx.context.base_url}/callback/#{provider_id}"
|
|
77
137
|
target = "#{target}?#{query}" unless query.empty?
|
|
78
138
|
raise ctx.redirect(target)
|
|
79
139
|
end
|
|
80
140
|
|
|
81
141
|
source = ctx.query
|
|
82
142
|
data = normalize_hash(source)
|
|
83
|
-
provider_id = fetch_value(ctx.params, "providerId").to_s
|
|
84
143
|
provider = social_provider(ctx.context, provider_id)
|
|
85
144
|
state = data["state"].to_s
|
|
86
145
|
state_data = state.empty? ? nil : Crypto.verify_jwt(state, ctx.context.secret)
|
|
@@ -132,7 +191,42 @@ module BetterAuth
|
|
|
132
191
|
end
|
|
133
192
|
|
|
134
193
|
def self.link_social
|
|
135
|
-
Endpoint.new(
|
|
194
|
+
Endpoint.new(
|
|
195
|
+
path: "/link-social",
|
|
196
|
+
method: "POST",
|
|
197
|
+
metadata: {
|
|
198
|
+
openapi: {
|
|
199
|
+
operationId: "linkSocialAccount",
|
|
200
|
+
description: "Link a social account to the current user",
|
|
201
|
+
requestBody: OpenAPI.json_request_body(
|
|
202
|
+
OpenAPI.object_schema(
|
|
203
|
+
{
|
|
204
|
+
provider: {type: "string"},
|
|
205
|
+
callbackURL: {type: ["string", "null"]},
|
|
206
|
+
errorCallbackURL: {type: ["string", "null"]},
|
|
207
|
+
disableRedirect: {type: ["boolean", "null"]},
|
|
208
|
+
scopes: {type: ["array", "null"], items: {type: "string"}},
|
|
209
|
+
idToken: {type: ["object", "null"]}
|
|
210
|
+
},
|
|
211
|
+
required: ["provider"]
|
|
212
|
+
)
|
|
213
|
+
),
|
|
214
|
+
responses: {
|
|
215
|
+
"200" => OpenAPI.json_response(
|
|
216
|
+
"Social account link started or completed",
|
|
217
|
+
OpenAPI.object_schema(
|
|
218
|
+
{
|
|
219
|
+
url: {type: "string"},
|
|
220
|
+
redirect: {type: "boolean"},
|
|
221
|
+
status: {type: ["boolean", "null"]}
|
|
222
|
+
},
|
|
223
|
+
required: ["url", "redirect"]
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
) do |ctx|
|
|
136
230
|
session = current_session(ctx)
|
|
137
231
|
body = normalize_hash(ctx.body)
|
|
138
232
|
provider_id = body["provider"].to_s
|
|
@@ -254,7 +348,8 @@ module BetterAuth
|
|
|
254
348
|
image: fetch_value(user_info, "image"),
|
|
255
349
|
emailVerified: !!fetch_value(user_info, "emailVerified")
|
|
256
350
|
},
|
|
257
|
-
account_info.merge("providerId" => provider_id, "accountId" => account_id)
|
|
351
|
+
account_info.merge("providerId" => provider_id, "accountId" => account_id),
|
|
352
|
+
context: ctx
|
|
258
353
|
)
|
|
259
354
|
user = created[:user]
|
|
260
355
|
new_user = true
|