better-auth 1.7.9 → 1.8.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/routes/sign-up.mjs +22 -10
- package/dist/oauth2/link-account.mjs +5 -6
- package/dist/package.mjs +1 -1
- package/dist/plugins/device-authorization/index.d.mts +24 -26
- package/dist/plugins/device-authorization/index.mjs +4 -12
- package/dist/plugins/device-authorization/routes.d.mts +42 -1
- package/dist/plugins/device-authorization/routes.mjs +144 -159
- 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/one-tap/client.d.mts +4 -2
- package/dist/plugins/one-tap/client.mjs +9 -25
- package/dist/plugins/one-tap/index.mjs +24 -5
- package/package.json +38 -8
|
@@ -5,7 +5,7 @@ import { setSessionCookie } from "../../cookies/index.mjs";
|
|
|
5
5
|
import { safeCloneRequest } from "../../utils/request.mjs";
|
|
6
6
|
import { createEmailVerificationToken } from "./email-verification.mjs";
|
|
7
7
|
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
8
|
-
import { runWithTransaction } from "@better-auth/core/context";
|
|
8
|
+
import { queueAfterTransactionHook, runWithTransaction } from "@better-auth/core/context";
|
|
9
9
|
import { isDevelopment } from "@better-auth/core/env";
|
|
10
10
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
11
11
|
import { generateId } from "@better-auth/core/utils/id";
|
|
@@ -198,7 +198,13 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
198
198
|
* between existing and non-existing emails.
|
|
199
199
|
*/
|
|
200
200
|
await ctx.context.password.hash(password);
|
|
201
|
-
|
|
201
|
+
const onExistingUserSignUp = ctx.context.options.emailAndPassword?.onExistingUserSignUp;
|
|
202
|
+
if (onExistingUserSignUp) {
|
|
203
|
+
const request = safeCloneRequest(ctx.request);
|
|
204
|
+
await queueAfterTransactionHook(async () => {
|
|
205
|
+
await ctx.context.runInBackgroundOrAwait(onExistingUserSignUp({ user: dbUser.user }, request));
|
|
206
|
+
}, { onError: (error) => ctx.context.logger.error("Failed to run onExistingUserSignUp after sign-up", error) });
|
|
207
|
+
}
|
|
202
208
|
return buildGenericDuplicateResponse();
|
|
203
209
|
}
|
|
204
210
|
throw APIError.from("UNPROCESSABLE_ENTITY", BASE_ERROR_CODES.USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL);
|
|
@@ -238,15 +244,21 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
238
244
|
accountId: createdUser.id,
|
|
239
245
|
password: hash
|
|
240
246
|
});
|
|
241
|
-
|
|
242
|
-
|
|
247
|
+
const shouldSendVerificationEmail = ctx.context.options.emailVerification?.sendOnSignUp ?? ctx.context.options.emailAndPassword.requireEmailVerification;
|
|
248
|
+
const sendVerificationEmail = ctx.context.options.emailVerification?.sendVerificationEmail;
|
|
249
|
+
if (shouldSendVerificationEmail && sendVerificationEmail) {
|
|
250
|
+
const user = createdUser;
|
|
243
251
|
const callbackURL = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/");
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
user
|
|
247
|
-
url
|
|
248
|
-
|
|
249
|
-
|
|
252
|
+
const request = safeCloneRequest(ctx.request);
|
|
253
|
+
await queueAfterTransactionHook(async () => {
|
|
254
|
+
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
255
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
256
|
+
await ctx.context.runInBackgroundOrAwait(sendVerificationEmail({
|
|
257
|
+
user,
|
|
258
|
+
url,
|
|
259
|
+
token
|
|
260
|
+
}, request));
|
|
261
|
+
}, { onError: (error) => ctx.context.logger.error("Failed to send the sign-up verification email", error) });
|
|
250
262
|
}
|
|
251
263
|
if (shouldSkipAutoSignIn) return ctx.json({
|
|
252
264
|
token: null,
|
|
@@ -310,16 +310,16 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
310
310
|
isRegister: false
|
|
311
311
|
};
|
|
312
312
|
const requireEmailVerification = c.context.socialProviders.find((p) => p.id === account.providerId)?.options?.requireEmailVerification;
|
|
313
|
-
if (isRegister && !user.emailVerified && (c.context.options.emailVerification?.sendOnSignUp ?? requireEmailVerification)) await dispatchVerificationEmail(c, user, callbackURL
|
|
313
|
+
if (isRegister && !user.emailVerified && (c.context.options.emailVerification?.sendOnSignUp ?? requireEmailVerification)) await dispatchVerificationEmail(c, user, callbackURL);
|
|
314
314
|
if (requireEmailVerification && !user.emailVerified) {
|
|
315
|
-
if (!isRegister && c.context.options.emailVerification?.sendOnSignIn) await dispatchVerificationEmail(c, user, callbackURL
|
|
315
|
+
if (!isRegister && c.context.options.emailVerification?.sendOnSignIn) await dispatchVerificationEmail(c, user, callbackURL);
|
|
316
316
|
return {
|
|
317
317
|
error: OAUTH_CALLBACK_ERROR_CODES.EMAIL_NOT_VERIFIED,
|
|
318
318
|
data: null,
|
|
319
319
|
isRegister
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
|
-
const session = await c.context.internalAdapter.createSession(user.id, void 0, void 0, void 0, { deferSecondaryStorageWrites:
|
|
322
|
+
const session = await c.context.internalAdapter.createSession(user.id, void 0, void 0, void 0, { deferSecondaryStorageWrites: true });
|
|
323
323
|
if (!session) return {
|
|
324
324
|
error: "unable to create session",
|
|
325
325
|
data: null,
|
|
@@ -339,7 +339,7 @@ async function handleOAuthUserInfo(c, opts) {
|
|
|
339
339
|
accountCookie: pendingAccountCookie
|
|
340
340
|
};
|
|
341
341
|
}
|
|
342
|
-
async function dispatchVerificationEmail(c, user, callbackURL
|
|
342
|
+
async function dispatchVerificationEmail(c, user, callbackURL) {
|
|
343
343
|
const sendVerificationEmail = c.context.options.emailVerification?.sendVerificationEmail;
|
|
344
344
|
if (!sendVerificationEmail) return;
|
|
345
345
|
const send = async () => {
|
|
@@ -355,8 +355,7 @@ async function dispatchVerificationEmail(c, user, callbackURL, deferUntilAfterTr
|
|
|
355
355
|
c.context.logger.error("Failed to send OAuth verification email", e);
|
|
356
356
|
}
|
|
357
357
|
};
|
|
358
|
-
|
|
359
|
-
else await send();
|
|
358
|
+
await queueAfterTransactionHook(send);
|
|
360
359
|
}
|
|
361
360
|
/**
|
|
362
361
|
* Apply the `account.accountLinking.updateUserInfoOnLink` policy: when enabled,
|
package/dist/package.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TimeString, ms, sec } from "../../utils/time.mjs";
|
|
2
2
|
import { DeviceCode } from "./schema.mjs";
|
|
3
|
-
import { DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, redeemDeviceCode } from "./routes.mjs";
|
|
3
|
+
import { DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, assertDeviceCodePending, buildVerificationUris, decideDeviceCode, findDeviceCodeByUserCode, redeemDeviceCode } from "./routes.mjs";
|
|
4
4
|
import { GenericEndpointContext, StandardSchemaV1 } from "@better-auth/core";
|
|
5
5
|
import { DBFieldAttribute } from "@better-auth/core/db";
|
|
6
6
|
import * as _$_better_auth_core_utils_error_codes0 from "@better-auth/core/utils/error-codes";
|
|
@@ -74,7 +74,7 @@ interface DeviceAuthorizationGrantAuthorization {
|
|
|
74
74
|
* A token grant that contributes its request state to device authorization
|
|
75
75
|
* without expanding the standalone plugin's database or endpoint contracts.
|
|
76
76
|
*/
|
|
77
|
-
interface DeviceAuthorizationGrant<RequestFields extends z.ZodRawShape = z.ZodRawShape
|
|
77
|
+
interface DeviceAuthorizationGrant<RequestFields extends z.ZodRawShape = z.ZodRawShape> {
|
|
78
78
|
/** Additional request fields accepted only when this grant is configured. */
|
|
79
79
|
requestSchemaFields: RequestFields;
|
|
80
80
|
/** Additional request errors introduced by the grant's protocol extensions. */
|
|
@@ -90,15 +90,16 @@ interface DeviceAuthorizationGrant<RequestFields extends z.ZodRawShape = z.ZodRa
|
|
|
90
90
|
ctx: GenericEndpointContext;
|
|
91
91
|
request: DeviceAuthorizationRequest & z.infer<z.ZodObject<RequestFields>>;
|
|
92
92
|
}) => DeviceAuthorizationGrantAuthorization | undefined | Promise<DeviceAuthorizationGrantAuthorization | undefined>;
|
|
93
|
-
/**
|
|
94
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Throw when a code this grant issued reaches a standalone endpoint:
|
|
95
|
+
* `/device/token` (`redeem`) or `/device`, `/device/approve`, `/device/deny`
|
|
96
|
+
* (`approve`). The error should name the endpoint to use instead.
|
|
97
|
+
*/
|
|
98
|
+
assertStandaloneUse: (input: {
|
|
95
99
|
ctx: GenericEndpointContext;
|
|
96
100
|
deviceCode: Record<string, unknown>;
|
|
101
|
+
use: "redeem" | "approve";
|
|
97
102
|
}) => void | Promise<void>;
|
|
98
|
-
/** Add grant-owned information to the owner-only verification response. */
|
|
99
|
-
getVerificationContext: (deviceCode: Record<string, unknown>) => VerificationContext | undefined;
|
|
100
|
-
/** OpenAPI properties matching `getVerificationContext()`. */
|
|
101
|
-
verificationOpenAPIProperties?: Record<string, Record<string, unknown>>;
|
|
102
103
|
}
|
|
103
104
|
type DeviceAuthorizationPluginOptions<Grant extends DeviceAuthorizationGrant | undefined = undefined> = Partial<DeviceAuthorizationOptions> & {
|
|
104
105
|
/** Optional token grant that extends the device authorization flow. */grant?: Grant;
|
|
@@ -158,37 +159,37 @@ declare const deviceAuthorization: <Grant extends DeviceAuthorizationGrant | und
|
|
|
158
159
|
cloneRequest: true;
|
|
159
160
|
body: z.ZodObject<(("scope" | "user_id" | "client_id") & keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
160
161
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
161
|
-
}
|
|
162
|
+
}>> ? RequestFields : Record<never, never>) extends never ? {
|
|
162
163
|
client_id: z.ZodString;
|
|
163
164
|
user_id: z.ZodOptional<z.ZodString>;
|
|
164
165
|
scope: z.ZodOptional<z.ZodString>;
|
|
165
166
|
} & ((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
166
167
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
167
|
-
}
|
|
168
|
+
}>> ? RequestFields : Record<never, never>) extends infer T_1 ? { -readonly [P in keyof T_1]: T_1[P] } : never) : ({
|
|
168
169
|
client_id: z.ZodString;
|
|
169
170
|
user_id: z.ZodOptional<z.ZodString>;
|
|
170
171
|
scope: z.ZodOptional<z.ZodString>;
|
|
171
172
|
} extends infer T_2 extends z.core.util.SomeObject ? { [K in keyof T_2 as K extends keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
172
173
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
173
|
-
}
|
|
174
|
+
}>> ? RequestFields : Record<never, never>) ? never : K]: T_2[K] } : never) & (((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
174
175
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
175
|
-
}
|
|
176
|
+
}>> ? RequestFields : Record<never, never>) extends infer T_4 ? { -readonly [P in keyof T_4]: T_4[P] } : never) extends infer T_3 extends z.core.util.SomeObject ? { [K_1 in keyof T_3]: T_3[K_1] } : never)) extends infer T ? { [k in keyof T]: T[k] } : never, z.core.$strip> | z.ZodObject<(("scope" | "user_id" | "client_id") & keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
176
177
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
177
|
-
}
|
|
178
|
+
}>> ? RequestFields : Record<never, never>) extends never ? {
|
|
178
179
|
user_id: z.ZodOptional<z.ZodString>;
|
|
179
180
|
scope: z.ZodOptional<z.ZodString>;
|
|
180
181
|
client_id: z.ZodOptional<z.ZodString>;
|
|
181
182
|
} & ((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
182
183
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
183
|
-
}
|
|
184
|
+
}>> ? RequestFields : Record<never, never>) extends infer T_6 ? { -readonly [P in keyof T_6]: T_6[P] } : never) : ({
|
|
184
185
|
user_id: z.ZodOptional<z.ZodString>;
|
|
185
186
|
scope: z.ZodOptional<z.ZodString>;
|
|
186
187
|
client_id: z.ZodOptional<z.ZodString>;
|
|
187
188
|
} extends infer T_7 extends z.core.util.SomeObject ? { [K_2 in keyof T_7 as K_2 extends keyof (Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
188
189
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
189
|
-
}
|
|
190
|
+
}>> ? RequestFields : Record<never, never>) ? never : K_2]: T_7[K_2] } : never) & (((Grant extends DeviceAuthorizationGrant<infer RequestFields extends Readonly<{
|
|
190
191
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
191
|
-
}
|
|
192
|
+
}>> ? RequestFields : Record<never, never>) extends infer T_9 ? { -readonly [P in keyof T_9]: T_9[P] } : never) extends infer T_8 extends z.core.util.SomeObject ? { [K_1 in keyof T_8]: T_8[K_1] } : never)) extends infer T_5 ? { [k_1 in keyof T_5]: T_5[k_1] } : never, z.core.$strip>;
|
|
192
193
|
error: z.ZodObject<{
|
|
193
194
|
error: z.ZodEnum<{ [k_3 in (readonly ["invalid_request", "invalid_client", "unauthorized_client", "invalid_scope", ...Grant extends {
|
|
194
195
|
requestErrorCodes: infer ErrorCodes extends readonly string[];
|
|
@@ -307,10 +308,10 @@ declare const deviceAuthorization: <Grant extends DeviceAuthorizationGrant | und
|
|
|
307
308
|
error: z.ZodObject<{
|
|
308
309
|
error: z.ZodEnum<{
|
|
309
310
|
invalid_request: "invalid_request";
|
|
310
|
-
authorization_pending: "authorization_pending";
|
|
311
|
-
slow_down: "slow_down";
|
|
312
311
|
expired_token: "expired_token";
|
|
313
312
|
access_denied: "access_denied";
|
|
313
|
+
authorization_pending: "authorization_pending";
|
|
314
|
+
slow_down: "slow_down";
|
|
314
315
|
invalid_grant: "invalid_grant";
|
|
315
316
|
}>;
|
|
316
317
|
error_description: z.ZodString;
|
|
@@ -414,13 +415,11 @@ declare const deviceAuthorization: <Grant extends DeviceAuthorizationGrant | und
|
|
|
414
415
|
};
|
|
415
416
|
};
|
|
416
417
|
}, {
|
|
417
|
-
user_code: string;
|
|
418
|
-
status: string;
|
|
419
418
|
client_id?: string | undefined;
|
|
420
419
|
scope?: string | undefined;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
420
|
+
user_code: string;
|
|
421
|
+
status: string;
|
|
422
|
+
}>;
|
|
424
423
|
deviceApprove: _$better_call0.StrictEndpoint<"/device/approve", {
|
|
425
424
|
method: "POST";
|
|
426
425
|
body: z.ZodObject<{
|
|
@@ -431,7 +430,6 @@ declare const deviceAuthorization: <Grant extends DeviceAuthorizationGrant | und
|
|
|
431
430
|
invalid_request: "invalid_request";
|
|
432
431
|
expired_token: "expired_token";
|
|
433
432
|
access_denied: "access_denied";
|
|
434
|
-
device_code_already_processed: "device_code_already_processed";
|
|
435
433
|
unauthorized: "unauthorized";
|
|
436
434
|
}>;
|
|
437
435
|
error_description: z.ZodString;
|
|
@@ -526,7 +524,7 @@ declare const deviceAuthorization: <Grant extends DeviceAuthorizationGrant | und
|
|
|
526
524
|
options: {
|
|
527
525
|
grant: DeviceAuthorizationGrant<Readonly<{
|
|
528
526
|
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
529
|
-
}
|
|
527
|
+
}>> | undefined;
|
|
530
528
|
expiresIn: TimeString;
|
|
531
529
|
interval: TimeString;
|
|
532
530
|
deviceCodeLength: number;
|
|
@@ -555,4 +553,4 @@ declare const deviceAuthorization: <Grant extends DeviceAuthorizationGrant | und
|
|
|
555
553
|
};
|
|
556
554
|
};
|
|
557
555
|
//#endregion
|
|
558
|
-
export { DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, type DeviceCode, type DeviceCodeRedemptionAuthorization, type DeviceCodeRedemptionResult, TimeString, deviceAuthorization, deviceAuthorizationOptionsSchema, ms, redeemDeviceCode, sec };
|
|
556
|
+
export { DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, type DeviceCode, type DeviceCodeRedemptionAuthorization, type DeviceCodeRedemptionResult, TimeString, assertDeviceCodePending, buildVerificationUris, decideDeviceCode, deviceAuthorization, deviceAuthorizationOptionsSchema, findDeviceCodeByUserCode, ms, redeemDeviceCode, sec };
|
|
@@ -3,7 +3,7 @@ import { ms } from "../../utils/time.mjs";
|
|
|
3
3
|
import { PACKAGE_VERSION } from "../../version.mjs";
|
|
4
4
|
import { DEVICE_AUTHORIZATION_ERROR_CODES } from "./error-codes.mjs";
|
|
5
5
|
import { schema } from "./schema.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { assertDeviceCodePending, buildVerificationUris, decideDeviceCode, deviceCode, deviceDecisionEndpoint, deviceToken, deviceVerify, findDeviceCodeByUserCode, redeemDeviceCode } from "./routes.mjs";
|
|
7
7
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
//#region src/plugins/device-authorization/index.ts
|
|
@@ -33,19 +33,11 @@ const deviceAuthorizationRequestFields = new Set([
|
|
|
33
33
|
"user_id",
|
|
34
34
|
"scope"
|
|
35
35
|
]);
|
|
36
|
-
const deviceVerificationResponseFields = new Set([
|
|
37
|
-
"user_code",
|
|
38
|
-
"status",
|
|
39
|
-
"client_id",
|
|
40
|
-
"scope"
|
|
41
|
-
]);
|
|
42
36
|
function assertGrantFieldsAreAdditional(grant) {
|
|
43
37
|
const conflictingDeviceCodeFields = Object.keys(grant?.deviceCodeSchemaFields ?? {}).filter((field) => field in schema.deviceCode.fields);
|
|
44
38
|
if (conflictingDeviceCodeFields.length > 0) throw new BetterAuthError(`Device authorization grant fields must be additional and cannot redefine deviceCode fields: ${conflictingDeviceCodeFields.join(", ")}`);
|
|
45
39
|
const conflictingRequestFields = Object.keys(grant?.requestSchemaFields ?? {}).filter((field) => deviceAuthorizationRequestFields.has(field));
|
|
46
40
|
if (conflictingRequestFields.length > 0) throw new BetterAuthError(`Device authorization grant request fields must be additional and cannot redefine request fields: ${conflictingRequestFields.join(", ")}`);
|
|
47
|
-
const conflictingVerificationFields = Object.keys(grant?.verificationOpenAPIProperties ?? {}).filter((field) => deviceVerificationResponseFields.has(field));
|
|
48
|
-
if (conflictingVerificationFields.length > 0) throw new BetterAuthError(`Device authorization grant verification fields must be additional and cannot redefine response fields: ${conflictingVerificationFields.join(", ")}`);
|
|
49
41
|
}
|
|
50
42
|
const deviceAuthorization = (options = {}) => {
|
|
51
43
|
const { grant: configuredGrant, ...deviceAuthorizationOptions } = options;
|
|
@@ -66,8 +58,8 @@ const deviceAuthorization = (options = {}) => {
|
|
|
66
58
|
deviceCode: deviceCode(opts, grant),
|
|
67
59
|
deviceToken: deviceToken(opts, grant),
|
|
68
60
|
deviceVerify: deviceVerify(grant),
|
|
69
|
-
deviceApprove,
|
|
70
|
-
deviceDeny
|
|
61
|
+
deviceApprove: deviceDecisionEndpoint("/device/approve", grant),
|
|
62
|
+
deviceDeny: deviceDecisionEndpoint("/device/deny", grant)
|
|
71
63
|
},
|
|
72
64
|
rateLimit: [{
|
|
73
65
|
pathMatcher(path) {
|
|
@@ -84,4 +76,4 @@ const deviceAuthorization = (options = {}) => {
|
|
|
84
76
|
};
|
|
85
77
|
};
|
|
86
78
|
//#endregion
|
|
87
|
-
export { deviceAuthorization, deviceAuthorizationOptionsSchema, redeemDeviceCode };
|
|
79
|
+
export { assertDeviceCodePending, buildVerificationUris, decideDeviceCode, deviceAuthorization, deviceAuthorizationOptionsSchema, findDeviceCodeByUserCode, redeemDeviceCode };
|
|
@@ -3,6 +3,39 @@ import { User } from "../../types/models.mjs";
|
|
|
3
3
|
import { DeviceCode } from "./schema.mjs";
|
|
4
4
|
import { GenericEndpointContext } from "@better-auth/core";
|
|
5
5
|
//#region src/plugins/device-authorization/routes.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Preserve exact custom user codes, and normalize only default-alphabet codes.
|
|
8
|
+
*/
|
|
9
|
+
declare function findDeviceCodeByUserCode(ctx: GenericEndpointContext, userCode: string): Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
deviceCode: string;
|
|
12
|
+
userCode: string;
|
|
13
|
+
expiresAt: Date;
|
|
14
|
+
status: string;
|
|
15
|
+
userId?: string | undefined;
|
|
16
|
+
lastPolledAt?: Date | undefined;
|
|
17
|
+
pollingInterval?: number | undefined;
|
|
18
|
+
clientId?: string | undefined;
|
|
19
|
+
scope?: string | undefined;
|
|
20
|
+
} | null>;
|
|
21
|
+
/** Throws unless the device code is unexpired and still awaiting a decision. */
|
|
22
|
+
declare function assertDeviceCodePending(deviceCode: DeviceCode): void;
|
|
23
|
+
/**
|
|
24
|
+
* Records `userId`'s approval or denial of a pending, unexpired device code,
|
|
25
|
+
* binding an unclaimed code to that user in the same write. The write is
|
|
26
|
+
* guarded, so a decision that loses a race with another decision, or outlasts
|
|
27
|
+
* the code, fails instead of overwriting it.
|
|
28
|
+
*/
|
|
29
|
+
declare function decideDeviceCode(ctx: GenericEndpointContext, input: {
|
|
30
|
+
deviceCode: DeviceCode;
|
|
31
|
+
userId: string;
|
|
32
|
+
decision: "approved" | "denied";
|
|
33
|
+
/**
|
|
34
|
+
* Fields to persist with the decision, such as grant-owned fields or a
|
|
35
|
+
* scope the user narrowed on a consent page.
|
|
36
|
+
*/
|
|
37
|
+
fields?: Record<string, unknown> | undefined;
|
|
38
|
+
}): Promise<void>;
|
|
6
39
|
interface DeviceCodeRedemptionAuthorization<AuthorizationContext> {
|
|
7
40
|
/** Additional ownership predicate used by the atomic claim. */
|
|
8
41
|
ownershipWhere: Where;
|
|
@@ -28,5 +61,13 @@ declare function redeemDeviceCode<DeviceCodeFields extends Record<string, unknow
|
|
|
28
61
|
authorizeRedemption: (deviceCode: DeviceCode & DeviceCodeFields) => DeviceCodeRedemptionAuthorization<AuthorizationContext> | Promise<DeviceCodeRedemptionAuthorization<AuthorizationContext>>;
|
|
29
62
|
prepareRedemption: (deviceCode: DeviceCode & DeviceCodeFields, authorizationContext: AuthorizationContext) => RedemptionContext | Promise<RedemptionContext>;
|
|
30
63
|
}): Promise<DeviceCodeRedemptionResult<DeviceCodeFields, AuthorizationContext, RedemptionContext>>;
|
|
64
|
+
/**
|
|
65
|
+
* Builds the `verification_uri` and `verification_uri_complete` for a user code,
|
|
66
|
+
* resolving a relative `verificationUri` against the auth base URL.
|
|
67
|
+
*/
|
|
68
|
+
declare const buildVerificationUris: (verificationUri: string | undefined, baseURL: string, userCode: string) => {
|
|
69
|
+
verificationUri: string;
|
|
70
|
+
verificationUriComplete: string;
|
|
71
|
+
};
|
|
31
72
|
//#endregion
|
|
32
|
-
export { DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, redeemDeviceCode };
|
|
73
|
+
export { DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, assertDeviceCodePending, buildVerificationUris, decideDeviceCode, findDeviceCodeByUserCode, redeemDeviceCode };
|
|
@@ -66,6 +66,97 @@ async function findDeviceCodeByUserCode(ctx, userCode) {
|
|
|
66
66
|
if (normalizedUserCode === userCode || !defaultUserCodePattern.test(normalizedUserCode)) return exactDeviceCode;
|
|
67
67
|
return findByUserCode(normalizedUserCode);
|
|
68
68
|
}
|
|
69
|
+
const expiredUserCode = () => new APIError("BAD_REQUEST", {
|
|
70
|
+
error: "expired_token",
|
|
71
|
+
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE.message
|
|
72
|
+
});
|
|
73
|
+
const deviceCodeAlreadyProcessed = () => new APIError("BAD_REQUEST", {
|
|
74
|
+
error: "invalid_request",
|
|
75
|
+
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED.message
|
|
76
|
+
});
|
|
77
|
+
/** Throws unless the device code is unexpired and still awaiting a decision. */
|
|
78
|
+
function assertDeviceCodePending(deviceCode) {
|
|
79
|
+
if (deviceCode.expiresAt < /* @__PURE__ */ new Date()) throw expiredUserCode();
|
|
80
|
+
if (deviceCode.status !== "pending") throw deviceCodeAlreadyProcessed();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Records `userId`'s approval or denial of a pending, unexpired device code,
|
|
84
|
+
* binding an unclaimed code to that user in the same write. The write is
|
|
85
|
+
* guarded, so a decision that loses a race with another decision, or outlasts
|
|
86
|
+
* the code, fails instead of overwriting it.
|
|
87
|
+
*/
|
|
88
|
+
async function decideDeviceCode(ctx, input) {
|
|
89
|
+
const { deviceCode, userId, decision } = input;
|
|
90
|
+
if (deviceCode.userId && deviceCode.userId !== userId) throw new APIError("FORBIDDEN", {
|
|
91
|
+
error: "access_denied",
|
|
92
|
+
error_description: `You are not authorized to ${decision === "approved" ? "approve" : "deny"} this device authorization`
|
|
93
|
+
});
|
|
94
|
+
if (!await ctx.context.adapter.incrementOne({
|
|
95
|
+
model: "deviceCode",
|
|
96
|
+
where: [
|
|
97
|
+
{
|
|
98
|
+
field: "id",
|
|
99
|
+
value: deviceCode.id
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
field: "status",
|
|
103
|
+
value: "pending"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
field: "userId",
|
|
107
|
+
operator: "eq",
|
|
108
|
+
value: deviceCode.userId ?? null
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
field: "expiresAt",
|
|
112
|
+
operator: "gt",
|
|
113
|
+
value: /* @__PURE__ */ new Date()
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
increment: {},
|
|
117
|
+
set: {
|
|
118
|
+
...input.fields,
|
|
119
|
+
userId,
|
|
120
|
+
status: decision
|
|
121
|
+
}
|
|
122
|
+
})) throw deviceCode.expiresAt <= /* @__PURE__ */ new Date() ? expiredUserCode() : deviceCodeAlreadyProcessed();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Finds the code a standalone endpoint (`/device`, `/device/approve`,
|
|
126
|
+
* `/device/deny`) acts on, refusing codes that a grant approves itself.
|
|
127
|
+
*/
|
|
128
|
+
async function findStandaloneDeviceCode(ctx, grant, userCode) {
|
|
129
|
+
const deviceCode = await findDeviceCodeByUserCode(ctx, userCode);
|
|
130
|
+
if (!deviceCode) throw new APIError("BAD_REQUEST", {
|
|
131
|
+
error: "invalid_request",
|
|
132
|
+
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE.message
|
|
133
|
+
});
|
|
134
|
+
await grant?.assertStandaloneUse({
|
|
135
|
+
ctx,
|
|
136
|
+
deviceCode,
|
|
137
|
+
use: "approve"
|
|
138
|
+
});
|
|
139
|
+
return deviceCode;
|
|
140
|
+
}
|
|
141
|
+
/** Handles `/device/approve` and `/device/deny` for a code the user claimed. */
|
|
142
|
+
async function recordDeviceCodeDecision(ctx, grant, userCode, decision) {
|
|
143
|
+
const session = await getSessionFromCtx(ctx);
|
|
144
|
+
if (!session) throw new APIError("UNAUTHORIZED", {
|
|
145
|
+
error: "unauthorized",
|
|
146
|
+
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.AUTHENTICATION_REQUIRED.message
|
|
147
|
+
});
|
|
148
|
+
const deviceCodeRecord = await findStandaloneDeviceCode(ctx, grant, userCode);
|
|
149
|
+
assertDeviceCodePending(deviceCodeRecord);
|
|
150
|
+
if (!deviceCodeRecord.userId) throw new APIError("BAD_REQUEST", {
|
|
151
|
+
error: "invalid_request",
|
|
152
|
+
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_NOT_CLAIMED.message
|
|
153
|
+
});
|
|
154
|
+
await decideDeviceCode(ctx, {
|
|
155
|
+
deviceCode: deviceCodeRecord,
|
|
156
|
+
userId: session.user.id,
|
|
157
|
+
decision
|
|
158
|
+
});
|
|
159
|
+
}
|
|
69
160
|
const deviceCodeBodySchema = z.object({
|
|
70
161
|
client_id: z.string().meta({ description: "The client ID of the application" }),
|
|
71
162
|
user_id: z.string().meta({ description: "The user ID to which the device code should be pre-bound." }).optional(),
|
|
@@ -299,6 +390,19 @@ async function redeemDeviceCode(input) {
|
|
|
299
390
|
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_DEVICE_CODE.message
|
|
300
391
|
});
|
|
301
392
|
const authorization = await input.authorizeRedemption(deviceCodeRecord);
|
|
393
|
+
if (deviceCodeRecord.expiresAt < /* @__PURE__ */ new Date()) {
|
|
394
|
+
await input.ctx.context.adapter.delete({
|
|
395
|
+
model: "deviceCode",
|
|
396
|
+
where: [{
|
|
397
|
+
field: "id",
|
|
398
|
+
value: deviceCodeRecord.id
|
|
399
|
+
}]
|
|
400
|
+
});
|
|
401
|
+
throw new APIError("BAD_REQUEST", {
|
|
402
|
+
error: "expired_token",
|
|
403
|
+
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_DEVICE_CODE.message
|
|
404
|
+
});
|
|
405
|
+
}
|
|
302
406
|
if (deviceCodeRecord.lastPolledAt && deviceCodeRecord.pollingInterval) {
|
|
303
407
|
if (Date.now() - new Date(deviceCodeRecord.lastPolledAt).getTime() < deviceCodeRecord.pollingInterval) throw new APIError("BAD_REQUEST", {
|
|
304
408
|
error: "slow_down",
|
|
@@ -313,19 +417,6 @@ async function redeemDeviceCode(input) {
|
|
|
313
417
|
}],
|
|
314
418
|
update: { lastPolledAt: /* @__PURE__ */ new Date() }
|
|
315
419
|
});
|
|
316
|
-
if (deviceCodeRecord.expiresAt < /* @__PURE__ */ new Date()) {
|
|
317
|
-
await input.ctx.context.adapter.delete({
|
|
318
|
-
model: "deviceCode",
|
|
319
|
-
where: [{
|
|
320
|
-
field: "id",
|
|
321
|
-
value: deviceCodeRecord.id
|
|
322
|
-
}]
|
|
323
|
-
});
|
|
324
|
-
throw new APIError("BAD_REQUEST", {
|
|
325
|
-
error: "expired_token",
|
|
326
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_DEVICE_CODE.message
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
420
|
if (deviceCodeRecord.status === "pending") throw new APIError("BAD_REQUEST", {
|
|
330
421
|
error: "authorization_pending",
|
|
331
422
|
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.AUTHORIZATION_PENDING.message
|
|
@@ -438,9 +529,10 @@ Follow [rfc8628#section-3.4](https://datatracker.ietf.org/doc/html/rfc8628#secti
|
|
|
438
529
|
error: "invalid_grant",
|
|
439
530
|
error_description: "Client ID mismatch"
|
|
440
531
|
});
|
|
441
|
-
await grant?.
|
|
532
|
+
await grant?.assertStandaloneUse({
|
|
442
533
|
ctx,
|
|
443
|
-
deviceCode: deviceCodeRecord
|
|
534
|
+
deviceCode: deviceCodeRecord,
|
|
535
|
+
use: "redeem"
|
|
444
536
|
});
|
|
445
537
|
return {
|
|
446
538
|
ownershipWhere: {
|
|
@@ -508,23 +600,15 @@ const deviceVerify = (grant) => createAuthEndpoint("/device", {
|
|
|
508
600
|
scope: {
|
|
509
601
|
type: "string",
|
|
510
602
|
description: "The requested scopes, returned only to the authenticated user who owns this request"
|
|
511
|
-
}
|
|
512
|
-
...grant?.verificationOpenAPIProperties
|
|
603
|
+
}
|
|
513
604
|
}
|
|
514
605
|
} } }
|
|
515
606
|
} }
|
|
516
607
|
} }
|
|
517
608
|
}, async (ctx) => {
|
|
518
609
|
const { user_code } = ctx.query;
|
|
519
|
-
const deviceCodeRecord = await
|
|
520
|
-
if (
|
|
521
|
-
error: "invalid_request",
|
|
522
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE.message
|
|
523
|
-
});
|
|
524
|
-
if (deviceCodeRecord.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
|
|
525
|
-
error: "expired_token",
|
|
526
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE.message
|
|
527
|
-
});
|
|
610
|
+
const deviceCodeRecord = await findStandaloneDeviceCode(ctx, grant, user_code);
|
|
611
|
+
if (deviceCodeRecord.expiresAt < /* @__PURE__ */ new Date()) throw expiredUserCode();
|
|
528
612
|
const session = await getSessionFromCtx(ctx);
|
|
529
613
|
if (session?.user?.id && !deviceCodeRecord.userId && deviceCodeRecord.status === "pending") {
|
|
530
614
|
if (await ctx.context.adapter.incrementOne({
|
|
@@ -549,149 +633,50 @@ const deviceVerify = (grant) => createAuthEndpoint("/device", {
|
|
|
549
633
|
})) deviceCodeRecord.userId = session.user.id;
|
|
550
634
|
}
|
|
551
635
|
const canReviewRequest = session?.user.id !== void 0 && deviceCodeRecord.userId === session.user.id;
|
|
552
|
-
|
|
553
|
-
const response = {
|
|
554
|
-
...canReviewRequest ? grantContext : void 0,
|
|
636
|
+
return ctx.json({
|
|
555
637
|
user_code,
|
|
556
638
|
status: deviceCodeRecord.status,
|
|
557
639
|
...canReviewRequest ? {
|
|
558
640
|
client_id: deviceCodeRecord.clientId,
|
|
559
641
|
scope: deviceCodeRecord.scope
|
|
560
642
|
} : {}
|
|
561
|
-
};
|
|
562
|
-
return ctx.json(response);
|
|
563
|
-
});
|
|
564
|
-
const deviceApprove = createAuthEndpoint("/device/approve", {
|
|
565
|
-
method: "POST",
|
|
566
|
-
body: z.object({ userCode: z.string().meta({ description: "The user code to approve" }) }),
|
|
567
|
-
error: z.object({
|
|
568
|
-
error: z.enum([
|
|
569
|
-
"invalid_request",
|
|
570
|
-
"expired_token",
|
|
571
|
-
"device_code_already_processed",
|
|
572
|
-
"unauthorized",
|
|
573
|
-
"access_denied"
|
|
574
|
-
]).meta({ description: "Error code" }),
|
|
575
|
-
error_description: z.string().meta({ description: "Detailed error description" })
|
|
576
|
-
}),
|
|
577
|
-
requireHeaders: true,
|
|
578
|
-
metadata: { openapi: {
|
|
579
|
-
description: "Approve device authorization",
|
|
580
|
-
responses: { 200: {
|
|
581
|
-
description: "Success",
|
|
582
|
-
content: { "application/json": { schema: {
|
|
583
|
-
type: "object",
|
|
584
|
-
properties: { success: { type: "boolean" } }
|
|
585
|
-
} } }
|
|
586
|
-
} }
|
|
587
|
-
} }
|
|
588
|
-
}, async (ctx) => {
|
|
589
|
-
const session = await getSessionFromCtx(ctx);
|
|
590
|
-
if (!session) throw new APIError("UNAUTHORIZED", {
|
|
591
|
-
error: "unauthorized",
|
|
592
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.AUTHENTICATION_REQUIRED.message
|
|
593
|
-
});
|
|
594
|
-
const { userCode } = ctx.body;
|
|
595
|
-
const deviceCodeRecord = await findDeviceCodeByUserCode(ctx, userCode);
|
|
596
|
-
if (!deviceCodeRecord) throw new APIError("BAD_REQUEST", {
|
|
597
|
-
error: "invalid_request",
|
|
598
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE.message
|
|
599
643
|
});
|
|
600
|
-
if (deviceCodeRecord.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
|
|
601
|
-
error: "expired_token",
|
|
602
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE.message
|
|
603
|
-
});
|
|
604
|
-
if (deviceCodeRecord.status !== "pending") throw new APIError("BAD_REQUEST", {
|
|
605
|
-
error: "invalid_request",
|
|
606
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED.message
|
|
607
|
-
});
|
|
608
|
-
if (!deviceCodeRecord.userId) throw new APIError("BAD_REQUEST", {
|
|
609
|
-
error: "invalid_request",
|
|
610
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_NOT_CLAIMED.message
|
|
611
|
-
});
|
|
612
|
-
if (deviceCodeRecord.userId !== session.user.id) throw new APIError("FORBIDDEN", {
|
|
613
|
-
error: "access_denied",
|
|
614
|
-
error_description: "You are not authorized to approve this device authorization"
|
|
615
|
-
});
|
|
616
|
-
await ctx.context.adapter.update({
|
|
617
|
-
model: "deviceCode",
|
|
618
|
-
where: [{
|
|
619
|
-
field: "id",
|
|
620
|
-
value: deviceCodeRecord.id
|
|
621
|
-
}],
|
|
622
|
-
update: {
|
|
623
|
-
status: "approved",
|
|
624
|
-
userId: session.user.id
|
|
625
|
-
}
|
|
626
|
-
});
|
|
627
|
-
return ctx.json({ success: true });
|
|
628
644
|
});
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
645
|
+
/** `/device/approve` and `/device/deny`, which differ only in the decision. */
|
|
646
|
+
const deviceDecisionEndpoint = (path, grant) => {
|
|
647
|
+
const decision = path === "/device/approve" ? "approved" : "denied";
|
|
648
|
+
const action = decision === "approved" ? "Approve" : "Deny";
|
|
649
|
+
return createAuthEndpoint(path, {
|
|
650
|
+
method: "POST",
|
|
651
|
+
body: z.object({ userCode: z.string().meta({ description: `The user code to ${action.toLowerCase()}` }) }),
|
|
652
|
+
error: z.object({
|
|
653
|
+
error: z.enum([
|
|
654
|
+
"invalid_request",
|
|
655
|
+
"expired_token",
|
|
656
|
+
"unauthorized",
|
|
657
|
+
"access_denied"
|
|
658
|
+
]).meta({ description: "Error code" }),
|
|
659
|
+
error_description: z.string().meta({ description: "Detailed error description" })
|
|
660
|
+
}),
|
|
661
|
+
requireHeaders: true,
|
|
662
|
+
metadata: { openapi: {
|
|
663
|
+
description: `${action} device authorization`,
|
|
664
|
+
responses: { 200: {
|
|
665
|
+
description: "Success",
|
|
666
|
+
content: { "application/json": { schema: {
|
|
667
|
+
type: "object",
|
|
668
|
+
properties: { success: { type: "boolean" } }
|
|
669
|
+
} } }
|
|
670
|
+
} }
|
|
650
671
|
} }
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (!session) throw new APIError("UNAUTHORIZED", {
|
|
655
|
-
error: "unauthorized",
|
|
656
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.AUTHENTICATION_REQUIRED.message
|
|
657
|
-
});
|
|
658
|
-
const { userCode } = ctx.body;
|
|
659
|
-
const deviceCodeRecord = await findDeviceCodeByUserCode(ctx, userCode);
|
|
660
|
-
if (!deviceCodeRecord) throw new APIError("BAD_REQUEST", {
|
|
661
|
-
error: "invalid_request",
|
|
662
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.INVALID_USER_CODE.message
|
|
663
|
-
});
|
|
664
|
-
if (deviceCodeRecord.expiresAt < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
|
|
665
|
-
error: "expired_token",
|
|
666
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.EXPIRED_USER_CODE.message
|
|
667
|
-
});
|
|
668
|
-
if (deviceCodeRecord.status !== "pending") throw new APIError("BAD_REQUEST", {
|
|
669
|
-
error: "invalid_request",
|
|
670
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_ALREADY_PROCESSED.message
|
|
671
|
-
});
|
|
672
|
-
if (!deviceCodeRecord.userId) throw new APIError("BAD_REQUEST", {
|
|
673
|
-
error: "invalid_request",
|
|
674
|
-
error_description: DEVICE_AUTHORIZATION_ERROR_CODES.DEVICE_CODE_NOT_CLAIMED.message
|
|
675
|
-
});
|
|
676
|
-
if (deviceCodeRecord.userId !== session.user.id) throw new APIError("FORBIDDEN", {
|
|
677
|
-
error: "access_denied",
|
|
678
|
-
error_description: "You are not authorized to deny this device authorization"
|
|
679
|
-
});
|
|
680
|
-
await ctx.context.adapter.update({
|
|
681
|
-
model: "deviceCode",
|
|
682
|
-
where: [{
|
|
683
|
-
field: "id",
|
|
684
|
-
value: deviceCodeRecord.id
|
|
685
|
-
}],
|
|
686
|
-
update: {
|
|
687
|
-
status: "denied",
|
|
688
|
-
userId: session.user.id
|
|
689
|
-
}
|
|
672
|
+
}, async (ctx) => {
|
|
673
|
+
await recordDeviceCodeDecision(ctx, grant, ctx.body.userCode, decision);
|
|
674
|
+
return ctx.json({ success: true });
|
|
690
675
|
});
|
|
691
|
-
|
|
692
|
-
});
|
|
676
|
+
};
|
|
693
677
|
/**
|
|
694
|
-
*
|
|
678
|
+
* Builds the `verification_uri` and `verification_uri_complete` for a user code,
|
|
679
|
+
* resolving a relative `verificationUri` against the auth base URL.
|
|
695
680
|
*/
|
|
696
681
|
const buildVerificationUris = (verificationUri, baseURL, userCode) => {
|
|
697
682
|
const uri = verificationUri || "/device";
|
|
@@ -722,4 +707,4 @@ const defaultGenerateUserCode = (length) => {
|
|
|
722
707
|
return Array.from(crypto.getRandomValues(chars)).map((byte) => defaultCharset[byte % 32]).join("");
|
|
723
708
|
};
|
|
724
709
|
//#endregion
|
|
725
|
-
export {
|
|
710
|
+
export { assertDeviceCodePending, buildVerificationUris, decideDeviceCode, deviceCode, deviceDecisionEndpoint, deviceToken, deviceVerify, findDeviceCodeByUserCode, redeemDeviceCode };
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -15,7 +15,7 @@ import { captcha } from "./captcha/index.mjs";
|
|
|
15
15
|
import { CustomSessionPluginOptions, customSession } from "./custom-session/index.mjs";
|
|
16
16
|
import { TimeString, ms, sec } from "../utils/time.mjs";
|
|
17
17
|
import { DeviceCode } from "./device-authorization/schema.mjs";
|
|
18
|
-
import { DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, redeemDeviceCode } from "./device-authorization/routes.mjs";
|
|
18
|
+
import { DeviceCodeRedemptionAuthorization, DeviceCodeRedemptionResult, assertDeviceCodePending, buildVerificationUris, decideDeviceCode, findDeviceCodeByUserCode, redeemDeviceCode } from "./device-authorization/routes.mjs";
|
|
19
19
|
import { DeviceAuthorizationGrant, DeviceAuthorizationGrantAuthorization, DeviceAuthorizationOptions, DeviceAuthorizationPluginOptions, DeviceAuthorizationRequest, deviceAuthorization, deviceAuthorizationOptionsSchema } from "./device-authorization/index.mjs";
|
|
20
20
|
import { EmailOTPOptions } from "./email-otp/types.mjs";
|
|
21
21
|
import { emailOTP } from "./email-otp/index.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, BotIdVerification, 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, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, ValidateRequestContext, VercelBotIdOptions, 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 };
|
|
69
|
+
export { AccessControl, AdminOptions, AnonymousOptions, AnonymousSession, ArrayElement, Auth0Options, AuthorizeResponse, BackupCodeOptions, BaseCaptchaOptions, BaseOAuthProviderOptions, BearerOptions, BotIdVerification, 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, TestAuthOptions, TestCookie, TestHelpers, TestUtilsOptions, TimeString, TwoFactorOptions, TwoFactorProvider, TwoFactorTable, USERNAME_ERROR_CODES, UserWithAnonymous, UserWithPhoneNumber, UserWithRole, UserWithTwoFactor, UsernameOptions, UsernamePlugin, UsernamePluginWithoutDisplayUsername, ValidateRequestContext, VercelBotIdOptions, YandexOptions, admin, anonymous, assertDeviceCodePending, auth0, backupCode2fa, bearer, buildVerificationUris, captcha, createAccessControl, createJwk, customSession, decideDeviceCode, defaultRolesSchema, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, encodeBackupCodes, findDeviceCodeByUserCode, 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
|
@@ -11,7 +11,7 @@ import { anonymous } from "./anonymous/index.mjs";
|
|
|
11
11
|
import { bearer } from "./bearer/index.mjs";
|
|
12
12
|
import { captcha } from "./captcha/index.mjs";
|
|
13
13
|
import { customSession } from "./custom-session/index.mjs";
|
|
14
|
-
import { redeemDeviceCode } from "./device-authorization/routes.mjs";
|
|
14
|
+
import { assertDeviceCodePending, buildVerificationUris, decideDeviceCode, findDeviceCodeByUserCode, redeemDeviceCode } from "./device-authorization/routes.mjs";
|
|
15
15
|
import { deviceAuthorization, deviceAuthorizationOptionsSchema } from "./device-authorization/index.mjs";
|
|
16
16
|
import { emailOTP } from "./email-otp/index.mjs";
|
|
17
17
|
import { auth0 } from "./generic-oauth/providers/auth0.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, 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 };
|
|
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, assertDeviceCodePending, auth0, bearer, buildVerificationUris, captcha, createAccessControl, createJwk, customSession, decideDeviceCode, deviceAuthorization, deviceAuthorizationOptionsSchema, emailOTP, findDeviceCodeByUserCode, 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 };
|
|
@@ -9,6 +9,7 @@ const lastLoginMethod = (userConfig) => {
|
|
|
9
9
|
const path = ctx.path;
|
|
10
10
|
if (!path) return null;
|
|
11
11
|
if (path.startsWith("/callback/")) return ctx.params?.id || path.split("/").pop();
|
|
12
|
+
if (path === "/one-tap/callback") return "google";
|
|
12
13
|
if (path === "/sign-in/email" || path === "/sign-up/email") return "email";
|
|
13
14
|
if (path.includes("siwe")) return "siwe";
|
|
14
15
|
if (path.includes("/passkey/verify-authentication")) return "passkey";
|
|
@@ -81,7 +81,7 @@ interface GoogleOneTapOptions {
|
|
|
81
81
|
/**
|
|
82
82
|
* Cancel the flow when the user taps outside the prompt
|
|
83
83
|
*
|
|
84
|
-
* Note:
|
|
84
|
+
* Note: This may have no effect, as FedCM lets the browser manage dismissal
|
|
85
85
|
*/
|
|
86
86
|
cancelOnTapOutside?: boolean | undefined;
|
|
87
87
|
/**
|
|
@@ -119,7 +119,9 @@ interface GoogleOneTapOptions {
|
|
|
119
119
|
*/
|
|
120
120
|
maxAttempts?: number;
|
|
121
121
|
/**
|
|
122
|
-
* Whether to
|
|
122
|
+
* Whether to call `navigator.credentials.preventSilentAccess()` on
|
|
123
|
+
* sign-out to clear the browser's FedCM credential state. FedCM for
|
|
124
|
+
* the prompt itself is managed by Google and cannot be disabled.
|
|
123
125
|
*
|
|
124
126
|
* @see {@link https://developer.chrome.com/docs/identity/fedcm/overview}
|
|
125
127
|
* @default true
|
|
@@ -9,10 +9,7 @@ function isFedCMSupported() {
|
|
|
9
9
|
* Reasons that should NOT trigger a retry.
|
|
10
10
|
* @see https://developers.google.com/identity/gsi/web/reference/js-reference
|
|
11
11
|
*/
|
|
12
|
-
const noRetryReasons = {
|
|
13
|
-
dismissed: ["credential_returned", "cancel_called"],
|
|
14
|
-
skipped: ["user_cancel", "tap_outside"]
|
|
15
|
-
};
|
|
12
|
+
const noRetryReasons = { dismissed: ["credential_returned", "cancel_called"] };
|
|
16
13
|
const oneTapClient = (options) => {
|
|
17
14
|
return {
|
|
18
15
|
id: "one-tap",
|
|
@@ -58,9 +55,9 @@ const oneTapClient = (options) => {
|
|
|
58
55
|
if (isSafeUrlScheme(target)) window.location.href = target;
|
|
59
56
|
}
|
|
60
57
|
}
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
58
|
+
const autoSelect = opts?.autoSelect ?? options.autoSelect;
|
|
59
|
+
const cancelOnTapOutside = opts?.cancelOnTapOutside ?? options.cancelOnTapOutside;
|
|
60
|
+
const contextValue = opts?.context ?? options.context ?? "signin";
|
|
64
61
|
window.google?.accounts.id.initialize({
|
|
65
62
|
client_id: options.clientId,
|
|
66
63
|
callback: async (response) => {
|
|
@@ -76,7 +73,6 @@ const oneTapClient = (options) => {
|
|
|
76
73
|
ux_mode: opts?.uxMode || "popup",
|
|
77
74
|
nonce: opts?.nonce,
|
|
78
75
|
itp_support: true,
|
|
79
|
-
use_fedcm_for_prompt: useFedCM,
|
|
80
76
|
...options.additionalOptions
|
|
81
77
|
});
|
|
82
78
|
window.google?.accounts.id.renderButton(container, opts.button.config ?? { type: "icon" });
|
|
@@ -97,8 +93,9 @@ const oneTapClient = (options) => {
|
|
|
97
93
|
if (isSafeUrlScheme(target)) window.location.href = target;
|
|
98
94
|
}
|
|
99
95
|
}
|
|
100
|
-
const
|
|
101
|
-
const
|
|
96
|
+
const autoSelect = opts?.autoSelect ?? options.autoSelect;
|
|
97
|
+
const cancelOnTapOutside = opts?.cancelOnTapOutside ?? options.cancelOnTapOutside;
|
|
98
|
+
const contextValue = opts?.context ?? options.context ?? "signin";
|
|
102
99
|
isRequestInProgress = true;
|
|
103
100
|
try {
|
|
104
101
|
await loadGoogleScript();
|
|
@@ -106,7 +103,6 @@ const oneTapClient = (options) => {
|
|
|
106
103
|
let isResolved = false;
|
|
107
104
|
const baseDelay = options.promptOptions?.baseDelay ?? 1e3;
|
|
108
105
|
const maxAttempts = options.promptOptions?.maxAttempts ?? 5;
|
|
109
|
-
const useFedCM = options.promptOptions?.fedCM !== false;
|
|
110
106
|
window.google?.accounts.id.initialize({
|
|
111
107
|
client_id: options.clientId,
|
|
112
108
|
callback: async (response) => {
|
|
@@ -128,7 +124,6 @@ const oneTapClient = (options) => {
|
|
|
128
124
|
* @see {@link https://developers.google.com/identity/gsi/web/guides/overview}
|
|
129
125
|
*/
|
|
130
126
|
itp_support: true,
|
|
131
|
-
use_fedcm_for_prompt: useFedCM,
|
|
132
127
|
...options.additionalOptions
|
|
133
128
|
});
|
|
134
129
|
const handlePrompt = (attempt) => {
|
|
@@ -150,19 +145,8 @@ const oneTapClient = (options) => {
|
|
|
150
145
|
resolve();
|
|
151
146
|
}
|
|
152
147
|
} else if (notification.isSkippedMoment?.()) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
opts?.onPromptNotification?.(notification);
|
|
156
|
-
resolve();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (attempt < maxAttempts) {
|
|
160
|
-
const delay = Math.pow(2, attempt) * baseDelay;
|
|
161
|
-
setTimeout(() => handlePrompt(attempt + 1), delay);
|
|
162
|
-
} else {
|
|
163
|
-
opts?.onPromptNotification?.(notification);
|
|
164
|
-
resolve();
|
|
165
|
-
}
|
|
148
|
+
opts?.onPromptNotification?.(notification);
|
|
149
|
+
resolve();
|
|
166
150
|
} else if (notification.isNotDisplayed?.()) {
|
|
167
151
|
opts?.onPromptNotification?.(notification);
|
|
168
152
|
resolve();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { parseUserOutput } from "../../db/schema.mjs";
|
|
2
2
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
3
|
+
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
4
|
+
import { toOAuthProfileRecord } from "../../oauth2/account-key.mjs";
|
|
3
5
|
import { OAUTH_CALLBACK_ERROR_CODES } from "../../oauth2/errors.mjs";
|
|
4
6
|
import { handleOAuthUserInfo } from "../../oauth2/link-account.mjs";
|
|
5
7
|
import { APIError as APIError$1 } from "../../api/index.mjs";
|
|
@@ -58,16 +60,32 @@ const oneTap = (options) => ({
|
|
|
58
60
|
ctx.context.logger.error(`Google One Tap sign-in rejected: id token hosted domain (hd) "${payload.hd ?? "<missing>"}" does not satisfy the configured "hd" option "${configuredHostedDomain}".`);
|
|
59
61
|
throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
|
|
60
62
|
}
|
|
61
|
-
const
|
|
63
|
+
const googleOAuthProvider = await getAwaitableValue(ctx.context.socialProviders, { value: "google" });
|
|
64
|
+
const providerResult = googleOAuthProvider ? await googleOAuthProvider.getUserInfo({ idToken }) : {
|
|
65
|
+
user: {
|
|
66
|
+
name: payload.name,
|
|
67
|
+
email: payload.email,
|
|
68
|
+
image: payload.picture,
|
|
69
|
+
emailVerified: toBoolean(payload.email_verified)
|
|
70
|
+
},
|
|
71
|
+
data: payload
|
|
72
|
+
};
|
|
73
|
+
if (!providerResult?.user) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
|
|
74
|
+
const providerUser = providerResult.user;
|
|
75
|
+
const { sub } = payload;
|
|
76
|
+
const { email: rawEmail, name, image } = providerUser;
|
|
62
77
|
if (typeof rawEmail !== "string" || !rawEmail) throw new APIError$1("BAD_REQUEST", { message: "Email not available in token" });
|
|
63
78
|
if (typeof sub !== "string" || !sub) throw new APIError$1("BAD_REQUEST", { message: "invalid id token" });
|
|
79
|
+
const email = rawEmail.toLowerCase();
|
|
80
|
+
const emailVerified = toBoolean(providerUser.emailVerified);
|
|
64
81
|
const result = await handleOAuthUserInfo(ctx, {
|
|
65
82
|
userInfo: {
|
|
83
|
+
...providerUser,
|
|
66
84
|
id: sub,
|
|
67
|
-
email
|
|
68
|
-
emailVerified
|
|
85
|
+
email,
|
|
86
|
+
emailVerified,
|
|
69
87
|
name: typeof name === "string" ? name : "",
|
|
70
|
-
image: typeof
|
|
88
|
+
image: typeof image === "string" ? image : void 0
|
|
71
89
|
},
|
|
72
90
|
account: {
|
|
73
91
|
providerId: "google",
|
|
@@ -76,11 +94,12 @@ const oneTap = (options) => ({
|
|
|
76
94
|
scope: "openid,profile,email"
|
|
77
95
|
},
|
|
78
96
|
disableSignUp: options?.disableSignup || googleProvider?.disableSignUp,
|
|
97
|
+
overrideUserInfo: googleProvider?.overrideUserInfoOnSignIn,
|
|
79
98
|
source: {
|
|
80
99
|
method: "oauth",
|
|
81
100
|
oauth: {
|
|
82
101
|
providerId: "google",
|
|
83
|
-
profile:
|
|
102
|
+
profile: toOAuthProfileRecord(providerResult.data)
|
|
84
103
|
}
|
|
85
104
|
}
|
|
86
105
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0-beta.0",
|
|
4
4
|
"description": "The most comprehensive authentication framework for TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -221,6 +221,11 @@
|
|
|
221
221
|
"types": "./dist/plugins/bearer/index.d.mts",
|
|
222
222
|
"default": "./dist/plugins/bearer/index.mjs"
|
|
223
223
|
},
|
|
224
|
+
"./plugins/captcha": {
|
|
225
|
+
"dev-source": "./src/plugins/captcha/index.ts",
|
|
226
|
+
"types": "./dist/plugins/captcha/index.d.mts",
|
|
227
|
+
"default": "./dist/plugins/captcha/index.mjs"
|
|
228
|
+
},
|
|
224
229
|
"./plugins/custom-session": {
|
|
225
230
|
"dev-source": "./src/plugins/custom-session/index.ts",
|
|
226
231
|
"types": "./dist/plugins/custom-session/index.d.mts",
|
|
@@ -276,6 +281,16 @@
|
|
|
276
281
|
"types": "./dist/plugins/one-time-token/index.d.mts",
|
|
277
282
|
"default": "./dist/plugins/one-time-token/index.mjs"
|
|
278
283
|
},
|
|
284
|
+
"./plugins/one-tap": {
|
|
285
|
+
"dev-source": "./src/plugins/one-tap/index.ts",
|
|
286
|
+
"types": "./dist/plugins/one-tap/index.d.mts",
|
|
287
|
+
"default": "./dist/plugins/one-tap/index.mjs"
|
|
288
|
+
},
|
|
289
|
+
"./plugins/open-api": {
|
|
290
|
+
"dev-source": "./src/plugins/open-api/index.ts",
|
|
291
|
+
"types": "./dist/plugins/open-api/index.d.mts",
|
|
292
|
+
"default": "./dist/plugins/open-api/index.mjs"
|
|
293
|
+
},
|
|
279
294
|
"./plugins/phone-number": {
|
|
280
295
|
"dev-source": "./src/plugins/phone-number/index.ts",
|
|
281
296
|
"types": "./dist/plugins/phone-number/index.d.mts",
|
|
@@ -355,6 +370,9 @@
|
|
|
355
370
|
"api": [
|
|
356
371
|
"./dist/api/index.d.mts"
|
|
357
372
|
],
|
|
373
|
+
"test": [
|
|
374
|
+
"./dist/test-utils/index.d.mts"
|
|
375
|
+
],
|
|
358
376
|
"db": [
|
|
359
377
|
"./dist/db/index.d.mts"
|
|
360
378
|
],
|
|
@@ -415,6 +433,9 @@
|
|
|
415
433
|
"plugins/bearer": [
|
|
416
434
|
"./dist/plugins/bearer/index.d.mts"
|
|
417
435
|
],
|
|
436
|
+
"plugins/captcha": [
|
|
437
|
+
"./dist/plugins/captcha/index.d.mts"
|
|
438
|
+
],
|
|
418
439
|
"plugins/custom-session": [
|
|
419
440
|
"./dist/plugins/custom-session/index.d.mts"
|
|
420
441
|
],
|
|
@@ -433,12 +454,21 @@
|
|
|
433
454
|
"plugins/one-time-token": [
|
|
434
455
|
"./dist/plugins/one-time-token/index.d.mts"
|
|
435
456
|
],
|
|
457
|
+
"plugins/one-tap": [
|
|
458
|
+
"./dist/plugins/one-tap/index.d.mts"
|
|
459
|
+
],
|
|
460
|
+
"plugins/open-api": [
|
|
461
|
+
"./dist/plugins/open-api/index.d.mts"
|
|
462
|
+
],
|
|
436
463
|
"plugins/jwt": [
|
|
437
464
|
"./dist/plugins/jwt/index.d.mts"
|
|
438
465
|
],
|
|
439
466
|
"plugins/magic-link": [
|
|
440
467
|
"./dist/plugins/magic-link/index.d.mts"
|
|
441
468
|
],
|
|
469
|
+
"plugins/multi-session": [
|
|
470
|
+
"./dist/plugins/multi-session/index.d.mts"
|
|
471
|
+
],
|
|
442
472
|
"plugins/organization": [
|
|
443
473
|
"./dist/plugins/organization/index.d.mts"
|
|
444
474
|
],
|
|
@@ -473,13 +503,13 @@
|
|
|
473
503
|
"kysely": "^0.28.17 || ^0.29.0",
|
|
474
504
|
"nanostores": "^1.3.0",
|
|
475
505
|
"zod": "^4.5.4",
|
|
476
|
-
"@better-auth/core": "1.
|
|
477
|
-
"@better-auth/drizzle-adapter": "1.
|
|
478
|
-
"@better-auth/kysely-adapter": "1.
|
|
479
|
-
"@better-auth/memory-adapter": "1.
|
|
480
|
-
"@better-auth/mongo-adapter": "1.
|
|
481
|
-
"@better-auth/prisma-adapter": "1.
|
|
482
|
-
"@better-auth/telemetry": "1.
|
|
506
|
+
"@better-auth/core": "1.8.0-beta.0",
|
|
507
|
+
"@better-auth/drizzle-adapter": "1.8.0-beta.0",
|
|
508
|
+
"@better-auth/kysely-adapter": "1.8.0-beta.0",
|
|
509
|
+
"@better-auth/memory-adapter": "1.8.0-beta.0",
|
|
510
|
+
"@better-auth/mongo-adapter": "1.8.0-beta.0",
|
|
511
|
+
"@better-auth/prisma-adapter": "1.8.0-beta.0",
|
|
512
|
+
"@better-auth/telemetry": "1.8.0-beta.0"
|
|
483
513
|
},
|
|
484
514
|
"devDependencies": {
|
|
485
515
|
"@lynx-js/react": "^0.121.2",
|