mirror of
https://github.com/documenso/documenso.git
synced 2025-11-21 20:21:38 +10:00
166 lines
5.0 KiB
TypeScript
166 lines
5.0 KiB
TypeScript
import type { BetterAuthPlugin } from 'better-auth';
|
|
import { createAuthEndpoint, createAuthMiddleware } from 'better-auth/plugins';
|
|
|
|
export const passkeyPlugin = () =>
|
|
({
|
|
id: 'passkeyPlugin',
|
|
schema: {
|
|
user: {
|
|
fields: {
|
|
// twoFactorEnabled: {
|
|
// type: 'boolean',
|
|
// required: false,
|
|
// },
|
|
// twoFactorBackupCodes: {
|
|
// type: 'string',
|
|
// required: false,
|
|
// },
|
|
// twoFactorSecret: {
|
|
// type: 'string',
|
|
// required: false,
|
|
// },
|
|
// birthday: {
|
|
// type: 'date', // string, number, boolean, date
|
|
// required: true, // if the field should be required on a new record. (default: false)
|
|
// unique: false, // if the field should be unique. (default: false)
|
|
// reference: null, // if the field is a reference to another table. (default: null)
|
|
// },
|
|
},
|
|
},
|
|
},
|
|
endpoints: {
|
|
authorize: createAuthEndpoint(
|
|
'/passkey/authorize',
|
|
{
|
|
method: 'POST',
|
|
// use: [],
|
|
},
|
|
async (ctx) => {
|
|
const csrfToken = credentials?.csrfToken;
|
|
|
|
if (typeof csrfToken !== 'string' || csrfToken.length === 0) {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
|
}
|
|
|
|
let requestBodyCrediential: TAuthenticationResponseJSONSchema | null = null;
|
|
|
|
try {
|
|
const parsedBodyCredential = JSON.parse(req.body?.credential);
|
|
requestBodyCrediential = ZAuthenticationResponseJSONSchema.parse(parsedBodyCredential);
|
|
} catch {
|
|
throw new AppError(AppErrorCode.INVALID_REQUEST);
|
|
}
|
|
|
|
const challengeToken = await prisma.anonymousVerificationToken
|
|
.delete({
|
|
where: {
|
|
id: csrfToken,
|
|
},
|
|
})
|
|
.catch(() => null);
|
|
|
|
if (!challengeToken) {
|
|
return null;
|
|
}
|
|
|
|
if (challengeToken.expiresAt < new Date()) {
|
|
throw new AppError(AppErrorCode.EXPIRED_CODE);
|
|
}
|
|
|
|
const passkey = await prisma.passkey.findFirst({
|
|
where: {
|
|
credentialId: Buffer.from(requestBodyCrediential.id, 'base64'),
|
|
},
|
|
include: {
|
|
User: {
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
name: true,
|
|
emailVerified: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!passkey) {
|
|
throw new AppError(AppErrorCode.NOT_SETUP);
|
|
}
|
|
|
|
const user = passkey.User;
|
|
|
|
const { rpId, origin } = getAuthenticatorOptions();
|
|
|
|
const verification = await verifyAuthenticationResponse({
|
|
response: requestBodyCrediential,
|
|
expectedChallenge: challengeToken.token,
|
|
expectedOrigin: origin,
|
|
expectedRPID: rpId,
|
|
authenticator: {
|
|
credentialID: new Uint8Array(Array.from(passkey.credentialId)),
|
|
credentialPublicKey: new Uint8Array(passkey.credentialPublicKey),
|
|
counter: Number(passkey.counter),
|
|
},
|
|
}).catch(() => null);
|
|
|
|
const requestMetadata = extractNextAuthRequestMetadata(req);
|
|
|
|
if (!verification?.verified) {
|
|
await prisma.userSecurityAuditLog.create({
|
|
data: {
|
|
userId: user.id,
|
|
ipAddress: requestMetadata.ipAddress,
|
|
userAgent: requestMetadata.userAgent,
|
|
type: UserSecurityAuditLogType.SIGN_IN_PASSKEY_FAIL,
|
|
},
|
|
});
|
|
|
|
return null;
|
|
}
|
|
|
|
await prisma.passkey.update({
|
|
where: {
|
|
id: passkey.id,
|
|
},
|
|
data: {
|
|
lastUsedAt: new Date(),
|
|
counter: verification.authenticationInfo.newCounter,
|
|
},
|
|
});
|
|
|
|
return {
|
|
id: Number(user.id),
|
|
email: user.email,
|
|
name: user.name,
|
|
emailVerified: user.emailVerified?.toISOString() ?? null,
|
|
} satisfies User;
|
|
},
|
|
),
|
|
},
|
|
hooks: {
|
|
before: [
|
|
{
|
|
matcher: (context) => context.path.startsWith('/sign-in/email'),
|
|
handler: createAuthMiddleware(async (ctx) => {
|
|
console.log('here...');
|
|
|
|
const { birthday } = ctx.body;
|
|
|
|
if ((!birthday) instanceof Date) {
|
|
throw new APIError('BAD_REQUEST', { message: 'Birthday must be of type Date.' });
|
|
}
|
|
|
|
const today = new Date();
|
|
const fiveYearsAgo = new Date(today.setFullYear(today.getFullYear() - 5));
|
|
|
|
if (birthday >= fiveYearsAgo) {
|
|
throw new APIError('BAD_REQUEST', { message: 'User must be above 5 years old.' });
|
|
}
|
|
|
|
return { context: ctx };
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
}) satisfies BetterAuthPlugin;
|