OAuth
OAuth의 원리
OAuth는 인증을 중개해 주는 메커니즘입니다. 보안된 리소스에 액세스하기 위해 클라이언트에게 권한을 제공하는 프로세스를 단순화하는 프로토콜입니다. 즉, 이미 사용자 정보를 가지고 있는 웹 서비스(Naver, Kakao, Google 등)에서 사용자의 인증을 대신해 주고, 접근 권한에 대한 토큰을 발급한 후, 이를 이용해 내 서버에서 인증이 가능해집니다.

Resource Owner
- OAuth 인증을 통해 소셜 로그인을 하고 싶어 하는 사용자를 Resource Owner라고 부릅니다.
- Resource는 사용자의 이름, 전화번호 등의 정보를 뜻합니다. 이러한 정보의 주인이 바로 사용자이기 때문에 Resource Owner라고 합니다.
Resource Server & Authorization Server
- 사용자가 소셜 로그인을 하기 위해서 사용하는, 이미 사용 중인 서비스(Naver, Kakao, Google 등)의 서버 중 사용자의 정보를 저장하고 있는 서버를 특정해서 Resource Server라고 부릅니다.
- 이미 사용 중인 서비스의 서버 중 인증을 담당하는 서버를 특정해서 Authorization Server라고 부릅니다.
Application
- 사용자가 소셜 로그인을 활용해 이용하고자 하는 새로운 서비스는 환경에 따라서 조금씩 다르게 불립니다. 여기서는 Application이라고 부르도록 하겠습니다.
- 경우에 따라서 Applicaiton을 Client와 Server로 세분화해서 지칭하기도 합니다.
OAuth 인증 방식의 종류와 흐름
Implicit Grant Type, Authorization Code Grant Type, 그리고 Refresh Token Grant Type, 이렇게 세 가지에 대해서 알아보겠습니다.
1. Implicit Grant Type
- Grant Type : Authorization Server에서 Access Token을 받아오는 방식
-
사용자가 Application에 접속합니다.
-
Application에서 Authorization Server로 인증 요청을 보냅니다.
-
Authorizaiton Server는 유효한 인증 요청인지 확인한 후 액세스 토큰을 발급합니다.
Client ID: 클라이언트 등록, Client Secret: 클라이언트 비밀번호 공개 금지!, Authorized redirect URI: code 혹은 token을 받는 URI
-
Authorization Server에서 Application으로 액세스 토큰을 전달합니다.
-
Application은 발급받은 액세스 토큰을 담아 Resource Server로 사용자의 정보를 요청합니다.
-
Resource Server는 Application에게서 전달받은 액세스 토큰이 유효한 토큰인지 확인합니다.
-
유효한 토큰이라면, Application이 요청한 사용자의 정보를 전달합니다.
이렇게 인증을 중개받아 새로운 서비스를 이용할 수 있게 되었습니다. 하지만 소셜 로그인에서 Implicit Grant Type은 잘 사용하지 않습니다. 기존 서비스에 로그인만 되어있다면 새로운 서비스에 바로 액세스 토큰을 내어주기 때문에 보안성이 조금 떨어지기 때문인데요. 그래서 보통은 여기에 인증 단계를 한 단계 추가한 인증 방식인 Authorization Code Grant Type을 주로 사용하게 됩니다.
2. Authorization Code Grant Type

- 사용자가 Application에 접속합니다.
- Application에서 Authorization Server로 인증 요청을 보냅니다.
- Authorizaiton Server는 유효한 인증 요청인지 확인한 후 Authorization Code를 발급합니다.
- Authorization Server에서 Application으로 Authorization Code를 전달합니다.
- Application이 Authorization Code로 발급받은 Authorization Code를 전달합니다.
- Authorizaiton Server는 유효한 Authorization Code인지 확인한 후 액세스 토큰을 발급합니다.
- Authorization Server에서 Application으로 액세스 토큰을 전달합니다.
- Application은 발급받은 액세스 토큰을 담아 Resource Server로 사용자의 정보를 요청합니다.
- Resource Server는 Application에게서 전달받은 액세스 토큰이 유효한 토큰인지 확인합니다.
- 유효한 토큰이라면, Application이 요청한 사용자의 정보를 전달합니다.
Implicit Grant Type과 비교해 보면, Authorization Code를 사용한 인증 단계가 추가로 있기 때문에 비교적 더 안전합니다. 또한, 원한다면 아래와 같이 토큰을 Application의 Client에 노출시키지 않고 Server에서만 관리하도록 만들 수도 있기 때문에 소셜 로그인을 구현하는 방식의 선택지가 늘어나게 됩니다.

