Skip to content

Commit

Permalink
store the auth token in a cookie instead of localStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
alan2207 committed May 3, 2024
1 parent d99d6d9 commit 3ba597a
Show file tree
Hide file tree
Showing 12 changed files with 92 additions and 81 deletions.
6 changes: 3 additions & 3 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ Authentication is a process of identifying who the user is. The most common way

The safest option is to store the token in the app state, but if the user refreshes the app, its token will be lost.

That is why tokens are stored in `localStorage/sessionStorage` or in a cookie.
That is why tokens are stored in a cookie or `localStorage/sessionStorage`..

#### `localStorage` vs cookie for storing tokens

Storing it in `localStorage` could bring a security issue, if your application is vulnerable to [XSS](https://owasp.org/www-community/attacks/xss/) someone could steal your token.

Storing tokens in a cookie might be safer if the cookie is set to be `HttpOnly` which would mean it wouldn't be accessible from the client side JavaScript. The `localStorage` way is being used here for simplicity reasons, if you want to be more secure, you should consider using cookies but that is a decision that should be made together with the backend team.
Storing tokens in a cookie might be safer if the cookie is set to be `HttpOnly` which would mean it wouldn't be accessible from the client side JavaScript.

To keep the application safe, instead of focusing only on where to store the token safely, it would be recommended to make the entire application as resistant as possible to XSS attacks E.g - every input from the user should be sanitized before it's injected into the DOM.
To keep the application safe, instead of focusing only on where to store the token safely, it is recommended to make the entire application as resistant as possible to XSS attacks E.g - every input from the user should be sanitized before it's injected into the DOM.

[HTML Sanitization Example Code](../src/components/Elements/MDPreview/MDPreview.tsx)

Expand Down
5 changes: 5 additions & 0 deletions src/features/auth/api/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { axios } from '@/lib/axios';

export const logout = (): Promise<void> => {
return axios.post('/auth/logout');
};
1 change: 1 addition & 0 deletions src/features/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './api/getUser';
export * from './api/login';
export * from './api/register';
export * from './api/logout';

export * from './routes';

Expand Down
13 changes: 4 additions & 9 deletions src/lib/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,16 @@ import {
UserResponse,
LoginCredentialsDTO,
RegisterCredentialsDTO,
logout,
} from '@/features/auth';
import storage from '@/utils/storage';

async function handleUserResponse(data: UserResponse) {
const { jwt, user } = data;
storage.setToken(jwt);
const { user } = data;
return user;
}

async function userFn() {
if (storage.getToken()) {
const data = await getUser();
return data;
}
return null;
return getUser();
}

async function loginFn(data: LoginCredentialsDTO) {
Expand All @@ -37,7 +32,7 @@ async function registerFn(data: RegisterCredentialsDTO) {
}

async function logoutFn() {
storage.clearToken();
await logout();
window.location.assign(window.location.origin as unknown as string);
}

Expand Down
7 changes: 0 additions & 7 deletions src/lib/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@ import Axios, { InternalAxiosRequestConfig } from 'axios';

import { env } from '@/config/env';
import { useNotificationStore } from '@/stores/notifications';
import storage from '@/utils/storage';

function authRequestInterceptor(config: InternalAxiosRequestConfig) {
const token = storage.getToken();
if (token) {
if (config.headers) {
config.headers.authorization = `${token}`;
}
}
if (config.headers) {
config.headers.Accept = 'application/json';
}
Expand Down
38 changes: 32 additions & 6 deletions src/test/server/handlers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { nanoid } from 'nanoid';
import { env } from '@/config/env';

import { db, persistDb } from '../db';
import { authenticate, hash, requireAuth } from '../utils';
import { authenticate, hash, requireAuth, AUTH_COOKIE } from '../utils';

type RegisterBody = {
firstName: string;
Expand Down Expand Up @@ -59,7 +59,12 @@ export const authHandlers = [
});

if (!existingTeam) {
throw new Error('The team you are trying to join does not exist!');
return HttpResponse.json(
{
message: 'The team you are trying to join does not exist!',
},
{ status: 400 }
);
}
teamId = userObject.teamId;
role = 'USER';
Expand All @@ -78,7 +83,12 @@ export const authHandlers = [

const result = authenticate({ email: userObject.email, password: userObject.password });

return HttpResponse.json(result);
return HttpResponse.json(result, {
headers: {
// with a real API servier, the token cookie should also be Secure and HttpOnly
'Set-Cookie': `${AUTH_COOKIE}=${result.jwt}; Path=/; SameSite=Strict; Max-Age=3600;`,
},
});
} catch (error: any) {
return HttpResponse.json({ message: error?.message || 'Server Error' }, { status: 500 });
}
Expand All @@ -88,15 +98,31 @@ export const authHandlers = [
try {
const credentials = (await request.json()) as LoginBody;
const result = authenticate(credentials);
return HttpResponse.json(result);
return HttpResponse.json(result, {
headers: {
// with a real API servier, the token cookie should also be Secure and HttpOnly
'Set-Cookie': `${AUTH_COOKIE}=${result.jwt}; Path=/; SameSite=Strict; Max-Age=3600;`,
},
});
} catch (error: any) {
return HttpResponse.json({ message: error?.message || 'Server Error' }, { status: 500 });
}
}),

http.get(`${env.API_URL}/auth/me`, ({ request }) => {
http.post(`${env.API_URL}/auth/logout`, () => {
return HttpResponse.json(
{ message: 'Logged out' },
{
headers: {
'Set-Cookie': `${AUTH_COOKIE}=; Path=/; SameSite=Strict; Max-Age=0;`,
},
}
);
}),

http.get(`${env.API_URL}/auth/me`, ({ cookies }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies, false);
return HttpResponse.json(user);
} catch (error: any) {
return HttpResponse.json({ message: error?.message || 'Server Error' }, { status: 500 });
Expand Down
16 changes: 8 additions & 8 deletions src/test/server/handlers/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ type CreateCommentBody = {
};

export const commentsHandlers = [
http.get(`${env.API_URL}/comments`, ({ request }) => {
http.get(`${env.API_URL}/comments`, ({ request, cookies }) => {
try {
requireAuth(request);
requireAuth(cookies);
const url = new URL(request.url);
const discussionId = url.searchParams.get('discussionId') || '';
const comments = db.comment
Expand Down Expand Up @@ -44,12 +44,12 @@ export const commentsHandlers = [
}
}),

http.post(`${env.API_URL}/comments`, async ({ request }) => {
http.post(`${env.API_URL}/comments`, async ({ request, cookies }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const data = (await request.json()) as CreateCommentBody;
const result = db.comment.create({
authorId: user.id,
authorId: user?.id,
id: nanoid(),
createdAt: Date.now(),
...data,
Expand All @@ -61,16 +61,16 @@ export const commentsHandlers = [
}
}),

http.delete(`${env.API_URL}/comments/:commentId`, ({ request, params }) => {
http.delete(`${env.API_URL}/comments/:commentId`, ({ params, cookies }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const commentId = params.commentId as string;
const result = db.comment.delete({
where: {
id: {
equals: commentId,
},
...(user.role === 'USER' && {
...(user?.role === 'USER' && {
authorId: {
equals: user.id,
},
Expand Down
30 changes: 15 additions & 15 deletions src/test/server/handlers/discussions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ type DiscussionBody = {
};

export const discussionsHandlers = [
http.get(`${env.API_URL}/discussions`, async ({ request }) => {
http.get(`${env.API_URL}/discussions`, async ({ cookies }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const result = db.discussion
.findMany({
where: {
teamId: {
equals: user.teamId,
equals: user?.teamId,
},
},
})
Expand All @@ -42,17 +42,17 @@ export const discussionsHandlers = [
}
}),

http.get(`${env.API_URL}/discussions/:discussionId`, async ({ request, params }) => {
http.get(`${env.API_URL}/discussions/:discussionId`, async ({ params, cookies }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const discussionId = params.discussionId as string;
const discussion = db.discussion.findFirst({
where: {
id: {
equals: discussionId,
},
teamId: {
equals: user.teamId,
equals: user?.teamId,
},
},
});
Expand Down Expand Up @@ -82,16 +82,16 @@ export const discussionsHandlers = [
}
}),

http.post(`${env.API_URL}/discussions`, async ({ request }) => {
http.post(`${env.API_URL}/discussions`, async ({ request, cookies }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const data = (await request.json()) as DiscussionBody;
requireAdmin(user);
const result = db.discussion.create({
teamId: user.teamId,
teamId: user?.teamId,
id: nanoid(),
createdAt: Date.now(),
authorId: user.id,
authorId: user?.id,
...data,
});
persistDb('discussion');
Expand All @@ -101,16 +101,16 @@ export const discussionsHandlers = [
}
}),

http.patch(`${env.API_URL}/discussions/:discussionId`, async ({ request, params }) => {
http.patch(`${env.API_URL}/discussions/:discussionId`, async ({ request, params, cookies }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const data = (await request.json()) as DiscussionBody;
const discussionId = params.discussionId as string;
requireAdmin(user);
const result = db.discussion.update({
where: {
teamId: {
equals: user.teamId,
equals: user?.teamId,
},
id: {
equals: discussionId,
Expand All @@ -125,9 +125,9 @@ export const discussionsHandlers = [
}
}),

http.delete(`${env.API_URL}/discussions/:discussionId`, async ({ request, params }) => {
http.delete(`${env.API_URL}/discussions/:discussionId`, async ({ cookies, params }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const discussionId = params.discussionId as string;
requireAdmin(user);
const result = db.discussion.delete({
Expand Down
18 changes: 9 additions & 9 deletions src/test/server/handlers/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ type ProfileBody = {
};

export const usersHandlers = [
http.get(`${env.API_URL}/users`, async ({ request }) => {
http.get(`${env.API_URL}/users`, async ({ cookies }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const result = db.user
.findMany({
where: {
teamId: {
equals: user.teamId,
equals: user?.teamId,
},
},
})
Expand All @@ -32,14 +32,14 @@ export const usersHandlers = [
}
}),

http.patch(`${env.API_URL}/users/profile`, async ({ request }) => {
http.patch(`${env.API_URL}/users/profile`, async ({ request, cookies }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const data = (await request.json()) as ProfileBody;
const result = db.user.update({
where: {
id: {
equals: user.id,
equals: user?.id,
},
},
data,
Expand All @@ -51,9 +51,9 @@ export const usersHandlers = [
}
}),

http.delete(`${env.API_URL}/users/:userId`, async ({ request, params }) => {
http.delete(`${env.API_URL}/users/:userId`, async ({ cookies, params }) => {
try {
const user = requireAuth(request);
const user = requireAuth(cookies);
const userId = params.userId as string;
requireAdmin(user);
const result = db.user.delete({
Expand All @@ -62,7 +62,7 @@ export const usersHandlers = [
equals: userId,
},
teamId: {
equals: user.teamId,
equals: user?.teamId,
},
},
});
Expand Down
19 changes: 13 additions & 6 deletions src/test/server/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { StrictRequest, DefaultBodyType } from 'msw';

import { db } from './db';

export const encode = (obj: any) => window.btoa(JSON.stringify(obj));
Expand Down Expand Up @@ -48,11 +46,17 @@ export function authenticate({ email, password }: { email: string; password: str
throw error;
}

export function requireAuth(request: StrictRequest<DefaultBodyType>) {
export const AUTH_COOKIE = `bulletproof_react_app_token`;

export function requireAuth(cookies: Record<string, string>, shouldThrow = true) {
try {
const encodedToken = request.headers.get('authorization');
const encodedToken = cookies[AUTH_COOKIE];
if (!encodedToken) {
throw new Error('No authorization token provided!');
if (shouldThrow) {
throw new Error('No authorization token provided!');
}

return null;
}
const decodedToken = decode(encodedToken) as { id: string };

Expand All @@ -65,7 +69,10 @@ export function requireAuth(request: StrictRequest<DefaultBodyType>) {
});

if (!user) {
throw Error('Unauthorized');
if (shouldThrow) {
throw Error('Unauthorized');
}
return null;
}

return sanitizeUser(user);
Expand Down
Loading

0 comments on commit 3ba597a

Please sign in to comment.