OAuth는 인증을 중개해주는 매커니즘이다.
이미 사용자 정보를 가지고 있는 웹 서비스(Naver, Kakao, Google 등)에서 사용자의 인증을 대신해주고, 접근 권한에 대한 토큰을 발급한 후, 이를 이용해 내 서버에서 인증을 한다.
기존 서비스에 로그인만 되어있다면 새로운 서비스에 바로 액세스 토큰을 내어주기 때문에 보안성이 조금 떨어진다.
Authorization Code를 사용한 인증 단계가 추가로 있기 때문에 비교적 더 안전하다.
그러나 사용자가 새로운 서비스를 이용하다가 액세스 토큰이 만료되었을 때, 매번 이 과정을 거쳐서 액세스 토큰을 다시 발급받아야 한다면 사용자 편의성에 있어서는 좋지 않을 수 있다.
1. 사이트 접속
2. 로그인
client에서 Authorizaiton Server로 인증 요청을 보낸다
const loginRequestHandler = () => {
// GitHub URL로 사용자 인증 요청
// OAuth 인증이 완료되면 authorization code와 함께 callback url로 리디렉션
return window.location.assign(
`https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}`
);
};
3~4. Authorization Code 전달
인증 요청을 받은 Authorizaiton Server는 유효한 인증 요청인지 확인한 후 Authorization Code를 발급해 다시 클라이언트로 (url 파라미터를 통해) 보내준다.
그리고 클라이언트는 Authorization Code를 로컬 서버에 보내준다.
// Authorization Server로부터 클라이언트로 리디렉션된 경우, Authorization Code가 함께 전달된다.
//ex) http://localhost:3000/mypage?code=5e52fb85d6a1ed46a51f
useEffect(() => {
const url = new URL(window.location.href);
const authorizationCode = url.searchParams.get('code');
}, []);
// 받아온 Authorization Code를 로컬 서버로 보낸다.
// 왜냐하면 Access Token은 보안 유지가 필요하기 때문에
//클라이언트에서 직접 OAuth App에 요청을 하는 방법은 보안에 취약할 수 있다.
// 그러므로 보안을 위해 Authorization Code를 서버로 보내주고 서버에서 Access Token 요청을 하는 것이 적절하다.
const getAccessToken = async (authorizationCode) => {
return axios
.post('http://localhost:4000/callback', {authorizationCode}) // 로컬 서버로 보냄
.then((res) => { //요청 성공하면 로컬 서버로부터 Access Token 받고 로그인됨
setAccessToken(res.data.accessToken)
setIsLogin(true)
})
5~6. Local Server에서 Authorization Server으로 액세트 토큰 요청 및 액세트 토큰 전달
Authorization Code를 받은 로컬 서버는 Authorization Code과 함께 액세스 토큰을 Authorizaiton Server에 요청한다.
Authorizaiton Server는 유효한 Authorization Code인지 확인한 후 액세스 토큰을 발급해
로컬서버로 액세스 토큰을 전달한다.
module.exports = async (req, res) => {
// req의 body로 authorization code가 들어온다.
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;//인증성공 시 accessToken 받음
return res.status(200).send({ accessToken });// 전달 받은 accessToken 클라이언트로 전달
} catch (err) {
return res.status(401).send({ message: 'error' });
}
};
7~9. 액세스 토큰을 담아 Resource Server로 사용자의 정보를 요청
Resource Server는 Application에게서 전달 받은 액세스 토큰이 유효한 토큰인지 확인 후 사용자의 정보를 로컬서버에 보내준다.
useEffect(() => { // 클라이언트가 로컬서버로 accessToken과 함께 사용자 정보 요청
axios.post('http://localhost:4000/userinfo', {accessToken})
.then((res) => { // 요청 성공시 정보 받아서 상태 변경
setGithubUser(res.data.githubUserData)
setServerResource(res.data.serverResource)
setIsLoading(false)
})
}, []);
module.exports = async (req, res) => {
const { accessToken } = req.body;
// 클라이언트에서 전달받은 access token를 이용해 Resource Server로 사용자의 정보요청
return axios
.get('https://api.github.com/user', {
headers: {
Authorization: `token ${accessToken}`,
},
})
.then((res) => res.data) // 성공시 사용자 정보 받음
.then((githubUserData) => { // 받은 정보 클라이언트로 전달
res.send({ githubUserData, serverResource });
})
.catch((e) => {
res.sendStatus(403);
});
};
const logoutHandler = () => {
// prop으로 받은 Access Token을 이용해 /logout 엔드포인트로 로컬서버에게 로그아웃 요청
axios.delete('http://localhost:4000/logout', {data: {accessToken}})
.then((res) => { // 요청 성공 로그아웃으로 상태 변경
setIsLogin(false);
setGithubUser(null);
setServerResource(null);
setAccessToken(null)
})
};
axios.delete 로 body에 정보 실어 보내는 법
post와 다르게 그냥 {accessToken}에 담아 보내면 안되고 {data: {accessToken}} 이런 형식으로 보내주어야 서버에서 req.body로 받을 수 있다.
module.exports = (req, res) => {
const { accessToken } = req.body;
// 클라이언트에서 전달받은 access token를 이용해 사용자의 권한 부여를 취소 요청
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) => {
});
};