Files
sign/packages/lib/next-auth/auth-options.ts

348 lines
11 KiB
TypeScript
Raw Normal View History

/// <reference types="../types/next-auth.d.ts" />
2023-06-09 18:21:18 +10:00
import { PrismaAdapter } from '@next-auth/prisma-adapter';
2024-03-07 18:30:22 +11:00
import { compare } from '@node-rs/bcrypt';
feat: add passkeys (#989) ## Description Add support to login with passkeys. Passkeys can be added via the user security settings page. Note: Currently left out adding the type of authentication method for the 'user security audit logs' because we're using the `signIn` next-auth event which doesn't appear to provide the context. Will look into it at another time. ## Changes Made - Add passkeys to login - Add passkeys feature flag - Add page to manage passkeys - Add audit logs relating to passkeys - Updated prisma schema to support passkeys & anonymous verification tokens ## Testing Performed To be done. MacOS: - Safari ✅ - Chrome ✅ - Firefox ✅ Windows: - Chrome [Untested] - Firefox [Untested] Linux: - Chrome [Untested] - Firefox [Untested] iOS: - Safari ✅ ## Checklist <!--- Please check the boxes that apply to this pull request. --> <!--- You can add or remove items as needed. --> - [X] I have tested these changes locally and they work as expected. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced Passkey authentication, including creation, sign-in, and management of passkeys. - Added a Passkeys section in Security Settings for managing user passkeys. - Implemented UI updates for Passkey authentication, including a new dialog for creating passkeys and a data table for managing them. - Enhanced security settings with server-side feature flags to conditionally display new security features. - **Bug Fixes** - Improved UI consistency in the Settings Security Activity Page. - Updated button styling in the 2FA Recovery Codes component for better visibility. - **Refactor** - Streamlined authentication options to include WebAuthn credentials provider. - **Chores** - Updated database schema to support passkeys and related functionality. - Added new audit log types for passkey-related activities. - Enhanced server-only authentication utilities for passkey registration and management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-03-26 21:11:59 +08:00
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
2023-10-28 20:57:26 +11:00
import { DateTime } from 'luxon';
import type { AuthOptions, Session, User } from 'next-auth';
import type { JWT } from 'next-auth/jwt';
2023-06-09 18:21:18 +10:00
import CredentialsProvider from 'next-auth/providers/credentials';
import type { GoogleProfile } from 'next-auth/providers/google';
import GoogleProvider from 'next-auth/providers/google';
2024-01-25 10:48:20 +02:00
import { env } from 'next-runtime-env';
2023-06-09 18:21:18 +10:00
import { prisma } from '@documenso/prisma';
2024-01-31 12:27:40 +11:00
import { IdentityProvider, UserSecurityAuditLogType } from '@documenso/prisma/client';
2023-06-09 18:21:18 +10:00
feat: add passkeys (#989) ## Description Add support to login with passkeys. Passkeys can be added via the user security settings page. Note: Currently left out adding the type of authentication method for the 'user security audit logs' because we're using the `signIn` next-auth event which doesn't appear to provide the context. Will look into it at another time. ## Changes Made - Add passkeys to login - Add passkeys feature flag - Add page to manage passkeys - Add audit logs relating to passkeys - Updated prisma schema to support passkeys & anonymous verification tokens ## Testing Performed To be done. MacOS: - Safari ✅ - Chrome ✅ - Firefox ✅ Windows: - Chrome [Untested] - Firefox [Untested] Linux: - Chrome [Untested] - Firefox [Untested] iOS: - Safari ✅ ## Checklist <!--- Please check the boxes that apply to this pull request. --> <!--- You can add or remove items as needed. --> - [X] I have tested these changes locally and they work as expected. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced Passkey authentication, including creation, sign-in, and management of passkeys. - Added a Passkeys section in Security Settings for managing user passkeys. - Implemented UI updates for Passkey authentication, including a new dialog for creating passkeys and a data table for managing them. - Enhanced security settings with server-side feature flags to conditionally display new security features. - **Bug Fixes** - Improved UI consistency in the Settings Security Activity Page. - Updated button styling in the 2FA Recovery Codes component for better visibility. - **Refactor** - Streamlined authentication options to include WebAuthn credentials provider. - **Chores** - Updated database schema to support passkeys and related functionality. - Added new audit log types for passkey-related activities. - Enhanced server-only authentication utilities for passkey registration and management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-03-26 21:11:59 +08:00
import { AppError, AppErrorCode } from '../errors/app-error';
import { isTwoFactorAuthenticationEnabled } from '../server-only/2fa/is-2fa-availble';
import { validateTwoFactorAuthentication } from '../server-only/2fa/validate-2fa';
2024-02-13 06:01:25 +00:00
import { getMostRecentVerificationTokenByUserId } from '../server-only/user/get-most-recent-verification-token-by-user-id';
2023-06-09 18:21:18 +10:00
import { getUserByEmail } from '../server-only/user/get-user-by-email';
2024-01-30 12:54:48 +02:00
import { sendConfirmationToken } from '../server-only/user/send-confirmation-token';
feat: add passkeys (#989) ## Description Add support to login with passkeys. Passkeys can be added via the user security settings page. Note: Currently left out adding the type of authentication method for the 'user security audit logs' because we're using the `signIn` next-auth event which doesn't appear to provide the context. Will look into it at another time. ## Changes Made - Add passkeys to login - Add passkeys feature flag - Add page to manage passkeys - Add audit logs relating to passkeys - Updated prisma schema to support passkeys & anonymous verification tokens ## Testing Performed To be done. MacOS: - Safari ✅ - Chrome ✅ - Firefox ✅ Windows: - Chrome [Untested] - Firefox [Untested] Linux: - Chrome [Untested] - Firefox [Untested] iOS: - Safari ✅ ## Checklist <!--- Please check the boxes that apply to this pull request. --> <!--- You can add or remove items as needed. --> - [X] I have tested these changes locally and they work as expected. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced Passkey authentication, including creation, sign-in, and management of passkeys. - Added a Passkeys section in Security Settings for managing user passkeys. - Implemented UI updates for Passkey authentication, including a new dialog for creating passkeys and a data table for managing them. - Enhanced security settings with server-side feature flags to conditionally display new security features. - **Bug Fixes** - Improved UI consistency in the Settings Security Activity Page. - Updated button styling in the 2FA Recovery Codes component for better visibility. - **Refactor** - Streamlined authentication options to include WebAuthn credentials provider. - **Chores** - Updated database schema to support passkeys and related functionality. - Added new audit log types for passkey-related activities. - Enhanced server-only authentication utilities for passkey registration and management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-03-26 21:11:59 +08:00
import type { TAuthenticationResponseJSONSchema } from '../types/webauthn';
import { ZAuthenticationResponseJSONSchema } from '../types/webauthn';
2024-01-31 12:27:40 +11:00
import { extractNextAuthRequestMetadata } from '../universal/extract-request-metadata';
feat: add passkeys (#989) ## Description Add support to login with passkeys. Passkeys can be added via the user security settings page. Note: Currently left out adding the type of authentication method for the 'user security audit logs' because we're using the `signIn` next-auth event which doesn't appear to provide the context. Will look into it at another time. ## Changes Made - Add passkeys to login - Add passkeys feature flag - Add page to manage passkeys - Add audit logs relating to passkeys - Updated prisma schema to support passkeys & anonymous verification tokens ## Testing Performed To be done. MacOS: - Safari ✅ - Chrome ✅ - Firefox ✅ Windows: - Chrome [Untested] - Firefox [Untested] Linux: - Chrome [Untested] - Firefox [Untested] iOS: - Safari ✅ ## Checklist <!--- Please check the boxes that apply to this pull request. --> <!--- You can add or remove items as needed. --> - [X] I have tested these changes locally and they work as expected. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced Passkey authentication, including creation, sign-in, and management of passkeys. - Added a Passkeys section in Security Settings for managing user passkeys. - Implemented UI updates for Passkey authentication, including a new dialog for creating passkeys and a data table for managing them. - Enhanced security settings with server-side feature flags to conditionally display new security features. - **Bug Fixes** - Improved UI consistency in the Settings Security Activity Page. - Updated button styling in the 2FA Recovery Codes component for better visibility. - **Refactor** - Streamlined authentication options to include WebAuthn credentials provider. - **Chores** - Updated database schema to support passkeys and related functionality. - Added new audit log types for passkey-related activities. - Enhanced server-only authentication utilities for passkey registration and management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-03-26 21:11:59 +08:00
import { getAuthenticatorRegistrationOptions } from '../utils/authenticator';
2023-09-05 11:29:23 +10:00
import { ErrorCode } from './error-codes';
2023-06-09 18:21:18 +10:00
export const NEXT_AUTH_OPTIONS: AuthOptions = {
adapter: PrismaAdapter(prisma),
secret: process.env.NEXTAUTH_SECRET ?? 'secret',
session: {
strategy: 'jwt',
},
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
totpCode: {
label: 'Two-factor Code',
type: 'input',
placeholder: 'Code from authenticator app',
},
backupCode: { label: 'Backup Code', type: 'input', placeholder: 'Two-factor backup code' },
2023-06-09 18:21:18 +10:00
},
2024-01-31 12:27:40 +11:00
authorize: async (credentials, req) => {
2023-06-09 18:21:18 +10:00
if (!credentials) {
2023-09-05 11:29:23 +10:00
throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND);
2023-06-09 18:21:18 +10:00
}
const { email, password, backupCode, totpCode } = credentials;
2023-06-09 18:21:18 +10:00
2023-08-27 07:10:41 +05:30
const user = await getUserByEmail({ email }).catch(() => {
2023-09-05 11:29:23 +10:00
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
2023-08-27 07:10:41 +05:30
});
2023-06-09 18:21:18 +10:00
2023-08-27 07:10:41 +05:30
if (!user.password) {
2023-09-05 11:29:23 +10:00
throw new Error(ErrorCode.USER_MISSING_PASSWORD);
2023-06-09 18:21:18 +10:00
}
2023-08-27 07:10:41 +05:30
const isPasswordsSame = await compare(password, user.password);
2024-01-31 12:27:40 +11:00
const requestMetadata = extractNextAuthRequestMetadata(req);
2023-06-09 18:21:18 +10:00
if (!isPasswordsSame) {
2024-01-31 12:27:40 +11:00
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_FAIL,
},
});
2023-09-05 11:29:23 +10:00
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
2023-06-09 18:21:18 +10:00
}
const is2faEnabled = isTwoFactorAuthenticationEnabled({ user });
if (is2faEnabled) {
const isValid = await validateTwoFactorAuthentication({ backupCode, totpCode, user });
if (!isValid) {
2024-01-31 12:27:40 +11:00
await prisma.userSecurityAuditLog.create({
data: {
userId: user.id,
ipAddress: requestMetadata.ipAddress,
userAgent: requestMetadata.userAgent,
type: UserSecurityAuditLogType.SIGN_IN_2FA_FAIL,
},
});
throw new Error(
totpCode
? ErrorCode.INCORRECT_TWO_FACTOR_CODE
: ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE,
);
}
}
2024-01-25 15:42:40 +02:00
if (!user.emailVerified) {
2024-02-13 06:01:25 +00:00
const mostRecentToken = await getMostRecentVerificationTokenByUserId({
userId: user.id,
});
2024-01-30 12:54:48 +02:00
2024-02-13 06:01:25 +00:00
if (
!mostRecentToken ||
mostRecentToken.expires.valueOf() <= Date.now() ||
DateTime.fromJSDate(mostRecentToken.createdAt).diffNow('minutes').minutes > -5
) {
2024-01-30 12:54:48 +02:00
await sendConfirmationToken({ email });
}
throw new Error(ErrorCode.UNVERIFIED_EMAIL);
}
2023-06-09 18:21:18 +10:00
return {
2023-07-31 13:53:55 +10:00
id: Number(user.id),
2023-06-09 18:21:18 +10:00
email: user.email,
name: user.name,
emailVerified: user.emailVerified?.toISOString() ?? null,
2023-06-09 18:21:18 +10:00
} satisfies User;
},
}),
2023-07-31 13:53:55 +10:00
GoogleProvider<GoogleProfile>({
clientId: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID ?? '',
clientSecret: process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET ?? '',
allowDangerousEmailAccountLinking: true,
2023-10-28 20:57:26 +11:00
profile(profile) {
return {
2023-07-31 13:53:55 +10:00
id: Number(profile.sub),
name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(),
email: profile.email,
emailVerified: profile.email_verified ? new Date().toISOString() : null,
};
},
}),
feat: add passkeys (#989) ## Description Add support to login with passkeys. Passkeys can be added via the user security settings page. Note: Currently left out adding the type of authentication method for the 'user security audit logs' because we're using the `signIn` next-auth event which doesn't appear to provide the context. Will look into it at another time. ## Changes Made - Add passkeys to login - Add passkeys feature flag - Add page to manage passkeys - Add audit logs relating to passkeys - Updated prisma schema to support passkeys & anonymous verification tokens ## Testing Performed To be done. MacOS: - Safari ✅ - Chrome ✅ - Firefox ✅ Windows: - Chrome [Untested] - Firefox [Untested] Linux: - Chrome [Untested] - Firefox [Untested] iOS: - Safari ✅ ## Checklist <!--- Please check the boxes that apply to this pull request. --> <!--- You can add or remove items as needed. --> - [X] I have tested these changes locally and they work as expected. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced Passkey authentication, including creation, sign-in, and management of passkeys. - Added a Passkeys section in Security Settings for managing user passkeys. - Implemented UI updates for Passkey authentication, including a new dialog for creating passkeys and a data table for managing them. - Enhanced security settings with server-side feature flags to conditionally display new security features. - **Bug Fixes** - Improved UI consistency in the Settings Security Activity Page. - Updated button styling in the 2FA Recovery Codes component for better visibility. - **Refactor** - Streamlined authentication options to include WebAuthn credentials provider. - **Chores** - Updated database schema to support passkeys and related functionality. - Added new audit log types for passkey-related activities. - Enhanced server-only authentication utilities for passkey registration and management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-03-26 21:11:59 +08:00
CredentialsProvider({
id: 'webauthn',
name: 'Keypass',
credentials: {
csrfToken: { label: 'csrfToken', type: 'csrfToken' },
},
async authorize(credentials, req) {
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 } = getAuthenticatorRegistrationOptions();
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;
},
}),
2023-06-09 18:21:18 +10:00
],
callbacks: {
async jwt({ token, user, trigger, account }) {
2023-10-28 20:57:26 +11:00
const merged = {
...token,
...user,
emailVerified: user?.emailVerified ? new Date(user.emailVerified).toISOString() : null,
} satisfies JWT;
2023-10-28 20:57:26 +11:00
if (!merged.email || typeof merged.emailVerified !== 'string') {
2023-10-28 20:57:26 +11:00
const userId = Number(merged.id ?? token.sub);
const retrieved = await prisma.user.findFirst({
where: {
id: userId,
},
});
if (!retrieved) {
return token;
}
merged.id = retrieved.id;
merged.name = retrieved.name;
merged.email = retrieved.email;
merged.emailVerified = retrieved.emailVerified?.toISOString() ?? null;
2023-07-31 13:53:55 +10:00
}
2023-10-28 20:57:26 +11:00
if (
2023-11-04 13:14:20 +11:00
merged.id &&
(!merged.lastSignedIn ||
DateTime.fromISO(merged.lastSignedIn).plus({ hours: 1 }) <= DateTime.now())
2023-10-28 20:57:26 +11:00
) {
merged.lastSignedIn = new Date().toISOString();
const user = await prisma.user.update({
2023-10-28 20:57:26 +11:00
where: {
id: Number(merged.id),
},
data: {
lastSignedIn: merged.lastSignedIn,
},
});
merged.emailVerified = user.emailVerified?.toISOString() ?? null;
}
if ((trigger === 'signIn' || trigger === 'signUp') && account?.provider === 'google') {
merged.emailVerified = user?.emailVerified
? new Date(user.emailVerified).toISOString()
: new Date().toISOString();
await prisma.user.update({
where: {
id: Number(merged.id),
},
data: {
emailVerified: merged.emailVerified,
identityProvider: IdentityProvider.GOOGLE,
},
});
}
return {
2023-10-28 20:57:26 +11:00
id: merged.id,
name: merged.name,
email: merged.email,
lastSignedIn: merged.lastSignedIn,
emailVerified: merged.emailVerified,
} satisfies JWT;
},
2023-07-31 13:53:55 +10:00
2023-08-29 13:01:19 +10:00
session({ token, session }) {
2023-07-31 13:53:55 +10:00
if (token && token.email) {
return {
...session,
user: {
id: Number(token.id),
name: token.name,
email: token.email,
emailVerified: token.emailVerified ?? null,
},
2023-07-31 13:53:55 +10:00
} satisfies Session;
}
2023-07-31 13:53:55 +10:00
return session;
},
async signIn({ user }) {
2023-12-15 20:41:54 +11:00
// We do this to stop OAuth providers from creating an account
// when signups are disabled
if (env('NEXT_PUBLIC_DISABLE_SIGNUP') === 'true') {
const userData = await getUserByEmail({ email: user.email! });
return !!userData;
}
2023-12-15 20:41:54 +11:00
return true;
},
},
2024-01-30 17:31:27 +11:00
// Note: `events` are handled in `apps/web/src/pages/api/auth/[...nextauth].ts` to allow access to the request.
2023-06-09 18:21:18 +10:00
};