Skip to content

Commit

Permalink
feat(adapter-nextjs): set cookie secure: false with non-SSL domain (#…
Browse files Browse the repository at this point in the history
…13841)

* feat(adapter-nextjs): allow cookie secure: false with non-SSL domain

* fix(adapter-nextjs): wrong naming and impl. of isSSLOrigin

* chore(adapter-nextjs): resolve comment
  • Loading branch information
HuiSF authored Jan 3, 2025
1 parent 5bbd0ff commit f1a44ab
Show file tree
Hide file tree
Showing 17 changed files with 232 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
isNextApiRequest,
isNextApiResponse,
isNextRequest,
isValidOrigin,
} from '../../src/auth/utils';

jest.mock('@aws-amplify/core/internals/utils');
Expand Down Expand Up @@ -63,12 +64,17 @@ const mockIsNextRequest = jest.mocked(isNextRequest);
const mockIsAuthRoutesHandlersContext = jest.mocked(
isAuthRoutesHandlersContext,
);
const mockIsValidOrigin = jest.mocked(isValidOrigin);
const mockRunWithAmplifyServerContext =
jest.fn() as jest.MockedFunction<NextServer.RunOperationWithContext>;

describe('createAuthRoutesHandlersFactory', () => {
const AMPLIFY_APP_ORIGIN = 'https://example.com';

beforeAll(() => {
mockIsValidOrigin.mockReturnValue(true);
});

it('throws an error if the `amplifyAppOrigin` param has value of `undefined`', () => {
expect(() =>
createAuthRouteHandlersFactory({
Expand All @@ -80,6 +86,20 @@ describe('createAuthRoutesHandlersFactory', () => {
).toThrow('Could not find the AMPLIFY_APP_ORIGIN environment variable.');
});

it('throws an error if the AMPLIFY_APP_ORIGIN environment variable is invalid', () => {
mockIsValidOrigin.mockReturnValueOnce(false);
expect(() =>
createAuthRouteHandlersFactory({
config: mockAmplifyConfig,
runtimeOptions: mockRuntimeOptions,
amplifyAppOrigin: 'domain-without-protocol.com',
runWithAmplifyServerContext: mockRunWithAmplifyServerContext,
}),
).toThrow(
'AMPLIFY_APP_ORIGIN environment variable contains an invalid origin string.',
);
});

it('calls config assertion functions to validate the Auth configuration', () => {
createAuthRouteHandlersFactory({
config: mockAmplifyConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
createSignInFlowProofCookies,
createSignUpEndpoint,
createUrlSearchParamsForSignInSignUp,
isSSLOrigin,
} from '../../../src/auth/utils';

jest.mock('../../../src/auth/utils');
Expand All @@ -30,6 +31,7 @@ const mockCreateSignUpEndpoint = jest.mocked(createSignUpEndpoint);
const mockCreateUrlSearchParamsForSignInSignUp = jest.mocked(
createUrlSearchParamsForSignInSignUp,
);
const mockIsSSLOrigin = jest.mocked(isSSLOrigin);

describe('handleSignInSignUpRequest', () => {
const mockCustomState = 'mockCustomState';
Expand All @@ -41,6 +43,10 @@ describe('handleSignInSignUpRequest', () => {
};
const mockToCodeChallenge = jest.fn(() => 'mockCodeChallenge');

beforeAll(() => {
mockIsSSLOrigin.mockReturnValue(true);
});

afterEach(() => {
mockAppendSetCookieHeaders.mockClear();
mockCreateAuthFlowProofCookiesSetOptions.mockClear();
Expand All @@ -50,6 +56,7 @@ describe('handleSignInSignUpRequest', () => {
mockCreateSignUpEndpoint.mockClear();
mockCreateUrlSearchParamsForSignInSignUp.mockClear();
mockToCodeChallenge.mockClear();
mockIsSSLOrigin.mockClear();
});

test.each(['signIn' as const, 'signUp' as const])(
Expand Down Expand Up @@ -145,11 +152,19 @@ describe('handleSignInSignUpRequest', () => {
);
}

expect(mockCreateAuthFlowProofCookiesSetOptions).toHaveBeenCalledWith(
mockSetCookieOptions,
{
secure: true,
},
);

expect(mockAppendSetCookieHeaders).toHaveBeenCalledWith(
expect.any(Headers),
mockCreateSignInFlowProofCookiesResult,
mockCreateAuthFlowProofCookiesSetOptionsResult,
);
expect(isSSLOrigin).toHaveBeenCalledWith(mockOrigin);
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createSignInFlowProofCookies,
createSignUpEndpoint,
createUrlSearchParamsForSignInSignUp,
isSSLOrigin,
} from '../../../src/auth/utils';
import { createMockNextApiResponse } from '../testUtils';

Expand All @@ -34,6 +35,7 @@ const mockCreateSignUpEndpoint = jest.mocked(createSignUpEndpoint);
const mockCreateUrlSearchParamsForSignInSignUp = jest.mocked(
createUrlSearchParamsForSignInSignUp,
);
const mockIsSSLOrigin = jest.mocked(isSSLOrigin);

describe('handleSignInSignUpRequest', () => {
const mockCustomState = 'mockCustomState';
Expand All @@ -54,6 +56,10 @@ describe('handleSignInSignUpRequest', () => {
mockResponse,
} = createMockNextApiResponse();

beforeAll(() => {
mockIsSSLOrigin.mockReturnValue(true);
});

afterEach(() => {
mockAppendSetCookieHeadersToNextApiResponse.mockClear();
mockCreateAuthFlowProofCookiesSetOptions.mockClear();
Expand All @@ -63,6 +69,7 @@ describe('handleSignInSignUpRequest', () => {
mockCreateSignUpEndpoint.mockClear();
mockCreateUrlSearchParamsForSignInSignUp.mockClear();
mockToCodeChallenge.mockClear();
mockIsSSLOrigin.mockClear();

mockResponseAppendHeader.mockClear();
mockResponseEnd.mockClear();
Expand Down Expand Up @@ -170,11 +177,19 @@ describe('handleSignInSignUpRequest', () => {
);
}

expect(mockCreateAuthFlowProofCookiesSetOptions).toHaveBeenCalledWith(
mockSetCookieOptions,
{
secure: true,
},
);

expect(mockAppendSetCookieHeadersToNextApiResponse).toHaveBeenCalledWith(
mockResponse,
mockCreateSignInFlowProofCookiesResult,
mockCreateAuthFlowProofCookiesSetOptionsResult,
);
expect(isSSLOrigin).toHaveBeenCalledWith(mockOrigin);
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createAuthFlowProofCookiesSetOptions,
createLogoutEndpoint,
createSignOutFlowProofCookies,
isSSLOrigin,
resolveRedirectSignOutUrl,
} from '../../../src/auth/utils';

Expand All @@ -23,14 +24,20 @@ const mockCreateSignOutFlowProofCookies = jest.mocked(
createSignOutFlowProofCookies,
);
const mockResolveRedirectSignOutUrl = jest.mocked(resolveRedirectSignOutUrl);
const mockIsSSLOrigin = jest.mocked(isSSLOrigin);

describe('handleSignOutRequest', () => {
beforeAll(() => {
mockIsSSLOrigin.mockReturnValue(true);
});

afterEach(() => {
mockAppendSetCookieHeaders.mockClear();
mockCreateAuthFlowProofCookiesSetOptions.mockClear();
mockCreateLogoutEndpoint.mockClear();
mockCreateSignOutFlowProofCookies.mockClear();
mockResolveRedirectSignOutUrl.mockClear();
mockIsSSLOrigin.mockClear();
});

it('returns a 302 response with the correct headers and cookies', async () => {
Expand Down Expand Up @@ -93,8 +100,12 @@ describe('handleSignOutRequest', () => {
expect.any(URLSearchParams),
);
expect(mockCreateSignOutFlowProofCookies).toHaveBeenCalled();
expect(mockIsSSLOrigin).toHaveBeenCalledWith(mockOrigin);
expect(mockCreateAuthFlowProofCookiesSetOptions).toHaveBeenCalledWith(
mockSetCookieOptions,
{
secure: true,
},
);
expect(mockAppendSetCookieHeaders).toHaveBeenCalledWith(
expect.any(Headers),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createAuthFlowProofCookiesSetOptions,
createLogoutEndpoint,
createSignOutFlowProofCookies,
isSSLOrigin,
resolveRedirectSignOutUrl,
} from '../../../src/auth/utils';
import { createMockNextApiResponse } from '../testUtils';
Expand All @@ -26,6 +27,7 @@ const mockCreateSignOutFlowProofCookies = jest.mocked(
createSignOutFlowProofCookies,
);
const mockResolveRedirectSignOutUrl = jest.mocked(resolveRedirectSignOutUrl);
const mockIsSSLOrigin = jest.mocked(isSSLOrigin);

describe('handleSignOutRequest', () => {
const {
Expand All @@ -37,6 +39,10 @@ describe('handleSignOutRequest', () => {
mockResponse,
} = createMockNextApiResponse();

beforeAll(() => {
mockIsSSLOrigin.mockReturnValue(true);
});

afterEach(() => {
mockAppendSetCookieHeadersToNextApiResponse.mockClear();
mockCreateAuthFlowProofCookiesSetOptions.mockClear();
Expand Down Expand Up @@ -117,8 +123,12 @@ describe('handleSignOutRequest', () => {
expect.any(URLSearchParams),
);
expect(mockCreateSignOutFlowProofCookies).toHaveBeenCalled();
expect(mockIsSSLOrigin).toHaveBeenCalledWith(mockOrigin);
expect(mockCreateAuthFlowProofCookiesSetOptions).toHaveBeenCalledWith(
mockSetCookieOptions,
{
secure: true,
},
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ describe('createAuthFlowProofCookiesSetOptions', () => {
expires: new Date(0 + AUTH_FLOW_PROOF_COOKIE_EXPIRY),
});
});

it('returns expected cookie serialization options with specified parameters with overridden secure attribute', () => {
const setCookieOptions: CookieStorage.SetCookieOptions = {
domain: '.example.com',
sameSite: 'strict',
};

const options = createAuthFlowProofCookiesSetOptions(setCookieOptions, {
secure: false,
});

expect(nowSpy).toHaveBeenCalled();
expect(options).toEqual({
domain: setCookieOptions?.domain,
path: '/',
httpOnly: true,
secure: false,
sameSite: 'lax' as const,
expires: new Date(0 + AUTH_FLOW_PROOF_COOKIE_EXPIRY),
});
});
});

describe('createAuthFlowProofCookiesRemoveOptions', () => {
Expand Down
59 changes: 59 additions & 0 deletions packages/adapter-nextjs/__tests__/auth/utils/isValidOrigin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { isSSLOrigin, isValidOrigin } from '../../../src/auth/utils/origin';

describe('isValidOrigin', () => {
test.each([
// Valid origins
['http://example.com', true],
['https://example.com', true],
['http://www.example.com', true],
['https://subdomain.example.com', true],
['http://example.com:8080', true],
['https://example.com:443', true],
['http://localhost', true],
['http://localhost:3000', true],
['https://localhost:8080', true],
['http://127.0.0.1', true],
['http://127.0.0.1:8000', true],

// Invalid origins
['http://example.com/path', false],
['https://example.com/path/to/resource', false],
['http://example.com:8080/path', false],
['ftp://example.com', false],
['example.com', false],
['http:/example.com', false],
['https:example.com', false],
['http://', false],
['https://', false],
['localhost', false],
['http:localhost', false],
['https://localhost:', false],
['http://127.0.0.1:', false],
['https://.com', false],
['http://example.', false],
['https://example.com:abc', false],
['http:// example.com', false],
['https://exam ple.com', false],
['http://exa mple.com:8080', false],
['https://example.com:8080:8081', false],
['http://example.com:80:80', false],
['https://.example.com', false],
['http://example..com', false],
['https://exam_ple.com', false],
['https://example.com?query=param', false],
['https://example.com:80/path#fragment', false],
] as [string, boolean][])('validates origin %s as %s', (origin, expected) => {
expect(isValidOrigin(origin)).toBe(expected);
});
});

describe('isSSLOrigin', () => {
test.each([
['https://some-app.com', true],
['http://localhost', false],
['http://localhost:3000', false],
['https:// some-app.com', false],
])('check origin SSL %s status as %s', (origin, expected) => {
expect(isSSLOrigin(origin)).toBe(expected);
});
});
21 changes: 21 additions & 0 deletions packages/adapter-nextjs/__tests__/auth/utils/tokenCookies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,27 @@ describe('createTokenCookiesSetOptions', () => {

dateNowSpy.mockRestore();
});

it('returns an object with the correct cookie options with overridden secure attribute', () => {
const mockSetCookieOptions: CookieStorage.SetCookieOptions = {
domain: '.example.com',
sameSite: 'strict',
expires: new Date('2024-09-17'),
};

const result = createTokenCookiesSetOptions(mockSetCookieOptions, {
secure: false,
});

expect(result).toEqual({
domain: mockSetCookieOptions.domain,
path: '/',
httpOnly: true,
secure: false,
sameSite: 'strict',
expires: mockSetCookieOptions.expires,
});
});
});

describe('createTokenCookiesRemoveOptions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isNextApiRequest,
isNextApiResponse,
isNextRequest,
isValidOrigin,
} from './utils';
import { handleAuthApiRouteRequestForAppRouter } from './handleAuthApiRouteRequestForAppRouter';
import { handleAuthApiRouteRequestForPagesRouter } from './handleAuthApiRouteRequestForPagesRouter';
Expand All @@ -30,12 +31,22 @@ export const createAuthRouteHandlersFactory = ({
amplifyAppOrigin,
runWithAmplifyServerContext,
}: CreateAuthRouteHandlersFactoryInput): InternalCreateAuthRouteHandlers => {
if (!amplifyAppOrigin)
if (!amplifyAppOrigin) {
throw new AmplifyServerContextError({
message: 'Could not find the AMPLIFY_APP_ORIGIN environment variable.',
recoverySuggestion:
'Add the AMPLIFY_APP_ORIGIN environment variable to the `.env` file of your Next.js project.',
});
}

if (!isValidOrigin(amplifyAppOrigin)) {
throw new AmplifyServerContextError({
message:
'AMPLIFY_APP_ORIGIN environment variable contains an invalid origin string.',
recoverySuggestion:
'Ensure the AMPLIFY_APP_ORIGIN environment variable is a valid origin string.',
});
}

assertTokenProviderConfig(resourcesConfig.Auth?.Cognito);
assertOAuthConfig(resourcesConfig.Auth.Cognito);
Expand Down
Loading

0 comments on commit f1a44ab

Please sign in to comment.