웹 커뮤니티를 개발하면서 사용자의 편의성을 위해 소셜 로그인기능을 추가한 경험이 있다.
Django프레임 에서 allauth 라이브러리를 이용해 구현했지만, 따라하는 수준에 그쳐 더 배우지 못해 아쉬움이 남았다.
한번 알아보자.
OAuth는 주로 소셜 로그인과 같은 경우에 사용되는 권한 위임 프로토콜이다. 사용자가 특정 웹사이트에서 다른 웹사이트의 사용자 정보를 이용하고자 할 때, OAuth를 통해 안전하게 인증과 인가를 수행할 수 있다.
원본 사이트가 다른 웹사이트의 로그인 정보를 알지 않아도 액세스 토큰으로 안전하고 편리하게 사용자 인증을 할 수 있다는 장점이 있다.
다음과 같은 네가지 주체가 존재한다.
1. 리소스 소유자: 사용자를 의미하며, 자신의 자원에 대한 접근 권한을 제어한다.
2. 클라이언트: 리소스 소유자의 정보를 필요로 하는 애플리케이션이다. 예를 들어, 소셜 미디어 애플리케이션이 있다.
3. 리소스 서버: 보호된 자원을 저정하고 있는 서버로, 일반적으로 API를 통해 접근한다.
4. 인가 서버: 사용자의 인증을 담당하고 액세스 토큰을 발급한다.
간단히 말해, 클라이언트는 리소스 소유자의 인가를 받고 인가 서버에게서 액세스 토큰을 받는다. 그리고 리소스 서버를 통해 자원에 접근할 때 액세스토큰을 제시해 인증을 받고 리소스에대한 접근 권한을 얻는 것이다.
클라이언트에 대한 신뢰보다 더 신뢰있는 웹사이트에 자원을 저장함으로써 리소스 소유자는 민감정보를 보호할 수 있으며 클라이언트도 민감정보를 저장하고 관리하는 비용을 줄일 수 있다.
한번 OAuth2.0에 대한 간단한 소셜 로그인 예제를 실습해보자.
사용자가 애플리케이션에 로그인하려고 할 때, 아래와 같은 URL로 리디렉션된다.
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=MY_CLIENT_ID&redirect_url=MY_REDIRECT_URL&scope=SCOPE&state=STATE
사용자가 권한을 승인하면, 인가 서버는 리디렉션 URI로 돌아가며 인가 코드를 전달한다. 이 코드를 서버에서 처리한다.
서버 측에서 인가코드를 사용해 액세스 토큰을 요청한다. 다음과 같은 POST요청을 보낸다.
POST https://oauth2.googleapis.com/token
요청 본문 (application/x-www-form-urlencoded):
code=AUTHORIZATION_CODE
&client_id=MY_CLIENT_ID
&client_secret=MY_CLIENT_SECRET
&redirect_url=MY_REDIRECT_URI
&grant_type=authorization_code
해당 요청이 성공하면, 응답으로 액세스 토큰과 리프레시 토큰이 반환된다.
발급받은 액세스 토큰을 사용해 Google API에 요청을 보낸다. 예를 들어, 사용자의 프로필 정보를 가져오려면 다음과 같이 GET 요청을 보낸다.
GET https://www.googleapis.com/oauth2/v2/userinfo
요청 헤더:
AUTHORIZATION: Bearer ACCESS_TOKEN
Node.js와 Express를 사용한 간단한 예제이다. express는 Node.js 웹 애플리케이션 프레임워크로, HTTP 요청을 쉽게 처리해주기 때문에 사용했다.
const express = require('express')
const axois = require('axios')
const querystring = require('querystring')
const app = express();
const POST = 8080;
const CLIENT_ID = "MY_CLIENT_ID";
const CLIENT_SECRET = "MY_CLIENT_SECRET";
const REDIRECT_URI = "http://localhost:8080/callback";
app.get("/login", (req, res) => {
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${CLIENT_ID}&redirect_url=${REDIRECT_URI}&scope=email profile&state=STATE;
res.redirect(authUrl);
});
app.get('/callback', async (req, res) => {
const { code } = req.query;
const tokenResponse = await axois.post('https://oatuh2.googleapis.com/token', queryString.stringify({
code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
}));
const accessToken = tokenResponse.data.access_token;
const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {Authorization: `Bearer ${accessToken}` }
});
res.json(userinfoResponse.data);
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
간단히 예제를 설명하면 다음과 같다.
/callback이란 중간 리디렉션 경로를 넣어 client와 인가 서버가 서로 만나는 장을 만드는 것 같다.