Skip to content

Commit 85b0f9b

Browse files
pi1814parth inamdar
and
parth inamdar
authored
Security enhancements: (boxyhq#2041)
Co-authored-by: parth inamdar <[email protected]>
1 parent 1fbf1bc commit 85b0f9b

File tree

8 files changed

+160
-12
lines changed

8 files changed

+160
-12
lines changed

components/invitation/EmailDomainMismatch.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Button } from 'react-daisyui';
2-
import { signOut } from 'next-auth/react';
32
import { useTranslation } from 'next-i18next';
43

54
import { Invitation } from '@prisma/client';
5+
import { useCustomSignOut } from 'hooks/useCustomSignout';
66

77
interface EmailDomainMismatchProps {
88
invitation: Invitation;
@@ -15,6 +15,7 @@ const EmailDomainMismatch = ({
1515
}: EmailDomainMismatchProps) => {
1616
const { t } = useTranslation('common');
1717
const { allowedDomains } = invitation;
18+
const signOut = useCustomSignOut();
1819

1920
const allowedDomainsString =
2021
allowedDomains.length === 1
@@ -34,9 +35,7 @@ const EmailDomainMismatch = ({
3435
color="error"
3536
size="md"
3637
variant="outline"
37-
onClick={() => {
38-
signOut();
39-
}}
38+
onClick={signOut}
4039
>
4140
{t('logout')}
4241
</Button>

components/invitation/EmailMismatch.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Button } from 'react-daisyui';
2-
import { signOut } from 'next-auth/react';
32
import { useTranslation } from 'next-i18next';
3+
import { useCustomSignOut } from 'hooks/useCustomSignout';
44

55
interface EmailMismatchProps {
66
email: string;
77
}
88

99
const EmailMismatch = ({ email }: EmailMismatchProps) => {
1010
const { t } = useTranslation('common');
11+
const signOut = useCustomSignOut();
1112

1213
return (
1314
<>
@@ -22,9 +23,7 @@ const EmailMismatch = ({ email }: EmailMismatchProps) => {
2223
color="error"
2324
size="md"
2425
variant="outline"
25-
onClick={() => {
26-
signOut();
27-
}}
26+
onClick={signOut}
2827
>
2928
{t('logout')}
3029
</Button>

components/shared/shell/Header.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
import { ChevronDownIcon } from '@heroicons/react/20/solid';
1111
import useTheme from 'hooks/useTheme';
1212
import env from '@/lib/env';
13-
import { signOut } from 'next-auth/react';
1413
import { useTranslation } from 'next-i18next';
14+
import { useCustomSignOut } from 'hooks/useCustomSignout';
1515

1616
interface HeaderProps {
1717
setSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
@@ -21,6 +21,7 @@ const Header = ({ setSidebarOpen }: HeaderProps) => {
2121
const { toggleTheme } = useTheme();
2222
const { status, data } = useSession();
2323
const { t } = useTranslation('common');
24+
const signOut = useCustomSignOut();
2425

2526
if (status === 'loading' || !data) {
2627
return null;
@@ -95,7 +96,7 @@ const Header = ({ setSidebarOpen }: HeaderProps) => {
9596
<button
9697
className="block px-2 py-1 text-sm leading-6 text-gray-900 dark:text-gray-50 cursor-pointer"
9798
type="button"
98-
onClick={() => signOut()}
99+
onClick={signOut}
99100
>
100101
<div className="flex items-center">
101102
<ArrowRightOnRectangleIcon className="w-5 h-5 mr-1" />{' '}

hooks/useCustomSignout.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useRouter } from 'next/router';
2+
3+
export function useCustomSignOut() {
4+
const router = useRouter();
5+
6+
const signOut = async () => {
7+
try {
8+
const response = await fetch('/api/auth/custom-signout', {
9+
method: 'POST',
10+
headers: {
11+
'Content-Type': 'application/json',
12+
},
13+
});
14+
15+
if (!response.ok) {
16+
throw new Error('Signout failed');
17+
}
18+
19+
router.push('/auth/login');
20+
} catch (error) {
21+
console.error('Error during sign out:', error);
22+
}
23+
};
24+
25+
return signOut;
26+
}

lib/env.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const env = {
44
databaseUrl: `${process.env.DATABASE_URL}`,
55
appUrl: `${process.env.APP_URL}`,
66
redirectIfAuthenticated: '/dashboard',
7+
securityHeadersEnabled: process.env.SECURITY_HEADERS_ENABLED ?? false,
78

89
// SMTP configuration for NextAuth
910
smtp: {

lib/nextAuth.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { forceConsume } from '@/lib/server-common';
3737

3838
const adapter = PrismaAdapter(prisma);
3939
const providers: Provider[] = [];
40-
const sessionMaxAge = 30 * 24 * 60 * 60; // 30 days
40+
const sessionMaxAge = 14 * 24 * 60 * 60; // 14 days
4141
const useSecureCookie = env.appUrl.startsWith('https://');
4242

4343
export const sessionTokenCookieName =

middleware.ts

+68-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,56 @@ import type { NextRequest } from 'next/server';
55

66
import env from './lib/env';
77

8+
// Constants for security headers
9+
const SECURITY_HEADERS = {
10+
'Referrer-Policy': 'strict-origin-when-cross-origin',
11+
'Permissions-Policy': 'geolocation=(), microphone=()',
12+
'Cross-Origin-Embedder-Policy': 'require-corp',
13+
'Cross-Origin-Opener-Policy': 'same-origin',
14+
'Cross-Origin-Resource-Policy': 'same-site',
15+
} as const;
16+
17+
// Generate CSP
18+
const generateCSP = (): string => {
19+
const policies = {
20+
'default-src': ["'self'"],
21+
'img-src': [
22+
"'self'",
23+
'boxyhq.com',
24+
'*.boxyhq.com',
25+
'*.dicebear.com',
26+
'data:',
27+
],
28+
'script-src': [
29+
"'self'",
30+
"'unsafe-inline'",
31+
"'unsafe-eval'",
32+
'*.gstatic.com',
33+
'*.google.com',
34+
],
35+
'style-src': ["'self'", "'unsafe-inline'"],
36+
'connect-src': [
37+
"'self'",
38+
'*.google.com',
39+
'*.gstatic.com',
40+
'boxyhq.com',
41+
'*.ingest.sentry.io',
42+
'*.mixpanel.com',
43+
],
44+
'frame-src': ["'self'", '*.google.com', '*.gstatic.com'],
45+
'font-src': ["'self'"],
46+
'object-src': ["'none'"],
47+
'base-uri': ["'self'"],
48+
'form-action': ["'self'"],
49+
'frame-ancestors': ["'none'"],
50+
};
51+
52+
return Object.entries(policies)
53+
.map(([key, values]) => `${key} ${values.join(' ')}`)
54+
.concat(['upgrade-insecure-requests'])
55+
.join('; ');
56+
};
57+
858
// Add routes that don't require authentication
959
const unAuthenticatedRoutes = [
1060
'/api/hello',
@@ -63,8 +113,25 @@ export default async function middleware(req: NextRequest) {
63113
}
64114
}
65115

116+
const requestHeaders = new Headers(req.headers);
117+
const csp = generateCSP();
118+
119+
requestHeaders.set('Content-Security-Policy', csp);
120+
121+
const response = NextResponse.next({
122+
request: { headers: requestHeaders },
123+
});
124+
125+
if (env.securityHeadersEnabled) {
126+
// Set security headers
127+
response.headers.set('Content-Security-Policy', csp);
128+
Object.entries(SECURITY_HEADERS).forEach(([key, value]) => {
129+
response.headers.set(key, value);
130+
});
131+
}
132+
66133
// All good, let the request through
67-
return NextResponse.next();
134+
return response;
68135
}
69136

70137
export const config = {

pages/api/auth/custom-signout.ts

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextApiRequest, NextApiResponse } from 'next';
2+
import { getServerSession } from 'next-auth/next';
3+
import { getAuthOptions, sessionTokenCookieName } from '@/lib/nextAuth';
4+
import { prisma } from '@/lib/prisma';
5+
import { getCookie } from 'cookies-next';
6+
import env from '@/lib/env';
7+
import { deleteSession } from 'models/session';
8+
9+
export default async function handler(
10+
req: NextApiRequest,
11+
res: NextApiResponse
12+
) {
13+
if (req.method !== 'POST') {
14+
return res.status(405).json({ error: 'Method not allowed' });
15+
}
16+
17+
try {
18+
const authOptions = getAuthOptions(req, res);
19+
const session = await getServerSession(req, res, authOptions);
20+
21+
if (!session || !session.user) {
22+
return res.status(401).json({ error: 'Unauthorized' });
23+
}
24+
25+
if (env.nextAuth.sessionStrategy === 'database') {
26+
const sessionToken = await getCookie(sessionTokenCookieName, {
27+
req,
28+
res,
29+
});
30+
const sessionDBEntry = await prisma.session.findFirst({
31+
where: {
32+
sessionToken: sessionToken,
33+
},
34+
});
35+
36+
if (sessionDBEntry) {
37+
await deleteSession({
38+
where: {
39+
sessionToken: sessionToken,
40+
},
41+
});
42+
}
43+
}
44+
45+
res.setHeader(
46+
'Set-Cookie',
47+
'next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; HttpOnly; Secure; SameSite=Lax'
48+
);
49+
50+
return res.status(200).json({ success: true });
51+
} catch (error) {
52+
console.error('Signout error:', error);
53+
return res.status(500).json({ error: 'Failed to sign out' });
54+
}
55+
}

0 commit comments

Comments
 (0)