Skip to content
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

✨ add a utility getAccessToken to customerClient #1607

Merged
merged 3 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions .changeset/three-taxis-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/hydrogen': patch
---

✨ add a utility `getAccessToken` to customerClient
♻️ Refactor the internal of customerClient to use only one session key
michenly marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 9 additions & 2 deletions packages/hydrogen/docs/generated/generated_docs_data.json

Large diffs are not rendered by default.

115 changes: 37 additions & 78 deletions packages/hydrogen/src/customer/auth.helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
import type {HydrogenSession} from '../hydrogen';
import {CUSTOMER_ACCOUNT_SESSION_KEY} from './constants';
import {checkExpires, clearSession, refreshToken} from './auth.helpers';

vi.mock('./BadRequest', () => {
Expand Down Expand Up @@ -61,12 +62,12 @@ describe('auth.helpers', () => {
}

await expect(run).rejects.toThrowError(
'Unauthorized No refresh_token in the session. Make sure your session is configured correctly and passed to `createCustomerClient`.',
'Unauthorized No refreshToken found in the session. Make sure your session is configured correctly and passed to `createCustomerClient`.',
);
});

it('Throws Unauthorized when refresh token fails', async () => {
(session.get as any).mockResolvedValueOnce('refresh_token');
(session.get as any).mockReturnValueOnce({refreshToken: undefined});

fetch.mockResolvedValue(createFetchResponse('Unauthorized', {ok: false}));

Expand All @@ -83,7 +84,9 @@ describe('auth.helpers', () => {
});

it('Throws when there is no valid authorization code in the session', async () => {
(session.get as any).mockResolvedValueOnce('refresh_token');
(session.get as any).mockReturnValueOnce({
refreshToken: 'refreshToken',
});

fetch.mockResolvedValue(
createFetchResponse(
Expand All @@ -107,12 +110,14 @@ describe('auth.helpers', () => {
}

await expect(run).rejects.toThrowError(
'Unauthorized No access token found in the session. Make sure your session is configured correctly and passed to `createCustomerClient`',
'Unauthorized oAuth access token was not provided during token exchange.',
);
});

it('Refreshes the token', async () => {
(session.get as any).mockResolvedValue('value');
(session.get as any).mockReturnValueOnce({
refreshToken: 'old_refresh_token',
});

fetch.mockResolvedValue(
createFetchResponse(
Expand All @@ -135,24 +140,13 @@ describe('auth.helpers', () => {

expect(session.set).toHaveBeenNthCalledWith(
1,
'customer_authorization_code_token',
'access_token',
);
expect(session.set).toHaveBeenNthCalledWith(
2,
'expires_at',
expect.anything(),
);
expect(session.set).toHaveBeenNthCalledWith(3, 'id_token', 'id_token');
expect(session.set).toHaveBeenNthCalledWith(
4,
'refresh_token',
'refresh_token',
);
expect(session.set).toHaveBeenNthCalledWith(
5,
'customer_access_token',
'access_token',
CUSTOMER_ACCOUNT_SESSION_KEY,
{
accessToken: 'access_token',
expiresAt: expect.any(String),
refreshToken: 'refresh_token',
idToken: 'id_token',
},
);
});
});
Expand All @@ -173,16 +167,7 @@ describe('auth.helpers', () => {

it('Clears the session', async () => {
clearSession(session);
expect(session.unset).toHaveBeenCalledWith('code-verifier');
expect(session.unset).toHaveBeenCalledWith(
'customer_authorization_code_token',
);
expect(session.unset).toHaveBeenCalledWith('expires_at');
expect(session.unset).toHaveBeenCalledWith('id_token');
expect(session.unset).toHaveBeenCalledWith('refresh_token');
expect(session.unset).toHaveBeenCalledWith('customer_access_token');
expect(session.unset).toHaveBeenCalledWith('state');
expect(session.unset).toHaveBeenCalledWith('nonce');
expect(session.unset).toHaveBeenCalledWith(CUSTOMER_ACCOUNT_SESSION_KEY);
});
});

Expand Down Expand Up @@ -216,7 +201,9 @@ describe('auth.helpers', () => {
});

it('Refreshes the token', async () => {
(session.get as any).mockResolvedValue('value');
(session.get as any).mockReturnValueOnce({
refreshToken: 'old_refresh_token',
});

fetch.mockResolvedValue(
createFetchResponse(
Expand All @@ -241,29 +228,20 @@ describe('auth.helpers', () => {

expect(session.set).toHaveBeenNthCalledWith(
1,
'customer_authorization_code_token',
'access_token',
);
expect(session.set).toHaveBeenNthCalledWith(
2,
'expires_at',
expect.anything(),
);
expect(session.set).toHaveBeenNthCalledWith(3, 'id_token', 'id_token');
expect(session.set).toHaveBeenNthCalledWith(
4,
'refresh_token',
'refresh_token',
);
expect(session.set).toHaveBeenNthCalledWith(
5,
'customer_access_token',
'access_token',
CUSTOMER_ACCOUNT_SESSION_KEY,
{
accessToken: 'access_token',
expiresAt: expect.any(String),
refreshToken: 'refresh_token',
idToken: 'id_token',
},
);
});

it('does not refresh the token when a refresh is already in process', async () => {
(session.get as any).mockResolvedValue('value');
(session.get as any).mockReturnValueOnce({
refreshToken: 'old_refresh_token',
});

fetch.mockResolvedValue(
createFetchResponse(
Expand All @@ -289,31 +267,12 @@ describe('auth.helpers', () => {
origin: 'https://localhost',
});

expect(session.set).not.toHaveBeenNthCalledWith(
1,
'customer_authorization_code_token',
'access_token',
);
expect(session.set).not.toHaveBeenNthCalledWith(
2,
'expires_at',
expect.anything(),
);
expect(session.set).not.toHaveBeenNthCalledWith(
3,
'id_token',
'id_token',
);
expect(session.set).not.toHaveBeenNthCalledWith(
4,
'refresh_token',
'refresh_token',
);
expect(session.set).not.toHaveBeenNthCalledWith(
5,
'customer_access_token',
'access_token',
);
expect(session.set).not.toHaveBeenNthCalledWith(1, 'customerAccount', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely simpler at least in the tests when it's all grouped together :)

accessToken: 'access_token',
expiresAt: expect.any(String),
refreshToken: 'refresh_token',
idToken: 'id_token',
});
});
});
});
55 changes: 23 additions & 32 deletions packages/hydrogen/src/customer/auth.helpers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type {HydrogenSession} from '../hydrogen';
import {BadRequest} from './BadRequest';
import {LIB_VERSION} from '../version';
import {
USER_AGENT,
CUSTOMER_API_CLIENT_ID,
CUSTOMER_ACCOUNT_SESSION_KEY,
} from './constants';

export interface Locks {
refresh?: Promise<any>;
}

export const USER_AGENT = `Shopify Hydrogen ${LIB_VERSION}`;

const CUSTOMER_API_CLIENT_ID = '30243aa5-17c1-465a-8493-944bcc4e88aa';

export function redirect(
path: string,
options: {status?: number; headers?: {}} = {},
Expand Down Expand Up @@ -44,12 +44,13 @@ export async function refreshToken({
}) {
const newBody = new URLSearchParams();

const refreshToken = session.get('refresh_token');
const customerAccount = session.get(CUSTOMER_ACCOUNT_SESSION_KEY);
const refreshToken = customerAccount?.refreshToken;

if (!refreshToken)
throw new BadRequest(
'Unauthorized',
'No refresh_token in the session. Make sure your session is configured correctly and passed to `createCustomerClient`.',
'No refreshToken found in the session. Make sure your session is configured correctly and passed to `createCustomerClient`.',
);

newBody.append('grant_type', 'refresh_token');
Expand Down Expand Up @@ -81,34 +82,25 @@ export async function refreshToken({
const {access_token, expires_in, id_token, refresh_token} =
await response.json<AccessTokenResponse>();

session.set('customer_authorization_code_token', access_token);
// Store the date in future the token expires, separated by two minutes
session.set(
'expires_at',
new Date(new Date().getTime() + (expires_in - 120) * 1000).getTime() + '',
);
session.set('id_token', id_token);
session.set('refresh_token', refresh_token);

const customerAccessToken = await exchangeAccessToken(
session,
const accessToken = await exchangeAccessToken(
access_token,
customerAccountId,
customerAccountUrl,
origin,
);

session.set('customer_access_token', customerAccessToken);
session.set(CUSTOMER_ACCOUNT_SESSION_KEY, {
accessToken,
// Store the date in future the token expires, separated by two minutes
expiresAt:
new Date(new Date().getTime() + (expires_in - 120) * 1000).getTime() + '',
refreshToken: refresh_token,
idToken: id_token,
});
}

export function clearSession(session: HydrogenSession): void {
session.unset('code-verifier');
session.unset('customer_authorization_code_token');
session.unset('expires_at');
session.unset('id_token');
session.unset('refresh_token');
session.unset('customer_access_token');
session.unset('state');
session.unset('nonce');
session.unset(CUSTOMER_ACCOUNT_SESSION_KEY);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is better too, if we add other properties to the session, at least we won't accidentally forget to clear them.

}

export async function checkExpires({
Expand Down Expand Up @@ -193,26 +185,25 @@ export async function generateState(): Promise<string> {
}

export async function exchangeAccessToken(
session: HydrogenSession,
authAccessToken: string | undefined,
customerAccountId: string,
customerAccountUrl: string,
origin: string,
) {
const clientId = customerAccountId;
const accessToken = session.get('customer_authorization_code_token');

if (!accessToken)
if (!authAccessToken)
throw new BadRequest(
'Unauthorized',
'No access token found in the session. Make sure your session is configured correctly and passed to `createCustomerClient`.',
'oAuth access token was not provided during token exchange.',
);

const body = new URLSearchParams();

body.append('grant_type', 'urn:ietf:params:oauth:grant-type:token-exchange');
body.append('client_id', clientId);
body.append('audience', CUSTOMER_API_CLIENT_ID);
body.append('subject_token', accessToken);
body.append('subject_token', authAccessToken);
body.append(
'subject_token_type',
'urn:ietf:params:oauth:token-type:access_token',
Expand Down
6 changes: 6 additions & 0 deletions packages/hydrogen/src/customer/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {LIB_VERSION} from '../version';

export const DEFAULT_CUSTOMER_API_VERSION = '2024-01';
export const USER_AGENT = `Shopify Hydrogen ${LIB_VERSION}`;
export const CUSTOMER_API_CLIENT_ID = '30243aa5-17c1-465a-8493-944bcc4e88aa';
export const CUSTOMER_ACCOUNT_SESSION_KEY = 'customerAccount';
Loading
Loading