Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use middleware to add nonce and CSP headers #553

Merged
merged 7 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion __tests__/middleware.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it, beforeAll, afterEach } from '@jest/globals';
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import { middleware } from '@/middleware.ts';
import middleware from '@/middleware.ts';
// Need to disable eslint for this import because
// you need to import the module you're going to mock with Jest
// eslint-disable-next-line no-unused-vars
Expand Down Expand Up @@ -57,6 +57,10 @@ describe('/login', () => {
expect(location).toMatch('state=baz');
expect(location).toMatch('response_type=code');
});

it('has CSP headers present', () => {
expect(response.headers.get('content-security-policy')).not.toBeNull();
});
});

describe('auth/login/callback', () => {
Expand Down Expand Up @@ -290,4 +294,16 @@ describe('/orgs/* when logged in', () => {
expect(response.cookies.get('lastViewedOrgId')).toBeUndefined();
});
});

describe('withCSP', () => {
it('should modify request headers', async () => {
// setup
const request = new NextRequest(new URL('/', process.env.ROOT_URL));

const response = await middleware(request);

// Assert that the headers were added as expected
expect(response.headers.get('content-security-policy')).not.toBeNull();
});
});
});
26 changes: 0 additions & 26 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,10 @@
const path = require('path');

const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`;

module.exports = {
generateBuildId: async () => {
// placeholder build id for development
return '0.0.1';
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\n/g, ''),
},
],
},
];
},
sassOptions: {
includePaths: [
path.join(__dirname, 'node_modules', '@uswds', 'uswds', 'packages'),
Expand Down
4 changes: 4 additions & 0 deletions src/app/orgs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
'use server';

import { headers } from 'next/headers';
import { getOrgsPage } from '@/controllers/controllers';
import { OrganizationsList } from '@/components/OrganizationsList/OrganizationsList';
import { PageHeader } from '@/components/PageHeader';
import { LastViewedOrgLink } from '@/components/LastViewedOrgLink';
import { Timestamp } from '@/components/Timestamp';

export default async function OrgsPage() {
const headersList = await headers();
const nonce = headersList.get('x-nonce') || undefined;
const { payload } = await getOrgsPage();

return (
Expand All @@ -28,6 +31,7 @@ export default async function OrgsPage() {
memoryCurrentUsage={payload.memoryCurrentUsage}
spaceCounts={payload.spaceCounts}
roles={payload.roles}
nonce={nonce}
/>
</div>
);
Expand Down
13 changes: 10 additions & 3 deletions src/assets/stylesheets/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,14 @@
'palette-color-system-green-cool-vivid',
'palette-color-system-red-cool-vivid',
'palette-color-system-red-vivid'
// no trailing comma,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
// no trailing comma,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
),

$border-color-palettes: (
'palette-color-system-green-cool',
'palette-color-system-red-vivid',
'palette-color-system-gray-cool'
// no trailing comma,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
// no trailing comma,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
),

$top-palettes: (
Expand Down Expand Up @@ -164,6 +164,9 @@
text-overflow: clip;
text-overflow: ellipsis;
}
.text-underline-offset {
text-underline-offset: 0.7em;
}

/* Custom selector styles */

Expand Down Expand Up @@ -429,7 +432,11 @@ $error-color-dark: 'red-40v';
// ProgressBar

.progress__bg--infinite {
background: linear-gradient(90deg, color('blue-cool-20v') 78.42%, color('blue-cool-30v') 100%);
background: linear-gradient(
90deg,
color('blue-cool-20v') 78.42%,
color('blue-cool-30v') 100%
);
}

.progress__infinity-logo {
Expand Down
13 changes: 13 additions & 0 deletions src/components/Image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import NextImage, { getImageProps } from 'next/image';
echappen marked this conversation as resolved.
Show resolved Hide resolved
import { ComponentProps } from 'react';

export default function Image(props: ComponentProps<typeof NextImage>) {
const { props: nextProps } = getImageProps({
...props,
});

// eslint-disable-next-line no-unused-vars
const { style: _omit, ...delegated } = nextProps;
echappen marked this conversation as resolved.
Show resolved Hide resolved

return <img {...delegated} />;
}
echappen marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 3 additions & 1 deletion src/components/MemoryBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import { formatMb } from '@/helpers/numbers';
export function MemoryBar({
memoryUsed,
memoryAllocated,
nonce,
}: {
memoryUsed?: number | null | undefined;
memoryAllocated?: number | null | undefined;
nonce: string | undefined;
}) {
const memoryUsedNum = memoryUsed || 0;
const mbRemaining = (memoryAllocated || 0) - memoryUsedNum;
return (
<div className="margin-top-3" data-testid="memory-bar">
<p className="font-sans-3xs text-uppercase text-bold">Memory:</p>

<ProgressBar total={memoryAllocated} fill={memoryUsedNum} />
<ProgressBar total={memoryAllocated} fill={memoryUsedNum} nonce={nonce} />

<div className="margin-top-1 display-flex flex-justify font-sans-3xs">
<div className="margin-right-1">
Expand Down
2 changes: 1 addition & 1 deletion src/components/NavGlobal/NavGlobal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Image from 'next/image';
import Image from '@/components/Image';

import CloudGovLogo from '@/components/svgs/CloudGovLogo';
import cloudPagesIcon from '@/../public/img/logos/cloud-pages-icon.svg';
Expand Down
2 changes: 1 addition & 1 deletion src/components/OrgPicker/OrgPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
'use client';
import React from 'react';
import { useState, useRef, useEffect } from 'react';
import Image from 'next/image';
import Image from '@/components/Image';
import { usePathname } from 'next/navigation';
import collapseIcon from '@/../public/img/uswds/usa-icons/expand_more.svg';
import { OrgPickerList } from './OrgPickerList';
Expand Down
3 changes: 3 additions & 0 deletions src/components/OrganizationsList/OrganizationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function OrganizationsList({
memoryCurrentUsage,
spaceCounts,
roles,
nonce,
}: {
orgs: Array<OrgObj>;
userCounts: { [orgGuid: string]: number };
Expand All @@ -20,6 +21,7 @@ export function OrganizationsList({
memoryCurrentUsage: { [orgGuid: string]: number };
spaceCounts: { [orgGuid: string]: number };
roles: { [orgGuid: string]: Array<string> };
nonce: string | undefined;
}) {
if (!orgs.length) {
return <>no orgs found</>;
Expand All @@ -44,6 +46,7 @@ export function OrganizationsList({
memoryCurrentUsage={memoryCurrentUsage[org.guid]}
spaceCount={spaceCounts[org.guid]}
roles={roles[org.guid]}
nonce={nonce}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { formatInt } from '@/helpers/numbers';
import { MemoryBar } from '@/components/MemoryBar';
import { formatOrgRoleName } from '@/helpers/text';

export function OrganizationsListCard({
export async function OrganizationsListCard({
org,
userCount,
appCount,
memoryAllocated,
memoryCurrentUsage,
spaceCount,
roles,
nonce,
}: {
org: OrgObj;
userCount: number;
Expand All @@ -23,6 +24,7 @@ export function OrganizationsListCard({
memoryCurrentUsage: number;
spaceCount: number;
roles: Array<string>;
nonce: string | undefined;
}) {
const getOrgRolesText = (orgGuid: string): React.ReactNode => {
if (!roles || !roles.length) {
Expand Down Expand Up @@ -81,6 +83,7 @@ export function OrganizationsListCard({
<MemoryBar
memoryUsed={memoryCurrentUsage}
memoryAllocated={memoryAllocated}
nonce={nonce}
/>
</Card>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/OverlayDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import React, { useRef, useEffect } from 'react';
import Image from 'next/image';
import Image from '@/components/Image';
import closeIcon from '@/../public/img/uswds/usa-icons/close.svg';

export function OverlayDrawer({
Expand Down
5 changes: 1 addition & 4 deletions src/components/Overlays/OverlayHeaderUsername.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ export function OverlayHeaderUsername({
}) {
return (
<>
<h2
className="margin-top-0 margin-bottom-7 text-uppercase text-light underline-base-light text-underline font-sans-xs"
style={{ textUnderlineOffset: '0.7em' }}
>
<h2 className="margin-top-0 margin-bottom-7 text-uppercase text-light underline-base-light text-underline text-underline-offset font-sans-xs">
{header}
</h2>
{serviceAccount && (
Expand Down
3 changes: 3 additions & 0 deletions src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ export function ProgressBar({
threshold1 = 75, // percentage where color should change first, between 0 and 100
threshold2 = 90, // percentage where color should change next, between 0 and 100
changeColors = true,
nonce,
}: {
total: number | null | undefined;
fill: number;
threshold1?: number;
threshold2?: number;
changeColors?: boolean;
nonce: string | undefined;
}) {
const heightClass = 'height-1';
const percentage = total ? Math.floor((fill / total) * 100) : 100;
Expand All @@ -33,6 +35,7 @@ export function ProgressBar({
className={`${heightClass} radius-pill ${color}`}
style={{ width: `${percentage}%` }}
data-testid="progress"
nonce={nonce}
></div>
{!total && (
<span className="progress__infinity-logo">
Expand Down
2 changes: 1 addition & 1 deletion src/components/uswds/Banner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import Image from 'next/image';
import Image from '@/components/Image';
import { useState } from 'react';

function BannerContent() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/uswds/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Image from 'next/image';
import Image from '@/components/Image';

import cloudGovIcon from '@/../public/img/logos/cloud-gov-logo-full-grey.svg';

Expand Down
2 changes: 1 addition & 1 deletion src/components/uswds/Identifier.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Image from 'next/image';
import Image from '@/components/Image';

export function Identifier() {
const links = [
Expand Down
2 changes: 1 addition & 1 deletion src/components/uswds/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useRef, useEffect } from 'react';
import Image from 'next/image';
import Image from '@/components/Image';
import closeIcon from '@/../public/img/uswds/usa-icons/close.svg';

export const modalHeadingId = (item: { guid: string }) =>
Expand Down
Loading