better-auth 1.7.2 → 1.7.4
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/dist/api/dispatch.mjs +4 -1
- package/dist/api/index.d.mts +6 -22
- package/dist/api/index.mjs +5 -1
- package/dist/api/routes/account.d.mts +1 -9
- package/dist/api/routes/account.mjs +0 -6
- package/dist/api/routes/callback.mjs +0 -2
- package/dist/api/routes/password.mjs +0 -2
- package/dist/api/routes/session.mjs +12 -5
- package/dist/api/routes/sign-in.d.mts +2 -2
- package/dist/api/routes/sign-in.mjs +1 -5
- package/dist/api/routes/sign-up.mjs +0 -2
- package/dist/api/routes/update-user.mjs +0 -2
- package/dist/api/to-auth-endpoints.mjs +2 -0
- package/dist/auth/base.mjs +13 -1
- package/dist/auth/trusted-origins.mjs +6 -4
- package/dist/client/config.d.mts +2 -0
- package/dist/client/config.mjs +1 -0
- package/dist/client/vue/index.d.mts +31 -20
- package/dist/client/vue/index.mjs +23 -5
- package/dist/context/create-context.mjs +2 -0
- package/dist/cookies/session-store.d.mts +0 -1
- package/dist/cookies/session-store.mjs +16 -5
- package/dist/db/adapter-kysely.mjs +1 -2
- package/dist/db/get-migration.d.mts +1 -0
- package/dist/db/get-migration.mjs +10 -32
- package/dist/db/internal-adapter.mjs +14 -17
- package/dist/db/schema.d.mts +0 -1
- package/dist/db/with-hooks.mjs +2 -1
- package/dist/index.mjs +0 -1
- package/dist/oauth2/account-key.mjs +1 -5
- package/dist/oauth2/link-account.d.mts +0 -1
- package/dist/oauth2/link-account.mjs +5 -11
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/routes.mjs +0 -3
- package/dist/plugins/device-authorization/index.d.mts +11 -11
- package/dist/plugins/email-otp/routes.mjs +0 -2
- package/dist/plugins/generic-oauth/index.mjs +14 -10
- package/dist/plugins/generic-oauth/providers/auth0.mjs +2 -4
- package/dist/plugins/generic-oauth/providers/keycloak.mjs +6 -9
- package/dist/plugins/generic-oauth/providers/line.mjs +0 -1
- package/dist/plugins/generic-oauth/providers/okta.mjs +6 -9
- package/dist/plugins/generic-oauth/providers/slack.mjs +0 -1
- package/dist/plugins/generic-oauth/types.d.mts +0 -9
- package/dist/plugins/haveibeenpwned/index.d.mts +9 -1
- package/dist/plugins/haveibeenpwned/index.mjs +31 -11
- package/dist/plugins/index.d.mts +3 -3
- package/dist/plugins/index.mjs +2 -2
- package/dist/plugins/last-login-method/index.mjs +1 -0
- package/dist/plugins/oauth-proxy/index.d.mts +17 -0
- package/dist/plugins/oauth-proxy/index.mjs +145 -99
- package/dist/plugins/one-tap/index.mjs +0 -1
- package/dist/plugins/open-api/generator.mjs +15 -1
- package/dist/plugins/open-api/index.mjs +0 -1
- package/dist/plugins/organization/has-permission.mjs +4 -2
- package/dist/plugins/phone-number/routes.mjs +0 -3
- package/dist/plugins/siwe/index.mjs +0 -3
- package/dist/plugins/test-utils/auth-helpers.mjs +8 -3
- package/dist/plugins/test-utils/index.d.mts +1 -1
- package/dist/plugins/test-utils/types.d.mts +10 -9
- package/dist/plugins/two-factor/client.d.mts +1 -0
- package/dist/plugins/two-factor/error-code.d.mts +1 -0
- package/dist/plugins/two-factor/error-code.mjs +1 -0
- package/dist/plugins/two-factor/index.d.mts +1 -0
- package/dist/plugins/two-factor/index.mjs +3 -2
- package/dist/state.d.mts +0 -13
- package/dist/test-utils/test-instance.mjs +10 -9
- package/package.json +12 -12
- package/dist/utils/index.mjs +0 -5
|
@@ -8,22 +8,50 @@ import { resolveOAuthAccountKey, toOAuthProfileRecord } from "../../oauth2/accou
|
|
|
8
8
|
import { redirectOnError } from "../../oauth2/errors.mjs";
|
|
9
9
|
import { getOAuthCallbackPath } from "../../oauth2/utils.mjs";
|
|
10
10
|
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
11
|
+
import { setOAuthState } from "../../api/state/oauth.mjs";
|
|
11
12
|
import { parseGenericState } from "../../state.mjs";
|
|
12
13
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
13
14
|
import { parseJSON } from "../../client/parser.mjs";
|
|
14
15
|
import { checkSkipProxy, resolveCurrentURL, stripTrailingSlash } from "./utils.mjs";
|
|
16
|
+
import { accountSchema, userSchema } from "@better-auth/core/db";
|
|
15
17
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
16
18
|
import { defu } from "defu";
|
|
17
19
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
18
20
|
import * as z from "zod";
|
|
19
21
|
//#region src/plugins/oauth-proxy/index.ts
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Passthrough payload containing OAuth profile data.
|
|
24
|
+
* Used to transfer OAuth credentials from production to preview
|
|
25
|
+
* without creating user/session on production.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
const passthroughPayloadSchema = z.looseObject({
|
|
29
|
+
userInfo: z.looseObject(userSchema.omit({
|
|
30
|
+
createdAt: true,
|
|
31
|
+
updatedAt: true
|
|
32
|
+
}).shape),
|
|
33
|
+
account: z.looseObject(accountSchema.omit({
|
|
34
|
+
id: true,
|
|
35
|
+
userId: true,
|
|
36
|
+
createdAt: true,
|
|
37
|
+
updatedAt: true
|
|
38
|
+
}).shape),
|
|
39
|
+
profile: z.record(z.string(), z.unknown()).optional(),
|
|
40
|
+
state: z.string().min(1),
|
|
41
|
+
callbackURL: z.string().min(1),
|
|
42
|
+
newUserURL: z.string().optional(),
|
|
43
|
+
errorURL: z.string().optional(),
|
|
44
|
+
disableSignUp: z.boolean().optional(),
|
|
45
|
+
timestamp: z.number()
|
|
46
|
+
});
|
|
47
|
+
const restoreOAuthProxyState = async (ctx, state) => {
|
|
21
48
|
try {
|
|
22
|
-
await parseGenericState(ctx, state, { skipStateCookieCheck: true });
|
|
23
|
-
|
|
49
|
+
const stateData = await parseGenericState(ctx, state, { skipStateCookieCheck: true });
|
|
50
|
+
await setOAuthState(stateData);
|
|
51
|
+
return stateData;
|
|
24
52
|
} catch (e) {
|
|
25
53
|
ctx.context.logger.warn("OAuth proxy state missing or invalid", e);
|
|
26
|
-
return
|
|
54
|
+
return null;
|
|
27
55
|
}
|
|
28
56
|
};
|
|
29
57
|
const oauthProxyQuerySchema = z.object({
|
|
@@ -38,104 +66,122 @@ const oauthCallbackQuerySchema = z.object({
|
|
|
38
66
|
const oAuthProxy = (opts) => {
|
|
39
67
|
const maxAge = opts?.maxAge ?? 60;
|
|
40
68
|
const getEncryptionKey = (ctx) => opts?.secret ?? ctx.context.secretConfig;
|
|
69
|
+
const oauthProxyCompletion = createAuthEndpoint("/callback/:id/oauth-proxy", {
|
|
70
|
+
method: "GET",
|
|
71
|
+
operationId: "oauthProxyCompletion",
|
|
72
|
+
query: oauthProxyQuerySchema,
|
|
73
|
+
use: [originCheck((ctx) => ctx.query.callbackURL)],
|
|
74
|
+
metadata: { scope: "http" }
|
|
75
|
+
}, async (ctx) => {
|
|
76
|
+
const baseURLStr = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : getOrigin(ctx.context.baseURL) || "";
|
|
77
|
+
const defaultErrorURL = ctx.context.options.onAPIError?.errorURL || `${stripTrailingSlash(baseURLStr)}/api/auth/error`;
|
|
78
|
+
const encryptedProfile = ctx.query.profile;
|
|
79
|
+
if (!encryptedProfile) {
|
|
80
|
+
ctx.context.logger.error("OAuth proxy callback missing profile data");
|
|
81
|
+
throw redirectOnError(ctx, defaultErrorURL, "missing_profile");
|
|
82
|
+
}
|
|
83
|
+
let decryptedPayload;
|
|
84
|
+
try {
|
|
85
|
+
decryptedPayload = await symmetricDecrypt({
|
|
86
|
+
key: getEncryptionKey(ctx),
|
|
87
|
+
data: encryptedProfile
|
|
88
|
+
});
|
|
89
|
+
} catch (e) {
|
|
90
|
+
ctx.context.logger.error("Failed to decrypt OAuth proxy profile", e);
|
|
91
|
+
throw redirectOnError(ctx, defaultErrorURL, "invalid_profile");
|
|
92
|
+
}
|
|
93
|
+
let payload;
|
|
94
|
+
try {
|
|
95
|
+
payload = passthroughPayloadSchema.parse(parseJSON(decryptedPayload));
|
|
96
|
+
} catch (e) {
|
|
97
|
+
ctx.context.logger.error("Failed to parse OAuth proxy payload", e);
|
|
98
|
+
throw redirectOnError(ctx, defaultErrorURL, "invalid_payload");
|
|
99
|
+
}
|
|
100
|
+
const errorURL = payload.errorURL || defaultErrorURL;
|
|
101
|
+
if (ctx.path?.startsWith("/callback/") && ctx.params.id !== payload.account.providerId) {
|
|
102
|
+
ctx.context.logger.warn("OAuth proxy callback provider mismatch");
|
|
103
|
+
throw redirectOnError(ctx, errorURL, "provider_mismatch");
|
|
104
|
+
}
|
|
105
|
+
const age = (Date.now() - payload.timestamp) / 1e3;
|
|
106
|
+
if (age > maxAge || age < -10) {
|
|
107
|
+
ctx.context.logger.error(`OAuth proxy payload expired or invalid (age: ${age}s, maxAge: ${maxAge}s)`);
|
|
108
|
+
throw redirectOnError(ctx, errorURL, "payload_expired");
|
|
109
|
+
}
|
|
110
|
+
if (!await restoreOAuthProxyState(ctx, payload.state)) throw redirectOnError(ctx, errorURL, "state_mismatch");
|
|
111
|
+
let result;
|
|
112
|
+
try {
|
|
113
|
+
result = await handleOAuthUserInfo(ctx, {
|
|
114
|
+
userInfo: payload.userInfo,
|
|
115
|
+
account: payload.account,
|
|
116
|
+
callbackURL: payload.callbackURL,
|
|
117
|
+
disableSignUp: payload.disableSignUp,
|
|
118
|
+
source: {
|
|
119
|
+
method: "oauth",
|
|
120
|
+
oauth: {
|
|
121
|
+
providerId: payload.account.providerId,
|
|
122
|
+
profile: payload.profile
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (isAPIError(e) && e.body?.code) throw redirectOnError(ctx, errorURL, e.body.code, e.body.message);
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
if (result.error) {
|
|
131
|
+
ctx.context.logger.error("OAuth proxy callback error", result.error);
|
|
132
|
+
throw redirectOnError(ctx, errorURL, result.error.split(" ").join("_"));
|
|
133
|
+
}
|
|
134
|
+
if (!result.data) {
|
|
135
|
+
ctx.context.logger.error("OAuth proxy callback missing session data");
|
|
136
|
+
throw redirectOnError(ctx, errorURL, "user_creation_failed");
|
|
137
|
+
}
|
|
138
|
+
await setSessionCookie(ctx, result.data);
|
|
139
|
+
const finalURL = result.isRegister ? payload.newUserURL || payload.callbackURL : payload.callbackURL;
|
|
140
|
+
throw ctx.redirect(finalURL);
|
|
141
|
+
});
|
|
41
142
|
return {
|
|
42
143
|
id: "oauth-proxy",
|
|
43
144
|
version: PACKAGE_VERSION,
|
|
44
145
|
options: opts,
|
|
45
|
-
endpoints: {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
146
|
+
endpoints: {
|
|
147
|
+
/**
|
|
148
|
+
* @deprecated OAuth proxy callbacks now use `/callback/:id/oauth-proxy`.
|
|
149
|
+
* This endpoint will be removed in the next minor release.
|
|
150
|
+
*/
|
|
151
|
+
oAuthProxy: createAuthEndpoint("/oauth-proxy-callback", {
|
|
152
|
+
method: "GET",
|
|
51
153
|
operationId: "oauthProxyCallback",
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
description: "
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
description: "
|
|
68
|
-
|
|
154
|
+
query: oauthProxyQuerySchema,
|
|
155
|
+
use: [originCheck((ctx) => ctx.query.callbackURL)],
|
|
156
|
+
metadata: { openapi: {
|
|
157
|
+
operationId: "oauthProxyCallback",
|
|
158
|
+
deprecated: true,
|
|
159
|
+
description: "OAuth Proxy Callback",
|
|
160
|
+
parameters: [{
|
|
161
|
+
in: "query",
|
|
162
|
+
name: "callbackURL",
|
|
163
|
+
required: true,
|
|
164
|
+
description: "The URL to redirect to after the proxy"
|
|
165
|
+
}, {
|
|
166
|
+
in: "query",
|
|
167
|
+
name: "profile",
|
|
168
|
+
required: false,
|
|
169
|
+
description: "Encrypted OAuth profile data"
|
|
170
|
+
}],
|
|
171
|
+
responses: { 302: {
|
|
172
|
+
description: "Redirect",
|
|
173
|
+
headers: { Location: {
|
|
174
|
+
description: "The URL to redirect to",
|
|
175
|
+
schema: { type: "string" }
|
|
176
|
+
} }
|
|
69
177
|
} }
|
|
70
178
|
} }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
ctx.context.logger.error("OAuth proxy callback missing profile data");
|
|
78
|
-
throw redirectOnError(ctx, defaultErrorURL, "missing_profile");
|
|
79
|
-
}
|
|
80
|
-
let decryptedPayload;
|
|
81
|
-
try {
|
|
82
|
-
decryptedPayload = await symmetricDecrypt({
|
|
83
|
-
key: getEncryptionKey(ctx),
|
|
84
|
-
data: encryptedProfile
|
|
85
|
-
});
|
|
86
|
-
} catch (e) {
|
|
87
|
-
ctx.context.logger.error("Failed to decrypt OAuth proxy profile", e);
|
|
88
|
-
throw redirectOnError(ctx, defaultErrorURL, "invalid_profile");
|
|
89
|
-
}
|
|
90
|
-
let payload;
|
|
91
|
-
try {
|
|
92
|
-
payload = parseJSON(decryptedPayload);
|
|
93
|
-
} catch (e) {
|
|
94
|
-
ctx.context.logger.error("Failed to parse OAuth proxy payload", e);
|
|
95
|
-
throw redirectOnError(ctx, defaultErrorURL, "invalid_payload");
|
|
96
|
-
}
|
|
97
|
-
if (typeof payload.timestamp !== "number" || !payload.userInfo || !payload.account || !payload.state || !payload.callbackURL) {
|
|
98
|
-
ctx.context.logger.error("Failed to parse OAuth proxy payload");
|
|
99
|
-
throw redirectOnError(ctx, defaultErrorURL, "invalid_payload");
|
|
100
|
-
}
|
|
101
|
-
const errorURL = payload.errorURL || defaultErrorURL;
|
|
102
|
-
const age = (Date.now() - payload.timestamp) / 1e3;
|
|
103
|
-
if (age > maxAge || age < -10) {
|
|
104
|
-
ctx.context.logger.error(`OAuth proxy payload expired or invalid (age: ${age}s, maxAge: ${maxAge}s)`);
|
|
105
|
-
throw redirectOnError(ctx, errorURL, "payload_expired");
|
|
106
|
-
}
|
|
107
|
-
if (!await consumeOAuthProxyState(ctx, payload.state)) throw redirectOnError(ctx, errorURL, "state_mismatch");
|
|
108
|
-
let result;
|
|
109
|
-
try {
|
|
110
|
-
result = await handleOAuthUserInfo(ctx, {
|
|
111
|
-
userInfo: payload.userInfo,
|
|
112
|
-
account: payload.account,
|
|
113
|
-
callbackURL: payload.callbackURL,
|
|
114
|
-
disableSignUp: payload.disableSignUp,
|
|
115
|
-
source: {
|
|
116
|
-
method: "oauth",
|
|
117
|
-
oauth: {
|
|
118
|
-
providerId: payload.account.providerId,
|
|
119
|
-
profile: payload.profile
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
} catch (e) {
|
|
124
|
-
if (isAPIError(e) && e.body?.code) throw redirectOnError(ctx, errorURL, e.body.code, e.body.message);
|
|
125
|
-
throw e;
|
|
126
|
-
}
|
|
127
|
-
if (result.error) {
|
|
128
|
-
ctx.context.logger.error("OAuth proxy callback error", result.error);
|
|
129
|
-
throw redirectOnError(ctx, errorURL, result.error.split(" ").join("_"));
|
|
130
|
-
}
|
|
131
|
-
if (!result.data) {
|
|
132
|
-
ctx.context.logger.error("OAuth proxy callback missing session data");
|
|
133
|
-
throw redirectOnError(ctx, errorURL, "user_creation_failed");
|
|
134
|
-
}
|
|
135
|
-
await setSessionCookie(ctx, result.data);
|
|
136
|
-
const finalURL = result.isRegister ? payload.newUserURL || payload.callbackURL : payload.callbackURL;
|
|
137
|
-
throw ctx.redirect(finalURL);
|
|
138
|
-
}) },
|
|
179
|
+
}, async (ctx) => oauthProxyCompletion({
|
|
180
|
+
...ctx,
|
|
181
|
+
params: { id: "oauth-proxy" }
|
|
182
|
+
})),
|
|
183
|
+
oAuthProxyCompletion: oauthProxyCompletion
|
|
184
|
+
},
|
|
139
185
|
hooks: {
|
|
140
186
|
before: [{
|
|
141
187
|
matcher(context) {
|
|
@@ -143,6 +189,8 @@ const oAuthProxy = (opts) => {
|
|
|
143
189
|
},
|
|
144
190
|
handler: createAuthMiddleware(async (ctx) => {
|
|
145
191
|
if (checkSkipProxy(ctx, opts)) return;
|
|
192
|
+
const providerId = ctx.body?.provider;
|
|
193
|
+
if (!providerId) return;
|
|
146
194
|
const currentURL = resolveCurrentURL(ctx, opts);
|
|
147
195
|
const productionURL = opts?.productionURL;
|
|
148
196
|
const originalCallbackURL = ctx.body?.callbackURL || ctx.context.baseURL;
|
|
@@ -150,8 +198,7 @@ const oAuthProxy = (opts) => {
|
|
|
150
198
|
const productionBaseURL = `${stripTrailingSlash(productionURL)}${ctx.context.options.basePath || "/api/auth"}`;
|
|
151
199
|
ctx.context.baseURL = productionBaseURL;
|
|
152
200
|
}
|
|
153
|
-
const newCallbackURL = `${stripTrailingSlash(currentURL.origin)}${ctx.context.options.basePath || "/api/auth"}/oauth-proxy
|
|
154
|
-
if (!ctx.body) return;
|
|
201
|
+
const newCallbackURL = `${stripTrailingSlash(currentURL.origin)}${ctx.context.options.basePath || "/api/auth"}/callback/${providerId}/oauth-proxy?callbackURL=${encodeURIComponent(originalCallbackURL)}`;
|
|
155
202
|
ctx.body.callbackURL = newCallbackURL;
|
|
156
203
|
})
|
|
157
204
|
}, {
|
|
@@ -261,7 +308,6 @@ const oAuthProxy = (opts) => {
|
|
|
261
308
|
},
|
|
262
309
|
profile: providerProfile,
|
|
263
310
|
account: {
|
|
264
|
-
providerId: provider.id,
|
|
265
311
|
...accountKey,
|
|
266
312
|
accessToken: tokens.accessToken,
|
|
267
313
|
refreshToken: tokens.refreshToken,
|
|
@@ -342,7 +388,7 @@ const oAuthProxy = (opts) => {
|
|
|
342
388
|
},
|
|
343
389
|
handler: createAuthMiddleware(async (ctx) => {
|
|
344
390
|
const location = ctx.context.responseHeaders?.get("location");
|
|
345
|
-
if (!location?.includes("/oauth-proxy-callback?callbackURL") || !location.startsWith("http")) return;
|
|
391
|
+
if (!location?.includes("/oauth-proxy?callbackURL") && !location?.includes("/oauth-proxy-callback?callbackURL") || !location.startsWith("http")) return;
|
|
346
392
|
const productionOrigin = getOrigin(opts?.productionURL || (typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : void 0) || ctx.context.baseURL);
|
|
347
393
|
const locationURL = new URL(location);
|
|
348
394
|
if (locationURL.origin === productionOrigin) {
|
|
@@ -113,6 +113,20 @@ function schemaAcceptsUndefined(zodType) {
|
|
|
113
113
|
}
|
|
114
114
|
return false;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve input optionality exactly as Zod's JSON Schema emitter does.
|
|
118
|
+
*
|
|
119
|
+
* @see https://github.com/colinhacks/zod/blob/v4.5.4/packages/zod/src/v4/core/json-schema-processors.ts#L294-L308
|
|
120
|
+
*/
|
|
121
|
+
function getZodInputOptionality(zodType) {
|
|
122
|
+
const def = zodType._zod.def;
|
|
123
|
+
if (def.type === "pipe") {
|
|
124
|
+
const pipeDef = def;
|
|
125
|
+
if (pipeDef.in._zod.traits.has("$ZodTransform")) return getZodInputOptionality(pipeDef.out);
|
|
126
|
+
}
|
|
127
|
+
if (def.type === "catch") return getZodInputOptionality(def.innerType);
|
|
128
|
+
return zodType._zod.optin;
|
|
129
|
+
}
|
|
116
130
|
function isUndefinedOnlySchema(zodType) {
|
|
117
131
|
return zodType instanceof z.ZodUndefined || zodType instanceof z.ZodVoid;
|
|
118
132
|
}
|
|
@@ -252,7 +266,7 @@ function toOpenApiSchema(zodType) {
|
|
|
252
266
|
Object.entries(shape).forEach(([key, value]) => {
|
|
253
267
|
if (value instanceof z.ZodType) {
|
|
254
268
|
properties[key] = toOpenApiSchema(value);
|
|
255
|
-
if (
|
|
269
|
+
if (getZodInputOptionality(value) === void 0) required.push(key);
|
|
256
270
|
}
|
|
257
271
|
});
|
|
258
272
|
return withDescription({
|
|
@@ -3,6 +3,7 @@ import { defaultRoles } from "./access/statement.mjs";
|
|
|
3
3
|
import { cacheAllRoles, hasPermissionFn } from "./permission.mjs";
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
//#region src/plugins/organization/has-permission.ts
|
|
6
|
+
const rolePermissionsSchema = z.record(z.string(), z.array(z.string()));
|
|
6
7
|
const hasPermission = async (input, ctx) => {
|
|
7
8
|
let acRoles = { ...input.options.roles || defaultRoles };
|
|
8
9
|
if (ctx && input.organizationId && input.options.dynamicAccessControl?.enabled && input.options.ac && !input.useMemoryCache) {
|
|
@@ -14,9 +15,10 @@ const hasPermission = async (input, ctx) => {
|
|
|
14
15
|
}]
|
|
15
16
|
});
|
|
16
17
|
for (const { role, permission: permissionsString } of roles) {
|
|
17
|
-
const
|
|
18
|
+
const permissions = JSON.parse(permissionsString);
|
|
19
|
+
const result = rolePermissionsSchema.safeParse(permissions);
|
|
18
20
|
if (!result.success) {
|
|
19
|
-
ctx.context.logger.error("[hasPermission] Invalid permissions for role " + role, { permissions
|
|
21
|
+
ctx.context.logger.error("[hasPermission] Invalid permissions for role " + role, { permissions });
|
|
20
22
|
throw new APIError("INTERNAL_SERVER_ERROR", { message: "Invalid permissions for role " + role });
|
|
21
23
|
}
|
|
22
24
|
const merged = { ...acRoles[role]?.statements };
|
|
@@ -4,9 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
|
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
6
6
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
7
|
-
import "../../utils/index.mjs";
|
|
8
7
|
import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
|
|
9
|
-
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
10
8
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
11
9
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
12
10
|
import * as z from "zod";
|
|
@@ -476,7 +474,6 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
476
474
|
if (!await ctx.context.internalAdapter.findCredentialAccount(user.id)) await ctx.context.internalAdapter.createAccount({
|
|
477
475
|
userId: user.id,
|
|
478
476
|
providerId: "credential",
|
|
479
|
-
issuer: createLocalAccountIssuer("credential"),
|
|
480
477
|
accountId: user.id,
|
|
481
478
|
password: hashedPassword
|
|
482
479
|
});
|
|
@@ -6,7 +6,6 @@ import { PACKAGE_VERSION } from "../../version.mjs";
|
|
|
6
6
|
import { toChecksumAddress } from "../../utils/hashing.mjs";
|
|
7
7
|
import { normalizeSiweDomain, parseSiweMessage } from "./parse-message.mjs";
|
|
8
8
|
import { schema } from "./schema.mjs";
|
|
9
|
-
import { createLocalAccountIssuer } from "@better-auth/core/db";
|
|
10
9
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
11
10
|
import * as z from "zod";
|
|
12
11
|
import { createPlaceholderEmail } from "@better-auth/core/utils/email";
|
|
@@ -215,7 +214,6 @@ const siwe = (options) => {
|
|
|
215
214
|
await ctx.context.internalAdapter.createAccount({
|
|
216
215
|
userId: user.id,
|
|
217
216
|
providerId: "siwe",
|
|
218
|
-
issuer: createLocalAccountIssuer("siwe"),
|
|
219
217
|
accountId: `${walletAddress}:${chainId}`,
|
|
220
218
|
createdAt: /* @__PURE__ */ new Date(),
|
|
221
219
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -234,7 +232,6 @@ const siwe = (options) => {
|
|
|
234
232
|
await ctx.context.internalAdapter.createAccount({
|
|
235
233
|
userId: user.id,
|
|
236
234
|
providerId: "siwe",
|
|
237
|
-
issuer: createLocalAccountIssuer("siwe"),
|
|
238
235
|
accountId: `${walletAddress}:${chainId}`,
|
|
239
236
|
createdAt: /* @__PURE__ */ new Date(),
|
|
240
237
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { createCookieHeaders, createTestCookie } from "./cookie-builder.mjs";
|
|
2
|
+
import { sessionSchema } from "@better-auth/core/db";
|
|
2
3
|
//#region src/plugins/test-utils/auth-helpers.ts
|
|
4
|
+
function createSession(ctx, opts) {
|
|
5
|
+
const additionalFields = Object.fromEntries(Object.entries(opts.session ?? {}).filter(([key]) => !Object.hasOwn(sessionSchema.shape, key)));
|
|
6
|
+
return ctx.internalAdapter.createSession(opts.userId, false, additionalFields, true);
|
|
7
|
+
}
|
|
3
8
|
function createLogin(ctx) {
|
|
4
9
|
return async (opts) => {
|
|
5
10
|
const user = await ctx.internalAdapter.findUserById(opts.userId);
|
|
6
11
|
if (!user) throw new Error(`User not found: ${opts.userId}`);
|
|
7
|
-
const session = await
|
|
12
|
+
const session = await createSession(ctx, opts);
|
|
8
13
|
return {
|
|
9
14
|
session,
|
|
10
15
|
user,
|
|
@@ -16,12 +21,12 @@ function createLogin(ctx) {
|
|
|
16
21
|
}
|
|
17
22
|
function createGetAuthHeaders(ctx) {
|
|
18
23
|
return async (opts) => {
|
|
19
|
-
return createCookieHeaders(ctx, (await
|
|
24
|
+
return createCookieHeaders(ctx, (await createSession(ctx, opts)).token);
|
|
20
25
|
};
|
|
21
26
|
}
|
|
22
27
|
function createGetCookies(ctx) {
|
|
23
28
|
return async (opts) => {
|
|
24
|
-
return createTestCookie(ctx, (await
|
|
29
|
+
return createTestCookie(ctx, (await createSession(ctx, opts)).token, opts.domain);
|
|
25
30
|
};
|
|
26
31
|
}
|
|
27
32
|
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LoginResult, TestCookie, TestHelpers, TestUtilsOptions } from "./types.mjs";
|
|
1
|
+
import { LoginResult, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions } from "./types.mjs";
|
|
2
2
|
import * as _$_better_auth_core0 from "@better-auth/core";
|
|
3
3
|
|
|
4
4
|
//#region src/plugins/test-utils/index.d.ts
|
|
@@ -21,6 +21,12 @@ interface LoginResult {
|
|
|
21
21
|
cookies: TestCookie[];
|
|
22
22
|
token: string;
|
|
23
23
|
}
|
|
24
|
+
/** Options for creating a session with the test auth helpers. */
|
|
25
|
+
interface TestAuthOptions {
|
|
26
|
+
userId: string;
|
|
27
|
+
/** Additional session fields. Standard session fields are ignored. */
|
|
28
|
+
session?: Record<string, unknown>;
|
|
29
|
+
}
|
|
24
30
|
interface TestHelpers {
|
|
25
31
|
createUser(overrides?: Partial<User> & Record<string, unknown>): User;
|
|
26
32
|
createOrganization?(overrides?: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -33,18 +39,13 @@ interface TestHelpers {
|
|
|
33
39
|
}): Promise<Record<string, unknown>>;
|
|
34
40
|
deleteUser(userId: string): Promise<void>;
|
|
35
41
|
deleteOrganization?(orgId: string): Promise<void>;
|
|
36
|
-
login(opts:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
getAuthHeaders(opts: {
|
|
40
|
-
userId: string;
|
|
41
|
-
}): Promise<Headers>;
|
|
42
|
-
getCookies(opts: {
|
|
43
|
-
userId: string;
|
|
42
|
+
login(opts: TestAuthOptions): Promise<LoginResult>;
|
|
43
|
+
getAuthHeaders(opts: TestAuthOptions): Promise<Headers>;
|
|
44
|
+
getCookies(opts: TestAuthOptions & {
|
|
44
45
|
domain?: string;
|
|
45
46
|
}): Promise<TestCookie[]>;
|
|
46
47
|
getOTP?(identifier: string): string | undefined;
|
|
47
48
|
clearOTPs?(): void;
|
|
48
49
|
}
|
|
49
50
|
//#endregion
|
|
50
|
-
export { LoginResult, TestCookie, TestHelpers, TestUtilsOptions };
|
|
51
|
+
export { LoginResult, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions };
|
|
@@ -61,6 +61,7 @@ declare const twoFactorClient: (options?: {
|
|
|
61
61
|
OTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_NOT_CONFIGURED">;
|
|
62
62
|
OTP_HAS_EXPIRED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_HAS_EXPIRED">;
|
|
63
63
|
TOTP_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_ENABLED">;
|
|
64
|
+
TOTP_ALREADY_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_ALREADY_ENABLED">;
|
|
64
65
|
TOTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_CONFIGURED">;
|
|
65
66
|
TWO_FACTOR_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TWO_FACTOR_NOT_ENABLED">;
|
|
66
67
|
BACKUP_CODES_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"BACKUP_CODES_NOT_ENABLED">;
|
|
@@ -6,6 +6,7 @@ declare const TWO_FACTOR_ERROR_CODES: {
|
|
|
6
6
|
OTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_NOT_CONFIGURED">;
|
|
7
7
|
OTP_HAS_EXPIRED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_HAS_EXPIRED">;
|
|
8
8
|
TOTP_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_ENABLED">;
|
|
9
|
+
TOTP_ALREADY_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_ALREADY_ENABLED">;
|
|
9
10
|
TOTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_CONFIGURED">;
|
|
10
11
|
TWO_FACTOR_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TWO_FACTOR_NOT_ENABLED">;
|
|
11
12
|
BACKUP_CODES_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"BACKUP_CODES_NOT_ENABLED">;
|
|
@@ -5,6 +5,7 @@ const TWO_FACTOR_ERROR_CODES = defineErrorCodes({
|
|
|
5
5
|
OTP_NOT_CONFIGURED: "OTP is not available",
|
|
6
6
|
OTP_HAS_EXPIRED: "OTP has expired",
|
|
7
7
|
TOTP_NOT_ENABLED: "TOTP not enabled",
|
|
8
|
+
TOTP_ALREADY_ENABLED: "TOTP is already enabled",
|
|
8
9
|
TOTP_NOT_CONFIGURED: "TOTP is not available",
|
|
9
10
|
TWO_FACTOR_NOT_ENABLED: "Two factor isn't enabled",
|
|
10
11
|
BACKUP_CODES_NOT_ENABLED: "Backup codes aren't enabled",
|
|
@@ -705,6 +705,7 @@ declare const twoFactor: <O extends TwoFactorOptions>(options?: O) => {
|
|
|
705
705
|
OTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_NOT_CONFIGURED">;
|
|
706
706
|
OTP_HAS_EXPIRED: _$_better_auth_core_utils_error_codes0.RawError<"OTP_HAS_EXPIRED">;
|
|
707
707
|
TOTP_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_ENABLED">;
|
|
708
|
+
TOTP_ALREADY_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_ALREADY_ENABLED">;
|
|
708
709
|
TOTP_NOT_CONFIGURED: _$_better_auth_core_utils_error_codes0.RawError<"TOTP_NOT_CONFIGURED">;
|
|
709
710
|
TWO_FACTOR_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"TWO_FACTOR_NOT_ENABLED">;
|
|
710
711
|
BACKUP_CODES_NOT_ENABLED: _$_better_auth_core_utils_error_codes0.RawError<"BACKUP_CODES_NOT_ENABLED">;
|
|
@@ -122,7 +122,6 @@ const twoFactor = (options) => {
|
|
|
122
122
|
await ctx.context.internalAdapter.deleteSession(ctx.context.session.session.token);
|
|
123
123
|
return ctx.json({ method: "otp" });
|
|
124
124
|
}
|
|
125
|
-
const backupCodes = await generateBackupCodes(ctx.context.secretConfig, backupCodeOptions);
|
|
126
125
|
const existingTwoFactor = await ctx.context.adapter.findOne({
|
|
127
126
|
model: opts.twoFactorTable,
|
|
128
127
|
where: [{
|
|
@@ -130,6 +129,8 @@ const twoFactor = (options) => {
|
|
|
130
129
|
value: user.id
|
|
131
130
|
}]
|
|
132
131
|
});
|
|
132
|
+
if (existingTwoFactor && existingTwoFactor.verified !== false) throw APIError.from("BAD_REQUEST", TWO_FACTOR_ERROR_CODES.TOTP_ALREADY_ENABLED);
|
|
133
|
+
const backupCodes = await generateBackupCodes(ctx.context.secretConfig, backupCodeOptions);
|
|
133
134
|
const secret = generateRandomString(32);
|
|
134
135
|
const encryptedSecret = await symmetricEncrypt({
|
|
135
136
|
key: ctx.context.secretConfig,
|
|
@@ -146,7 +147,7 @@ const twoFactor = (options) => {
|
|
|
146
147
|
const totpData = {
|
|
147
148
|
secret: encryptedSecret,
|
|
148
149
|
backupCodes: backupCodes.encryptedBackupCodes,
|
|
149
|
-
verified:
|
|
150
|
+
verified: !!options?.skipVerificationOnEnable
|
|
150
151
|
};
|
|
151
152
|
if (existingTwoFactor) await ctx.context.adapter.update({
|
|
152
153
|
model: opts.twoFactorTable,
|
package/dist/state.d.mts
CHANGED
|
@@ -9,26 +9,13 @@ declare const stateDataSchema: z.ZodObject<{
|
|
|
9
9
|
errorURL: z.ZodOptional<z.ZodString>;
|
|
10
10
|
newUserURL: z.ZodOptional<z.ZodString>;
|
|
11
11
|
expiresAt: z.ZodNumber;
|
|
12
|
-
/**
|
|
13
|
-
* CSRF nonce returned to the OAuth provider. When using cookie state storage,
|
|
14
|
-
* this must match the callback `state` query parameter.
|
|
15
|
-
*/
|
|
16
12
|
oauthState: z.ZodOptional<z.ZodString>;
|
|
17
13
|
link: z.ZodOptional<z.ZodObject<{
|
|
18
14
|
email: z.ZodString;
|
|
19
15
|
userId: z.ZodCoercedString<unknown>;
|
|
20
16
|
}, z.core.$strip>>;
|
|
21
17
|
requestSignUp: z.ZodOptional<z.ZodBoolean>;
|
|
22
|
-
/**
|
|
23
|
-
* OIDC nonce sent as the authorization request `nonce` parameter when the
|
|
24
|
-
* provider requires an ID token to be bound to this redirect flow.
|
|
25
|
-
*/
|
|
26
18
|
idTokenNonce: z.ZodOptional<z.ZodString>;
|
|
27
|
-
/**
|
|
28
|
-
* Server-controlled values that ride the state across the provider redirect.
|
|
29
|
-
* Populated only by `generateState` from `addOAuthServerContext`, never from
|
|
30
|
-
* the request body, so it is safe to trust on the callback.
|
|
31
|
-
*/
|
|
32
19
|
serverContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
33
20
|
}, z.core.$loose>;
|
|
34
21
|
type StateData = z.infer<typeof stateDataSchema>;
|
|
@@ -90,7 +90,7 @@ async function getTestInstance(options, config) {
|
|
|
90
90
|
advanced: { cookies: {} },
|
|
91
91
|
logger: { level: "debug" }
|
|
92
92
|
};
|
|
93
|
-
const
|
|
93
|
+
const authOptions = {
|
|
94
94
|
baseURL: "http://localhost:" + (config?.port || 3e3),
|
|
95
95
|
...opts,
|
|
96
96
|
...options,
|
|
@@ -100,7 +100,15 @@ async function getTestInstance(options, config) {
|
|
|
100
100
|
password: options?.emailAndPassword?.password ?? testPassword
|
|
101
101
|
},
|
|
102
102
|
plugins: [bearer(), ...options?.plugins || []]
|
|
103
|
-
}
|
|
103
|
+
};
|
|
104
|
+
if (testWith !== "mongodb") {
|
|
105
|
+
const { runMigrations } = await getMigrations({
|
|
106
|
+
...authOptions,
|
|
107
|
+
database: opts.database
|
|
108
|
+
});
|
|
109
|
+
await runMigrations();
|
|
110
|
+
}
|
|
111
|
+
const auth = betterAuth(authOptions);
|
|
104
112
|
const testUser = {
|
|
105
113
|
email: "test@test.com",
|
|
106
114
|
password: "test123456",
|
|
@@ -117,13 +125,6 @@ async function getTestInstance(options, config) {
|
|
|
117
125
|
headers
|
|
118
126
|
});
|
|
119
127
|
}
|
|
120
|
-
if (testWith !== "mongodb") {
|
|
121
|
-
const { runMigrations } = await getMigrations({
|
|
122
|
-
...auth.options,
|
|
123
|
-
database: opts.database
|
|
124
|
-
});
|
|
125
|
-
await runMigrations();
|
|
126
|
-
}
|
|
127
128
|
await createTestUser();
|
|
128
129
|
const cleanup = async () => {
|
|
129
130
|
if (testWith === "mongodb") {
|