Skip to content

Commit

Permalink
feat(common): LFG-177 corrects IDOR
Browse files Browse the repository at this point in the history
  • Loading branch information
bc-chaz committed Jun 24, 2021
1 parent 1bff62c commit a2e4d86
Show file tree
Hide file tree
Showing 10 changed files with 39 additions and 28 deletions.
3 changes: 1 addition & 2 deletions .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ CLIENT_SECRET={app secret}

AUTH_CALLBACK=https://{ngrok_id}.ngrok.io/api/auth

# Set cookie variables, replace jwt key with a 32+ random character secret
# Replace jwt key with a 32+ random character secret

COOKIE_NAME=__{NAME}
JWT_KEY={SECRET}

# Specify the type of database
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ To get the app running locally, follow these instructions:
- If deploying on Heroku, skip `.env` setup. Instead, enter `env` variables in the Heroku App Dashboard under `Settings -> Config Vars`.
6. [Replace client_id and client_secret in .env](https://devtools.bigcommerce.com/my/apps) (from `View Client ID` in the dev portal).
7. Update AUTH_CALLBACK in `.env` with the `ngrok_id` from step 5.
8. Enter a cookie name, as well as a jwt secret in `.env`.
- The cookie name should be unique
8. Enter a jwt secret in `.env`.
- JWT key should be at least 32 random characters (256 bits) for HS256
9. Specify DB_TYPE in `.env`
- If using Firebase, enter your firebase config keys. See [Firebase quickstart](https://firebase.google.com/docs/firestore/quickstart)
Expand Down
4 changes: 2 additions & 2 deletions context/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ContextValues } from '../types';
const SessionContext = createContext<Partial<ContextValues>>({});

const SessionProvider = ({ children }) => {
const [storeHash, setStoreHash] = useState('');
const value = { storeHash, setStoreHash };
const [context, setContext] = useState('');
const value = { context, setContext };

return (
<SessionContext.Provider value={value}>
Expand Down
16 changes: 13 additions & 3 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as jwt from 'jsonwebtoken';
import { NextApiRequest, NextApiResponse } from 'next';
import * as BigCommerce from 'node-bigcommerce';
import { QueryParams, SessionProps } from '../types';
import db from './db';

const { AUTH_CALLBACK, CLIENT_ID, CLIENT_SECRET } = process.env;
const { AUTH_CALLBACK, CLIENT_ID, CLIENT_SECRET, JWT_KEY } = process.env;

// Create BigCommerce instance
// https://github.com/getconversio/node-bigcommerce
Expand Down Expand Up @@ -48,9 +49,10 @@ export async function setSession(session: SessionProps) {

export async function getSession({ query: { context = '' } }: NextApiRequest) {
if (typeof context !== 'string') return;
const accessToken = await db.getStoreToken(context);
const decodedContext = decodePayload(context)?.context;
const accessToken = await db.getStoreToken(decodedContext);

return { accessToken, storeHash: context };
return { accessToken, storeHash: decodedContext };
}

export async function removeSession(res: NextApiResponse, session: SessionProps) {
Expand All @@ -60,3 +62,11 @@ export async function removeSession(res: NextApiResponse, session: SessionProps)
export async function removeUserData(res: NextApiResponse, session: SessionProps) {
await db.deleteUser(session);
}

export function encodePayload(context: string) {
return jwt.sign({ context }, JWT_KEY, { expiresIn: '24h' });
}

export function decodePayload(encodedContext: string) {
return jwt.verify(encodedContext, JWT_KEY);
}
16 changes: 8 additions & 8 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import useSWR from 'swr';
import { useSession } from '../context/session';
import { ListItem } from '../types';

function fetcher(url: string, storeHash: string) {
return fetch(`${url}?context=${storeHash}`).then(res => res.json());
function fetcher(url: string, encodedContext: string) {
return fetch(`${url}?context=${encodedContext}`).then(res => res.json());
}

// Reusable SWR hooks
// https://swr.vercel.app/
export function useProducts() {
const storeHash = useSession()?.storeHash;
const encodedContext = useSession()?.context;
// Request is deduped and cached; Can be shared across components
const { data, error } = useSWR(storeHash ? ['/api/products', storeHash] : null, fetcher);
const { data, error } = useSWR(encodedContext ? ['/api/products', encodedContext] : null, fetcher);

return {
summary: data,
Expand All @@ -21,9 +21,9 @@ export function useProducts() {
}

export function useProductList() {
const storeHash = useSession()?.storeHash;
const encodedContext = useSession()?.context;
// Use an array to send multiple arguments to fetcher
const { data, error, mutate: mutateList } = useSWR(storeHash ? ['/api/products/list', storeHash] : null, fetcher);
const { data, error, mutate: mutateList } = useSWR(encodedContext ? ['/api/products/list', encodedContext] : null, fetcher);

return {
list: data,
Expand All @@ -34,10 +34,10 @@ export function useProductList() {
}

export function useProductInfo(pid: number, list: ListItem[]) {
const storeHash = useSession()?.storeHash;
const encodedContext = useSession()?.context;
const product = list.find(item => item.id === pid);
// Conditionally fetch product if it doesn't exist in the list (e.g. deep linking)
const { data, error } = useSWR(!product && storeHash ? [`/api/products/${pid}`, storeHash] : null, fetcher);
const { data, error } = useSWR(!product && encodedContext ? [`/api/products/${pid}`, encodedContext] : null, fetcher);

return {
product: product ?? data,
Expand Down
5 changes: 3 additions & 2 deletions pages/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { getBCAuth, setSession } from '../../lib/auth';
import { encodePayload, getBCAuth, setSession } from '../../lib/auth';

export default async function auth(req: NextApiRequest, res: NextApiResponse) {
try {
// Authenticate the app on install
const session = await getBCAuth(req.query);
const storeHash = session?.context?.split('/')[1] || '';
const encodedContext = encodePayload(storeHash); // Signed JWT to validate/ prevent tampering

await setSession(session);
res.redirect(302, `/?context=${storeHash}`);
res.redirect(302, `/?context=${encodedContext}`);
} catch (error) {
const { message, response } = error;
res.status(response?.status || 500).json(message);
Expand Down
6 changes: 4 additions & 2 deletions pages/api/load.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { getBCVerify, setSession } from '../../lib/auth';
import { encodePayload, getBCVerify, setSession } from '../../lib/auth';

export default async function load(req: NextApiRequest, res: NextApiResponse) {
try {
// Verify when app loaded (launch)
const session = await getBCVerify(req.query);
const storeHash = session?.context?.split('/')[1] || '';
const encodedContext = encodePayload(storeHash); // Signed JWT to validate/ prevent tampering

await setSession(session);
res.redirect(302, `/?context=${storeHash}`);
res.redirect(302, `/?context=${encodedContext}`);
} catch (error) {
const { message, response } = error;
res.status(response?.status || 500).json(message);
Expand Down
6 changes: 3 additions & 3 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { useProducts } from '../lib/hooks';

const Index = ({ context }: { context: string }) => {
const { isLoading, summary } = useProducts();
const { setStoreHash } = useSession();
const { setContext } = useSession();

useEffect(() => {
if (context) setStoreHash(context);
}, [context, setStoreHash]);
if (context) setContext(context);
}, [context, setContext]);

if (isLoading) return <Loading />;

Expand Down
4 changes: 2 additions & 2 deletions pages/products/[pid].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FormData } from '../../types';

const ProductInfo = () => {
const router = useRouter();
const { storeHash } = useSession();
const encodedContext = useSession()?.context;
const pid = Number(router.query?.pid);
const { isError, isLoading, list = [], mutateList } = useProductList();
const { isLoading: isInfoLoading, product } = useProductInfo(pid, list);
Expand All @@ -24,7 +24,7 @@ const ProductInfo = () => {
mutateList([...filteredList, { ...product, ...data }], false);

// Update product details
await fetch(`/api/products/${pid}?context=${storeHash}`, {
await fetch(`/api/products/${pid}?context=${encodedContext}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
Expand Down
4 changes: 2 additions & 2 deletions types/data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface ContextValues {
storeHash: string;
setStoreHash: (key: string) => void;
context: string;
setContext: (key: string) => void;
}

export interface FormData {
Expand Down

0 comments on commit a2e4d86

Please sign in to comment.