@jskit-ai/auth-provider-supabase-core 0.1.54 → 0.1.56
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.
- package/package.descriptor.mjs +3 -3
- package/package.json +4 -3
- package/src/server/lib/accountFlows.js +53 -20
- package/src/server/lib/actions/auth.contributor.js +36 -29
- package/src/server/lib/authInputParsers.js +0 -174
- package/src/server/lib/authenticatedProfile.js +78 -0
- package/src/server/lib/oauthFlows.js +74 -15
- package/src/server/lib/passwordSecurityFlows.js +54 -16
- package/src/server/lib/service.js +22 -25
- package/src/server/lib/test-utils.js +1 -4
- package/test/authInputParsers.test.js +12 -25
- package/test/devAuthBootstrap.test.js +32 -0
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
"packageVersion": 1,
|
|
3
3
|
"packageId": "@jskit-ai/auth-provider-supabase-core",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.56",
|
|
5
5
|
"kind": "runtime",
|
|
6
6
|
"options": {
|
|
7
7
|
"auth-supabase-url": {
|
|
@@ -83,8 +83,8 @@ export default Object.freeze({
|
|
|
83
83
|
"mutations": {
|
|
84
84
|
"dependencies": {
|
|
85
85
|
"runtime": {
|
|
86
|
-
"@jskit-ai/auth-core": "0.1.
|
|
87
|
-
"@jskit-ai/kernel": "0.1.
|
|
86
|
+
"@jskit-ai/auth-core": "0.1.56",
|
|
87
|
+
"@jskit-ai/kernel": "0.1.57",
|
|
88
88
|
"dotenv": "^16.4.5",
|
|
89
89
|
"@supabase/supabase-js": "^2.57.4",
|
|
90
90
|
"jose": "^6.1.0"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/auth-provider-supabase-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
"./client": "./src/client/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@jskit-ai/auth-core": "0.1.
|
|
16
|
-
"@jskit-ai/kernel": "0.1.
|
|
15
|
+
"@jskit-ai/auth-core": "0.1.56",
|
|
16
|
+
"@jskit-ai/kernel": "0.1.57",
|
|
17
|
+
"json-rest-schema": "1.x.x",
|
|
17
18
|
"jose": "^6.1.0",
|
|
18
19
|
"@supabase/supabase-js": "^2.57.4"
|
|
19
20
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { authRegisterBodyValidator } from "@jskit-ai/auth-core/shared/commands/authRegisterCommand";
|
|
3
|
+
import { authRegisterConfirmationResendBodyValidator } from "@jskit-ai/auth-core/shared/commands/authRegisterConfirmationResendCommand";
|
|
4
|
+
import { authLoginPasswordBodyValidator } from "@jskit-ai/auth-core/shared/commands/authLoginPasswordCommand";
|
|
5
|
+
import { authLoginOtpRequestBodyValidator } from "@jskit-ai/auth-core/shared/commands/authLoginOtpRequestCommand";
|
|
6
|
+
import { authLoginOtpVerifyBodyValidator } from "@jskit-ai/auth-core/shared/commands/authLoginOtpVerifyCommand";
|
|
2
7
|
import {
|
|
3
8
|
requireAuthUser,
|
|
4
|
-
requireAuthUserSession
|
|
5
|
-
requireNoFieldErrors
|
|
9
|
+
requireAuthUserSession
|
|
6
10
|
} from "./flowGuards.js";
|
|
7
11
|
|
|
8
12
|
function normalizeLocalReturnToPath(value, { fallback = "" } = {}) {
|
|
@@ -21,7 +25,6 @@ function normalizeLocalReturnToPath(value, { fallback = "" } = {}) {
|
|
|
21
25
|
function createAccountFlows(deps) {
|
|
22
26
|
const {
|
|
23
27
|
ensureConfigured,
|
|
24
|
-
validators,
|
|
25
28
|
validationError,
|
|
26
29
|
getSupabaseClient,
|
|
27
30
|
displayNameFromEmail,
|
|
@@ -33,7 +36,6 @@ function createAccountFlows(deps) {
|
|
|
33
36
|
appPublicUrl,
|
|
34
37
|
isTransientSupabaseError,
|
|
35
38
|
isUserNotFoundLikeAuthError,
|
|
36
|
-
parseOtpLoginVerifyPayload,
|
|
37
39
|
mapOtpVerifyError,
|
|
38
40
|
setSessionFromRequestCookies,
|
|
39
41
|
mapProfileUpdateError,
|
|
@@ -82,8 +84,11 @@ function createAccountFlows(deps) {
|
|
|
82
84
|
async function register(payload) {
|
|
83
85
|
ensureConfigured();
|
|
84
86
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
+
const result = authRegisterBodyValidator.schema.create(payload);
|
|
88
|
+
if (Object.keys(result.errors).length > 0) {
|
|
89
|
+
throw validationError(result.errors);
|
|
90
|
+
}
|
|
91
|
+
const parsed = result.validatedObject;
|
|
87
92
|
|
|
88
93
|
const supabase = getSupabaseClient();
|
|
89
94
|
const response = await supabase.auth.signUp({
|
|
@@ -126,8 +131,11 @@ function createAccountFlows(deps) {
|
|
|
126
131
|
async function resendRegisterConfirmation(payload) {
|
|
127
132
|
ensureConfigured();
|
|
128
133
|
|
|
129
|
-
const
|
|
130
|
-
|
|
134
|
+
const result = authRegisterConfirmationResendBodyValidator.schema.create(payload);
|
|
135
|
+
if (Object.keys(result.errors).length > 0) {
|
|
136
|
+
throw validationError(result.errors);
|
|
137
|
+
}
|
|
138
|
+
const parsed = result.validatedObject;
|
|
131
139
|
|
|
132
140
|
const supabase = getSupabaseClient();
|
|
133
141
|
const emailRedirectTo = resolveRegisterEmailRedirectTo();
|
|
@@ -175,8 +183,11 @@ function createAccountFlows(deps) {
|
|
|
175
183
|
async function login(payload) {
|
|
176
184
|
ensureConfigured();
|
|
177
185
|
|
|
178
|
-
const
|
|
179
|
-
|
|
186
|
+
const result = authLoginPasswordBodyValidator.schema.create(payload);
|
|
187
|
+
if (Object.keys(result.errors).length > 0) {
|
|
188
|
+
throw validationError(result.errors);
|
|
189
|
+
}
|
|
190
|
+
const parsed = result.validatedObject;
|
|
180
191
|
|
|
181
192
|
const supabase = getSupabaseClient();
|
|
182
193
|
const response = await supabase.auth.signInWithPassword({
|
|
@@ -201,10 +212,13 @@ function createAccountFlows(deps) {
|
|
|
201
212
|
async function requestOtpLogin(payload) {
|
|
202
213
|
ensureConfigured();
|
|
203
214
|
|
|
204
|
-
const
|
|
205
|
-
|
|
215
|
+
const result = authLoginOtpRequestBodyValidator.schema.create(payload);
|
|
216
|
+
if (Object.keys(result.errors).length > 0) {
|
|
217
|
+
throw validationError(result.errors);
|
|
218
|
+
}
|
|
219
|
+
const parsed = result.validatedObject;
|
|
206
220
|
|
|
207
|
-
const emailRedirectTo = resolveOtpEmailRedirectTo(
|
|
221
|
+
const emailRedirectTo = resolveOtpEmailRedirectTo(parsed.returnTo);
|
|
208
222
|
const supabase = getSupabaseClient();
|
|
209
223
|
let response;
|
|
210
224
|
try {
|
|
@@ -250,22 +264,41 @@ function createAccountFlows(deps) {
|
|
|
250
264
|
async function verifyOtpLogin(payload) {
|
|
251
265
|
ensureConfigured();
|
|
252
266
|
|
|
253
|
-
const
|
|
254
|
-
|
|
267
|
+
const result = authLoginOtpVerifyBodyValidator.schema.patch(payload);
|
|
268
|
+
if (Object.keys(result.errors).length > 0) {
|
|
269
|
+
throw validationError(result.errors);
|
|
270
|
+
}
|
|
271
|
+
const parsed = result.validatedObject;
|
|
272
|
+
const fieldErrors = {};
|
|
273
|
+
const token = String(parsed.token || "").trim();
|
|
274
|
+
const tokenHash = String(parsed.tokenHash || "").trim();
|
|
275
|
+
const type = String(parsed.type || "email").trim().toLowerCase();
|
|
276
|
+
|
|
277
|
+
if (!token && !tokenHash) {
|
|
278
|
+
fieldErrors.token = "One-time code is required.";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!tokenHash && !parsed.email) {
|
|
282
|
+
fieldErrors.email = "Email is required.";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
286
|
+
throw validationError(fieldErrors);
|
|
287
|
+
}
|
|
255
288
|
|
|
256
289
|
const supabase = getSupabaseClient();
|
|
257
290
|
let response;
|
|
258
291
|
try {
|
|
259
|
-
if (
|
|
292
|
+
if (tokenHash) {
|
|
260
293
|
response = await supabase.auth.verifyOtp({
|
|
261
|
-
token_hash:
|
|
262
|
-
type
|
|
294
|
+
token_hash: tokenHash,
|
|
295
|
+
type
|
|
263
296
|
});
|
|
264
297
|
} else {
|
|
265
298
|
response = await supabase.auth.verifyOtp({
|
|
266
299
|
email: parsed.email,
|
|
267
|
-
token
|
|
268
|
-
type
|
|
300
|
+
token,
|
|
301
|
+
type
|
|
269
302
|
});
|
|
270
303
|
}
|
|
271
304
|
} catch (error) {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { createSchema } from "json-rest-schema";
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
+
emptyInputValidator
|
|
3
4
|
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
5
|
+
import {
|
|
6
|
+
composeSchemaDefinitions
|
|
7
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
8
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
4
9
|
import {
|
|
5
10
|
authRegisterCommand,
|
|
6
11
|
authRegisterConfirmationResendCommand,
|
|
@@ -15,6 +20,22 @@ import {
|
|
|
15
20
|
authPasswordResetCommand
|
|
16
21
|
} from "@jskit-ai/auth-core/shared/commands";
|
|
17
22
|
|
|
23
|
+
const authLoginOAuthStartInput = composeSchemaDefinitions([
|
|
24
|
+
authLoginOAuthStartCommand.operation.params,
|
|
25
|
+
authLoginOAuthStartCommand.operation.query
|
|
26
|
+
], {
|
|
27
|
+
mode: "patch",
|
|
28
|
+
context: "authContributor.authLoginOAuthStartInput"
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const authLogoutOutput = deepFreeze({
|
|
32
|
+
schema: createSchema({
|
|
33
|
+
ok: { type: "boolean", required: true },
|
|
34
|
+
clearSession: { type: "boolean", required: true }
|
|
35
|
+
}),
|
|
36
|
+
mode: "replace"
|
|
37
|
+
});
|
|
38
|
+
|
|
18
39
|
function requireRequestContext(context, actionId) {
|
|
19
40
|
const request = context?.requestMeta?.request || null;
|
|
20
41
|
if (request) {
|
|
@@ -30,7 +51,7 @@ const devLoginAsAction = Object.freeze({
|
|
|
30
51
|
kind: "command",
|
|
31
52
|
channels: ["api", "internal"],
|
|
32
53
|
surfacesFrom: "enabled",
|
|
33
|
-
|
|
54
|
+
input: authDevLoginAsCommand.operation.body,
|
|
34
55
|
idempotency: "none",
|
|
35
56
|
audit: {
|
|
36
57
|
actionName: "auth.dev.loginAs"
|
|
@@ -48,7 +69,7 @@ const authActionsBeforeDevLogin = Object.freeze([
|
|
|
48
69
|
kind: "command",
|
|
49
70
|
channels: ["api", "internal"],
|
|
50
71
|
surfacesFrom: "enabled",
|
|
51
|
-
|
|
72
|
+
input: authRegisterCommand.operation.body,
|
|
52
73
|
idempotency: "none",
|
|
53
74
|
audit: {
|
|
54
75
|
actionName: "auth.register"
|
|
@@ -64,7 +85,7 @@ const authActionsBeforeDevLogin = Object.freeze([
|
|
|
64
85
|
kind: "command",
|
|
65
86
|
channels: ["api", "internal"],
|
|
66
87
|
surfacesFrom: "enabled",
|
|
67
|
-
|
|
88
|
+
input: authRegisterConfirmationResendCommand.operation.body,
|
|
68
89
|
idempotency: "none",
|
|
69
90
|
audit: {
|
|
70
91
|
actionName: "auth.register.confirmation.resend"
|
|
@@ -80,7 +101,7 @@ const authActionsBeforeDevLogin = Object.freeze([
|
|
|
80
101
|
kind: "command",
|
|
81
102
|
channels: ["api", "internal"],
|
|
82
103
|
surfacesFrom: "enabled",
|
|
83
|
-
|
|
104
|
+
input: authLoginPasswordCommand.operation.body,
|
|
84
105
|
idempotency: "none",
|
|
85
106
|
audit: {
|
|
86
107
|
actionName: "auth.login.password"
|
|
@@ -96,7 +117,7 @@ const authActionsBeforeDevLogin = Object.freeze([
|
|
|
96
117
|
kind: "command",
|
|
97
118
|
channels: ["api", "internal"],
|
|
98
119
|
surfacesFrom: "enabled",
|
|
99
|
-
|
|
120
|
+
input: authLoginOtpRequestCommand.operation.body,
|
|
100
121
|
idempotency: "none",
|
|
101
122
|
audit: {
|
|
102
123
|
actionName: "auth.login.otp.request"
|
|
@@ -112,7 +133,7 @@ const authActionsBeforeDevLogin = Object.freeze([
|
|
|
112
133
|
kind: "command",
|
|
113
134
|
channels: ["api", "internal"],
|
|
114
135
|
surfacesFrom: "enabled",
|
|
115
|
-
|
|
136
|
+
input: authLoginOtpVerifyCommand.operation.body,
|
|
116
137
|
idempotency: "none",
|
|
117
138
|
audit: {
|
|
118
139
|
actionName: "auth.login.otp.verify"
|
|
@@ -128,7 +149,7 @@ const authActionsBeforeDevLogin = Object.freeze([
|
|
|
128
149
|
kind: "command",
|
|
129
150
|
channels: ["api", "internal"],
|
|
130
151
|
surfacesFrom: "enabled",
|
|
131
|
-
|
|
152
|
+
input: authLoginOAuthStartInput,
|
|
132
153
|
idempotency: "none",
|
|
133
154
|
audit: {
|
|
134
155
|
actionName: "auth.login.oauth.start"
|
|
@@ -144,7 +165,7 @@ const authActionsBeforeDevLogin = Object.freeze([
|
|
|
144
165
|
kind: "command",
|
|
145
166
|
channels: ["api", "internal"],
|
|
146
167
|
surfacesFrom: "enabled",
|
|
147
|
-
|
|
168
|
+
input: authLoginOAuthCompleteCommand.operation.body,
|
|
148
169
|
idempotency: "none",
|
|
149
170
|
audit: {
|
|
150
171
|
actionName: "auth.login.oauth.complete"
|
|
@@ -163,7 +184,7 @@ const authActionsAfterDevLogin = Object.freeze([
|
|
|
163
184
|
kind: "command",
|
|
164
185
|
channels: ["api", "internal"],
|
|
165
186
|
surfacesFrom: "enabled",
|
|
166
|
-
|
|
187
|
+
input: authPasswordResetRequestCommand.operation.body,
|
|
167
188
|
idempotency: "none",
|
|
168
189
|
audit: {
|
|
169
190
|
actionName: "auth.password.reset.request"
|
|
@@ -179,7 +200,7 @@ const authActionsAfterDevLogin = Object.freeze([
|
|
|
179
200
|
kind: "command",
|
|
180
201
|
channels: ["api", "internal"],
|
|
181
202
|
surfacesFrom: "enabled",
|
|
182
|
-
|
|
203
|
+
input: authPasswordRecoveryCompleteCommand.operation.body,
|
|
183
204
|
idempotency: "none",
|
|
184
205
|
audit: {
|
|
185
206
|
actionName: "auth.password.recovery.complete"
|
|
@@ -195,7 +216,7 @@ const authActionsAfterDevLogin = Object.freeze([
|
|
|
195
216
|
kind: "command",
|
|
196
217
|
channels: ["api", "internal"],
|
|
197
218
|
surfacesFrom: "enabled",
|
|
198
|
-
|
|
219
|
+
input: authPasswordResetCommand.operation.body,
|
|
199
220
|
idempotency: "none",
|
|
200
221
|
audit: {
|
|
201
222
|
actionName: "auth.password.reset"
|
|
@@ -211,22 +232,8 @@ const authActionsAfterDevLogin = Object.freeze([
|
|
|
211
232
|
kind: "command",
|
|
212
233
|
channels: ["api", "automation", "internal"],
|
|
213
234
|
surfacesFrom: "enabled",
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
schema: {
|
|
217
|
-
type: "object",
|
|
218
|
-
properties: {
|
|
219
|
-
ok: {
|
|
220
|
-
type: "boolean"
|
|
221
|
-
},
|
|
222
|
-
clearSession: {
|
|
223
|
-
type: "boolean"
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
required: ["ok", "clearSession"],
|
|
227
|
-
additionalProperties: false
|
|
228
|
-
}
|
|
229
|
-
},
|
|
235
|
+
input: emptyInputValidator,
|
|
236
|
+
output: authLogoutOutput,
|
|
230
237
|
idempotency: "none",
|
|
231
238
|
audit: {
|
|
232
239
|
actionName: "auth.logout"
|
|
@@ -250,7 +257,7 @@ const authActionsAfterDevLogin = Object.freeze([
|
|
|
250
257
|
kind: "query",
|
|
251
258
|
channels: ["api", "internal"],
|
|
252
259
|
surfacesFrom: "enabled",
|
|
253
|
-
|
|
260
|
+
input: emptyInputValidator,
|
|
254
261
|
idempotency: "none",
|
|
255
262
|
audit: {
|
|
256
263
|
actionName: "auth.session.read"
|
|
@@ -1,31 +1,8 @@
|
|
|
1
1
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
-
import {
|
|
3
|
-
AUTH_ACCESS_TOKEN_MAX_LENGTH,
|
|
4
|
-
AUTH_RECOVERY_TOKEN_MAX_LENGTH,
|
|
5
|
-
AUTH_REFRESH_TOKEN_MAX_LENGTH
|
|
6
|
-
} from "@jskit-ai/auth-core/shared/authConstraints";
|
|
7
2
|
import { normalizeOAuthProviderList } from "@jskit-ai/auth-core/shared/oauthProviders";
|
|
8
|
-
import { validators } from "@jskit-ai/auth-core/server/validators";
|
|
9
3
|
import { normalizeOAuthProviderFromCatalog } from "./oauthProviderCatalog.js";
|
|
10
4
|
import { validationError } from "./authErrorMappers.js";
|
|
11
5
|
|
|
12
|
-
const OTP_VERIFY_TYPE = "email";
|
|
13
|
-
const SESSION_PAIR_REQUIRED_ERRORS = Object.freeze({
|
|
14
|
-
accessToken: "Access token is required when a refresh token is provided.",
|
|
15
|
-
refreshToken: "Refresh token is required when an access token is provided."
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
function applySessionPairValidation(accessToken, refreshToken, fieldErrors) {
|
|
19
|
-
if ((accessToken && !refreshToken) || (!accessToken && refreshToken)) {
|
|
20
|
-
if (!accessToken) {
|
|
21
|
-
fieldErrors.accessToken = SESSION_PAIR_REQUIRED_ERRORS.accessToken;
|
|
22
|
-
}
|
|
23
|
-
if (!refreshToken) {
|
|
24
|
-
fieldErrors.refreshToken = SESSION_PAIR_REQUIRED_ERRORS.refreshToken;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
6
|
function resolveConfiguredOAuthProviders(options = {}) {
|
|
30
7
|
return normalizeOAuthProviderList(options.providerIds, { fallback: [] });
|
|
31
8
|
}
|
|
@@ -51,154 +28,6 @@ function normalizeOAuthProviderInput(value, options = {}) {
|
|
|
51
28
|
});
|
|
52
29
|
}
|
|
53
30
|
|
|
54
|
-
function validatePasswordRecoveryPayload(payload) {
|
|
55
|
-
const code = String(payload?.code || "").trim();
|
|
56
|
-
const tokenHash = String(payload?.tokenHash || "").trim();
|
|
57
|
-
const type = String(payload?.type || "recovery")
|
|
58
|
-
.trim()
|
|
59
|
-
.toLowerCase();
|
|
60
|
-
const accessToken = String(payload?.accessToken || "").trim();
|
|
61
|
-
const refreshToken = String(payload?.refreshToken || "").trim();
|
|
62
|
-
|
|
63
|
-
const fieldErrors = {};
|
|
64
|
-
|
|
65
|
-
if (type !== "recovery") {
|
|
66
|
-
fieldErrors.type = "Only recovery password reset links are supported.";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (code.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
|
|
70
|
-
fieldErrors.code = "Recovery code is too long.";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (tokenHash.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
|
|
74
|
-
fieldErrors.tokenHash = "Recovery token is too long.";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (accessToken.length > AUTH_ACCESS_TOKEN_MAX_LENGTH) {
|
|
78
|
-
fieldErrors.accessToken = "Access token is too long.";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (refreshToken.length > AUTH_REFRESH_TOKEN_MAX_LENGTH) {
|
|
82
|
-
fieldErrors.refreshToken = "Refresh token is too long.";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
applySessionPairValidation(accessToken, refreshToken, fieldErrors);
|
|
86
|
-
|
|
87
|
-
const hasCode = Boolean(code);
|
|
88
|
-
const hasTokenHash = Boolean(tokenHash);
|
|
89
|
-
const hasSessionPair = Boolean(accessToken && refreshToken);
|
|
90
|
-
|
|
91
|
-
if (!hasCode && !hasTokenHash && !hasSessionPair) {
|
|
92
|
-
fieldErrors.recovery = "Recovery token is required.";
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
code,
|
|
97
|
-
tokenHash,
|
|
98
|
-
type,
|
|
99
|
-
accessToken,
|
|
100
|
-
refreshToken,
|
|
101
|
-
hasCode,
|
|
102
|
-
hasTokenHash,
|
|
103
|
-
hasSessionPair,
|
|
104
|
-
fieldErrors
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function parseOAuthCompletePayload(payload = {}, options = {}) {
|
|
109
|
-
const code = String(payload.code || "").trim();
|
|
110
|
-
const accessToken = String(payload.accessToken || "").trim();
|
|
111
|
-
const refreshToken = String(payload.refreshToken || "").trim();
|
|
112
|
-
const errorCode = String(payload.error || payload.error_code || "").trim();
|
|
113
|
-
const errorDescription = String(payload.errorDescription || payload.error_description || "").trim();
|
|
114
|
-
const fieldErrors = {};
|
|
115
|
-
|
|
116
|
-
if (code.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
|
|
117
|
-
fieldErrors.code = "OAuth code is too long.";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (errorCode.length > 128) {
|
|
121
|
-
fieldErrors.error = "OAuth error code is too long.";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (errorDescription.length > 1024) {
|
|
125
|
-
fieldErrors.errorDescription = "OAuth error description is too long.";
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (accessToken.length > AUTH_ACCESS_TOKEN_MAX_LENGTH) {
|
|
129
|
-
fieldErrors.accessToken = "Access token is too long.";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (refreshToken.length > AUTH_REFRESH_TOKEN_MAX_LENGTH) {
|
|
133
|
-
fieldErrors.refreshToken = "Refresh token is too long.";
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
applySessionPairValidation(accessToken, refreshToken, fieldErrors);
|
|
137
|
-
|
|
138
|
-
const hasSessionPair = Boolean(accessToken && refreshToken);
|
|
139
|
-
const provider =
|
|
140
|
-
!hasSessionPair || code || errorCode
|
|
141
|
-
? normalizeOAuthProviderInput(payload.provider || options.defaultProvider, options)
|
|
142
|
-
: null;
|
|
143
|
-
|
|
144
|
-
if (!code && !errorCode && !hasSessionPair) {
|
|
145
|
-
fieldErrors.code = "OAuth code is required when access/refresh tokens are not provided.";
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
provider,
|
|
150
|
-
code,
|
|
151
|
-
accessToken,
|
|
152
|
-
refreshToken,
|
|
153
|
-
hasSessionPair,
|
|
154
|
-
errorCode,
|
|
155
|
-
errorDescription,
|
|
156
|
-
fieldErrors
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function parseOtpLoginVerifyPayload(payload = {}) {
|
|
161
|
-
const parsedEmail = validators.forgotPasswordInput(payload);
|
|
162
|
-
const token = String(payload?.token || "").trim();
|
|
163
|
-
const tokenHash = String(payload?.tokenHash || "").trim();
|
|
164
|
-
const type = String(payload?.type || OTP_VERIFY_TYPE)
|
|
165
|
-
.trim()
|
|
166
|
-
.toLowerCase();
|
|
167
|
-
const fieldErrors = {
|
|
168
|
-
...parsedEmail.fieldErrors
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
if (type !== OTP_VERIFY_TYPE) {
|
|
172
|
-
fieldErrors.type = "Only email OTP verification is supported.";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!token && !tokenHash) {
|
|
176
|
-
fieldErrors.token = "One-time code is required.";
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (token && token.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
|
|
180
|
-
fieldErrors.token = "One-time code is too long.";
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (tokenHash && tokenHash.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
|
|
184
|
-
fieldErrors.tokenHash = "One-time token hash is too long.";
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (token && parsedEmail.fieldErrors.email) {
|
|
188
|
-
fieldErrors.email = parsedEmail.fieldErrors.email;
|
|
189
|
-
} else if (tokenHash) {
|
|
190
|
-
delete fieldErrors.email;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
email: parsedEmail.email,
|
|
195
|
-
token,
|
|
196
|
-
tokenHash,
|
|
197
|
-
type,
|
|
198
|
-
fieldErrors
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
31
|
function mapOAuthCallbackError(errorCode) {
|
|
203
32
|
const normalizedCode = String(errorCode || "")
|
|
204
33
|
.trim()
|
|
@@ -213,8 +42,5 @@ function mapOAuthCallbackError(errorCode) {
|
|
|
213
42
|
|
|
214
43
|
export {
|
|
215
44
|
normalizeOAuthProviderInput,
|
|
216
|
-
validatePasswordRecoveryPayload,
|
|
217
|
-
parseOAuthCompletePayload,
|
|
218
|
-
parseOtpLoginVerifyPayload,
|
|
219
45
|
mapOAuthCallbackError
|
|
220
46
|
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createSchema } from "json-rest-schema";
|
|
2
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
3
|
+
import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
|
|
4
|
+
|
|
5
|
+
const authenticatedProfileSchema = createSchema({
|
|
6
|
+
id: {
|
|
7
|
+
type: "string",
|
|
8
|
+
required: true,
|
|
9
|
+
minLength: 1,
|
|
10
|
+
pattern: RECORD_ID_PATTERN
|
|
11
|
+
},
|
|
12
|
+
email: {
|
|
13
|
+
type: "string",
|
|
14
|
+
required: true,
|
|
15
|
+
minLength: 1,
|
|
16
|
+
lowercase: true
|
|
17
|
+
},
|
|
18
|
+
username: {
|
|
19
|
+
type: "string",
|
|
20
|
+
required: false,
|
|
21
|
+
lowercase: true
|
|
22
|
+
},
|
|
23
|
+
displayName: {
|
|
24
|
+
type: "string",
|
|
25
|
+
required: true,
|
|
26
|
+
minLength: 1
|
|
27
|
+
},
|
|
28
|
+
authProvider: {
|
|
29
|
+
type: "string",
|
|
30
|
+
required: true,
|
|
31
|
+
minLength: 1,
|
|
32
|
+
lowercase: true
|
|
33
|
+
},
|
|
34
|
+
authProviderUserSid: {
|
|
35
|
+
type: "string",
|
|
36
|
+
required: true,
|
|
37
|
+
minLength: 1
|
|
38
|
+
},
|
|
39
|
+
avatarStorageKey: {
|
|
40
|
+
type: "string",
|
|
41
|
+
required: false,
|
|
42
|
+
nullable: true
|
|
43
|
+
},
|
|
44
|
+
avatarVersion: {
|
|
45
|
+
type: "string",
|
|
46
|
+
required: false,
|
|
47
|
+
nullable: true
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function requireAuthenticatedProfile(profileLike, { context = "authenticated profile" } = {}) {
|
|
52
|
+
const source = profileLike && typeof profileLike === "object" && !Array.isArray(profileLike) ? profileLike : {};
|
|
53
|
+
const candidate = {
|
|
54
|
+
id: source.id,
|
|
55
|
+
email: source.email,
|
|
56
|
+
displayName: source.displayName,
|
|
57
|
+
authProvider: source.authProvider,
|
|
58
|
+
authProviderUserSid: source.authProviderUserSid
|
|
59
|
+
};
|
|
60
|
+
if (Object.hasOwn(source, "username")) {
|
|
61
|
+
candidate.username = source.username;
|
|
62
|
+
}
|
|
63
|
+
if (Object.hasOwn(source, "avatarStorageKey")) {
|
|
64
|
+
candidate.avatarStorageKey = source.avatarStorageKey;
|
|
65
|
+
}
|
|
66
|
+
if (Object.hasOwn(source, "avatarVersion")) {
|
|
67
|
+
candidate.avatarVersion = source.avatarVersion;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { validatedObject, errors } = authenticatedProfileSchema.create(candidate);
|
|
71
|
+
if (Object.keys(errors).length > 0) {
|
|
72
|
+
throw new AppError(500, `${context} is invalid.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return validatedObject;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { requireAuthenticatedProfile };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { authLoginOAuthStartParamsValidator, authLoginOAuthStartQueryValidator } from "@jskit-ai/auth-core/shared/commands/authLoginOAuthStartCommand";
|
|
3
|
+
import { authLoginOAuthCompleteBodyValidator } from "@jskit-ai/auth-core/shared/commands/authLoginOAuthCompleteCommand";
|
|
2
4
|
|
|
3
5
|
function createOauthFlows(deps) {
|
|
4
6
|
const {
|
|
@@ -13,7 +15,6 @@ function createOauthFlows(deps) {
|
|
|
13
15
|
mapAuthError,
|
|
14
16
|
setSessionFromRequestCookies,
|
|
15
17
|
buildOAuthLinkRedirectUrl,
|
|
16
|
-
parseOAuthCompletePayload,
|
|
17
18
|
validationError,
|
|
18
19
|
mapOAuthCallbackError,
|
|
19
20
|
mapRecoveryError,
|
|
@@ -50,8 +51,22 @@ function createOauthFlows(deps) {
|
|
|
50
51
|
async function oauthStart(payload = {}) {
|
|
51
52
|
ensureConfigured();
|
|
52
53
|
|
|
53
|
-
const
|
|
54
|
-
|
|
54
|
+
const paramsResult = authLoginOAuthStartParamsValidator.schema.patch({
|
|
55
|
+
provider: payload.provider || authOAuthDefaultProvider
|
|
56
|
+
});
|
|
57
|
+
if (Object.keys(paramsResult.errors).length > 0) {
|
|
58
|
+
throw validationError(paramsResult.errors);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const queryResult = authLoginOAuthStartQueryValidator.schema.patch({
|
|
62
|
+
returnTo: payload.returnTo
|
|
63
|
+
});
|
|
64
|
+
if (Object.keys(queryResult.errors).length > 0) {
|
|
65
|
+
throw validationError(queryResult.errors);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const provider = normalizeOAuthProviderInput(paramsResult.validatedObject.provider);
|
|
69
|
+
const returnTo = normalizeReturnToPath(queryResult.validatedObject.returnTo, { fallback: "/" });
|
|
55
70
|
const redirectTo = buildOAuthLoginRedirectUrl({
|
|
56
71
|
appPublicUrl,
|
|
57
72
|
provider,
|
|
@@ -72,9 +87,23 @@ function createOauthFlows(deps) {
|
|
|
72
87
|
async function startProviderLink(request, payload = {}) {
|
|
73
88
|
ensureConfigured();
|
|
74
89
|
|
|
90
|
+
const paramsResult = authLoginOAuthStartParamsValidator.schema.patch({
|
|
91
|
+
provider: payload.provider || authOAuthDefaultProvider
|
|
92
|
+
});
|
|
93
|
+
if (Object.keys(paramsResult.errors).length > 0) {
|
|
94
|
+
throw validationError(paramsResult.errors);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const queryResult = authLoginOAuthStartQueryValidator.schema.patch({
|
|
98
|
+
returnTo: payload.returnTo
|
|
99
|
+
});
|
|
100
|
+
if (Object.keys(queryResult.errors).length > 0) {
|
|
101
|
+
throw validationError(queryResult.errors);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const provider = normalizeOAuthProviderInput(paramsResult.validatedObject.provider);
|
|
105
|
+
const returnTo = normalizeReturnToPath(queryResult.validatedObject.returnTo, { fallback: "/" });
|
|
75
106
|
const supabase = getSupabaseClient();
|
|
76
|
-
const provider = normalizeOAuthProviderInput(payload.provider || authOAuthDefaultProvider);
|
|
77
|
-
const returnTo = normalizeReturnToPath(payload.returnTo, { fallback: "/" });
|
|
78
107
|
await setSessionFromRequestCookies(request, {
|
|
79
108
|
supabaseClient: supabase
|
|
80
109
|
});
|
|
@@ -102,25 +131,55 @@ function createOauthFlows(deps) {
|
|
|
102
131
|
async function oauthComplete(payload = {}) {
|
|
103
132
|
ensureConfigured();
|
|
104
133
|
|
|
105
|
-
const
|
|
106
|
-
if (Object.keys(
|
|
107
|
-
throw validationError(
|
|
134
|
+
const result = authLoginOAuthCompleteBodyValidator.schema.patch(payload);
|
|
135
|
+
if (Object.keys(result.errors).length > 0) {
|
|
136
|
+
throw validationError(result.errors);
|
|
137
|
+
}
|
|
138
|
+
const parsed = result.validatedObject;
|
|
139
|
+
const code = String(parsed.code || "").trim();
|
|
140
|
+
const accessToken = String(parsed.accessToken || "").trim();
|
|
141
|
+
const refreshToken = String(parsed.refreshToken || "").trim();
|
|
142
|
+
const errorCode = String(parsed.error || parsed.error_code || "").trim();
|
|
143
|
+
const errorDescription = String(parsed.errorDescription || parsed.error_description || "").trim();
|
|
144
|
+
const hasSessionPair = Boolean(accessToken && refreshToken);
|
|
145
|
+
const fieldErrors = {};
|
|
146
|
+
|
|
147
|
+
if ((accessToken && !refreshToken) || (!accessToken && refreshToken)) {
|
|
148
|
+
if (!accessToken) {
|
|
149
|
+
fieldErrors.accessToken = "Access token is required when a refresh token is provided.";
|
|
150
|
+
}
|
|
151
|
+
if (!refreshToken) {
|
|
152
|
+
fieldErrors.refreshToken = "Refresh token is required when an access token is provided.";
|
|
153
|
+
}
|
|
108
154
|
}
|
|
109
155
|
|
|
110
|
-
if (
|
|
111
|
-
|
|
156
|
+
if (!code && !errorCode && !hasSessionPair) {
|
|
157
|
+
fieldErrors.code = "OAuth code is required when access/refresh tokens are not provided.";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
161
|
+
throw validationError(fieldErrors);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const provider =
|
|
165
|
+
!hasSessionPair || code || errorCode
|
|
166
|
+
? normalizeOAuthProviderInput(parsed.provider || authOAuthDefaultProvider)
|
|
167
|
+
: null;
|
|
168
|
+
|
|
169
|
+
if (errorCode) {
|
|
170
|
+
throw mapOAuthCallbackError(errorCode, errorDescription);
|
|
112
171
|
}
|
|
113
172
|
|
|
114
173
|
const supabase = getSupabaseClient();
|
|
115
174
|
let response;
|
|
116
175
|
try {
|
|
117
|
-
if (
|
|
176
|
+
if (hasSessionPair) {
|
|
118
177
|
response = await supabase.auth.setSession({
|
|
119
|
-
access_token:
|
|
120
|
-
refresh_token:
|
|
178
|
+
access_token: accessToken,
|
|
179
|
+
refresh_token: refreshToken
|
|
121
180
|
});
|
|
122
181
|
} else {
|
|
123
|
-
response = await supabase.auth.exchangeCodeForSession(
|
|
182
|
+
response = await supabase.auth.exchangeCodeForSession(code);
|
|
124
183
|
}
|
|
125
184
|
} catch (error) {
|
|
126
185
|
throw mapRecoveryError(error);
|
|
@@ -133,7 +192,7 @@ function createOauthFlows(deps) {
|
|
|
133
192
|
const profile = await syncProfileFromSupabaseUser(response.data.user, response.data.user.email);
|
|
134
193
|
|
|
135
194
|
return {
|
|
136
|
-
provider
|
|
195
|
+
provider,
|
|
137
196
|
profile,
|
|
138
197
|
session: response.data.session
|
|
139
198
|
};
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { authPasswordResetRequestBodyValidator } from "@jskit-ai/auth-core/shared/commands/authPasswordResetRequestCommand";
|
|
3
|
+
import { authPasswordRecoveryCompleteBodyValidator } from "@jskit-ai/auth-core/shared/commands/authPasswordRecoveryCompleteCommand";
|
|
4
|
+
import { authPasswordResetBodyValidator } from "@jskit-ai/auth-core/shared/commands/authPasswordResetCommand";
|
|
2
5
|
import {
|
|
3
6
|
requireAuthUser,
|
|
4
7
|
requireAuthSession,
|
|
5
|
-
requireAuthUserSession
|
|
6
|
-
requireNoFieldErrors
|
|
8
|
+
requireAuthUserSession
|
|
7
9
|
} from "./flowGuards.js";
|
|
8
10
|
|
|
9
11
|
function createPasswordSecurityFlows(deps) {
|
|
10
12
|
const {
|
|
11
13
|
ensureConfigured,
|
|
12
|
-
validators,
|
|
13
14
|
validationError,
|
|
14
15
|
getSupabaseClient,
|
|
15
16
|
passwordResetRedirectUrl,
|
|
16
17
|
mapAuthError,
|
|
17
|
-
validatePasswordRecoveryPayload,
|
|
18
18
|
mapRecoveryError,
|
|
19
19
|
syncProfileFromSupabaseUser,
|
|
20
20
|
setSessionFromRequestCookies,
|
|
@@ -38,8 +38,11 @@ function createPasswordSecurityFlows(deps) {
|
|
|
38
38
|
async function requestPasswordReset(payload) {
|
|
39
39
|
ensureConfigured();
|
|
40
40
|
|
|
41
|
-
const
|
|
42
|
-
|
|
41
|
+
const result = authPasswordResetRequestBodyValidator.schema.create(payload);
|
|
42
|
+
if (Object.keys(result.errors).length > 0) {
|
|
43
|
+
throw validationError(result.errors);
|
|
44
|
+
}
|
|
45
|
+
const parsed = result.validatedObject;
|
|
43
46
|
|
|
44
47
|
const supabase = getSupabaseClient();
|
|
45
48
|
const options = { redirectTo: passwordResetRedirectUrl };
|
|
@@ -65,23 +68,51 @@ function createPasswordSecurityFlows(deps) {
|
|
|
65
68
|
async function completePasswordRecovery(payload) {
|
|
66
69
|
ensureConfigured();
|
|
67
70
|
|
|
68
|
-
const
|
|
69
|
-
|
|
71
|
+
const result = authPasswordRecoveryCompleteBodyValidator.schema.patch(payload);
|
|
72
|
+
if (Object.keys(result.errors).length > 0) {
|
|
73
|
+
throw validationError(result.errors);
|
|
74
|
+
}
|
|
75
|
+
const parsed = result.validatedObject;
|
|
76
|
+
const code = String(parsed.code || "").trim();
|
|
77
|
+
const tokenHash = String(parsed.tokenHash || "").trim();
|
|
78
|
+
const accessToken = String(parsed.accessToken || "").trim();
|
|
79
|
+
const refreshToken = String(parsed.refreshToken || "").trim();
|
|
80
|
+
const hasCode = Boolean(code);
|
|
81
|
+
const hasTokenHash = Boolean(tokenHash);
|
|
82
|
+
const hasSessionPair = Boolean(accessToken && refreshToken);
|
|
83
|
+
const fieldErrors = {};
|
|
84
|
+
|
|
85
|
+
if ((accessToken && !refreshToken) || (!accessToken && refreshToken)) {
|
|
86
|
+
if (!accessToken) {
|
|
87
|
+
fieldErrors.accessToken = "Access token is required when a refresh token is provided.";
|
|
88
|
+
}
|
|
89
|
+
if (!refreshToken) {
|
|
90
|
+
fieldErrors.refreshToken = "Refresh token is required when an access token is provided.";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!hasCode && !hasTokenHash && !hasSessionPair) {
|
|
95
|
+
fieldErrors.recovery = "Recovery token is required.";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
99
|
+
throw validationError(fieldErrors);
|
|
100
|
+
}
|
|
70
101
|
|
|
71
102
|
const supabase = getSupabaseClient();
|
|
72
103
|
let response;
|
|
73
104
|
try {
|
|
74
|
-
if (
|
|
75
|
-
response = await supabase.auth.exchangeCodeForSession(
|
|
76
|
-
} else if (
|
|
105
|
+
if (hasCode) {
|
|
106
|
+
response = await supabase.auth.exchangeCodeForSession(code);
|
|
107
|
+
} else if (hasTokenHash) {
|
|
77
108
|
response = await supabase.auth.verifyOtp({
|
|
78
109
|
type: "recovery",
|
|
79
|
-
token_hash:
|
|
110
|
+
token_hash: tokenHash
|
|
80
111
|
});
|
|
81
112
|
} else {
|
|
82
113
|
response = await supabase.auth.setSession({
|
|
83
|
-
access_token:
|
|
84
|
-
refresh_token:
|
|
114
|
+
access_token: accessToken,
|
|
115
|
+
refresh_token: refreshToken
|
|
85
116
|
});
|
|
86
117
|
}
|
|
87
118
|
/* c8 ignore next 3 -- defensive: supabase-js usually surfaces failures via response.error. */
|
|
@@ -105,8 +136,11 @@ function createPasswordSecurityFlows(deps) {
|
|
|
105
136
|
async function resetPassword(request, payload) {
|
|
106
137
|
ensureConfigured();
|
|
107
138
|
|
|
108
|
-
const
|
|
109
|
-
|
|
139
|
+
const result = authPasswordResetBodyValidator.schema.create(payload);
|
|
140
|
+
if (Object.keys(result.errors).length > 0) {
|
|
141
|
+
throw validationError(result.errors);
|
|
142
|
+
}
|
|
143
|
+
const parsed = result.validatedObject;
|
|
110
144
|
|
|
111
145
|
const supabase = getSupabaseClient();
|
|
112
146
|
const sessionResponse = await setSessionFromRequestCookies(request, {
|
|
@@ -267,6 +301,10 @@ function createPasswordSecurityFlows(deps) {
|
|
|
267
301
|
if (response.error) {
|
|
268
302
|
throw mapAuthError(response.error, Number(response.error?.status || 400));
|
|
269
303
|
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
ok: true
|
|
307
|
+
};
|
|
270
308
|
}
|
|
271
309
|
|
|
272
310
|
async function getSecurityStatus(request) {
|
|
@@ -6,8 +6,6 @@ import {
|
|
|
6
6
|
buildOAuthMethodId
|
|
7
7
|
} from "@jskit-ai/auth-core/shared/authMethods";
|
|
8
8
|
import { normalizeEmail } from "@jskit-ai/auth-core/server/utils";
|
|
9
|
-
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
10
|
-
import { validators } from "@jskit-ai/auth-core/server/validators";
|
|
11
9
|
import {
|
|
12
10
|
isTransientAuthMessage,
|
|
13
11
|
isTransientSupabaseError,
|
|
@@ -23,9 +21,6 @@ import {
|
|
|
23
21
|
import { displayNameFromEmail, resolveDisplayName, resolveDisplayNameFromClaims } from "./authProfileNames.js";
|
|
24
22
|
import {
|
|
25
23
|
normalizeOAuthProviderInput as normalizeOAuthProviderInputFromCatalog,
|
|
26
|
-
validatePasswordRecoveryPayload,
|
|
27
|
-
parseOAuthCompletePayload as parseOAuthCompletePayloadFromCatalog,
|
|
28
|
-
parseOtpLoginVerifyPayload,
|
|
29
24
|
mapOAuthCallbackError
|
|
30
25
|
} from "./authInputParsers.js";
|
|
31
26
|
import {
|
|
@@ -62,6 +57,7 @@ import {
|
|
|
62
57
|
resolveDevAuthConfig,
|
|
63
58
|
resolveDevAuthProfile
|
|
64
59
|
} from "./devAuthBootstrap.js";
|
|
60
|
+
import { requireAuthenticatedProfile } from "./authenticatedProfile.js";
|
|
65
61
|
import {
|
|
66
62
|
buildOAuthProviderCatalogResponse,
|
|
67
63
|
resolveOAuthProviderQueryParams,
|
|
@@ -166,13 +162,6 @@ function createService(options) {
|
|
|
166
162
|
});
|
|
167
163
|
}
|
|
168
164
|
|
|
169
|
-
function parseOAuthCompletePayload(payload) {
|
|
170
|
-
return parseOAuthCompletePayloadFromCatalog(payload, {
|
|
171
|
-
providerIds: authOAuthProviderIds,
|
|
172
|
-
defaultProvider: authOAuthDefaultProvider
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
165
|
function buildOAuthLoginRedirectUrlWithCatalog(payload) {
|
|
177
166
|
return buildOAuthLoginRedirectUrl({
|
|
178
167
|
...payload,
|
|
@@ -408,11 +397,16 @@ function createService(options) {
|
|
|
408
397
|
}
|
|
409
398
|
|
|
410
399
|
function requireSynchronizedProfile(profile) {
|
|
411
|
-
|
|
412
|
-
return profile
|
|
400
|
+
try {
|
|
401
|
+
return requireAuthenticatedProfile(profile, {
|
|
402
|
+
context: "authentication profile"
|
|
403
|
+
});
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (error instanceof AppError) {
|
|
406
|
+
throw new AppError(500, "Authentication profile synchronization failed. Please retry.");
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
413
409
|
}
|
|
414
|
-
|
|
415
|
-
throw new AppError(500, "Authentication profile synchronization failed. Please retry.");
|
|
416
410
|
}
|
|
417
411
|
|
|
418
412
|
function buildNormalizedIdentityKey(identityLike) {
|
|
@@ -583,7 +577,6 @@ function createService(options) {
|
|
|
583
577
|
|
|
584
578
|
const { register, resendRegisterConfirmation, login, requestOtpLogin, verifyOtpLogin, updateDisplayName } = createAccountFlows({
|
|
585
579
|
ensureConfigured,
|
|
586
|
-
validators,
|
|
587
580
|
validationError,
|
|
588
581
|
getSupabaseClient,
|
|
589
582
|
displayNameFromEmail,
|
|
@@ -595,7 +588,6 @@ function createService(options) {
|
|
|
595
588
|
appPublicUrl,
|
|
596
589
|
isTransientSupabaseError,
|
|
597
590
|
isUserNotFoundLikeAuthError,
|
|
598
|
-
parseOtpLoginVerifyPayload,
|
|
599
591
|
mapOtpVerifyError,
|
|
600
592
|
setSessionFromRequestCookies,
|
|
601
593
|
mapProfileUpdateError,
|
|
@@ -614,7 +606,6 @@ function createService(options) {
|
|
|
614
606
|
mapAuthError,
|
|
615
607
|
setSessionFromRequestCookies,
|
|
616
608
|
buildOAuthLinkRedirectUrl: buildOAuthLinkRedirectUrlWithCatalog,
|
|
617
|
-
parseOAuthCompletePayload,
|
|
618
609
|
validationError,
|
|
619
610
|
mapOAuthCallbackError,
|
|
620
611
|
mapRecoveryError,
|
|
@@ -636,12 +627,10 @@ function createService(options) {
|
|
|
636
627
|
getSecurityStatus
|
|
637
628
|
} = createPasswordSecurityFlows({
|
|
638
629
|
ensureConfigured,
|
|
639
|
-
validators,
|
|
640
630
|
validationError,
|
|
641
631
|
getSupabaseClient,
|
|
642
632
|
passwordResetRedirectUrl,
|
|
643
633
|
mapAuthError,
|
|
644
|
-
validatePasswordRecoveryPayload,
|
|
645
634
|
mapRecoveryError,
|
|
646
635
|
syncProfileFromSupabaseUser,
|
|
647
636
|
setSessionFromRequestCookies,
|
|
@@ -687,6 +676,14 @@ function createService(options) {
|
|
|
687
676
|
}
|
|
688
677
|
);
|
|
689
678
|
if (devAuthResult) {
|
|
679
|
+
if (devAuthResult.authenticated === true) {
|
|
680
|
+
return {
|
|
681
|
+
...devAuthResult,
|
|
682
|
+
profile: requireAuthenticatedProfile(devAuthResult.profile, {
|
|
683
|
+
context: "dev auth profile"
|
|
684
|
+
})
|
|
685
|
+
};
|
|
686
|
+
}
|
|
690
687
|
return devAuthResult;
|
|
691
688
|
}
|
|
692
689
|
|
|
@@ -837,10 +834,13 @@ function createService(options) {
|
|
|
837
834
|
|
|
838
835
|
async function devLoginAs(request, input = {}) {
|
|
839
836
|
ensureDevAuthBootstrapAvailable(devAuthConfig, request);
|
|
840
|
-
const
|
|
837
|
+
const rawProfile = await resolveDevAuthProfile(input, {
|
|
841
838
|
userProfilesRepository,
|
|
842
839
|
validationError
|
|
843
840
|
});
|
|
841
|
+
const profile = requireAuthenticatedProfile(rawProfile, {
|
|
842
|
+
context: "dev auth profile"
|
|
843
|
+
});
|
|
844
844
|
return {
|
|
845
845
|
profile,
|
|
846
846
|
session: await createDevAuthSession(profile, devAuthConfig)
|
|
@@ -878,7 +878,6 @@ function createService(options) {
|
|
|
878
878
|
}
|
|
879
879
|
|
|
880
880
|
const __testables = {
|
|
881
|
-
validatePasswordRecoveryPayload,
|
|
882
881
|
displayNameFromEmail,
|
|
883
882
|
resolveDisplayName,
|
|
884
883
|
resolveDisplayNameFromClaims,
|
|
@@ -901,8 +900,6 @@ const __testables = {
|
|
|
901
900
|
buildOAuthLoginRedirectUrl,
|
|
902
901
|
buildOAuthLinkRedirectUrl,
|
|
903
902
|
normalizeOAuthProviderInput: normalizeOAuthProviderInputFromCatalog,
|
|
904
|
-
parseOAuthCompletePayload: parseOAuthCompletePayloadFromCatalog,
|
|
905
|
-
parseOtpLoginVerifyPayload,
|
|
906
903
|
mapOAuthCallbackError,
|
|
907
904
|
resolveSupabaseOAuthProviderCatalog,
|
|
908
905
|
resolveOAuthProviderQueryParams,
|
|
@@ -1,36 +1,23 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import {
|
|
3
|
+
import { normalizeOAuthProviderInput } from "../src/server/lib/authInputParsers.js";
|
|
4
4
|
|
|
5
|
-
test("
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
providerIds: [],
|
|
13
|
-
defaultProvider: null
|
|
14
|
-
}
|
|
15
|
-
);
|
|
5
|
+
test("normalizeOAuthProviderInput uses the configured default provider", () => {
|
|
6
|
+
const provider = normalizeOAuthProviderInput("", {
|
|
7
|
+
providerIds: ["github", "google"],
|
|
8
|
+
defaultProvider: "github"
|
|
9
|
+
});
|
|
16
10
|
|
|
17
|
-
assert.equal(
|
|
18
|
-
assert.equal(parsed.provider, null);
|
|
19
|
-
assert.deepEqual(parsed.fieldErrors, {});
|
|
11
|
+
assert.equal(provider, "github");
|
|
20
12
|
});
|
|
21
13
|
|
|
22
|
-
test("
|
|
14
|
+
test("normalizeOAuthProviderInput rejects oauth sign-in when no providers are configured", () => {
|
|
23
15
|
assert.throws(
|
|
24
16
|
() =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
providerIds: [],
|
|
31
|
-
defaultProvider: null
|
|
32
|
-
}
|
|
33
|
-
),
|
|
17
|
+
normalizeOAuthProviderInput("github", {
|
|
18
|
+
providerIds: [],
|
|
19
|
+
defaultProvider: null
|
|
20
|
+
}),
|
|
34
21
|
(error) => {
|
|
35
22
|
assert.equal(error?.status, 400);
|
|
36
23
|
assert.equal(String(error?.details?.fieldErrors?.provider || "").length > 0, true);
|
|
@@ -120,6 +120,38 @@ test("dev auth bootstrap supports email lookup", async () => {
|
|
|
120
120
|
assert.equal(result.profile.email, "ada@example.com");
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
test("dev auth bootstrap canonicalizes producer profiles before returning them", async () => {
|
|
124
|
+
const rawProfile = createProfile({
|
|
125
|
+
email: " ADA@EXAMPLE.COM ",
|
|
126
|
+
username: " Ada ",
|
|
127
|
+
displayName: " Ada Example ",
|
|
128
|
+
authProvider: " SUPABASE ",
|
|
129
|
+
authProviderUserSid: " supabase-user-7 ",
|
|
130
|
+
avatarStorageKey: " avatars/7.png ",
|
|
131
|
+
avatarVersion: 7,
|
|
132
|
+
avatarUpdatedAt: "2026-01-01T00:00:00.000Z",
|
|
133
|
+
createdAt: "2026-01-01T00:00:00.000Z"
|
|
134
|
+
});
|
|
135
|
+
const authService = createServiceFixture({
|
|
136
|
+
userProfilesRepository: createUserProfilesRepository(rawProfile)
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const loginResult = await authService.devLoginAs(createLocalRequest(), {
|
|
140
|
+
userId: "7"
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(loginResult.profile, {
|
|
144
|
+
id: "7",
|
|
145
|
+
email: "ada@example.com",
|
|
146
|
+
username: "ada",
|
|
147
|
+
displayName: "Ada Example",
|
|
148
|
+
authProvider: "supabase",
|
|
149
|
+
authProviderUserSid: "supabase-user-7",
|
|
150
|
+
avatarStorageKey: "avatars/7.png",
|
|
151
|
+
avatarVersion: "7"
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
123
155
|
test("dev auth bootstrap rejects non-local requests and clears leaked dev sessions", async () => {
|
|
124
156
|
const authService = createServiceFixture();
|
|
125
157
|
const issued = await authService.devLoginAs(createLocalRequest(), {
|