그런데, 사용자가 새로운 서비스를 이용하다가 액세스 토큰이 만료되었을 때, 매번 이 과정을 거쳐서 액세스 토큰을 다시 발급받아야 한다면 사용자 편의성에 있어서는 좋지 않습니다. 그렇기 때문에 액세스 토큰을 발급해 줄 때 리프레시 토큰을 같이 발급해주기도 합니다. 이때, 리프레시 토큰을 사용해서 액세스 토큰을 받아오는 인증 방식을 Refresh Token Grant Type이라고 합니다.
3. Refresh Token Grant Type

Refresh Token Grant Type은 간단합니다. Authorization Server로 리프레시 토큰을 보내주면, Authorization Server는 리프레시 토큰을 검증한 다음 액세스 토큰을 다시 발급해 주게 됩니다. Application은 다시 발급받은 액세스 토큰을 사용해서 Resource Server에서 사용자의 정보를 받아오게 됩니다.
실습
- 각각의 요청 URI는 API 명세서를 통해 확인할 수 있으며, 필수 정보를 확인 할 수 있다.
Getting started with the REST API - GitHub Docs
등록하기
-
모든 웹사이트에서 자신의 어플리케이션을 등록해야 한다. 이때 등록하는 방법은 모두 유사하다. 먼저 어플리케이션 URL을 등록해주어야 하며, 추가적으로 Authorization code를 받을 callback URI을 등록해야 한다. 등록을 모두 마치면 CLient ID와 CLient KEY를 발급 받는다. 이때 Client Key는 노출되어서는 안된다.
-
네이버 애플리케이션 등록
-
카카오 애플리케이션 등록
-
카카오 사이트 DNS와 Redirect URI 등록하기
-
authorization code 발급하기
const loginRequestHandler = () => {
// TODO: GitHub로부터 사용자 인증을 위해 GitHub로 이동해야 합니다. 적절한 URL을 입력하세요.
// OAuth 인증이 완료되면 authorization code와 함께 callback url로 리디렉션 합니다.
// 참고: https://docs.github.com/en/free-pro-team@latest/developers/apps/identifying-and-authorizing-users-for-github-apps
return window.location.assign(
`https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}`,
);
};
각각의 API 명세서에 맞는 URL을 입력해서 Authorization server에 authorization code를 요청한다.
위의 authorization code를 요청하는 과정은 server를 거치지 않고 FE에서 실행하고 있다.
더불어 각각의 authorization code 발급받기 위한 URL 작성시, 필수적이지 않은 query를 추가할 수 있다. state 등을 추가해서 각각의 URL을 분기 처리할 수 있다.
useEffect(() => {
// Authorization Server로부터 클라이언트로 리디렉션된 경우, Authorization Code가 함께 전달됩니다.
// ex) http://localhost:3000/mypage?code=5e52fb85d6a1ed46a51f
const url = new URL(window.location.href);
const authorizationCode = url.searchParams.get('code');
const state = url.searchParams.get('state');
console.log(state, authorizationCode);
if (authorizationCode && state === null) {
getAccessToken(authorizationCode, state);
} else if (authorizationCode && state === 'naver') {
getNaverAccessTocken(authorizationCode, state);
} else if (authorizationCode && state === 'KAKAO') {
getKAKAOAccessTocken(authorizationCode, state);
}
}, []);
Access token 발급 및 요청
FE에서 받아온 authorization code를 서버로 전달한다. 이후 서버에서 Auth server에 Access token 발급을 요청한다.
const getAccessToken = async authorizationCode => {
// 받아온 Authorization Code로 다시 OAuth App에 요청해서 Access Token을 받을 수 있습니다.
// Access Token은 보안 유지가 필요하기 때문에 클라이언트에서 직접 OAuth App에 요청을 하는 방법은 보안에 취약할 수 있습니다.
// Authorization Code를 서버로 보내주고 서버에서 Access Token 요청을 하는 것이 적절합니다.
// TODO: 서버의 /callback 엔드포인트로 Authorization Code를 보내주고 Access Token을 받아옵니다.
// Access Token을 받아온 후 state에 Access Token을 저장하세요
axios
.post('http://localhost:4000/callback', {
authorizationCode,
state: 'GITHUB',
})
.then(res => {
console.log(res.data);
setAccessToken(res.data.accessToken);
setOAuth(res.data.state);
setIsLogin(true);
})
.catch(err => {
console.log(err.response.data);
console.log('ERREO');
});
};
module.exports = async (req, res) => {
// req의 body로 authorization code가 들어옵니다. console.log를 통해 서버의 터미널창에서 확인해보세요!
// authorization code를 이용해 access token을 발급받기 위한 post 요청을 보냅니다. 다음 링크를 참고하세요.
// https://docs.github.com/en/free-pro-team@latest/developers/apps/identifying-and-authorizing-users-for-github-apps#2-users-are-redirected-back-to-your-site-by-github
const { state } = req.body;
console.log(req.body);
if (state === "GITHUB") {
try {
const result = await axios({
method: "post",
url: `https://github.com/login/oauth/access_token`,
headers: {
accept: "application/json",
},
data: {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: req.body.authorizationCode,
},
});
const accessToken = result.data.access_token;
return res.status(200).send({ accessToken, state });
} catch (err) {
return res.status(401).send({ message: "error" });
}
}
유저 정보 요청하기
발급 받은 Access token을 활용해서 유저 정보를 받아온다.
if (OAuth === 'GITHUB') {
axios
.post('http://localhost:4000/userinfo', { accessToken, OAuth })
.then(res => {
console.log(res);
const { githubUserData, serverResource } = res.data;
setIsLoading(false);
setServerResource(serverResource);
setGithubUser(githubUserData);
})
.catch(err => {
console.log(err.response.data);
console.log('ERROR');
});
}
if (OAuth === 'GITHUB') {
return axios
.get('https://api.github.com/user', {
headers: {
Authorization: `token ${accessToken}`,
},
})
.then(res => res.data)
.then(githubUserData => {
res.cookie('userData', { githubUserData, serverResource }, cookieOption);
res.send({ githubUserData, serverResource });
})
.catch(e => {
res.sendStatus(403);
});
}
logout 구현하기
- logout 또한 동일하게 URL을 요청한다. access token을 활용해서 사용자의 권한 부여를 취소한다.
axios
.delete('http://localhost:4000/logout', {
data: { accessToken, hello: 'hello' },
})
.then(() => {
console.log('done');
setIsLogin(false);
})
.catch(err => {
console.log(err.response.data);
});
module.exports = (req, res) => {
const { accessToken } = req.body;
// 클라이언트에서 전달받은 access token를 이용해 사용자의 권한 부여를 취소합니다. 다음 링크를 참고하세요.
// https://docs.github.com/en/rest/apps/oauth-applications
console.log(req.body);
axios
.delete(`https://api.github.com/applications/${CLIENT_ID}/token`, {
data: {
access_token: accessToken,
},
auth: {
username: CLIENT_ID,
password: CLIENT_SECRET,
},
})
.then(() => {
res.status(205).send('Successfuly Logged Out');
})
.catch(e => {
//console.log(e.response);
console.log('ERREOR');
});
};
- 일련의 과정은 Github이나 다른 KAKAO와 NAVER와 유사했다. github은 refresh token 발급을 하지 않지만, 다른 두 사이트는 refresh token을 발급해주고 있었다. 또한 더 많은 유저 상세 정보를 옵션을 통해 요청할 수도 있다는 것을 배울 수 있었다. 그리고 전체 과정을 알고 이를 적용하니 그냥 동일한 작업임을 알 수 있었다.
- OAuth의 원리
- OAuth 인증 방식의 종류와 흐름
- - 1. Implicit Grant Type
- - 2. Authorization Code Grant Type
- - 3. Refresh Token Grant Type
- 실습
- - 등록하기
- - authorization code 발급하기
- - Access token 발급 및 요청
- - 유저 정보 요청하기
- - logout 구현하기