Skip to content

Commit

Permalink
Merge pull request #216 from sullog-official/fix/auth
Browse files Browse the repository at this point in the history
fix: 인증 관련 코드 수정, 토큰이 유효하지 않은 경우 대응
  • Loading branch information
syoung125 authored Oct 23, 2023
2 parents 77a82a9 + dc84f39 commit 792638d
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 122 deletions.
72 changes: 20 additions & 52 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,46 @@
import type { IncomingMessage, ServerResponse } from 'http';

import { Hydrate, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { NextPageContext } from 'next';
import App from 'next/app';
import type { AppContext, AppProps } from 'next/app';
import dynamic from 'next/dynamic';
import Script from 'next/script';
import { useEffect, useState } from 'react';

import { refreshAccessToken } from '@/shared/apis/auth/refreshAccessToken';
import ConfirmProvider from '@/shared/components/ConfirmProvider';
import CustomHead from '@/shared/components/CustomHead';
import { queryClient as sullogQueryClient } from '@/shared/configs/reactQuery';
import '@/assets/styles/index.scss';
import { NEXT_PUBLIC_TEST_USER_TOKEN } from '@/shared/constants';
import { usePageLoading } from '@/shared/hooks/usePageLoading';
import * as gtag from '@/shared/libs/gtags';
import {
getAccessToken,
getRefreshToken,
setAccessToken,
setRefreshToken,
} from '@/shared/utils/auth';

const Loading = dynamic(() => import('@/shared/components/Loading'));

type SullogAppProps = AppProps & {
tokens?: {
accessToken: string;
refreshToken: string;
};
accessToken?: string;
};

export default function SullogApp({
Component,
pageProps,
tokens,
accessToken,
}: SullogAppProps) {
const [queryClient] = useState(() => sullogQueryClient);
const { isPageLoading } = usePageLoading();
gtag.useGtag();

useEffect(() => {
if (tokens?.accessToken) {
setAccessToken(tokens.accessToken);
if (accessToken) {
setAccessToken(accessToken);
}
}, [tokens]);
}, [accessToken]);

return (
<>
Expand Down Expand Up @@ -78,61 +75,32 @@ export default function SullogApp({
);
}

const goToLogin = (res: ServerResponse<IncomingMessage> | undefined) => {
res?.writeHead(307, { location: `/login` });
res?.end();
const setTestUserTokens = ({ req, res }: NextPageContext) => {
if (process.env.NODE_ENV === 'development' && NEXT_PUBLIC_TEST_USER_TOKEN) {
setAccessToken(NEXT_PUBLIC_TEST_USER_TOKEN, { req, res });
setRefreshToken(NEXT_PUBLIC_TEST_USER_TOKEN, { req, res });
}
};

SullogApp.getInitialProps = async (appContext: AppContext) => {
const { ctx } = appContext;
const { req, res, pathname } = ctx;

const refreshToken = getRefreshToken({ req, res });
setTestUserTokens(ctx);

console.log(
'2',
res?.getHeaders(),
req?.headers.cookie,
pathname,
refreshToken
);
const accessToken = getAccessToken({ req, res });
const refreshToken = getRefreshToken({ req, res });

if (pathname !== '/login' && !refreshToken) {
console.log('3');
goToLogin(res);
if (pathname !== '/login' && !accessToken && !refreshToken) {
res?.writeHead(307, { location: `/login` });
res?.end();
return {};
}

let tokens: SullogAppProps['tokens'];
if (refreshToken) {
console.log('4', tokens);
try {
await refreshAccessToken(ctx);
const accessToken = getAccessToken();
const refreshToken = getRefreshToken({ req, res });
console.log('5', accessToken, refreshToken);

if (!accessToken || !refreshToken) {
console.log('6');
goToLogin(res);
return {};
}

tokens = {
accessToken,
refreshToken,
};
} catch (error) {
console.log('7', error);
goToLogin(res);
return {};
}
}

const pageProps = await App.getInitialProps(appContext);

return {
...pageProps,
tokens,
accessToken,
};
};
3 changes: 2 additions & 1 deletion src/pages/api/logout.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { NextApiRequest, NextApiResponse } from 'next';

import { deleteRefreshToken } from '@/shared/utils/auth';
import { deleteAccessToken, deleteRefreshToken } from '@/shared/utils/auth';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
deleteAccessToken({ req, res });
deleteRefreshToken({ req, res });
res.redirect(307, '/login');
}
50 changes: 50 additions & 0 deletions src/pages/api/refresh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import axios from 'axios';
import { NextApiRequest, NextApiResponse } from 'next';

import {
ACCESS_TOKEN_KEY,
NEXT_PUBLIC_API_BASE_URI,
REFRESH_TOKEN_KEY,
} from '@/shared/constants';
import {
getRefreshToken,
setAccessToken,
setRefreshToken,
} from '@/shared/utils/auth';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const refreshToken = getRefreshToken({ req, res });

if (!refreshToken) {
throw new Error('refreshToken is undefined');
}

const response = await axios({
baseURL: NEXT_PUBLIC_API_BASE_URI,
url: '/token/refresh',
method: 'get',
headers: {
Authorization: `Bearer ${refreshToken}`,
},
validateStatus: null,
});

const newAccessToken = response.headers[ACCESS_TOKEN_KEY];
const newRefreshToken = response.headers[REFRESH_TOKEN_KEY];

if (!newAccessToken || !newRefreshToken) {
throw new Error('Invalid tokens.');
}

setAccessToken(newAccessToken, { req, res });
setRefreshToken(newRefreshToken, { req, res });

res.status(200).json({ result: true });
} catch (err) {
res.status(200).json({ result: false });
}
}
50 changes: 0 additions & 50 deletions src/shared/apis/auth/refreshAccessToken.ts

This file was deleted.

8 changes: 8 additions & 0 deletions src/shared/apis/auth/refreshTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import axios from 'axios';

export const refreshTokens = async () => {
return await axios<{ result: boolean }>({
url: '/api/refresh',
method: 'get',
});
};
8 changes: 6 additions & 2 deletions src/shared/configs/axios.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import axios, { AxiosError, AxiosResponse } from 'axios';

import { refreshAccessToken } from '@/shared/apis/auth/refreshAccessToken';
import { NEXT_PUBLIC_API_BASE_URI } from '@/shared/constants';

import { refreshTokens } from '../apis/auth/refreshTokens';
import { getAccessToken } from '../utils/auth';
import { isServer } from '../utils/isServer';

Expand All @@ -17,7 +17,11 @@ let subscribers: ((accessToken: string) => Promise<void>)[] = [];
const handleUnauthorizedError = async (error: AxiosError) => {
if (error.response?.status === 401) {
try {
throw new Error('exceed retry limit count');
const { data } = await refreshTokens();

if (!data.result) {
throw new Error('Failed to refresh tokens');
}
} catch (error) {
// 새 액세스 토큰을 가져 오는 동안 오류가 발생하면 로그인 페이지로 리디렉션
if (!isServer()) {
Expand Down
3 changes: 2 additions & 1 deletion src/shared/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const NEXT_PUBLIC_API_BASE_URI = process.env.NEXT_PUBLIC_API_BASE_URI;

export const NEXT_PUBLIC_TEST_USER_TOKEN =
process.env.NEXT_PUBLIC_TEST_USER_TOKEN;
export const NEXT_PUBLIC_KAKAO_BASE_URI =
process.env.NEXT_PUBLIC_KAKAO_BASE_URI;
export const NEXT_PUBLIC_KAKAO_CLIENT_ID =
Expand Down
7 changes: 7 additions & 0 deletions src/shared/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@/shared/utils/auth';

import { logout as authLogout } from '../apis/auth/logout';
import { refreshTokens } from '../apis/auth/refreshTokens';

import useConfirm from './useConfirm';

Expand Down Expand Up @@ -39,6 +40,12 @@ const useAuth = () => {

const verifyLoggedIn = async (): Promise<boolean> => {
const accessToken = getAccessToken();

if (!accessToken) {
const { data } = await refreshTokens();
return !!data.result;
}

return !!accessToken;
};

Expand Down
53 changes: 37 additions & 16 deletions src/shared/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,57 @@ import {
NEXT_PUBLIC_KAKAO_SCOPE,
} from '@/shared/constants';

import { REFRESH_TOKEN_KEY } from '../constants';
import { REFRESH_TOKEN_KEY, ACCESS_TOKEN_KEY } from '../constants';

import { generateUrl } from './generateUrl';
import { InMemoryValue } from './inMemory';
import { isServer } from './isServer';

let inMemoryAccessToken: string | undefined;
const ONE_DAY = 24 * 60 * 60 * 1000;

/**
* 액세스 토큰을 인메모리에 저장
*/
export const setAccessToken = (accessToken: string) => {
inMemoryAccessToken = accessToken;
const inMemoryAccessToken = new InMemoryValue<string | undefined>(undefined);

export const setAccessToken = (
accessToken: string,
context?: { req?: IncomingMessage; res?: ServerResponse }
) => {
if (isServer() && context) {
setCookie(ACCESS_TOKEN_KEY, accessToken, {
...(context || {}),
httpOnly: true,
secure: process.env.NODE_ENV !== 'development',
maxAge: ONE_DAY,
sameSite: 'strict',
path: '/',
});
}
inMemoryAccessToken.set(accessToken);
};

export const getAccessToken = (): string | undefined => {
return inMemoryAccessToken;
export const getAccessToken = (context?: {
req?: IncomingMessage;
res?: ServerResponse;
}): string | undefined => {
if (isServer() && context) {
return getCookie(ACCESS_TOKEN_KEY, context) as string | undefined;
}
return inMemoryAccessToken.get();
};

export const deleteAccessToken = () => {
inMemoryAccessToken = '';
export const deleteAccessToken = (context?: {
req?: IncomingMessage;
res?: ServerResponse;
}) => {
if (isServer() && context) {
deleteCookie(ACCESS_TOKEN_KEY, context);
}
inMemoryAccessToken.delete();
};

/**
* 리프레시 토큰을 쿠키에 저장 (expires : 14일)
*/
const FOURTEEN_DAYS = 14 * 24 * 60 * 60 * 1000;

export const setRefreshToken = (
refreshToken: string,
/** required for server side cookies */
context?: { req?: IncomingMessage; res?: ServerResponse }
) => {
if (isServer() && !context) {
Expand All @@ -54,7 +75,7 @@ export const setRefreshToken = (
httpOnly: true,
secure: process.env.NODE_ENV !== 'development',
maxAge: FOURTEEN_DAYS,
sameSite: 'none',
sameSite: 'strict',
path: '/',
});
};
Expand Down
Loading

0 comments on commit 792638d

Please sign in to comment.