Skip to content

Fix the customer account implementation to clear all session data on logout #2843

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

Merged
merged 1 commit into from
Apr 24, 2025
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
13 changes: 13 additions & 0 deletions .changeset/four-trainers-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@shopify/hydrogen': patch
---

Fix the customer account implementation to clear all session data on logout. Previously we would only clear customer account credentials on logout. This change also clears any custom data in the session as well. You can opt out and keep custom data in the session by passing the `keepSession` option to logout:

```js
export async function action({context}: ActionFunctionArgs) {
return context.customerAccount.logout({
keepSession: true
});
}
```
37 changes: 35 additions & 2 deletions packages/hydrogen/src/customer/customer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ describe('customer', () => {
}) as HydrogenSession['get'],
set: vi.fn(),
unset: vi.fn(),
destroy: vi.fn(() => Promise.resolve('logout cookie')),
};
});

Expand Down Expand Up @@ -261,6 +262,8 @@ describe('customer', () => {
expect(session.unset).toHaveBeenCalledWith(
CUSTOMER_ACCOUNT_SESSION_KEY,
);

expect(session.destroy).toHaveBeenCalled();
});

it('Redirects to the customer account api logout url with postLogoutRedirectUri in the param', async () => {
Expand Down Expand Up @@ -295,7 +298,6 @@ describe('customer', () => {

it('Redirects to the customer account api logout url with optional headers from params included', async () => {
const origin = 'https://shop123.com';
const postLogoutRedirectUri = '/post-logout-landing-page';
const headers = {'Set-Cookie': 'cookie=test;'};

const customer = createCustomerAccountClient({
Expand All @@ -312,21 +314,49 @@ describe('customer', () => {
expect(url.origin).toBe('https://shopify.com');
expect(url.pathname).toBe('/authentication/1/logout');

expect(response.headers.get('Set-Cookie')).toBe('cookie=test;');
// Session destroyed
expect(response.headers.get('Set-Cookie')).toBe('logout cookie');

// Session is cleared
expect(session.unset).toHaveBeenCalledWith(
CUSTOMER_ACCOUNT_SESSION_KEY,
);
});

it('Keeps session data when keepSession is true', async () => {
const origin = 'https://shop123.com';
const headers = {'Set-Cookie': 'cookie=test;'};

const customer = createCustomerAccountClient({
session,
customerAccountId: 'customerAccountId',
shopId: '1',
request: new Request(origin),
waitUntil: vi.fn(),
});

const response = await customer.logout({headers, keepSession: true});

const url = new URL(response.headers.get('location')!);
expect(url.origin).toBe('https://shopify.com');
expect(url.pathname).toBe('/authentication/1/logout');

// Standard cookie, session isn't destroyed
expect(response.headers.get('Set-Cookie')).toBe('cookie=test;');

expect(session.unset).toHaveBeenCalledWith(
CUSTOMER_ACCOUNT_SESSION_KEY,
);
});

it('Redirects to app origin when customer is not login by default', async () => {
const origin = 'https://shop123.com';
const mockSession: HydrogenSession = {
commit: vi.fn(() => new Promise((resolve) => resolve('cookie'))),
get: vi.fn(() => undefined) as HydrogenSession['get'],
set: vi.fn(),
unset: vi.fn(),
destroy: vi.fn(() => Promise.resolve('logout cookie')),
};

const customer = createCustomerAccountClient({
Expand Down Expand Up @@ -357,6 +387,7 @@ describe('customer', () => {
get: vi.fn(() => undefined) as HydrogenSession['get'],
set: vi.fn(),
unset: vi.fn(),
destroy: vi.fn(() => Promise.resolve('logout cookie')),
};

const customer = createCustomerAccountClient({
Expand Down Expand Up @@ -390,6 +421,7 @@ describe('customer', () => {
get: vi.fn(() => undefined) as HydrogenSession['get'],
set: vi.fn(),
unset: vi.fn(),
destroy: vi.fn(() => Promise.resolve('logout cookie')),
};

const customer = createCustomerAccountClient({
Expand Down Expand Up @@ -720,6 +752,7 @@ describe('customer', () => {
}) as HydrogenSession['get'],
set: vi.fn(),
unset: vi.fn(),
destroy: vi.fn(() => Promise.resolve('logout cookie')),
};

const customer = createCustomerAccountClient({
Expand Down
19 changes: 18 additions & 1 deletion packages/hydrogen/src/customer/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,24 @@ export function createCustomerAccountClient({

clearSession(session);

return redirect(logoutUrl, {headers: options?.headers || {}});
const headers =
options?.headers instanceof Headers
? options?.headers
: new Headers(options?.headers);

if (!options?.keepSession) {
if (session.destroy) {
headers.set('Set-Cookie', await session.destroy());
} else {
console.warn(
'[h2:warn:customerAccount] session.destroy is not available on your session implementation. All session data might not be cleared on logout.',
);
}

session.isPending = false;
}

return redirect(logoutUrl, {headers});
},
isLoggedIn,
handleAuthStatus,
Expand Down
7 changes: 7 additions & 0 deletions packages/hydrogen/src/customer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,12 @@ export type LoginOptions = {
};

export type LogoutOptions = {
/** The url to redirect customer to after logout, should be a relative URL. This url will need to included in Customer Account API's application setup for logout URI. The default value is current app origin, which is automatically setup in admin when using `--customer-account-push` flag with dev. */
postLogoutRedirectUri?: string;
/** Add custom headers to the logout redirect. */
headers?: HeadersInit;
/** If true, custom data in the session will not be cleared on logout. */
keepSession?: boolean;
};

export type CustomerAccount = {
Expand Down Expand Up @@ -83,6 +87,7 @@ export type CustomerAccount = {
*
* @param options.postLogoutRedirectUri - The url to redirect customer to after logout, should be a relative URL. This url will need to included in Customer Account API's application setup for logout URI. The default value is current app origin, which is automatically setup in admin when using `--customer-account-push` flag with dev.
* @param options.headers - These will be passed along to the logout redirect. You can use these to set/clear cookies on logout, like the cart.
* @param options.keepSession - If true, custom data in the session will not be cleared on logout.
* */
logout: (options?: LogoutOptions) => Promise<Response>;
/** Execute a GraphQL query against the Customer Account API. This method execute `handleAuthStatus()` ahead of query. */
Expand Down Expand Up @@ -188,6 +193,8 @@ export type CustomerAccountForDocs = {
/** Logout the customer by clearing the session and redirecting to the login domain. It should be called and returned from a Remix action. The path app should redirect to after logout can be setup in Customer Account API settings in admin.
*
* @param options.postLogoutRedirectUri - The url to redirect customer to after logout, should be a relative URL. This url will need to included in Customer Account API's application setup for logout URI. The default value is current app origin, which is automatically setup in admin when using `--customer-account-push` flag with dev.
* @param options.headers - These will be passed along to the logout redirect. You can use these to set/clear cookies on logout, like the cart.
* @param options.keepSession - If true, custom data in the session will not be cleared on logout.
* */
logout?: (options?: LogoutOptions) => Promise<Response>;
/** Execute a GraphQL query against the Customer Account API. This method execute `handleAuthStatus()` ahead of query. */
Expand Down
3 changes: 3 additions & 0 deletions packages/hydrogen/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export interface HydrogenSession<
commit: () => ReturnType<
SessionStorage<HydrogenSessionData & Data, FlashData>['commitSession']
>;
destroy?: () => ReturnType<
SessionStorage<HydrogenSessionData & Data, FlashData>['destroySession']
>;
isPending?: boolean;
}

Expand Down