이번에 구현하고 있는 서비스에서는 카카오와 구글, 이렇게 두 가지의 OAuth를 지원하고 있다.
나는 그 중에서도 카카오를 맡게 되었으나, 구글의 OAuth 플로우도 거의 동일한 것 같아 전반적인 OAuth에 대한 공부를 해보았다.
프론트 단에서 처리하는 방법만 알기보다는, 서버에서는 어떤 과정을 거치는 지도 알아야할 것 같아서 백엔드를 담당하시는 분과도 이야기한 후 정리해보았다.
그렇다면 우선 OAuth가 무엇인지부터 알아보자 !
OAuth란?
OAuth (Open Authorization)는 제 3자 애플리케이션이 사용자의 비밀번호를 알지 않고도 해당 사용자의 정보를 안전하게 접근할 수 있게 해주는 인증을 위한 개방형 표준 프로토콜이다.
이를 통해 로그인 정보와 같은 민감한 데이터를 노출하지 않고도, 다른 애플리케이션이나 웹 사이트가 제한된 범위 내에서 자신의 데이터를 사용할 수 있게 허용할 수 있다.
한 마디로, 웹 사이트에서는 카카오나 구글, 네이버의 비밀번호를 알지 않고도 자사 사이트에서 그 회원 정보를 (제한된) 사용할 수 있는 것이다.
인증, 인가 과정을 거친 후 플랫폼 서버에 사용자 정보를 요청하게 되는데, 이 과정이 무엇인지 한 번 알아보자.
- 인증 (Authentication)
- 계정에 접근을 요청한 사용자가 실제 소유자인지 확인하는 절차로, 대표적인 인증 방법은 사용자가 입력한 ID와 비밀번호를 확인하는 것이다.
- 사용자가 카카오톡, 구글 등의 계정에 관한 아이디와 비밀번호를 입력하는 과정 (이미 로그인되어 있을 때는 계정을 선택하는 과정을 거치게 됨)
- 인가 (Authorization)
- 서비스에서 요청한 사용자 동의 필요 정보 또는 접근 권한 필요 기능을 API 플랫폼이 제공해도 될 지 사용자에게 동의를 구하는 일이다.
- 어떤 정보를 제공하는 지, 제공하는 것에 동의하는 지를 구하는 과정
그렇다면 실제로 OAuth 구현하기 위해 구체적으로 어떤 단계를 거쳐야할까?
OAuth 과정
1. 인가 코드 요청하기
클라이언트측에서 써드 파티(플랫폼)에 인가 코드를 요청하면 써드 파티측에서는 미리 전달한 redirectURI의 'code' param으로 인가 코드를 넘겨주게 된다.
- 사용자의 인증, 인가 과정을 거친 이후이며 이때 code에 인증 인가에 대한 정보가 담겨서 전달된다.
2. 인가 코드 추출하여 서버로 전달하기
param에 있는 code를 추출하여 서버에 전달해주고, 이렇게 전달된 code를 가지고 서버는 써드 파티 측에 사용자 토큰을 요청한다.
- 처음에 나는 이 부분도 프론트 단에서 처리하는 줄 알고 사용자 토큰까지 발급 받았었다. 하핳 !
- 백엔드 분과 이야기를 나눠보니 그렇게 해도 되긴 하지만, 이후에 다른 처리를 위해서는 서버 단에서 처리하는 것이 더 좋다고 하셨다. (발급 받은 사용자 토큰을 서버에서 계속 사용해야 하기 때문에 + 추가로 궁금했던 점에 더 자세히 적어두었다)
3. 서버에서 토큰 발급 받고, 사용자 정보 요청하기
클라이언트에서 전달해준 code를 통해 써드 파티 측으로 사용자 토큰을 요청한다.
이때 발급받은 사용자 토큰을 이용하여 서버로 사용자 정보를 받아오게 된다.
4. JWT를 클라이언트로 발급하여 전달하기
서버에서는 받아온 사용자 정보로 신규 가입 시키거나, 기존 회원 정보에서 검색하게 된다. 그리고 그에 맞게 JWT를 발급하여 클라이언트로 전달한다.
이는 클라이언트 측에서 accessToken으로 관리하게 된다.
5. 현재로 로그인한 사용자 정보 요청하기
클라이언트에서는 다른 api 요청을 할 때 headers의 Authorization에 accessToken을 함께 넣어서 보내게 되는데, 이 전에 클라이언트에서 전달해준 JWT를 넣어서 데이터를 패칭할 준비를 해주면 된다.
- accessToken을 관리할 때는 localStorage를 쓰는 경우가 있고, cookie를 쓰는 경우가 있고, sessionStorage를 쓰는 경우가 있는데 이는 각 자의 프로젝트에 맞게 선택해서 사용해주면 될 것 같다.
구현하기
1. 내 애플리케이션 설정
https://developers.kakao.com/
위의 사이트에 접속하여 내 애플리케이션 으로 이동한다.
- 1) 애플리케이션 추가하기 ( 앱 설정 > 플랫폼 )
- Web 사이트 도메인 추가하기 (배포 링크 추가해주면 됩니다)
- 2) 카카오 로그인 설정하기 ( 앱 설정_등록된 애플리케이션 클릭 > 카카오 로그인 )
- 활성화 설정 ON으로 바꿔주기
- redirect URI 등록하기 (로그인 하는 동안 보여질 짧은 로딩창 같은 화면)
- 등록한 redirectURI를 .env 파일에 넣어두기
- (옵션) Client Secret 설정하기 ( 제품 설정 > 카카오 로그인 > 보안 )
- 활성화 상태를 사용함으로 바꿔주기
- 코드를 서버에 전달하기 (백엔드 분께 전달) or 사용자 토큰을 클라이언트에서 처리한다면 .env 파일에 넣어두기** 저는 설정해주었습니다. **
- 3) 앱 키 확인하기 ( 앱 설정 > 앱 키 )
- 이 앱 키는 절대로 유출되면 안됩니다 !
- 필요한 키 (저는 REST API 키를 사용했습니다) 를 복사해서 .env 파일에 넣어두기
2. 인가 코드 받기
카카오톡 로그인을 위해 위와 같은 버튼을 이용하기 때문에 이 버튼의 핸들러 함수로 카카오의 인가 코드받는 url로 연결해주면 된다.
// '카카오톡으로 로그인' 버튼의 onClick 함수로 넣어주면 됨
const handleAuthorizationCode = () => {
window.location.href =
'https://kauth.kakao.com/oauth/authorize?' +
`response_type=code` +
`&client_id=${REST_API_KEY}` +
`&redirect_uri=${REDIRECT_URI}`;
}
// client_id 는 위에서 저장한 REST API키이고,
// REDIRECT_URI도 위에서 입력해준 URI 입력해주면 된다.
// 두 가지 모두 .env 파일에서 관리해야 한다.
이러한 인가 코드 받기 요청의 응답은 HTTP 302 리다이렉트 되어, redirect_uri에 GET 요청으로 전달된다.
=> href의 redirect_uri로 넘겨준 '/auth' 페이지가 로딩 페이지처럼 짧게 지나가는 것
- 이때의 redirect_uri의 params로 code가 전달되는데 이는 토큰 받기에 필요한 인가 코드가 전달되는 것이다.
- code : 토큰 받기에 필요한 인가 코드
- error : 인증 실패 시 반환되는 에러 코드
- 에러의 종류는 다양하기 때문에 에러 코드에 따라 아래의 문서를 참고하여 해결법을 찾는 것이 좋다.
- 카카오 로그인 문제 해결 링크
3. 인가 코드를 서버로 전달하기
'/auth' 페이지에서 code params를 추출하여 서버로 보내주면, 서버는 이를 이용하여 카카오 플랫폼에 사용자 토큰 발급을 요청할 것이다.
이 흐름을 모르고, 클라이언트에서 토큰까지 발급받았었는데 이렇게 받은 토큰으로 무엇을 해야하는 지 이해되지 않아 공부하게 되었다. 하하 !
토큰 받기
인가 코드로 토큰 발급을 요청한다. 인가 코드 받기만으로는 카카오 로그인이 완료되지 않는다.
필수 파라미터를 포함하여 POST 요청 보내기
const getKakaoToken = async (code: string) => {
return await axios.post(
'https://kauth.kakao.com/oauth/token',
{
grant_type: 'authorization_code',
client_id: `${import.meta.env.VITE_KAKAO_API_KEY}`,
redirect_uri: `${import.meta.env.VITE_KAKAO_REDIRECT_LOCAL_URL}`,
code: code,
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
},
);
};
POST 요청에 따른 리스폰스 정보
{
"token_type":"bearer",
"access_token":"${ACCESS_TOKEN}",
"expires_in":43199,
"refresh_token":"${REFRESH_TOKEN}",
"refresh_token_expires_in":5184000,
"scope":"account_email profile"
}
이렇게 받은 토큰으로 서버에서는 그 사용자를 회원 목록에 추가하거나 기존 회원 테이블에서 검색하고 accessToken을 클라이언트 측에 발급해줄 것이다.
이렇게 발급된 토큰을 클라이언트는 저장해서 (원하는 곳에) 우리가 원하는 대로 리다이렉트 시켜주거나, 이후의 사용자 정보를 저장하는 등 로직을 자유롭게 작성해주면 된다.
-> 우리는 accessToken으로 현재 로그인한 유저 정보를 GET 요청한 후, 그 유저가 신규인지 아닌지를 확인해서 다른 페이지로 리다이렉트 시켜주었다.
최종 코드
'/login' 페이지와 핸들러 함수
// login.tsx
const Login = () => {
return (
<div >
<div>
<img src={titleImage} />
<h1>- 한글 기념 소통창 -</h1>
</div>
<div>
<h2>
간편 로그인으로 <strong>와글와글</strong>을 이용해 보세요!
</h2>
<ul>
{SOCIAL_LOGIN_INFO.map(social => (
<li key={social.name} onClick={social.onClick}>
<img src={social.logo} alt="소셜 로그인 아이콘" />
<span $color={social.color}>{social.name}</span>
</S.LoginItem>
))}
</ul>
</div>
);
};
// socialLoginInfo.ts
const handleGoogleLoginClick = () => {
window.location.href =
'https://accounts.google.com/o/oauth2/auth?' +
`&client_id=${import.meta.env.VITE_GOOGLE_AUTH_CLIENT_ID}` +
`&redirect_uri=${import.meta.env.VITE_REDIRECT_URL}` +
'&response_type=code' +
'&scope=email profile';
};
const handleKakaoLoginClick = () => {
window.location.href =
'https://kauth.kakao.com/oauth/authorize?' +
`client_id=${import.meta.env.VITE_KAKAO_API_KEY}` +
`&redirect_uri=${import.meta.env.VITE_KAKAO_REDIRECT_URL}` +
'&response_type=code';
};
export const SOCIAL_LOGIN_INFO = [
{
name: '카카오톡',
logo: kakaoLogo,
background: '#FAE100',
color: '#371D1E',
onClick: handleKakaoLoginClick,
},
{
name: '구글',
logo: googleLogo,
background: '#FFFFFF',
color: '#222222',
onClick: handleGoogleLoginClick,
},
] as const;
'/auth' 페이지
const Auth = () => {
const navigate = useNavigate();
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const loginType = params.get('type');
// accessToken 유무를 먼저 확인하고 현재 로그인된 유저 정보를 불러오는 react-query hook
const { data, isLoading, refetch } = useUserQuery();
useEffect(() => {
const fetchToken = async () => {
if (!code) return navigate('/login');
try {
// kakao의 redirectUri 뒤에만 params를 붙일 수 있어서 이렇게 로직을 짬
const response = await axios.get(
`백엔드 측 OAuth api주소/${loginType ? 'kakao' : 'google'}?code=${code}`,
);
// 백엔드에서 토큰을 response의 헤더에 넣어줌
// 그걸 cookie에 저장하는 로직
if (response.status === 200) {
const accessToken = response.headers?.authorization.split(' ')[1];
setCookie('accessToken', accessToken, {
sameSite: 'None',
secure: true,
});
await refetch();
}
} catch (error) {
console.error('Failed to authenticate:', error);
navigate('/login');
}
};
fetchToken();
}, [code, navigate, refetch, loginType]);
useEffect(() => {
if (!isLoading && data) {
// 현재 회원 정보 조회 후 그 값에 따라서 리다이렉트 시켜주고 있음
// 이 로직은 프로젝트마다 변경되겠죠?
if (data.userState === 'VERIFIED') {
navigate(`/main/${data.uuid}`);
} else {
navigate('/setup');
}
}
}, [data, isLoading, navigate]);
return (
<div> 로그인하는 중입니다. </div>
)
}
더 궁금했던 점
1. 왜 Client Secret 를 사용하는 것이 보안적인 측면에서 좋을까?
Client Secret은 OAuth 클라이언트가 권한 서버와 통신할 때 자신이 누구인지 증명하는 데 사용하는 비밀 키이다.
이는 API를 통해서 애플리케이션 서버 자원에 접근할 때 이 키로 신원을 검증하는 방식인데, 이를 이용하면 악의적인 애플리케이션이 권한 서버에 접근하는 것을 방지할 수 있다.
또한, 악의적인 사용자가 중간에 액세스 토큰을 탈취하더라도 클라이언트와 권한 서버 간의 신뢰 관계가 Client Secret을 통해 보호되기 때문에 더욱 안전해진다.
+ 카카오에서는 필수로 하지 않았지만, OAuth 2.0의 권한 부여 코드 그랜트 방식에서 필수 요소로 사용하고 있다.
2. 써드 파티에서 사용자 토큰을 발급받을 때 서버에서 발급받는게 좋은 이유
앞서 보았듯, 클라이언트 측에서도 써드 파티에 사용자 토큰을 발급 받을 수는 있다. 그런데, 왜 서버에서 발급받는 것이 좋은 걸까?
보안적인 측면
- 클라이언트 환경은 비교적 쉽게 노출될 수 있기 때문에 민감한 정보를 다루는 ( Client ID, Client Secret 등) 요청은 서버에서 하는 것이 좋다.
- 서버에서 처리하면 해커의 공격으로부터 방어할 수 있다. (트래픽을 중간에서 가로채거나, 해당 서비스를 디컴파일 하는 등의)
- 서버는 써드 파티 플랫폼과의 통신을 HTTPS로 보호된 연결을 통해 수행할 수 있기 때문에 신뢰할 수 있는 환경에서 안전하게 통신을 주고 받을 수 있다.
로직 확장성 및 일관성 유지 측면
- 액세스 토큰이 만료되었을 때 갱신하는 과정도 서버에서 처리되기 때문에, 토큰 관리의 일관성이 유지된다.
- 클라이언트 환경은 다양한 플랫폼에서 작동하기 때문에 보안 설정이나 API 호출 방식이 플랫폼마다 변경될 수 있지만, 서버에서 통합적으로 관리하면 이러한 차이를 줄일 수 있다.
- 발급받은 토큰을 이용하기 전에 서버에서 사용자 권한을 추가로 확인하거나 토큰 검증 절차를 거치는 등, 자체적인 비즈니스 로직 추가를 위해서는 서버에서 처리하는 것이 좋다.
3. 인가 코드는 왜 한 번 사용되면 유효하지 않게 되는 걸까?
클라이언트에서 사용자 토큰을 발급 받아보았는데, 이때 이 code가 한 번 사용되면 유효하지 않게 된다는 것을 알게 되었다. 왜 한 번 쓰고 버리는 걸까?
이유를 예상했을 때 당연히 보안 관련 이유로 그렇게 동작하는 거겠지 싶었지만, 정확히 어떤 위험으로부터의 보안을 지키기 위한 것인지는 제대로 알지 못하기 때문에 알아보았다.
- 한 번 사용한 인가 코드가 계속해서 유효한 상태로 남아있다면, 악의적인 사용자가 탈취하여 반복적으로 사용자의 자원에 접근할 수 있다.
- 그래서 인가 코드의 유효 기간은 사용하지 않더라도 대개 몇 분 이내로 짧은 기간을 가진다.
- OAuth 2.0의 권한 부여 코드 그랜트 흐름 설계의 핵심으로, 신뢰를 보장하기 위한 인가 코드는 짧은 시간 동안 한 번만 사용할 수 있도록 제한된다.
느낀 점
처음으로 OAuth를 구현하면 많이 긴장하고 걱정했는데, 이 과정을 이해하고 나니 크게 어렵지 않은 것 같다는 생각이 들었다.
사실 회사에 들어가면 내가 OAuth를 구현할 일은 사실상 없겠지만, 그래도 궁금한 건 공부해봐야 직성이 풀리는 것 같다.
다른 분들도 OAuth에 대해서 이 글을 통해 조금이나마 이해할 수 있다면 좋겠다.
그럼 오늘은 이만 ~!
안뇽 !
'🗂️ 개발 이모저모' 카테고리의 다른 글
카카오톡 공유하기를 내 맘대로 바꿔보자 (with.기본 템플릿 커스텀) (6) | 2024.10.03 |
---|---|
카카오톡 공유하기를 원하는 대로 만들어보자 ! (with. 메시지 템플릿) (5) | 2024.09.29 |
내 이미지 어디갔어 !? (배포환경에서 svg 이미지가 로드되지 않는 이유) (5) | 2024.09.12 |
[타임세이버] query-key 줍다 지친 사람 여기 여기 붙어라 (with. query-key-factory) (0) | 2024.08.22 |
[FeedB] 무한 스크롤 구현 (with. App router & react-query) (0) | 2024.07.26 |