🛂 Auto ban IP on suspected bot publishing (#1095)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced sign-in error handling with specific messages for different
error types.
- Implemented IP-based restrictions for authentication and publishing
actions.

- **Bug Fixes**
- Updated the retrieval of user session information to improve
reliability.

- **Documentation**
- Updated usage instructions for `getServerSession` to reflect the new
authentication options.

- **Refactor**
- Replaced direct usage of `authOptions` with a new function
`getAuthOptions` to dynamically generate authentication options.
- Improved IP address extraction logic to handle various header formats.

- **Chores**
- Added a new `BannedIp` model to the database schema for managing
IP-based restrictions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2023-12-11 13:40:07 +01:00
committed by GitHub
parent eedb7145ac
commit fcfbd63443
13 changed files with 171 additions and 38 deletions

View File

@@ -19,6 +19,7 @@ import { Redis } from '@upstash/redis/nodejs'
import got from 'got'
import { env } from '@typebot.io/env'
import * as Sentry from '@sentry/nextjs'
import { getIp } from '@typebot.io/lib/getIp'
const providers: Provider[] = []
@@ -124,7 +125,11 @@ if (env.CUSTOM_OAUTH_WELL_KNOWN_URL) {
})
}
export const authOptions: AuthOptions = {
export const getAuthOptions = ({
restricted,
}: {
restricted?: 'ip-banned' | 'rate-limited'
}): AuthOptions => ({
adapter: customAdapter(prisma),
secret: env.ENCRYPTION_SECRET,
providers,
@@ -153,6 +158,8 @@ export const authOptions: AuthOptions = {
}
},
signIn: async ({ account, user }) => {
if (restricted === 'ip-banned') throw new Error('ip-banned')
if (restricted === 'rate-limited') throw new Error('rate-limited')
if (!account) return false
const isNewUser = !('createdAt' in user && isDefined(user.createdAt))
if (isNewUser && user.email) {
@@ -177,7 +184,7 @@ export const authOptions: AuthOptions = {
return true
},
},
}
})
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const isMockingSession =
@@ -188,24 +195,37 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const requestIsFromCompanyFirewall = req.method === 'HEAD'
if (requestIsFromCompanyFirewall) return res.status(200).end()
let restricted: 'ip-banned' | 'rate-limited' | undefined
if (
rateLimit &&
req.url === '/api/auth/signin/email' &&
env.RADAR_HIGH_RISK_KEYWORDS &&
req.url?.startsWith('/api/auth/signin') &&
req.method === 'POST'
) {
let ip = req.headers['x-real-ip'] as string | undefined
if (!ip) {
const forwardedFor = req.headers['x-forwarded-for']
if (Array.isArray(forwardedFor)) {
ip = forwardedFor.at(0)
} else {
ip = forwardedFor?.split(',').at(0) ?? 'Unknown'
}
const ip = getIp(req)
if (ip) {
const isIpBanned = await prisma.bannedIp.findFirst({
where: {
ip,
},
})
if (isIpBanned) restricted = 'ip-banned'
}
const { success } = await rateLimit.limit(ip as string)
if (!success) return res.status(429).json({ error: 'Too many requests' })
}
return await NextAuth(req, res, authOptions)
if (
rateLimit &&
req.url?.startsWith('/api/auth/signin/email') &&
req.method === 'POST'
) {
const ip = getIp(req)
if (ip) {
const { success } = await rateLimit.limit(ip)
if (!success) restricted = 'rate-limited'
}
}
return await NextAuth(req, res, getAuthOptions({ restricted }))
}
const updateLastActivityDate = async (user: User) => {

View File

@@ -3,7 +3,7 @@ import { User } from '@typebot.io/prisma'
import { isNotDefined } from '@typebot.io/lib'
import { sign } from 'jsonwebtoken'
import { getServerSession } from 'next-auth'
import { authOptions } from './api/auth/[...nextauth]'
import { getAuthOptions } from './api/auth/[...nextauth]'
import { env } from '@typebot.io/env'
export default function Page() {
@@ -11,7 +11,11 @@ export default function Page() {
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context.req, context.res, authOptions)
const session = await getServerSession(
context.req,
context.res,
getAuthOptions({})
)
if (isNotDefined(session?.user))
return {
redirect: {

View File

@@ -2,7 +2,7 @@ import { getServerSession } from 'next-auth'
import { User } from '@typebot.io/prisma'
import { isNotDefined } from '@typebot.io/lib'
import { sign } from 'jsonwebtoken'
import { authOptions } from '../api/auth/[...nextauth]'
import { getAuthOptions } from '../api/auth/[...nextauth]'
import { GetServerSidePropsContext } from 'next'
import { env } from '@typebot.io/env'
@@ -11,7 +11,11 @@ export default function Page() {
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context.req, context.res, authOptions)
const session = await getServerSession(
context.req,
context.res,
getAuthOptions({})
)
const feedbackId = context.query.feedbackId?.toString() as string
if (isNotDefined(session?.user))
return {

View File

@@ -1,6 +1,6 @@
import { GetServerSidePropsContext } from 'next'
import { getServerSession } from 'next-auth'
import { authOptions } from './api/auth/[...nextauth]'
import { getAuthOptions } from './api/auth/[...nextauth]'
export default function Page() {
return null
@@ -9,7 +9,11 @@ export default function Page() {
export const getServerSideProps = async (
context: GetServerSidePropsContext
) => {
const session = await getServerSession(context.req, context.res, authOptions)
const session = await getServerSession(
context.req,
context.res,
getAuthOptions({})
)
if (!session?.user) {
return {
redirect: {