Skip to content
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
21 changes: 21 additions & 0 deletions .github/dependency-review/dependency-review-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,24 @@ allow-licenses:
- 'Unlicense'
- 'WTFPL'
- 'Zlib'
allow-dependencies-licenses:
# These packages are needed for nextjs (more detailed sharp)
# they are dynamic libraries and thus do not ship the underlying library
# which means they are not under LGPL strictly speaking.
# so an exception is valid here
# ALSO: none of these are shipped to the customer. they're only used as dev dependencies.
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/[email protected]'
- 'pkg:npm/%2540img/sharp-wasm32'
- 'pkg:npm/%2540img/sharp-win32-arm64'
- 'pkg:npm/%2540img/sharp-win32-ia32'
- 'pkg:npm/%2540img/sharp-win32-x64'
- 'pkg:npm/[email protected]'
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
"**/on-headers": "1.1.0",
"**/form-data": "4.0.4",
"tmp": "^0.2.4",
"qs": "^6.14.1",
"js-yaml": "4.1.1"
},
"overrides": {
Expand Down
4 changes: 4 additions & 0 deletions packages/adapter-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
This package contains the AWS Amplify Next.js Adapter. For more information on using Next.js in your application please reference the [Amplify Dev Center](https://docs.amplify.aws/javascript/build-a-backend/server-side-rendering/nextjs/).

### Licensing Note
Although this repository is released and licensed under the Apache License (see [LICENSE](./LICENSE)), its devDependencies transitively use the third party [sharp](https://sharp.pixelplumbing.com/) project.

The sharp projects prebuilt binaries' licensing includes [LGPL-2.1](https://opensource.org/license/LGPL-2.1) and [LGPL-3.0-or-later](https://opensource.org/license/LGPL-3.0) licenses
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,38 @@ describe('appendSetCookieHeaders', () => {
].join(', '),
);
});

it('properly encodes cookie names for js-cookie compatibility', () => {
const headers = new Headers();
const cookies = [
{ name: 'cookie name', value: 'value1' }, // space should be encoded
{ name: 'cookie=name', value: 'value2' }, // equals should be encoded
{ name: 'cookie&name', value: 'value3' }, // ampersand should be decoded per js-cookie
];

appendSetCookieHeaders(headers, cookies);

const setCookieHeaders = headers.get('Set-Cookie')?.split(', ') || [];

expect(setCookieHeaders[0]).toContain('cookie%20name=value1;'); // space encoded
expect(setCookieHeaders[1]).toContain('cookie%3Dname=value2;'); // equals encoded
expect(setCookieHeaders[2]).toContain('cookie&name=value3;'); // ampersand decoded
});

it('handles complex cookie names with mixed special characters', () => {
const headers = new Headers();
const cookies = [
{ name: 'amplify.auth.token', value: 'token1' },
{ name: 'amplify auth token', value: 'token2' },
{ name: 'amplify=auth&token', value: 'token3' },
];

appendSetCookieHeaders(headers, cookies);

const setCookieHeaders = headers.get('Set-Cookie')?.split(', ') || [];

expect(setCookieHeaders[0]).toContain('amplify.auth.token=token1;');
expect(setCookieHeaders[1]).toContain('amplify%20auth%20token=token2;');
expect(setCookieHeaders[2]).toContain('amplify%3Dauth&token=token3;');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,66 @@ describe('appendSetCookieHeadersToNextApiResponse', () => {
'cookie2=value2;Domain=example.com;SameSite=strict;Path=/',
);
});

it('properly encodes cookie names for js-cookie compatibility', () => {
const mockAppendHeader = jest.fn();
const mockNextApiResponse = {
appendHeader: mockAppendHeader,
} as unknown as NextApiResponse;
const cookies = [
{ name: 'cookie name', value: 'value1' }, // space should be encoded
{ name: 'cookie=name', value: 'value2' }, // equals should be encoded
{ name: 'cookie&name', value: 'value3' }, // ampersand should be decoded per js-cookie
];

appendSetCookieHeadersToNextApiResponse(mockNextApiResponse, cookies);

expect(mockAppendHeader).toHaveBeenCalledTimes(3);
expect(mockAppendHeader).toHaveBeenNthCalledWith(
1,
'Set-Cookie',
'cookie%20name=value1;', // space encoded
);
expect(mockAppendHeader).toHaveBeenNthCalledWith(
2,
'Set-Cookie',
'cookie%3Dname=value2;', // equals encoded
);
expect(mockAppendHeader).toHaveBeenNthCalledWith(
3,
'Set-Cookie',
'cookie&name=value3;', // ampersand decoded per js-cookie behavior
);
});

it('handles complex cookie names with mixed special characters', () => {
const mockAppendHeader = jest.fn();
const mockNextApiResponse = {
appendHeader: mockAppendHeader,
} as unknown as NextApiResponse;
const cookies = [
{ name: 'amplify.auth.token', value: 'token1' },
{ name: 'amplify auth token', value: 'token2' },
{ name: 'amplify=auth&token', value: 'token3' },
];

appendSetCookieHeadersToNextApiResponse(mockNextApiResponse, cookies);

expect(mockAppendHeader).toHaveBeenCalledTimes(3);
expect(mockAppendHeader).toHaveBeenNthCalledWith(
1,
'Set-Cookie',
'amplify.auth.token=token1;',
);
expect(mockAppendHeader).toHaveBeenNthCalledWith(
2,
'Set-Cookie',
'amplify%20auth%20token=token2;',
);
expect(mockAppendHeader).toHaveBeenNthCalledWith(
3,
'Set-Cookie',
'amplify%3Dauth&token=token3;',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { ensureEncodedForJSCookie } from '../../../src/utils/cookie';

describe('ensureEncodedForJSCookie', () => {
it('should encode cookie names for js-cookie compatibility', () => {
// Test basic encoding
expect(ensureEncodedForJSCookie('simple')).toBe('simple');
expect(ensureEncodedForJSCookie('cookie-name')).toBe('cookie-name');
expect(ensureEncodedForJSCookie('cookie_name')).toBe('cookie_name');
});

it('should handle special characters that need encoding', () => {
// Test characters that should be encoded
expect(ensureEncodedForJSCookie('cookie name')).toBe('cookie%20name');
expect(ensureEncodedForJSCookie('cookie=name')).toBe('cookie%3Dname');
expect(ensureEncodedForJSCookie('cookie;name')).toBe('cookie%3Bname');
expect(ensureEncodedForJSCookie('cookie,name')).toBe('cookie%2Cname');
});

it('should decode specific encoded characters per js-cookie behavior', () => {
// Test characters that js-cookie decodes: %(2[346B]|5E|60|7C)
// These correspond to: #$&+^`|
expect(ensureEncodedForJSCookie('cookie#name')).toBe('cookie#name'); // %23 -> #
expect(ensureEncodedForJSCookie('cookie$name')).toBe('cookie$name'); // %24 -> $
expect(ensureEncodedForJSCookie('cookie&name')).toBe('cookie&name'); // %26 -> &
expect(ensureEncodedForJSCookie('cookie+name')).toBe('cookie+name'); // %2B -> +
expect(ensureEncodedForJSCookie('cookie^name')).toBe('cookie^name'); // %5E -> ^
expect(ensureEncodedForJSCookie('cookie`name')).toBe('cookie`name'); // %60 -> `
expect(ensureEncodedForJSCookie('cookie|name')).toBe('cookie|name'); // %7C -> |
});

it('should handle complex cookie names with mixed characters', () => {
// Test realistic auth cookie names that might contain special characters
expect(ensureEncodedForJSCookie('amplify.auth.token')).toBe(
'amplify.auth.token',
);
expect(ensureEncodedForJSCookie('amplify-auth-token')).toBe(
'amplify-auth-token',
);
expect(ensureEncodedForJSCookie('amplify_auth_token')).toBe(
'amplify_auth_token',
);

// Test with spaces and special chars
expect(ensureEncodedForJSCookie('amplify auth token')).toBe(
'amplify%20auth%20token',
);
expect(ensureEncodedForJSCookie('amplify=auth&token')).toBe(
'amplify%3Dauth&token',
);
});

it('should handle empty and edge case inputs', () => {
expect(ensureEncodedForJSCookie('')).toBe('');
expect(ensureEncodedForJSCookie('a')).toBe('a');
expect(ensureEncodedForJSCookie('123')).toBe('123');
});

it('should handle Unicode characters', () => {
expect(ensureEncodedForJSCookie('cookie🍪name')).toBe(
'cookie%F0%9F%8D%AAname',
);
expect(ensureEncodedForJSCookie('cookieñame')).toBe('cookie%C3%B1ame');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { CookieStorage } from 'aws-amplify/adapter-core';

import { serializeCookie } from '../../../src/utils/cookie';

describe('serializeCookie', () => {
it('should serialize basic cookie without options', () => {
expect(serializeCookie('name', 'value')).toBe('name=value;');
});

it('should encode cookie names for js-cookie compatibility', () => {
// Test basic encoding
expect(serializeCookie('simple', 'value')).toBe('simple=value;');
expect(serializeCookie('cookie-name', 'value')).toBe('cookie-name=value;');
expect(serializeCookie('cookie_name', 'value')).toBe('cookie_name=value;');

// Test characters that should be encoded
expect(serializeCookie('cookie name', 'value')).toBe(
'cookie%20name=value;',
);
expect(serializeCookie('cookie=name', 'value')).toBe(
'cookie%3Dname=value;',
);
expect(serializeCookie('cookie;name', 'value')).toBe(
'cookie%3Bname=value;',
);
expect(serializeCookie('cookie,name', 'value')).toBe(
'cookie%2Cname=value;',
);

// Test characters that js-cookie decodes: %(2[346B]|5E|60|7C)
// These correspond to: #$&+^`|
expect(serializeCookie('cookie#name', 'value')).toBe('cookie#name=value;');
expect(serializeCookie('cookie$name', 'value')).toBe('cookie$name=value;');
expect(serializeCookie('cookie&name', 'value')).toBe('cookie&name=value;');
expect(serializeCookie('cookie+name', 'value')).toBe('cookie+name=value;');
expect(serializeCookie('cookie^name', 'value')).toBe('cookie^name=value;');
expect(serializeCookie('cookie`name', 'value')).toBe('cookie`name=value;');
expect(serializeCookie('cookie|name', 'value')).toBe('cookie|name=value;');
});

it('should serialize cookie with options', () => {
const options: CookieStorage.SetCookieOptions = {
domain: 'example.com',
sameSite: 'strict',
path: '/',
httpOnly: true,
secure: true,
};

expect(serializeCookie('name', 'value', options)).toBe(
'name=value;Domain=example.com;HttpOnly;SameSite=strict;Secure;Path=/',
);
});

it('should handle complex cookie names with options', () => {
const options: CookieStorage.SetCookieOptions = {
domain: 'example.com',
path: '/',
};

expect(serializeCookie('amplify auth token', 'token123', options)).toBe(
'amplify%20auth%20token=token123;Domain=example.com;Path=/',
);

expect(serializeCookie('amplify=auth&token', 'token456', options)).toBe(
'amplify%3Dauth&token=token456;Domain=example.com;Path=/',
);
});

it('should handle expires option', () => {
const expires = new Date('2024-12-31T23:59:59.999Z');
const options: CookieStorage.SetCookieOptions = {
expires,
};

expect(serializeCookie('name', 'value', options)).toBe(
'name=value;Expires=Tue, 31 Dec 2024 23:59:59 GMT',
);
});

it('should handle maxAge option', () => {
const options: CookieStorage.SetCookieOptions = {
maxAge: 3600,
};

expect(serializeCookie('name', 'value', options)).toBe(
'name=value;Max-Age=3600',
);
});

it('should handle all options together', () => {
const expires = new Date('2024-12-31T23:59:59.999Z');
const options: CookieStorage.SetCookieOptions = {
domain: 'example.com',
expires,
httpOnly: true,
sameSite: 'lax',
secure: true,
path: '/auth',
maxAge: 7200,
};

expect(serializeCookie('auth token', 'abc123', options)).toBe(
'auth%20token=abc123;Domain=example.com;Expires=Tue, 31 Dec 2024 23:59:59 GMT;HttpOnly;SameSite=lax;Secure;Path=/auth;Max-Age=7200',
);
});
});
4 changes: 2 additions & 2 deletions packages/adapter-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "The adapter for the supporting of using Amplify APIs in Next.js.",
"peerDependencies": {
"aws-amplify": "^6.13.1",
"next": ">=13.5.0 <16.0.0"
"next": ">=13.5.0 <17.0.0"
},
"dependencies": {
"aws-jwt-verify": "^4.0.1"
Expand All @@ -16,7 +16,7 @@
"@types/react-dom": "^18.2.6",
"aws-amplify": "6.15.9",
"jest-fetch-mock": "3.0.3",
"next": ">= 13.5.0 < 15.0.0"
"next": ">= 13.5.0 <17.0.0"
},
"publishConfig": {
"access": "public"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

import { CookieStorage } from 'aws-amplify/adapter-core';

import { ensureEncodedForJSCookie } from './ensureEncodedForJSCookie';

export const serializeCookie = (
name: string,
value: string,
options?: CookieStorage.SetCookieOptions,
): string =>
`${name}=${value};${options ? serializeSetCookieOptions(options) : ''}`;
`${ensureEncodedForJSCookie(name)}=${value};${options ? serializeSetCookieOptions(options) : ''}`;

const serializeSetCookieOptions = (
options: CookieStorage.SetCookieOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ const createCookieStorageAdapterFromGetServerSidePropsContext = (

response.appendHeader(
'Set-Cookie',
serializeCookie(encodedName, value, options),
serializeCookie(name, value, options),
);
},
delete(name) {
Expand Down Expand Up @@ -259,10 +259,7 @@ const createMutableCookieStoreFromHeaders = (
return;
}

headers.append(
'Set-Cookie',
serializeCookie(ensureEncodedForJSCookie(name), value, options),
);
headers.append('Set-Cookie', serializeCookie(name, value, options));
};
const deleteFunc: CookieStorage.Adapter['delete'] = name => {
if (shouldIgnoreCookie(ignoreNonServerSideCookies, name)) {
Expand Down
Loading
Loading