Skip to content

Commit

Permalink
fix: hide bypass token on redirect when cookie is present
Browse files Browse the repository at this point in the history
  • Loading branch information
jsdalton committed Jun 3, 2024
1 parent 7ada49c commit 99f9fa3
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 23 deletions.
38 changes: 32 additions & 6 deletions lib/app-router/handlers/enable-draft.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { redirect } from 'next/navigation';
import { NextRequest } from 'next/server';
import { enableDraftHandler as GET } from './enable-draft';
import { draftMode } from 'next/headers';

vi.mock('next/navigation', () => {
return {
Expand All @@ -11,14 +12,10 @@ vi.mock('next/navigation', () => {

vi.mock('next/headers', () => {
return {
draftMode: vi.fn(() => draftModeMock),
draftMode: vi.fn(),
};
});

const draftModeMock = {
enable: vi.fn(),
};

describe('handler', () => {
const bypassToken = 'kByQez2ke5Jl4ulCY6kxQrpFMp1UIohs';
const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat`;
Expand All @@ -29,10 +26,22 @@ describe('handler', () => {
let request: NextRequest = new NextRequest(url);
request.cookies.set('_vercel_jwt', vercelJwt);

let draftModeMock: ReturnType<typeof draftMode>

beforeEach(() => {
vi.stubEnv('VERCEL_AUTOMATION_BYPASS_SECRET', bypassToken);
draftModeMock = {
enable: vi.fn(),
disable: vi.fn(),
isEnabled: true
}
vi.mocked(draftMode).mockReturnValue(draftModeMock)
});

afterEach(() => {
vi.resetAllMocks()
})

it('redirects safely to the provided path, without passing through the token and bypass cookie query params', async () => {
const result = await GET(request);
expect(result).to.be.undefined;
Expand Down Expand Up @@ -68,6 +77,23 @@ describe('handler', () => {
});
});

describe('when a x-vercel-protection-bypass token is also provided as a query param', () => {
beforeEach(() => {
const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=${bypassToken}`;
request = new NextRequest(url);
request.cookies.set('_vercel_jwt', vercelJwt);
});

it('redirects safely to the provided path and DOES NOT pass through the token and bypass cookie query params', async () => {
const result = await GET(request);
expect(result).to.be.undefined;
expect(draftModeMock.enable).toHaveBeenCalled();
expect(vi.mocked(redirect)).toHaveBeenCalledWith(
`https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/blogs/my-cat`,
);
});
});

describe('when the _vercel_jwt cookie is missing', () => {
beforeEach(() => {
const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat`;
Expand Down
23 changes: 17 additions & 6 deletions lib/app-router/handlers/enable-draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,30 @@ export async function enableDraftHandler(
return redirect(redirectUrl);
}

const vercelJwtCookie = getVercelJwtCookie(request)

let bypassToken: string;
let aud: string;
let vercelJwt: VercelJwt | null = null

if (bypassTokenFromQuery) {
bypassToken = bypassTokenFromQuery;
aud = host;
} else {
// if x-vercel-protection-bypass not provided in query, we defer to parsing the _vercel_jwt cookie
// which bundlees the bypass token value in its payload
let vercelJwt: VercelJwt
// if we don't have a bypass token from the query we fall back to the _vercel_jwt cookie to find
// the correct authorization bypass elements
if (!vercelJwtCookie) {
return new Response(
'Missing _vercel_jwt cookie required for authorization bypass',
{ status: 401 },
);
}
try {
const vercelJwtCookie = getVercelJwtCookie(request)
vercelJwt = parseVercelJwtCookie(vercelJwtCookie);
} catch (e) {
if (!(e instanceof Error)) throw e;
return new Response(
'Missing or malformed bypass authorization token in _vercel_jwt cookie',
'Malformed bypass authorization token in _vercel_jwt cookie',
{ status: 401 },
);
}
Expand Down Expand Up @@ -68,6 +75,10 @@ export async function enableDraftHandler(

draftMode().enable();

const redirectUrl = buildRedirectUrl({ path, base, bypassTokenFromQuery });
// if a _vercel_jwt cookie was found, we do _not_ want to pass through the bypassToken to the redirect query. this
// is because Vercel will not "process" (and remove) the query parameter when a _vercel_jwt cookie is present.
const bypassTokenForRedirect = vercelJwtCookie ? undefined : bypassTokenFromQuery

const redirectUrl = buildRedirectUrl({ path, base, bypassTokenFromQuery: bypassTokenForRedirect });
redirect(redirectUrl);
}
17 changes: 17 additions & 0 deletions lib/pages-router/handlers/enable-draft.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ describe('handler', () => {
});
});

describe('when a x-vercel-protection-bypass token is also provided as a query param', () => {
beforeEach(() => {
const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=${bypassToken}`;
request = makeNextApiRequest(url);
request.cookies['_vercel_jwt'] = vercelJwt;
});

it('redirects safely to the provided path and DOES NOT pass through the token and bypass cookie query params', async () => {
const result = await handler(request, response);
expect(result).to.be.undefined;
expect(apiResponseSpy.setDraftMode).toHaveBeenCalledWith({ enable: true });
expect(apiResponseSpy.redirect).toHaveBeenCalledWith(
`https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/blogs/my-cat`,
);
});
});

describe('when the _vercel_jwt cookie is missing', () => {
beforeEach(() => {
const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat`;
Expand Down
19 changes: 14 additions & 5 deletions lib/pages-router/handlers/enable-draft.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
import { buildRedirectUrl, parseNextApiRequest } from '../../utils/url';
import { parseVercelJwtCookie} from '../../utils/vercelJwt';
import { parseVercelJwtCookie } from '../../utils/vercelJwt';
import { type VercelJwt } from '../../types';

export const enableDraftHandler: NextApiHandler = async (
Expand All @@ -22,6 +22,7 @@ export const enableDraftHandler: NextApiHandler = async (
return
}

const vercelJwtCookie = request.cookies['_vercel_jwt']
let bypassToken: string;
let aud: string;

Expand All @@ -33,13 +34,17 @@ export const enableDraftHandler: NextApiHandler = async (
// which bundlees the bypass token value in its payload
let vercelJwt: VercelJwt;
try {
const vercelJwtCookie = request.cookies['_vercel_jwt']
if (!vercelJwtCookie) throw new Error('`_vercel_jwt` cookie not set');
if (!vercelJwtCookie) {
response.status(401).send(
'Missing _vercel_jwt cookie required for authorization bypass'
)
return
}
vercelJwt = parseVercelJwtCookie(vercelJwtCookie);
} catch (e) {
if (!(e instanceof Error)) throw e;
response.status(401).send(
'Missing or malformed bypass authorization token in _vercel_jwt cookie'
'Malformed bypass authorization token in _vercel_jwt cookie'
)
return
}
Expand Down Expand Up @@ -70,7 +75,11 @@ export const enableDraftHandler: NextApiHandler = async (

response.setDraftMode({ enable: true })

const redirectUrl = buildRedirectUrl({ path, base, bypassTokenFromQuery });
// if a _vercel_jwt cookie was found, we do _not_ want to pass through the bypassToken to the redirect query. this
// is because Vercel will not "process" (and remove) the query parameter when a _vercel_jwt cookie is present.
const bypassTokenForRedirect = vercelJwtCookie ? undefined : bypassTokenFromQuery

const redirectUrl = buildRedirectUrl({ path, base, bypassTokenFromQuery: bypassTokenForRedirect });
response.redirect(redirectUrl)
return
}
24 changes: 20 additions & 4 deletions lib/utils/vercelJwt.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';
import { getVercelJwtCookie, parseVercelJwtCookie } from './vercelJwt';
import { NextRequest } from 'next/server';

describe('getVercelJwtCookie', () => {
describe.only('getVercelJwtCookie', () => {
const url = 'http://example.com'
const request = new NextRequest(url);
request.cookies.set('_vercel_jwt', 'vercel-jwt');
let request: NextRequest

beforeEach(() => {
request = new NextRequest(url);
request.cookies.set('_vercel_jwt', 'vercel-jwt');
})

it('returns the _vercel_jwt cookie', () => {
const result = getVercelJwtCookie(request)
expect(result).toEqual('vercel-jwt')
})

describe('when cookie is not present', () => {
beforeEach(() => {
request = new NextRequest(url);
request.cookies.clear()
})

it('returns undefined', () => {
const result = getVercelJwtCookie(request)
expect(result).to.equal(undefined)
})
})
})

describe('parseVercelJwtCookie', () => {
Expand Down
4 changes: 2 additions & 2 deletions lib/utils/vercelJwt.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NextRequest } from "next/server";
import { type VercelJwt } from "../types";

export const getVercelJwtCookie = (request: NextRequest): string => {
export const getVercelJwtCookie = (request: NextRequest): string | undefined => {
const vercelJwtCookie = request.cookies.get('_vercel_jwt');
if (!vercelJwtCookie) throw new Error('`_vercel_jwt` cookie not set');
if (!vercelJwtCookie) return;
return vercelJwtCookie.value;
}

Expand Down

0 comments on commit 99f9fa3

Please sign in to comment.