Skip to content

Commit ccaf553

Browse files
authored
feat: react spa auth example (#405)
* feat: add basic generated repo * feat: working pwa example * chore: update readme * fix: prevent infinite loop * feat: rename app * feat: rename to spa
1 parent a4a6388 commit ccaf553

File tree

20 files changed

+3080
-0
lines changed

20 files changed

+3080
-0
lines changed

examples/authentication-local-storage/src/app/auth/StorefrontProvider.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export function StorefrontProvider({
2626
const interceptor: Parameters<
2727
typeof client.interceptors.request.use
2828
>[0] = async (request) => {
29+
// Bypass interceptor logic for token requests to prevent infinite loop
30+
if (request.url?.includes("/oauth/access_token")) {
31+
return request
32+
}
33+
2934
let credentials = JSON.parse(
3035
localStorage.getItem(CREDENTIALS_COOKIE_KEY) ?? "{}",
3136
) as AccessTokenResponse | undefined
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

examples/spa-authentication/README.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Client-Side Local Storage Authentication Example (React SPA with Vite)
2+
3+
This example demonstrates how to authenticate a storefront to Elastic Path Commerce Cloud using client-side local storage within a Single Page Application (SPA) built with React and Vite. This approach provides a simple method for connecting your frontend to Elastic Path's public-facing endpoints without requiring server-side infrastructure for authentication.
4+
5+
## ⚠️ Security Warning
6+
7+
**This example uses local storage for token storage, which has significant security implications:**
8+
9+
- **XSS Vulnerability**: Tokens stored in local storage are accessible by any JavaScript running on your page, making them vulnerable to Cross-Site Scripting (XSS) attacks. If an attacker can inject JavaScript into your site, they can steal the tokens.
10+
- **No HttpOnly Flag**: Unlike cookies, local storage cannot use the HttpOnly flag that would prevent JavaScript access to the token.
11+
- **Persistent by Default**: Tokens remain in local storage until explicitly removed or the browser storage is cleared, potentially exposing them for longer than necessary.
12+
- **CSRF Protection Needed**: When using local storage for authentication, you need to implement additional protection against Cross-Site Request Forgery (CSRF) attacks.
13+
14+
**For production applications with sensitive data, consider:**
15+
16+
- Using HTTP-only cookies for token storage (potentially with a backend-for-frontend)
17+
- Implementing server-side authentication flows
18+
- Implementing proper CSRF protection
19+
20+
This example demonstrates the technical implementation but should be adapted with appropriate security measures for production use.
21+
22+
## Overview
23+
24+
This Vite-based React SPA example shows:
25+
26+
- How to authenticate a storefront to Elastic Path using implicit authentication.
27+
- How to store authentication tokens in browser local storage.
28+
- How to automatically refresh expired tokens via SDK interceptors.
29+
- How to use the authenticated client to fetch product data from the Elastic Path backend.
30+
- How SDK interceptors automatically attach tokens from local storage to API requests.
31+
- Basic SPA setup using Vite.
32+
33+
## Authentication Flow
34+
35+
This example uses a React context provider (`StorefrontProvider`) to implement the authentication flow:
36+
37+
1. When the application loads, the `StorefrontProvider` sets up an interceptor to handle authentication.
38+
2. For each API request made via the SDK:
39+
- The interceptor checks for an existing authentication token in local storage.
40+
- If a token exists and is valid, it attaches it to the request.
41+
- If no token exists or the token has expired, it:
42+
- Requests a new access token using the Elastic Path SDK's `createAnAccessToken` method with the implicit grant type.
43+
- Stores the new token in the browser's local storage.
44+
- Attaches the token to the current request.
45+
- The interceptor bypasses this logic for requests to the token endpoint itself to prevent infinite loops.
46+
47+
## How the SDK is Used
48+
49+
The example uses the `@epcc-sdk/sdks-shopper` package to:
50+
51+
1. **Create and configure the client**: Setting the base URL for the Elastic Path API using Vite environment variables.
52+
53+
```typescript
54+
// src/auth/StorefrontProvider.tsx
55+
client.setConfig({
56+
baseUrl: import.meta.env.VITE_APP_EPCC_ENDPOINT_URL!,
57+
})
58+
```
59+
60+
2. **Create authentication tokens**: Using the `createAnAccessToken` function with the implicit grant flow.
61+
62+
```typescript
63+
// src/auth/StorefrontProvider.tsx
64+
const authResponse = await createAnAccessToken({
65+
body: {
66+
grant_type: "implicit",
67+
client_id: import.meta.env.VITE_APP_EPCC_CLIENT_ID, // Vite environment variable
68+
},
69+
})
70+
```
71+
72+
3. **Fetch data**: Using the `getByContextAllProducts` function to retrieve product data from the catalog.
73+
```typescript
74+
// src/App.tsx
75+
const response = await getByContextAllProducts()
76+
```
77+
78+
### SDK Interceptors
79+
80+
A key part of this implementation is the use of SDK interceptors to seamlessly handle authentication:
81+
82+
```typescript
83+
// src/auth/StorefrontProvider.tsx
84+
const interceptor = async (
85+
request: EpccRequesterRequest,
86+
): Promise<EpccRequesterRequest> => {
87+
// Bypass interceptor logic for token requests to prevent infinite loop
88+
if (request.url?.includes("/oauth/access_token")) {
89+
return request
90+
}
91+
92+
let credentials = JSON.parse(
93+
localStorage.getItem(CREDENTIALS_COOKIE_KEY) ?? "{}",
94+
) as AccessTokenResponse | undefined
95+
96+
// check if token expired or missing
97+
if (
98+
!credentials?.access_token ||
99+
(credentials.expires && tokenExpired(credentials.expires))
100+
) {
101+
const clientId = import.meta.env.VITE_APP_EPCC_CLIENT_ID
102+
// ... (token fetching logic) ...
103+
localStorage.setItem(CREDENTIALS_COOKIE_KEY, JSON.stringify(token))
104+
credentials = token
105+
}
106+
107+
if (credentials?.access_token) {
108+
request.headers.set("Authorization", `Bearer ${credentials.access_token}`)
109+
}
110+
return request
111+
}
112+
113+
client.interceptors.request.use(interceptor)
114+
```
115+
116+
This interceptor:
117+
118+
- Reads the token from local storage.
119+
- Checks if the token is expired or missing.
120+
- Automatically obtains a new token when needed (using Vite environment variables for client ID).
121+
- Attaches the token as a Bearer token in the Authorization header.
122+
- Handles this for all API requests made through the SDK client, except for token requests.
123+
124+
## Project Structure
125+
126+
- `public/`: Contains static assets for the SPA (e.g., favicon, images).
127+
- `src/`: Contains the React application source code.
128+
- `src/auth/StorefrontProvider.tsx`: React provider that handles authentication logic.
129+
- `src/App.tsx`: Main application component that fetches and displays products.
130+
- `src/constants.ts`: Constants including the local storage key for credentials and EPCC endpoint URL (using Vite env vars).
131+
- `src/main.tsx`: Entry point of the React application, wraps `App` with `StorefrontProvider`.
132+
- `index.html`: The main HTML file for the Vite application.
133+
- `vite.config.ts`: Vite configuration file.
134+
- `.env.example`: Example environment variables file.
135+
- `package.json`: Project dependencies and scripts.
136+
137+
## Local Storage Strategy
138+
139+
The authentication token is stored in the browser's local storage:
140+
141+
- Persists between page reloads and browser sessions.
142+
- Easily accessible from anywhere in the client-side application.
143+
- Automatically refreshed when expired by the SDK interceptor.
144+
145+
This approach is simpler than server-side cookies for client-heavy applications but has different security considerations as highlighted in the warning section.
146+
147+
## Getting Started
148+
149+
### Prerequisites
150+
151+
- An Elastic Path Commerce Cloud account.
152+
- A client ID for your storefront application.
153+
- Node.js and a package manager (npm, yarn, or pnpm).
154+
155+
### Environment Variables
156+
157+
1. Copy the `.env.example` file to a new file named `.env` in the root of the `examples/spa-authentication` directory (assuming you rename the parent folder):
158+
```bash
159+
# Assuming you are in the 'examples/spa-authentication' directory
160+
cp .env.example .env
161+
```
162+
2. Update the `.env` file with your specific Elastic Path Commerce Cloud credentials:
163+
164+
```bash
165+
VITE_APP_EPCC_ENDPOINT_URL=your_endpoint_url # e.g. https://useast.api.elasticpath.com
166+
VITE_APP_EPCC_CLIENT_ID=your_client_id
167+
```
168+
169+
Ensure `VITE_APP_EPCC_ENDPOINT_URL` points to the correct API host for your EPCC instance.
170+
171+
### Installation
172+
173+
Navigate to the example directory (once renamed) and install dependencies:
174+
175+
```bash
176+
cd examples/spa-authentication
177+
pnpm install
178+
# or
179+
# npm install
180+
# or
181+
# yarn install
182+
```
183+
184+
### Development
185+
186+
To run the development server:
187+
188+
```bash
189+
pnpm dev
190+
# or
191+
# npm run dev
192+
# or
193+
# yarn dev
194+
```
195+
196+
Open the URL provided by Vite (usually [http://localhost:5173](http://localhost:5173)) in your browser to see the result.
197+
198+
### Building for Production
199+
200+
To build the SPA for production:
201+
202+
```bash
203+
pnpm build
204+
# or
205+
# npm run build
206+
# or
207+
# yarn build
208+
```
209+
210+
This will create a `dist` folder with the production-ready assets. You can then serve the `dist` folder using a static file server.
211+
212+
## Learn More
213+
214+
For more information about Elastic Path Commerce Cloud:
215+
216+
- [Elastic Path Documentation](https://documentation.elasticpath.com/)
217+
- [Authentication with Elastic Path](https://documentation.elasticpath.com/commerce-cloud/docs/api/basics/authentication/index.html)
218+
- [Elastic Path Composable Frontend SDK](https://github.com/elasticpath/composable-frontend)
219+
- [Vite Documentation](https://vitejs.dev/)
220+
- [React Documentation](https://react.dev/)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import tseslint from 'typescript-eslint'
6+
7+
export default tseslint.config(
8+
{ ignores: ['dist'] },
9+
{
10+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
11+
files: ['**/*.{ts,tsx}'],
12+
languageOptions: {
13+
ecmaVersion: 2020,
14+
globals: globals.browser,
15+
},
16+
plugins: {
17+
'react-hooks': reactHooks,
18+
'react-refresh': reactRefresh,
19+
},
20+
rules: {
21+
...reactHooks.configs.recommended.rules,
22+
'react-refresh/only-export-components': [
23+
'warn',
24+
{ allowConstantExport: true },
25+
],
26+
},
27+
},
28+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "spa-authentication",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc -b && vite build",
9+
"lint": "eslint .",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"@epcc-sdk/sdks-shopper": "^0.0.28",
14+
"react": "^19.1.0",
15+
"react-dom": "^19.1.0"
16+
},
17+
"devDependencies": {
18+
"@eslint/js": "^9.25.0",
19+
"@types/react": "^19.1.2",
20+
"@types/react-dom": "^19.1.2",
21+
"@vitejs/plugin-react": "^4.4.1",
22+
"eslint": "^9.25.0",
23+
"eslint-plugin-react-hooks": "^5.2.0",
24+
"eslint-plugin-react-refresh": "^0.4.19",
25+
"globals": "^16.0.0",
26+
"typescript": "~5.8.3",
27+
"typescript-eslint": "^8.30.1",
28+
"vite": "^6.3.5"
29+
}
30+
}

0 commit comments

Comments
 (0)