better-auth 1.7.2 → 1.7.3
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/index.d.mts +6 -22
- package/dist/api/index.mjs +2 -0
- 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/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 +2 -2
- 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/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 +9 -9
- package/dist/utils/index.mjs +0 -5
|
@@ -22,20 +22,17 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
function okta(options) {
|
|
25
|
-
const defaultScopes = [
|
|
26
|
-
"openid",
|
|
27
|
-
"profile",
|
|
28
|
-
"email"
|
|
29
|
-
];
|
|
30
|
-
const issuer = options.issuer.replace(/\/$/, "");
|
|
31
25
|
return {
|
|
32
26
|
providerId: "okta",
|
|
33
|
-
|
|
34
|
-
discoveryUrl: `${issuer}/.well-known/openid-configuration`,
|
|
27
|
+
discoveryUrl: `${options.issuer.replace(/\/$/, "")}/.well-known/openid-configuration`,
|
|
35
28
|
clientId: options.clientId,
|
|
36
29
|
clientSecret: options.clientSecret,
|
|
37
30
|
tokenEndpointAuth: options.tokenEndpointAuth,
|
|
38
|
-
scopes: options.scopes ??
|
|
31
|
+
scopes: options.scopes ?? [
|
|
32
|
+
"openid",
|
|
33
|
+
"profile",
|
|
34
|
+
"email"
|
|
35
|
+
],
|
|
39
36
|
redirectURI: options.redirectURI,
|
|
40
37
|
endSessionEndpoint: options.endSessionEndpoint,
|
|
41
38
|
postLogoutRedirectURI: options.postLogoutRedirectURI,
|
|
@@ -41,7 +41,6 @@ function slack(options) {
|
|
|
41
41
|
return {
|
|
42
42
|
providerId: "slack",
|
|
43
43
|
accountSubject: ({ profile }) => profile.sub ?? "",
|
|
44
|
-
accountIssuer: "https://slack.com",
|
|
45
44
|
authorizationUrl: "https://slack.com/openid/connect/authorize",
|
|
46
45
|
tokenUrl: "https://slack.com/api/openid.connect.token",
|
|
47
46
|
userInfoUrl: "https://slack.com/api/openid.connect.userInfo",
|
|
@@ -36,15 +36,6 @@ interface GenericOAuthConfig<ID extends string = string> {
|
|
|
36
36
|
* mapping cannot redefine provider identity.
|
|
37
37
|
*/
|
|
38
38
|
accountSubject?: ((context: OAuthAccountKeyContext<GenericOAuthUserInfo>) => Awaitable<string | number>) | undefined;
|
|
39
|
-
/**
|
|
40
|
-
* Stable issuer namespace paired with the provider account ID.
|
|
41
|
-
*
|
|
42
|
-
* Discovery providers use the discovered issuer by default. Set this for
|
|
43
|
-
* providers without discovery, provider aliases that share one identity
|
|
44
|
-
* namespace, or tenant-specific issuers derived from verified provider data.
|
|
45
|
-
* The resolver must not use unverified request input.
|
|
46
|
-
*/
|
|
47
|
-
accountIssuer?: string | ((context: OAuthAccountKeyContext<GenericOAuthUserInfo>) => Awaitable<string>) | undefined;
|
|
48
39
|
/**
|
|
49
40
|
* URL to fetch OAuth 2.0 configuration.
|
|
50
41
|
* If provided, the authorization and token endpoints will be fetched from this URL.
|
|
@@ -9,6 +9,14 @@ declare module "@better-auth/core" {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Checks whether a password appears in the Have I Been Pwned password corpus.
|
|
14
|
+
* Only the first five characters of its SHA-1 hash are sent to the service.
|
|
15
|
+
*
|
|
16
|
+
* @returns Whether the password has been compromised.
|
|
17
|
+
* @throws {APIError} When the password could not be checked.
|
|
18
|
+
*/
|
|
19
|
+
declare function isPasswordCompromised(password: string): Promise<boolean>;
|
|
12
20
|
interface HaveIBeenPwnedOptions {
|
|
13
21
|
/**
|
|
14
22
|
* Custom error message shown when a compromised password is detected.
|
|
@@ -52,4 +60,4 @@ declare const haveIBeenPwned: (options?: HaveIBeenPwnedOptions | undefined) => {
|
|
|
52
60
|
};
|
|
53
61
|
};
|
|
54
62
|
//#endregion
|
|
55
|
-
export { HaveIBeenPwnedOptions, haveIBeenPwned };
|
|
63
|
+
export { HaveIBeenPwnedOptions, haveIBeenPwned, isPasswordCompromised };
|
|
@@ -7,26 +7,46 @@ import { createHash } from "@better-auth/utils/hash";
|
|
|
7
7
|
import { betterFetch } from "@better-fetch/fetch";
|
|
8
8
|
//#region src/plugins/haveibeenpwned/index.ts
|
|
9
9
|
const ERROR_CODES = defineErrorCodes({ PASSWORD_COMPROMISED: "The password you entered has been compromised. Please choose a different password." });
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
function getPasswordCompromiseCount(response, hashSuffix) {
|
|
11
|
+
const matchingEntryPrefix = `${hashSuffix.toUpperCase()}:`;
|
|
12
|
+
for (const line of response.split(/\r?\n/)) {
|
|
13
|
+
if (line.slice(0, matchingEntryPrefix.length).toUpperCase() !== matchingEntryPrefix) continue;
|
|
14
|
+
const compromiseCountText = line.slice(matchingEntryPrefix.length);
|
|
15
|
+
const compromiseCount = Number(compromiseCountText);
|
|
16
|
+
if (!(Number.isSafeInteger(compromiseCount) && compromiseCount >= 0 && String(compromiseCount) === compromiseCountText)) throw new Error("Invalid password compromise count");
|
|
17
|
+
return compromiseCount;
|
|
18
|
+
}
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Checks whether a password appears in the Have I Been Pwned password corpus.
|
|
23
|
+
* Only the first five characters of its SHA-1 hash are sent to the service.
|
|
24
|
+
*
|
|
25
|
+
* @returns Whether the password has been compromised.
|
|
26
|
+
* @throws {APIError} When the password could not be checked.
|
|
27
|
+
*/
|
|
28
|
+
async function isPasswordCompromised(password) {
|
|
15
29
|
try {
|
|
30
|
+
const sha1Hash = (await createHash("SHA-1", "hex").digest(password)).toUpperCase();
|
|
31
|
+
const prefix = sha1Hash.substring(0, 5);
|
|
32
|
+
const suffix = sha1Hash.substring(5);
|
|
16
33
|
const { data, error } = await betterFetch(`https://api.pwnedpasswords.com/range/${prefix}`, { headers: {
|
|
17
34
|
"Add-Padding": "true",
|
|
18
35
|
"User-Agent": "BetterAuth Password Checker"
|
|
19
36
|
} });
|
|
20
37
|
if (error) throw new APIError("INTERNAL_SERVER_ERROR", { message: `Failed to check password. Status: ${error.status}` });
|
|
21
|
-
|
|
22
|
-
message: customMessage || ERROR_CODES.PASSWORD_COMPROMISED.message,
|
|
23
|
-
code: ERROR_CODES.PASSWORD_COMPROMISED.code
|
|
24
|
-
});
|
|
38
|
+
return getPasswordCompromiseCount(data, suffix) > 0;
|
|
25
39
|
} catch (error) {
|
|
26
40
|
if (isAPIError(error)) throw error;
|
|
27
41
|
throw new APIError("INTERNAL_SERVER_ERROR", { message: "Failed to check password. Please try again later." });
|
|
28
42
|
}
|
|
29
43
|
}
|
|
44
|
+
async function rejectCompromisedPassword(password, customMessage) {
|
|
45
|
+
if (await isPasswordCompromised(password)) throw APIError.from("BAD_REQUEST", {
|
|
46
|
+
message: customMessage || ERROR_CODES.PASSWORD_COMPROMISED.message,
|
|
47
|
+
code: ERROR_CODES.PASSWORD_COMPROMISED.code
|
|
48
|
+
});
|
|
49
|
+
}
|
|
30
50
|
const haveIBeenPwned = (options) => {
|
|
31
51
|
const paths = options?.paths || [
|
|
32
52
|
"/sign-up/email",
|
|
@@ -48,7 +68,7 @@ const haveIBeenPwned = (options) => {
|
|
|
48
68
|
if (options?.enabled === false) return originalHash(password);
|
|
49
69
|
const c = getCurrentAuthEndpointContext();
|
|
50
70
|
if (!c.path || !paths.includes(c.path)) return originalHash(password);
|
|
51
|
-
await
|
|
71
|
+
await rejectCompromisedPassword(password, options?.customPasswordCompromisedMessage);
|
|
52
72
|
return originalHash(password);
|
|
53
73
|
}
|
|
54
74
|
} } };
|
|
@@ -58,4 +78,4 @@ const haveIBeenPwned = (options) => {
|
|
|
58
78
|
};
|
|
59
79
|
};
|
|
60
80
|
//#endregion
|
|
61
|
-
export { haveIBeenPwned };
|
|
81
|
+
export { haveIBeenPwned, isPasswordCompromised };
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -31,7 +31,7 @@ import { PatreonOptions, patreon } from "./generic-oauth/providers/patreon.mjs";
|
|
|
31
31
|
import { SlackOptions, slack } from "./generic-oauth/providers/slack.mjs";
|
|
32
32
|
import { YandexOptions, yandex } from "./generic-oauth/providers/yandex.mjs";
|
|
33
33
|
import { BaseOAuthProviderOptions, genericOAuth } from "./generic-oauth/index.mjs";
|
|
34
|
-
import { HaveIBeenPwnedOptions, haveIBeenPwned } from "./haveibeenpwned/index.mjs";
|
|
34
|
+
import { HaveIBeenPwnedOptions, haveIBeenPwned, isPasswordCompromised } from "./haveibeenpwned/index.mjs";
|
|
35
35
|
import { JWKOptions, JWSAlgorithms, Jwk, JwtOptions, ResolvedSigningKey } from "./jwt/types.mjs";
|
|
36
36
|
import { getJwtToken, resolveSigningKey, signJWT } from "./jwt/sign.mjs";
|
|
37
37
|
import { createJwk, generateExportedKeyPair, toExpJWT } from "./jwt/utils.mjs";
|
|
@@ -66,4 +66,4 @@ import { USERNAME_ERROR_CODES } from "./username/error-codes.mjs";
|
|
|
66
66
|
import { UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, username } from "./username/index.mjs";
|
|
67
67
|
import { hasPermission } from "./organization/has-permission.mjs";
|
|
68
68
|
import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization, parseRoles } from "./organization/organization.mjs";
|
|
69
|
-
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, DeviceCode, DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GenericOAuthUserInfo, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, ResolvedSigningKey, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, YandexOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, jwt, keycloak, lastLoginMethod, line, magicLink, memberSchema, microsoftEntraId, ms, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, yandex };
|
|
69
|
+
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, CaptchaFoxOptions, CaptchaOptions, CloudflareTurnstileOptions, CustomSessionPluginOptions, DefaultOrganizationPlugin, DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, DeviceCode, DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, DynamicAccessControlEndpoints, MULTI_SESSION_ERROR_CODES as ERROR_CODES, EmailOTPOptions, ExactRoleStatements, FieldSchema, GenericOAuthConfig, GenericOAuthOptions, GenericOAuthUserInfo, GoogleRecaptchaOptions, GumroadOptions, HCaptchaOptions, HIDE_METADATA, HaveIBeenPwnedOptions, HubSpotOptions, InferAdminRolesFromOption, InferInvitation, InferMember, InferOptionSchema, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferPluginContext, InferPluginErrorCodes, InferPluginIDs, InferTeam, Invitation, InvitationInput, InvitationStatus, JWKOptions, JWSAlgorithms, Jwk, JwtOptions, KeycloakOptions, LastLoginMethodOptions, LineOptions, LoginResult, MagicLinkOptions, Member, MemberInput, MicrosoftEntraIdOptions, MultiSessionConfig, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, OAuthPopupData, OAuthPopupMessage, OAuthProxyOptions, OTPOptions, OktaOptions, OneTapOptions, OneTimeTokenOptions, OpenAPIModelSchema, OpenAPIOptions, OpenAPIParameter, OpenAPISchema, Organization, OrganizationCreator, OrganizationEndpoints, OrganizationInput, OrganizationOptions, OrganizationPlugin, OrganizationRole, OrganizationSchema, POPUP_MARKER_COOKIE, Path, PatreonOptions, PhoneNumberOptions, Provider, ResolvedSigningKey, Role, RoleAuthorizeRequest, RoleInput, RoleStatements, SIWEPluginOptions, SessionWithImpersonatedBy, SlackOptions, Statements, SubArray, Subset, TOTPOptions, TWO_FACTOR_ERROR_CODES, Team, TeamEndpoints, TeamInput, TeamMember, TeamMemberInput, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, YandexOptions, admin, anonymous, auth0, backupCode2fa, bearer, captcha, createAccessControl, createJwk, customSession, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, generateBackupCodes, generateExportedKeyPair, generator, genericOAuth, getBackupCodes, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, invitationSchema, invitationStatus, isPasswordCompromised, jwt, keycloak, lastLoginMethod, line, magicLink, memberSchema, microsoftEntraId, ms, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, organizationRoleSchema, organizationSchema, otp2fa, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, roleSchema, sec, signJWT, siwe, slack, teamMemberSchema, teamSchema, testUtils, toExpJWT, totp2fa, twoFactor, twoFactorClient, username, verifyBackupCode, verifyJWT, yandex };
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { patreon } from "./generic-oauth/providers/patreon.mjs";
|
|
|
25
25
|
import { slack } from "./generic-oauth/providers/slack.mjs";
|
|
26
26
|
import { yandex } from "./generic-oauth/providers/yandex.mjs";
|
|
27
27
|
import { genericOAuth } from "./generic-oauth/index.mjs";
|
|
28
|
-
import { haveIBeenPwned } from "./haveibeenpwned/index.mjs";
|
|
28
|
+
import { haveIBeenPwned, isPasswordCompromised } from "./haveibeenpwned/index.mjs";
|
|
29
29
|
import { createJwk, generateExportedKeyPair, toExpJWT } from "./jwt/utils.mjs";
|
|
30
30
|
import { getJwtToken, resolveSigningKey, signJWT } from "./jwt/sign.mjs";
|
|
31
31
|
import { verifyJWT } from "./jwt/verify.mjs";
|
|
@@ -46,4 +46,4 @@ import { siwe } from "./siwe/index.mjs";
|
|
|
46
46
|
import { testUtils } from "./test-utils/index.mjs";
|
|
47
47
|
import { twoFactor } from "./two-factor/index.mjs";
|
|
48
48
|
import { username } from "./username/index.mjs";
|
|
49
|
-
export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, POPUP_MARKER_COOKIE, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, jwt, keycloak, lastLoginMethod, line, magicLink, microsoftEntraId, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, yandex };
|
|
49
|
+
export { MULTI_SESSION_ERROR_CODES as ERROR_CODES, HIDE_METADATA, OAUTH_POPUP_COMPLETE_SCRIPT, OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_ERROR_CODES, OAUTH_POPUP_MESSAGE_TYPE, OAUTH_POPUP_SCRIPT_CSP_HASH, POPUP_MARKER_COOKIE, TWO_FACTOR_ERROR_CODES, USERNAME_ERROR_CODES, admin, anonymous, auth0, bearer, captcha, createAccessControl, createJwk, customSession, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, generateExportedKeyPair, genericOAuth, getJwtToken, getOrgAdapter, gumroad, hasPermission, haveIBeenPwned, hubspot, isPasswordCompromised, jwt, keycloak, lastLoginMethod, line, magicLink, microsoftEntraId, multiSession, oAuthProxy, oauthPopup, okta, oneTap, oneTimeToken, openAPI, organization, parseRoles, patreon, phoneNumber, redeemDeviceCode, resolveSigningKey, role, signJWT, siwe, slack, testUtils, toExpJWT, twoFactor, twoFactorClient, username, verifyJWT, yandex };
|
|
@@ -13,6 +13,7 @@ const lastLoginMethod = (userConfig) => {
|
|
|
13
13
|
if (path.includes("siwe")) return "siwe";
|
|
14
14
|
if (path.includes("/passkey/verify-authentication")) return "passkey";
|
|
15
15
|
if (path.startsWith("/magic-link/verify")) return "magic-link";
|
|
16
|
+
if (path === "/sign-in/email-otp") return "email-otp";
|
|
16
17
|
return null;
|
|
17
18
|
};
|
|
18
19
|
const getResolveContext = (ctx) => {
|
|
@@ -58,6 +58,10 @@ declare const oAuthProxy: <O extends OAuthProxyOptions>(opts?: O) => {
|
|
|
58
58
|
version: string;
|
|
59
59
|
options: NoInfer<O>;
|
|
60
60
|
endpoints: {
|
|
61
|
+
/**
|
|
62
|
+
* @deprecated OAuth proxy callbacks now use `/callback/:id/oauth-proxy`.
|
|
63
|
+
* This endpoint will be removed in the next minor release.
|
|
64
|
+
*/
|
|
61
65
|
oAuthProxy: _$better_call0.StrictEndpoint<"/oauth-proxy-callback", {
|
|
62
66
|
method: "GET";
|
|
63
67
|
operationId: string;
|
|
@@ -69,6 +73,7 @@ declare const oAuthProxy: <O extends OAuthProxyOptions>(opts?: O) => {
|
|
|
69
73
|
metadata: {
|
|
70
74
|
openapi: {
|
|
71
75
|
operationId: string;
|
|
76
|
+
deprecated: boolean;
|
|
72
77
|
description: string;
|
|
73
78
|
parameters: ({
|
|
74
79
|
in: "query";
|
|
@@ -97,6 +102,18 @@ declare const oAuthProxy: <O extends OAuthProxyOptions>(opts?: O) => {
|
|
|
97
102
|
};
|
|
98
103
|
};
|
|
99
104
|
}, never>;
|
|
105
|
+
oAuthProxyCompletion: _$better_call0.StrictEndpoint<"/callback/:id/oauth-proxy", {
|
|
106
|
+
method: "GET";
|
|
107
|
+
operationId: string;
|
|
108
|
+
query: z.ZodObject<{
|
|
109
|
+
callbackURL: z.ZodString;
|
|
110
|
+
profile: z.ZodOptional<z.ZodString>;
|
|
111
|
+
}, z.core.$strip>;
|
|
112
|
+
use: _$better_call0.Middleware<_$better_call0.MiddlewareOptions, (inputContext: _$better_call0.MiddlewareInputContext<_$better_call0.MiddlewareOptions>) => Promise<void>>[];
|
|
113
|
+
metadata: {
|
|
114
|
+
scope: "http";
|
|
115
|
+
};
|
|
116
|
+
}, never>;
|
|
100
117
|
};
|
|
101
118
|
hooks: {
|
|
102
119
|
before: {
|
|
@@ -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
|
});
|