Skip to content

Commit

Permalink
Merge branch 'develop' into feature/GAP-2110-migration-banner
Browse files Browse the repository at this point in the history
  • Loading branch information
john-tco authored Sep 19, 2023
2 parents 27b8dfc + 5b7b035 commit febe6ae
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 87 deletions.
3 changes: 2 additions & 1 deletion packages/admin/src/middleware.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export async function middleware(req: NextRequest) {
const isValidAdminSession = await isAdminSessionValid(auth_cookie);
if (!isValidAdminSession) {
return NextResponse.redirect(
getLoginUrl({ redirectToApplicant: true })
getLoginUrl({ redirectToApplicant: true }),
{ status: 302 }
);
}
}
Expand Down
47 changes: 30 additions & 17 deletions packages/applicant/src/middleware.page.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import cookie from 'cookie';
import cookieParser from 'cookie-parser';
// eslint-disable-next-line @next/next/no-server-import-in-page
import { NextRequest, NextResponse, URLPattern } from 'next/server';
import { verifyToken } from './services/JwtService';
Expand Down Expand Up @@ -48,6 +50,29 @@ export function buildMiddlewareResponse(req: NextRequest, redirectUri: string) {
return res;
}

export const getJwtFromMiddlewareCookies = (req: NextRequest) => {
const COOKIE_SECRET = process.env.COOKIE_SECRET;

// Implementation below replicates that of a lambda function
// cabinet office have:
// https://github.com/cabinetoffice/x-co-login-auth-lambda/blob/22ce5fa104d2a36016a79f914d238f53ddabcee4/src/controllers/http/v1/request/utils.js#L81
const cookieValue = req.cookies.get(USER_TOKEN_NAME);
const parsedCookie = cookie.parse(`connect.sid=${cookieValue}`)[
'connect.sid'
];

// If the cookie is not a signed cookie, the parser will return the provided value
const unsignedCookie = cookieParser.signedCookie(parsedCookie, COOKIE_SECRET);

if (!unsignedCookie || unsignedCookie === 'undefined') {
throw new Error(
`Failed to verify signature for ${USER_TOKEN_NAME} cookie: ${cookieValue}`
);
}

return unsignedCookie;
};

export async function middleware(req: NextRequest) {
if (signInDetailsPage.test({ pathname: req.nextUrl.pathname })) {
if (!ONE_LOGIN_ENABLED) {
Expand All @@ -59,23 +84,11 @@ export async function middleware(req: NextRequest) {
}
}

const cookie = req.cookies.get(USER_TOKEN_NAME);

const response = await fetch(`${HOST}/api/getJwt`, {
method: 'get',
headers: {
Cookie: `${USER_TOKEN_NAME}=${cookie}`,
},
});

const jwt = await response.text();

if (!response.ok || jwt === 'undefined') {
if (!response.ok) {
const err = await response.text();
console.error(err);
}

let jwt: string;
try {
jwt = await getJwtFromMiddlewareCookies(req);
} catch (err) {
console.error(err);
const res = buildMiddlewareResponse(req, HOST);
return res;
}
Expand Down
27 changes: 4 additions & 23 deletions packages/applicant/src/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,31 @@ import { verifyToken } from './services/JwtService';

jest.mock('./services/JwtService');

global.fetch = jest.fn();
const mockedFetch = jest.mocked(global.fetch);
const mockedVerifyToken = jest.mocked(verifyToken);

describe('Middleware', () => {
beforeEach(jest.clearAllMocks);

it('redirects to host if response is not OK or JWT is undefined', async () => {
mockedFetch.mockResolvedValueOnce({
ok: false,
text: jest.fn().mockResolvedValueOnce('undefined'),
} as unknown as Response);

const req = new NextRequest(new Request('https://some.website.com/page'));
it('redirects to host if no JWT in cookies ', async () => {
const req = new NextRequest(new Request('https://www.website.com/page'));
const res = await middleware(req);

expect(res.status).toBe(307);
expect(res.headers.get('Location')).toBe(process.env.HOST);
});

it('redirects to host if JWT is not valid', async () => {
mockedFetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce('someJwt'),
} as unknown as Response);

mockedVerifyToken.mockResolvedValueOnce({ valid: false });

const req = new NextRequest(new Request('https://some.website.com/page'));
req.cookies.set(process.env.USER_TOKEN_NAME, 'invalid');
const res = await middleware(req);

expect(res.status).toBe(307);
expect(res.headers.get('Location')).toBe(process.env.HOST);
});

it('redirects to refresh URL if JWT is close to expiration', async () => {
mockedFetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce('someJwt'),
} as unknown as Response);

const expiresAt = new Date();
expiresAt.setMinutes(expiresAt.getMinutes() + 10); // Expiring in 10 minutes

Expand All @@ -55,6 +39,7 @@ describe('Middleware', () => {
});

const req = new NextRequest(new Request('https://some.website.com/test'));
req.cookies.set(process.env.USER_TOKEN_NAME, 'valid');
const res = await middleware(req);

expect(res.status).toBe(307);
Expand All @@ -64,10 +49,6 @@ describe('Middleware', () => {
});

it('sets redirect cookie if URL matches new application pattern', async () => {
mockedFetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce('someJwt'),
} as unknown as Response);
const pathname = 'applications/123';
const applicationId = '123';

Expand Down
27 changes: 0 additions & 27 deletions packages/applicant/src/pages/api/getJwt.page.tsx

This file was deleted.

14 changes: 5 additions & 9 deletions packages/applicant/src/pages/applications/index.page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { GetServerSideProps } from 'next';
import Link from 'next/link';
import Layout from '../../components/partials/Layout';
import Meta from '../../components/partials/Meta';
import {
Expand Down Expand Up @@ -116,18 +115,15 @@ const ExistingApplications = ({
{application.applicationName}
</p>
) : (
<Link
<a
className="govuk-link govuk-link--no-visited-state govuk-!-font-weight-regular"
data-cy={`cy-application-link-${application.applicationName}`}
href={routes.submissions.sections(
application.grantSubmissionId
)}
>
<a
className="govuk-link govuk-link--no-visited-state govuk-!-font-weight-regular"
data-cy={`cy-application-link-${application.applicationName}`}
>
{application.applicationName}
</a>
</Link>
{application.applicationName}
</a>
)}
</th>

Expand Down
15 changes: 7 additions & 8 deletions packages/applicant/src/pages/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,13 @@ export const ApplicantDashboard: FC<ApplicantDashBoardProps> = ({
<p className="govuk-body">
See your past and current applications
</p>
<Link href={'/applications'}>
<a
className="govuk-link govuk-link--no-visited-state"
data-cy="cy-your-applications-link"
>
View your applications
</a>
</Link>
<a
className="govuk-link govuk-link--no-visited-state"
data-cy="cy-your-applications-link"
href={'/applications'}
>
View your applications
</a>
</>
) : (
<>
Expand Down
6 changes: 4 additions & 2 deletions packages/applicant/src/utils/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ export const getJwtFromCookies = (
// If the cookie is not a signed cookie, the parser will return the provided value
const unsignedCookie = cookieParser.signedCookie(parsedCookie, COOKIE_SECRET);

if (!unsignedCookie) {
throw new Error('Failed to verify cookie signature');
if (!unsignedCookie || unsignedCookie === 'undefined') {
throw new Error(
`Failed to verify signature for ${USER_TOKEN_NAME} cookie: ${cookieValue}`
);
}

return unsignedCookie;
Expand Down

0 comments on commit febe6ae

Please sign in to comment.