오늘은 어제 공부했던 OAuth
를 공식문서와 어느 정도 검증된 곳들에서 설명해 준 것들을 다시 한 번 살펴보고, sequelize
sprint 에서 막혔던 부분도 다시 한 번 공부할 예정이다. 또, JWT
(JSON Web Token) 도 살펴보라는 이야기를 많이 해서(실무에서 정말 많이 쓴다고 한다) 겸사겸사 살펴보려고 한다. 해야 할 공부가 정말 많다!
Getting Ready - OAuth 2.0 Simplified
이번에도 공식문서를 보고 정리하는 식으로 공부를 했다. 구글링을 해 보니 naver D2 에서도 정리를 해 놨던데, 코드스테이츠 측에서 일단은 Getting Ready, Accessing Data in an OAuth Server 두 문단을 공식문서를 보는 걸 추천해서 공식문서의 두 단락을 먼저 보고 나서 나머지 글들을 한 번 확인해 볼 생각이다.
웹앱을 만들든, 모바일앱을 만들든 간에 OAuth 의 사용을 시작할 때 염두해야 할 사항들에 대해 적은 단락이다.
애플리케이션을 만들 때 일반적으로 계정을 만든다. 그리고 사용자는 계정 생성(회원가입) 을 통해 웹사이트의 로고, 웹사이트의 이름 등등 간단한 것들에 접근할 수 있는 권한을 부여받는다 (authorization) 일반적으로는 그렇게 회원가입 과정을 거치면 client_id
라는 걸 부여받는다. 이를 통해 사용자는 서비스와 소통한다(interact)
이 과정에서 중요한 점은 redirection URL 을 만들라는 것이다. OAuth 는 유저가 해당 애플리케이션에서 authorization 을 마친 뒤 redirection URL 으로 redirect 시키는데, 이 과정은 굉장히 중요하다. 여기서 자칫하면 유저의 정보를 탈취하려는 또 다른 애플리케이션들이 만들어지기 쉽다.
OAuth 2.0 API 는 그저 지정한 redirect URL 로 유저를 redirect 시킬 뿐이다. authorization code 나 token 등이 탈취되는 과정을 막기 위해서이다. 이런 redirection URL 을 만들 때 유의할 점들이 있다.
일단은 다양한 redirect URL 을 준비하면 좋다. 하나의 client_id
를 여러 개의 viewport 에서 동시에 사용하는 상황을 handling 할 수 있기 때문이다. 그래서 몇몇 서비스는 해당 상황을 감안해 다양한 redirect URL 을 만들 수 있게 한다.
또 이런 redirect URL 을 만들 때의 protocol 방식은 https
가 되어야 한다. 그렇게 해야 authorization 과정에서 정보가 도중에 탈취당하는 (intercept) 상황 등을 방지할 수 있다.
그리고 redirection URL 에 query string 을 노출시키는 상황은 최대한 피해라. https://example.com/auth?destination=account
이렇게 노출시키지 말고 그냥 endpoint 정도만 바꿔주어라. https://example.com/auth
정도로만.
위에 적은 여러 viewport 에서의 동시접속 handling 뿐만이 아니라, 다양한 곳에 가입경로를 포함시키는 경우때문에 여러 개의 redirection URL 이 필요한 경우도 있을 것이다. 예를 들자면 홈페이지의 로그인 화면에도 authentication button 을 구현해놓고, 어떤 public item 을 볼 때도 authentication button 을 구현하는 상황 등등. 이런 경우는 "다양한 곳에서 request 를 받아야 하니, 다양한 redirection URL 이 필요하겠네" 할 수도 있겠으나, OAuth에서는 위같은 경우를 위해 state
parameter 를 제공한다.
시작하기 전에 github 에 가서 어플리케이션을 만들어주어야 한다. 앞으로 사용할 client id
와 clietn secret
을 받아와야 하기 때문이다. 해당 과정은 github 의 developer setting 에서 진행할 수 있다.
이런 화면을 볼 수 있다. 우리가 아까 말했던 항목들이 보인다. authorization 을 완료한 use 를 token 과 같은 정보와 함께 해당 endpoint 로 redirect 를 시켜주는 데 사용하는 authorization callback URL 이라든지, 우리의 애플리케이션의 메인 주소로 사용할 homepage URL 이라든지. 저 부분을 우리가 만들어서 채워준다.
중요한 점은 client secret
은 정말정말 중요하게 취급해야 한다는 점이다. client id
의 경우는 사실상 공개정보로 취급된다. 왜냐면 해당정보는 authorization URL 을 만드는 데도 필요하고, 웹페이지의 JS source code 에 들어가야 할 수도 있기 때문이다.
그러나 client secret 은 정말 중요하게 취급해야 한다. 이를 절대 JS 파일 내부에 직접 작성하거나, git 에 올리는 일이 없도록 하자.
Accept
와 User-agent
라는 header 를 세팅한다.JSON
으로 파싱한다.이전 단락에서 2-2 의 과정을 통해 기본적인 설정을 다 완료하였으니, 이번 단락에서는 그 설정을 기반으로 OAuth 를 사용하는 과정을 설명한다.
?action=login
이다. state
라는 parameter 가 생성된다. client 를 보호하기 위해 사용한다. 이는 client 가 생성하는 임의의 문자열이고, session 안에 저장된다.state
를 query string 에 담는다.code
와 state
parameter 를 request
에 담아 넘겨준다.code
라는 요소를 access-token
과 맞교환 할 것이다(exchange)2-3 의 과정에서 authentication 을 거친 user 가 다시 우리의 애플리케이션으로 돌아왔다면, 이제 code
와 state
라는 parameter 를 query string 으로부터 가져올 수 있게 된다.
state
parameter 는 우리가 초기에 부여해주었던 것과 똑같다, 우리는 애플리케이션에서 그 state
parameter 가 초기값과 같은지 검사를 함으로써 혹시라도 우리의 정보를 탈취하려는 시도를 막아낼 수 있다. 다시 말 해, CSRF attack 을 방어하는 과정이라는 이야기이다.
그렇게 state
의 동일함을 확인한 뒤에는 github 의 token endpoint 로 request 요청을 보내 authorization code 를 access token 과 교환한다. 해당 request 에는 우리들의 public 한 정보인 client ID
와, 우리가 소중히 관리해야 한다던 client secret
이 담겨있다. redirect URL 도 이전에 authorization code 와 관련하여 사용했던 것과 똑같은 URL 을 발송한다.
이 모든 요청이 정상적으로 진행되었으면, 이제 github 는 access token 을 발급하고 response 에 담아 전달한다. 우리는 해당 token 을 session 에 담고, 홈페이지로 redirect 를 걸어주고, 유저는 로그인을 한다.
{
"access_token": "e2f8c8e136c73b1e909bb1021b3b4c29",
"token_type": "Bearer",
"scope": "public_repo,user"
}
우리가 github 로 부터 받는 response 는 이런 식이다.
우리는 이 access_token
을 받아 우리의 session 에 저장한다. 이 다음에 해당 페이지를 방문할 때도, 페이지는 이미 우리가 앞의 과정들을 통해 발급받은 access-token 을 통해 user 를 판별하고, 우리가 이전에 만들었던 로그인을 했을 때 나오는 화면들을 띄워준다.
OAuth 의 예시 코드에서 error handling 을 한 부분은 없었다. 실제로 우리가 서비스를 기획할 때에는 github 로 부터 error 가 발생한 경우, 사용자에게 적절한 메시지를 띄워주는 error handling 과정도 포함해야 한다.
이제 한 번 OAuth 를 통한 github 인증 과정을 JS 로 구현해보려고 한다. accessing data in an OAuth Server
단락을 쭉 참고하고, PHP 로 작성된 코드를 최대한 JS 로 구현해볼 예정이다.
일단은 기본적으론 express
를 기반으로 서버를 구성하였으며, request 로깅을 위한 미들웨어로 morgan
을 사용하였다. session
객체 핸들링은 express-session
을, 그리고 외부에서 로컬로 접속할 수 있도록 하기 위해서 ngrok
을 사용하였다.
일단은 외부에서 접속할 수 있도록 ngrok 을 통해 도메인을 만들어주었다. 나는 8080
포트를 사용하고 있기에, ngrok http 8080
을 통해 실행해주었다. 또, session 관련 처리는 express-session
을 이용하였다.
추가로, ngrok 의 설치는 현재 macOS 를 사용하는 관계로 brew cask install ngrok
명령어를 통해 진행하였다.
ngrok by @inconshreveable (Ctrl+C to quit)
Session Status online
Session Expires 7 hours, 59 minutes
Version 2.3.35
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://c8bcdd82693b.ngrok.io -> http://localhost:8
Forwarding https://c8bcdd82693b.ngrok.io -> http://localhost:
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
이런 창이 나오는 걸 확인할 수 있었다. http 와 https 의 도메인이 존재하는데, 홈페이지 도메인으로는 http 를, 그리고 redirection 도메인으로는 https 를 사용하였다. 그 이유는 OAuth 의 문서에서 말 한 것처럼 redirection 용 링크는 https 를 사용하여 정보를 탈취할 수 있는 가능성을 조기차단하는 것이 좋다고 했기 때문이다.
공식문서에서 나온대로 설정을 해 주었고, 보이진 않지만 위에 있는 client ID 와 client SECRET 또한 발급을 잘 받았다.
이렇게 postman 을 통해 보낸 GET request 도 잘 날아가고 있는 걸 확인할 수 있다.
Deprecation Notice: GitHub will discontinue the OAuth Authorizations API, which is used by integrations to create personal access tokens and OAuth tokens, and you must now create these tokens using our web application flow. The OAuth Authorizations API will be removed on November, 13, 2020. For more information, including scheduled brownouts, see the blog post.
해당 문서를 보고있는데, OAuth authorizations API 가 조만간 사라지고(2020/10/13 부로), github 에서 제공하는 새로운 web application flow 를 통하여 token 을 발급하라는 이야기가 있었다. 그래서 해당 문서에 나와있는 API를 참조하기로 했다.
GET https://github.com/login/oauth/authorize
일단 이 API 를 보면서 가장 먼저 든 생각은, "어떻게 request 를 할까" 였다. 물론 기본적으로 지원하는 fetch 가 있지만, axios 라는 라이브러리를 통해서 request 를 하는 경우도 꽤 보았기 때문이다. 그래서 "axios vs fetch" 라는 키워드로 검색하니, 나랑 비슷한 고민을 하는 사람들이 많았고, 그중에서 그 고민에 대한 나름의 간단한 답을 찾을 수 있을 법한 글을 찾았다.
[React / React Naive TIPS] axios 와 fetch 어떤 것을 사용할까?
18년도 글이긴 한데, 일단 fetch API 가 크게 바뀌었을 거 같진 않다는 생각이 들었다. 그리고 axios 문서를 들어가 보았는데 fetch 보다 조금 더 간결해 보였다. 이 글에서도 "axios 가 확실히 조금 더 편하게 사용할 수 있게 디자인되었다" 라는 이야기를 했는데, 그 이야기가 생각나며 약간 공감이 갔다.
사실 모든 걸 차치하고서라도 axios 를 한 번도 사용해 본 적이 없기 때문에, axios 를 사용해 보자는 생각에 axios 를 사용해 보기로 했다. npm 옆의 weekly download 그래프도 도 쭉 올라가고 있으니 대세에 편승한다는 느낌으로다가, npm install axios --save
를 통해 설치해주었다.
const axios = require('axios');
let authRequest = () => {
axios.get("https://github.com/login/oauth/authorize", {
params: {
client_id: "535d14a5308cacbed013",
state: "pravda"
}
}).then(
(res) => console.log(res)
)
}
이런 식으로 요청을 보내보았다. 200
코드가 잘 떴고, 무수한(...) 정보들이 쏟아져 나왔다. 그렇게 해서 건진 query string 의 code
를 express 의 query
를 통해서 res.query.code
로 접근하였다. 그 다음에는 그 값과 내 client_id
, client_secret
을 통해 [https://github.com/login/oauth/access_token](https://github.com/login/oauth/access_token)
에 post 요청을 보내주었고, 그렇게 해서 받은 response 를 res.data
를 통해 접근하여 내 access-token
을 알아낼 수 있었다.
app.get('/callback', async (req, res) => {
let userCode = await req.query.code
axios.post('https://github.com/login/oauth/access_token', {
client_id: "535d14a5308cacbed013",
client_secret: process.env.PASSWORD,
code: String(userCode)
}).then(
(res) => console.log(res.data)
)
})
해당 코드를 통해 github API 로 body 에 필요한 정보들을 담아 POST
request 를 발송하였고
GET / 304 - - 3.377 ms
GET / 304 - - 1.322 ms
access_token=mytoken...&scope=&token_type=bearer
그렇게 token 을 얻을 수 있었다. 이제 이렇게 발행한 token 으로 다른 API들을 사용하면 된다!
토큰 받아오는 거 밑바닥부터 구현하는 데만 이렇게 걸려버렸다 ㅠㅠ DB sprint 재도전과 JWT 는 내일 좀 쉬다가, 마저 정리해야겠다.
Cross-Site Request Forgery, 사이트 간 요청 위조 공격을 의미하며, 유저가 의도하지 않은 행동을 요청을 위조하여 실행하는 과정이다.
cookie 나 session 은 분명히 유저가 정상적으로 취득한 것이 맞는데, 해당 쿠키를 지닌 상태로 유저가 호기심을 가지게끔 이상한 URL 등을 보내, 그 쿠키를 기반으로 유저가 실행하려 하지 않았던 일들을 한다는 게 중요하다.