2016년 8월 24일 수요일

JWT 토큰은 어디에 저장하는게 좋을까?

이전에 JWT 토큰에 대한 글을 쓴 적이 있는데 그것의 연장선 상에 있는 이야기이다. JWT 를 구현하는데에 있어서 토큰을 어디에 저장해야 하는가는 한번 생각해 볼만한 문제이다. 토큰 인증이란 결국 사용자가 인증받은 토큰을 들고 다니면서 서버에 자원을 요청시에 함께 전달해야 하는 것인데 그러기 위해서는 어딘가에는 토큰을 클라이언트가 저장을 해야 하는 것이다.

웹 어플리케이션을 제작중이라면 생각해볼 수 있는 2가지 방법이 있다.

  1. HTML5 web storage (Local Storage, Session Storage)
  2. Cookies
그럼 이 두 가지를 하나씩 살펴 보면서 비교해보자.

1. HTML5 web storage

web storage 는 HTML5 부터 지원되는 기술로 클라이언트(브라우저)에 데이터를 저장할 수 있는 방법 중 하나이다. 이전에는 클라이언트에 데이터를 저장하기 위해서는 쿠키를 사용했었다. web storage 는 쿠키에 비해서 보안 좀 더 뛰어나며 웹사이트 성능에 영향 없이 더 많은 데이터(최소 5MB) 를 저장할 수 있다는 장점이 있다. web storage 는 각 도메인과 프로토콜 단위로 저장되어 진다. web storage 는 Local Storage 와 Session Storage 가 있는데 둘의 차이점은 Session Storage 는 브라우저 창이 꺼지면 데이터가 삭제 된다는 점이다.


(크롬 개발자 도구를 사용하면 집접 이 값을 추가/수정/삭제 가 가능하다.)

다시 토큰이야기로 돌아와보자. 처음 사용자 인증을 통해서 받게된 토큰을 바로 이제 여기에 저장할 수 있는 것이다. 저장은 자바스크립트 localStorage 객체를 통해 쉽게할 수 있다.

// response.token 에 api 를 통해 받은 토큰 값이 정해져있다고 가정.

var token = response.token; 
localStorage.setItem("token",token);

(크롬 개발자 도구를 통해 Local Storage 를 확인해보면 저장 되었음을 확인할 수 있다.)

이렇게 토큰을 저장해두면 나중에 서버에 HTTP 로 요청할때에 함께 보내면 된다. 보통은 HTTP 이용시 Authorization 헤더에 Bearer 스키마로 함께 보낸다. 서버측에서는 이 내용을 파싱해서 토큰을 확인하는 식이다.

이렇게 web storage 에 저장하는 방법은 나쁘지 않다. 자바스크립트로 값을 쉽게 저장하고 가져올 수 있기 때문에 편리한 이점이 있다. 토큰을 디코딩 해서 페이로드에 담긴 정보를 활용하기도 쉽다. 그런데 이러한 점은 사실 보안 측면에서는 좋지 않다. 자바스크립트도 제어 가능하다는 것은 곧 XSS(cross-site scripting) 공격에 취약할 수 있음을 의미한다. XSS 는 쉽게 말하면 해커가 자바스크립트 코드를 웹페이지에 심어 사용자의 정보를 탈취하는 종류의 공격이다. 일반적으로 웹 어플리케이션들은 사용자로부터 데이터를 입력받게 되는데 이 데이터에 해커가 자바스크립트 코드를 심어 놓을 수 있는 것이다. 쉽게 생각해 게시판에 글을 쓴다고 생각해보자. 글 내용에 해커가 자바스크립트 코드를 심어 놓고 사용자들이 이 글을 보면서 동시에 자바스크립트 코드가 실행되어 해커가 원하는 것을 얻을 수 있게 된다. 물론 이런 공격은 많이 알려져 있기 때문에 대부분에 이와 같은 입력은 사전에 필터링을 통해 차단한다. 그럼에도 불구하고 이와 같은 공격에는 여러가지 우회 방법이 존재할 수 있기 때문에 튼튼하게 방어해 놓지 않는다면 위험이 늘 존재하는 것이다. XSS 공격에 대해 자세히 알고 싶다면 이곳을 참조하자.


2. Cookies

다음으로 생각해볼 수 있는 것은 쿠키다. 쿠키는 예전부터 많이 써오던 기술이다. 간단히 설명하면 사용자 인증을 하게 되면 서버측에서는 이를 HTTP Set-Cookie 헤더를 통해서 토큰을 보낸다. 브라우저는 이를 통해서 쿠키를 생성하고 토큰을 저장한다. 이후에 해당 API 에 요청을 하게 될때에는 브라우저는 자동으로 이 쿠키를 실어서 보낸다. 간단하다. 

쿠키를 서버에 전달한다는 방식에 있어 브라우저가 자동으로 전달해주기 때문에 구현하는 입장에서는 더 편리한 점도 있다. 그런데 web storage 와 마찬가지로 쿠키 또한 자바스크립트를 통해서 조작이 가능한데 web storage 가 쿠키보다 더 좋은 점이 많은데 차이점은 무엇일까? 쿠키는 자바스크립트로 조작이 가능하지만 옵션 설정을 하면 이를 막을 수 있다는 점이 큰 차이점이다. 그 첫번째 옵션은 HttpOnly 이다. 쿠키 생성시에 이 옵션을 주게 되면 쿠키는 자바스크립트로 접근이 불가능하다. 오로지 HTTP 통신을 통해서만 쿠키가 전송된다. 그렇기 때문에 web storage 에서 발생할 수 있었던 XSS 공격에 대해 방어할 수 있다. 여기서 그치지 않고 Secure 옵션을 주게 되면 쿠키는 HTTPS 통신으로만 전송되기 때문에 보안 수준을 한 단계 더 높여줄 수 있다.

여기까지 보면 확실히 web storage 보다 cookie 를 사용하는 것이 보안적인 측면으로 뛰어나 보인다. 그러나 이 방식 또한 완벽하지는 않다. 이방식은 CSRF(cross-site request forgery) 라고 불리우는 또 다른 공격에 취약할 수 있다. CSRF 공격은 쿠키 전송방식의 취약점을 활용하는 방식이다. 쉽게 설명하면 A.com 에서 생성한 쿠키는 B.com 에서는 열어볼 수 없는 것이다. 아주 당연한 이야기이다. 위에서 설명했듯이 A.com 으로 HTTP 요청을 하면 브라우저는 알아서 쿠키를 헤더에 담아서 보낸다. HttpOnly 옵션을 준 경우 해커는 쿠키를 자바스크립트를 통해서 얻을 수 없고 결국 HTTP 헤더를 통해서 탈취를 해야 한다. 그런데 쿠키가 헤더에 포함되어 전송되는 것은 해당 도메인에 한해서만인데 어떻게 이걸 중간에 가로챌 수 있단 말인가? 힌트는 쿠키가 전송될 때에는 목적지의 도메인에 따라서 포함 여부가 결정되지 출발지가 어디인지에 대해서는 신경쓰지 않는 다는 것에 있다. 복잡한데 예를 들어 생각해보자.

해커는 cometome.hack 이라는 사이트를 제작했다. 그리고 a.com 을 사용하는 사용자에게 이메일로 그럴듯한 메일을 보내서 클릭하면 cometome.hack 해당 사이트에는 접속하자마자 해당 유저의 브라우저가 a.com 에 http 요청을 보내도록 한다. 이를테면 HTML 에 아래와 같은 코드를 삽입해 놓는 것이다.

<img src="http://a.com/api/changeMyName/idiot">

낚시에 걸려든 사용자는 이 사이트에 접속되자 마자 자기도 모르는 사이 a.com 의 닉네임을 변경하는 api 를 호출하고 있는 것이다. 그런데 이미 a.com 에 접속해서 얻은 쿠키가 유효하기 때문에 이 요청은 이 쿠키를 담아서 보내질 것이고 API 는 해당 쿠키의 토큰을 통해 인증 여부를 확인하고 요청한 작업을 처리할 것이다.

이렇다면  결국 Cookie 를 사용 하는 것도 문제인 것인가? 그렇지 않다. 여기에는 이를 방어할 수 있는 방법들이 있다. API에 대한 HTTP 요청을 특정 함수를 통해서만 이루어지도록 하는 것이다. 그래서 이 함수를 통한 요청이 아닐 경우에는 인증을 거부하는 것이다. (토큰에 특정 해쉬 값을 추가해서 발송하는 식으로) 이런 방법 외에도  HTTP 헤더를 분석해서 막는 방법도 있다. HTTP 헤더에 있는 Referer 와 Origin 값을 통해서 엉뚱한 곳에서 요청이 날아 왔다면 이를 거부할 수 있다.

결론
위에서 설명한 2가지 방식 중에서 무엇을 선택해야 하는가의 기준이 되는 것은 결국 보안이다. 우리가 논의하고 있는 것은 결국 인증을 위한 것인데 사용자 인증에 있어서 가장 중요한 것은 보안이기 때문이다. 2가지 방법 모두 저마다의 취약점은 존재하기 마련이다. 그런데 개인적으로 생각하기에는 쿠키에 저장하는 것이 보안이 더 높다고 생각한다. XSS 공격은 다방면에서 스크립트를 차단해야 하기 때문에 자칫 실수로 공격에 노출될 수 있는데 이에 반해 CSRF 공격은 위에 소개된 방법을 통해 보다 더 쉽고 단단하게 방어할 수 있다고 생각되기 때문이다. 물론 어느 방법도 완벽하다고 이야기할 수는 없다. 보안이라는 것은 늘 취약점을 찾아 들어오는것 이기에 프로그래머가 이를 의식하지 않고 개발한다면 어떠한 방식이든 뚫릴 수 있기 때문이다.



2016년 8월 21일 일요일

창업가들에게 전하는 마크 주커버그의 조언


Facebook의 창업자이자 실리콘밸리의 슈퍼스타인 마크 주커버그는 최근에 Y Combinator 와 동영상 인터뷰를 진행했다. 그는 자신의 페이스북에 이 인터뷰 중에서 일부 영상 클립과 함께 글을 올렸는데 꽤나 인상적인 부분이 있어 기록을 남긴다.

Mark Zuckerberg
People often ask me what advice I'd give someone who wants to start their own company.

My answer is that every good company that I can think of started with someone caring about changing something, not someone deciding to start a company. Instead of trying to build a company, focus on the change you want to see in the world and just keep pushing forward.

Here's a clip from a conversation I had about entrepreneurship with Sam Altman, who runs the startup incubator called Y Combinator. You can watch the full interview on their Facebook page.


창업을 준비하는 사람들이 자신에게 조언을 구할때 자신의 대답을 말해주고 있다. 그가 생각하기에는 좋은 회사들은 단지 회사를 만들어야지 라고 생각한 사람들이 아니라 무언가를 변화 시키고자 하는 사람들이 함께 시작했다는 것이다.

이는 곧 창업의 동기와도 연결된다. 창업을 시작하는 많은 사람들은 저마다의 동기가 있고 이는 곧 회사가 성공하기까지 중요한 원동력이 된다고 생각한다. 그런데 처음 창업을 결심하게 되는 사람들의 동기를 종종 들어보면 대게는 단지 새로운 돈벌이를 찾기 위한 경우가 많다. 이를테면 현재 회사가 마음에 들지 않아서 혹은 보수가 마음에 들지 않아서 인 것이다. 자기 하고 싶은거 하면서 더 큰 돈을 벌고 싶은 것이다. 이러한 동기도 나쁜 것이 아니다. 그렇게 시작해서 다양한 시도 끝에 좋은 기회를 발견하여 잘 될 수도 있으니까. 그러나 이런 경우는 매우 드물고 대개의 경우 중간에 쓰러지는 경우가 많은 것 같다.

그래서 창업을 한다는 것은 단순히 사업을 해야겠어. 회사를 세우겠어. 라기 보다는 보다 본질적인 동기가 있어야 하는 것 같다. 마크 주버커그가 이야기 한것처럼 무언가를 변화 시키고자 하는 생각이 좋은 출발점이 될 수 있다고 생각한다. 자신이 위치한 곳에서 끈임없이 세상을 관찰하고 변화시킬 무언가를 찾아내야 한다. 그리고 본인이 그것을 할 수 있을만한 능력과 강한의지가 있는지를 생각해 보아야 하지 않을까?

원본 링크 : https://www.facebook.com/zuck/posts/10103038737662161
인터뷰 풀 영상 : http://www.themacro.com/articles/2016/08/mark-zuckerberg-future-interview/
번역 영상 : https://www.facebook.com/stage5/



2016년 8월 18일 목요일

Amazon RDS 시간 설정

지난 글에서는 EC2 인스턴스 서버의 표준 시간대를 변경하는 방법을 알아 보았는데 이번에는 DB 의 시간대를 변경하려고 한다. 서버의 시간대가 변경 되었다고 해도 DB의 시간대는 변경되지 않기 때문에 반드시 DB 의 시간대도 설정 해주어야 한다.

필자는 Amazon RDS 를 통해서 MariaDB 인스턴스를 생성했다. 따라서 이 글의 내용은 MariaDB 기준이다. 그러나 다른 DBMS 를 택하더라도 설정하는 부분이 RDS 콘솔에서 이루어지기 때문에 비슷하지 않을까 싶다. 여기서 하고자 하는 일은 매우 기본적인 내용이기에 Amazon 에서 문서로 방법을 제공하고 있을 확률이 높다. 아니나 다를까 검색을 통해 쉽게 문서를 찾을 수 있다. 아래 링크를 통해 들어가면 DB 의 Local Time Zone 을 설정할 수 있는 방법에 대해 설명하고 있다.

Local Time Zone for MariaDB DB Instance

http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_MariaDB.html#MariaDB.Concepts.LocalTimeZone

요약하자면 로컬 시간대를 변경하기 위해서는 parameter group 에서 time_zone 이라는 parameter 를 설정하라고 되어있다. parameter group 이라는 것은 이름에서 알 수 있듯이 해당 DB 인스턴스에 대한 설정 변수들의 모음이다. 그럼 이제 바꾸는 방법을 알아 보자.

AWS Console 에서 RDS 로 들어간 후 좌측 메뉴 중에 Parameter Groups 라는 메뉴를 클릭하면 현재 생성되어있는 그룹들을 볼 수 있다. 보통 DB 인스턴스를 생성하였다면 그 DB 에 맞는 기본 그룹이 설정되어있다. 필자의 경우에는 mariadb 인스턴스를 생성했기 때문에 아래와 같이 default.mariadb10.0 이라는 그룹이 만들어져 있다.


이제 저 파라미터 그룹의 time_zone 을 수정하면 될 것이라고 생각하지만 체크 박스로 해당 그룹을 선택하고 Edit Paramters 를 수정하려 하면 해당 버튼이 비활성화 되어있는 것을 볼 수 있다. 이는 default DB parameter group 은 parameter 를 수정할 수 없게 되어있기 때문이다. 그래서 parameter 를 수정하기 위해서는 별도의 그룹 생성(customer-created DB parameter group) 을 생성해 주어야 한다. 위의 스크린샷 화면에서 파란색 Create Parameter Group 을 클릭한다.


위와 같은 화면이 나타난다.

Paramter Group Family : 적용하고자 하는 DBMS 를 설정한다.
Group Name : 이 그룹을 식별하기 위한 이름을 지정한다.
Description : 이 그룹에 대한 설명을 입력한다.

각각의 항목을 자신에게 맞게 설정 한후 파란색 Create 버튼을 눌러 완료한다. 다시 Parameter Groups 리스트 화면에서 보면 새롭게 생성도니 그룹을 확인할 수 있다. 이제 이 그룹의 체크박스를 선택하면 Edit Parameter 버튼이 활성화 됨을 볼 수 있다. Edit Paramters 를 누르면 아래와 같이 parameter 목록이 나온다.


스크롤을 해서 내려보면 많은 parameter 를 확인할 수 있는데 우리가 지금 바꾸어야 하는것은 time_zone 이다. time_zone 항목을 찾아 보면 우측에 select 박스가 있는데 기본적으로는 <engine-default> 라고 되어있다. 클릭하여 우리가 변경하고자 하는 시간대를 선택한다.




설정후에 상단에 위치한 파란색 Save Changes 를 누른다. 다시 목록으로 돌아온다. 이제는 지금까지의 과정을 통해 생성한 parameter group 을 DB 인스턴스에 적용 시켜주어야 한다. 좌측 메뉴중에서 Instances 를 누른 후에 적용하고자 하는 인스턴스를 선택한 후 상단 메뉴 버튼 중 Instance Actions 버튼을 누르고 modify 를 선택한다.













인스턴스 설정 화면을 볼 수 있는데 이 중에서 Database Options 라는 부분이 있는데 이곳에서 DB Parameter Group 을 설정할 수 있다. 방금 전에 생성한 그룹을 선택한 후 Continue 를 누른다.

이제 적용은 끝났다. 그러나 아직 한 가지 남은 것이 있다. 변경된 내용을 적용 시키려면 DB 인스턴스를 재부팅 해야 한다. RDS 인스턴스 목록에서 방금 적용시킨 DB 인스턴스 선택, 상단 Instance Actions 버튼 클릭, Reboot 클릭. status 가 rebooting 으로 변경된다. 몇 분의 시간이 지나가면 재부팅이 완료 될 것이고 DB 시간대 변경은 완료 된다.


참조문서
1) http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_MariaDB.html#MariaDB.Concepts.LocalTimeZone
2) http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithParamGroups.html

2016년 8월 16일 화요일

Amazon EC2 Redhat 표준 시간대 설정.

최근 프로젝트를 EC2 인스턴스(Redhat7) 생성하여 진행하고 있다.
서버 셋팅후 표준 시간대는 항상 확인 해야 한다. 그렇지 않으면 서비스 운영상에 의도치 않은 결과를 맞이할 확률이 매우 높다.

우선 현재 자신의 서버의 표준 시간대를 확인 해보자.
터미널에서 date 라고 치기만 하면된다.

[ec2-user@xxx.xxx.xxx.xxx ~]$ date
2016. 08. 15. (월) 14:03:47 EDT

현재 시간은 8월 15일 광복절이다. (대한독립만세~)
그러나 이글을 쓰는 시점은 15일을 넘겨 16일 새벽이다. 그런데 왜 내 서버는 아직도 15일인가.
서버 표준 시간대가 우리나라로 설정이 되어있지 않기 때문이다.
날짜 뒤에 3글자 'EDT'  가 현재 서버 시간대를 나타낸다.
EDT 는 Eastern Daylight Time Zone 이다. 이 표준 시간대는 미국 동부 쪽의 시간을 나타낸다.

국내를 대상으로 하는 서비스라면 국내 표준 시간대로 맞추어 주는 것이 좋다.
EC2 인스턴스 서버 시간대를 맞추어주는 방법은 아마존에서 제공하는 문서에도 설명이 잘 되어있다.
EC2 Amazone Linux 인스턴스를 사용하는 사람은 아래 링크에 설명된 대로 따라하면 쉽게 표준 시간대를 변경할 수 있다.

http://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/set-time.html#change_time_zone

그런데 나의 경우에는 Redhat7 배포판인데 위의 설명과 좀 다른 부분이 있다.
/etc/sysconfig/ 안에 clock 이라는 파일이 없기 때문이다. Redhat7 의 경우에는 timedatectl 이라는 유틸리티를 사용하면 쉽게 바꿀 수 있다. (timedatectl 에 대한 자세한 설명은 아래 링크 참조)

[ec2-user@xxx.xxx.xxx.xxx ~]$ sudo timedatectl set-timezone Asia/Seoul
[ec2-user@xxx.xxx.xxx.xxx ~]$ date
2016. 08. 16. (화) 03:06:53 KST

timedatectl 을 이용하여 타임존을 Asia/Seoul 로 설정한 후에 date 로 현재 시간을 확인해보니 정상적으로 변경되었음을 확인할 수 있다.

만약 서울이 아닌 다른 나라의 시간대로 셋팅 하고 싶을때에는 Asia/Seoul 부문을 해당 지역으로 바꾸어주기만 하면된다. 그렇다고 아무 문자나 써서는 안된다. 이미 존재하는 시간대 데이터 파일명을 지정해 주어야 한다. 그럼 시간대 데이터 파일은 어디에서 확인할 수 있을까?

[ec2-user@xxx.xxx.xxx.xxx ~]$ ls /usr/share/zoneinfo
Africa      Atlantic   Canada  EST5EDT  GB       GMT0       Indian   Kwajalein  Mexico   PST8PDT   ROK        UTC        iso3166.tab
....(생략)

ls -al 명령어를 통해서 보게되면 어떤건 파일이고 어떤건 디렉토리로 되어있다. Seoul 은 처음 목록에서 보여지지 않고 Asia 폴더에 들어가면 찾을 수 있다.

추가로  Timezone 변경하는 다른 방법.
[ec2-user@xxx.xxx.xxx.xxx ~]$ sudo rm /etc/localtime

[ec2-user@xxx.xxx.xxx.xxx ~]$ sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

기존에 이미 존재하는 localtime 파일이 있기 때문에 이를 삭제한 후 서버의 로컬 타임이 서울 시간대를 참조하도록 하는 심볼릭 링크를 만들어 내는 명령어이다. 위의 timedatectl 은 이러한 심볼링 링크를 자동 관리한다.


참조문서 :
https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/System_Administrators_Guide/chap-Configuring_the_Date_and_Time.html

2016년 8월 14일 일요일

Facebook 로그인, 이메일은 신뢰할 수 있을까?


많은 IT 서비스에서 Facebook API 를 이용해서 가입/로그인 을 지원하는 경우가 많다. 이는 기업과 고객 모두에게 이득이다. 기업은 회원가입의 장벽을 낮추어 많은 회원을 확보할 수 있고 고객 또한 간편하고 빠르게 가입하여 서비스를 이용할 수 있기 때문이다.

그런데 페이스북을 통한 가입 기능을 구현하다 보면 한가지 궁금한 점이 생긴다. " 페이스북의 이메일은 과연 신뢰할 수 있을까? " 왜 이런 생각을 하게 되었는지는 그 과정을 생각해보면 알 수 있다.

페이스북은 기본적으로 이메일을 아이디로 사용하고 있다. 그래서 이메일은 페이스북을 통해 우리의 서비스 유저 DB 와 통합할때 중요한 키 값으로 활용된다. 페이스북 로그인 과정은 간단하게 보면 아래와 같다.

1. 페이스북 로그인 요청
2. 페이스북 앱에서 해당 서비스에 대한 접근 권한 승인 요청
3. 요청 APP 에 승인 여부와 함께 요청한 회원 정보 전송.
4. 요청 APP 은 이 정보를 토대로 자신들의 유저 DB 에 해당 유저를 등록 시킨다.

만약 가입하려는 유저가 google@gmail.com 이라는 이메일로 페이스북에 가입되어 있었다면 새로이 가입하는 APP 에도 google@gmail.com 으로 가입이 되는 것이다. (물론 APP에 따라 아이디로 이메일을 사용하지 않을 수도 있다.) 여기서 우리는 페이스북 에서 제공하는 이메일 주소가 신뢰할 만한 것인지를 생각해 볼 필요가 있는 것이다. 두가지 시나리오를 생각해 볼 수 있다.

[시나리오 1 : 타인의 이메일로 APP 에 가입할 수 있는 가능성 ]

어떤 누군가가 타인의 이메일 주소인 king@inthenorth.com 통해서 페이스북 에 가입했다. 그리고 페이스북 아이디를 통해 '먹스타그램'APP 에 가입을 하였다. 몇 개월의 시간이 지난후 '먹스타그램' APP 은 매우 유명해져서 많은 이들이 가입하게 된다. 이때 king@inthenorth.com의 실제 주인은 가입을 시도 하지만 자꾸 중복된 이메일 이라고 나온다. 그는 자신이 이전에 가입했던 기억이 있었나 하고 어리둥절해 할 것이다.

[시나리오 2 : 타인의 계정 권한을 획득할 수 있는 가능성 ]

북부의 왕은 '먹스타그램' APP 이 일찍이 핫 하다는것을 깨달았다. 그는 페이스북을 사용하지 않는 고집있는 사람이었고 평소 사용하는 이메일 'king@inthenorth.com' 로 가입을 했다. 그런데 왕좌를 노리는 누군가가 그의 이메일 주소로 페이스북에 가입을 하였고 다시 이 페이스북 아이디로 '먹스타그램' APP 에 가입후 로그인 한다. 그리고는 해당 계정에 북부의 왕의 흑역사 사진을 마구 올려 그를 왕좌를 위태롭게 만들었다.

위의 2가지의 시나리오는 서비스의 내부 구현에 따라서 조금씩 달라질 수 있다. 하지만 페이스북 메일이 본인 확인되지 않은 경우에는 생각해볼 수 있는 시나리오이다. 그러나 페이스북에서 제공하는 이메일이 이미 확인(verified) 된 이메일 이라면 위와 같은 시나리오는 발생할 수 없다. 타인이 해당 이메일을 도용하여 페이스북을 이용할 수 없을뿐 더러 새로운 APP 에도 가입할 수 없기 때문이다.

이런 의문이 들자마자 임의의 이메일로 페이스북에 가입을 시도 해보았다. 가입은 이루어지며 로그인 이후에 이메일 인증을 하라는 메시지가 나온다. 그렇다면 이제 이 상태에서 이 아이디로 다른 APP 에 가입할 수 있을까? 답은 '아니다' 이다. 이메일 인증을 거치지 않은 계정에 대해서는 Facebook API 가 사용을 허용하지 않기 때문이다. 이와 같은 궁금증은 이미 stackoverflow.com 에 질문으로 등록되어 답변이 잘 정리 되어있다. 결론은 내가 말한 것과 같다. 글 전체를 통해 보다 자세한 설명을 보고 싶다면 아래 링크를 통해 확인할 수 있다.

http://stackoverflow.com/questions/14280535/is-it-possible-to-check-if-an-email-is-confirmed-on-facebook

2016년 8월 10일 수요일

JWT : jsonwebtoken 토큰 인증


웹 서비스를 만들때에 기본적으로 구현해야 하는 것중 하나가 인증이 아닐까 싶다. 처음에 웹 개발이라는걸 접했을때에는 서버에 저장하는 세션 방식으로 인증을 구현했다. 그런데 웹이 아닌 모바일과 같은 클라이언트들이 많이 생겨나면서 이러한 기존의 세션/쿠키 방식에 한계가 생겨났다. 그래서 요즘에는 세션/쿠키 방식이 아닌 토큰을 이용한 인증 방식을 많이 사용한다. 그 중에서도 오늘은 최근 많이 사용하고 있는 JWT 에 대한 이야기이다.

JWT 는 JSON Web Token 의 약자이다. 이름에서 알 수 있듯이 JSON 형태로 되어있는 토큰이다. 이 토큰 인증 방식의 특징은 서버에 토큰 정보를 저장할 필요가 없다는 것이다. 서버는 해당 토큰이 유효한 지만 체크하면 되는 것이다.쉽게 생각해서 사람들이 주머니 속에 토큰을 들고 다닌다고 생각하면 된다. 아래의 상황을 통해 이해해 보자.

불금에 한잔 하러 클럽을 갔는데 입구에서 가드가 앞을 가로 막는다. 
가드 : " 여긴 아무나 입장할 수 있는 곳이 아냐~ " 
개미 : " 나에겐 토큰이 있소. 길을 내주시오.~ " 
(주머니를 뒤적 거린 후 토큰을 꺼내어 보여준다.) 
가드 : " 엇 이것은 JWT ? 네가 이런걸 가지고 있다니... 말도 안돼. 가짜가 아닌지 한번 보겠어. base64 디코딩을 해주고 SHA1-256 으로 암호화 되었군. 잠만 키가 어디있더라.... 여기있군. 우리 클럽의 토큰이 확실하군. 입장하게~ "

토큰 인증 프로세스

간략하게 토큰 인증 프로세스가 어떻게 이루어 지는지 보자.

1. 사용자는 아이디, 암호를 입력하여 서버에 인증을 요청한다.
2. 서버에서는 인증이 완료되면 토큰을 생성하여 사용자에게 건넨다.
3. 사용자는 해당 토큰을 받아 인증이 필요할때마다 토큰을 함께 전달한다.
4. 서버는 매번 들어오는 요청 마다 토큰을 검증하여 이에 따라 요청한 자원을 제공한다.


JWT 토큰 구성
그럼 JWT 토큰은 어떻게 만드는지 보자. 우선 완성된 토큰을 보자.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

괴상하게 생겼다. 그러나 처음부터 이렇게 괴상하게 생긴건 아니었다. 탄생의 시점부터 살펴보면 지금 모습이 이해가 될 것이다. 우선 JWT 토큰은 3가지 부분으로 구성되어있다.



JWT = 머리(header) + 몸통(payload) + 서명(signature) 



저 위의 괴상한 모습은 사실 이 3가지가 합쳐진 모습이다. 그럼 하나씩 살펴보자.

머리(header)

머리 부분은 이 토큰이 서명에 어떤 암호화 방식을 사용하고 있는지에 대한 정보를 담고 있다.

header = '{"alg":"HS256", "typ":"JWT"}'

JSON 으로 형태로 header 를 정의 하였다. 보면 alg 와 typ 라는 2개의 속성이 있다. 눈치가 빠르다면 이미 다 알겠지만 alg 는 서명에 사용하는 암호화 방식을 의미하며, typ 는 이 토큰이 JWT 라는 것을 의미한다. 여기서 HS256 은 암호화 방식중의 하나이다.

몸통(payload)
몸통은 우리가 원하는 정보를 담을 수 있는 곳이다. 사용자 아이디 혹은 부서, 권한, 기타등등 원하는 정보를 담을 수 있다. 공식 JWT 스펙에서는 iat 라는 timestamp 속성을 두어야 한다고 한다. iat 는 "issued at" 을 줄인 말이다. 이는 토큰을 발행한 시점을 말하며 토큰의 유효기간 검사를 위해 필요하다.

서명(signature)
이제 다 왔다. 마지막 부분은 서명 부분이다. 우선 서명 부분이 왜 있어야 하는지 부터 알아야 한다. 서명이 없는 토큰을 생각해보자. 토큰의 기본적 형태를 알고 있다면 누구나 쉽게 토큰을 만들어 내서 위조할 수 있을 것이다. 미성년자가 조금 일찍 술을 만나고 싶은 나머지 주민등록증을 위조 하는 것과 비슷한 것이다. 매의 눈을 가진 사람이라면 단번에 이를 알아 보겠지만, 고도의 기술과 노안을 지닌 학생이 이를 제시한다면 속아 넘어가기 쉬울 것이다. 결국 서명이란 것은 이러한 위조를 방지하기 위한 것이다.

서명은 암호화를 통해 이러한 위조를 막는다. 위에서 설명한 머리와 몸통 두부분을 base64 로 인코딩하여 합친 후 이 내용을 위에서 설명한 암호화 방식(여기서는 HS256) 으로 암호화 한다. 그럼 위조하는 사람이 똑같이 암호화 하면 어떻게 하냐고 물을 수 있다. 예리한 지적이다. 그렇기 때문에 암호화 과정에는 key 값이 필요하다. key 값에 따라서 같은 내용이라 하더라도 다른 암호화 결과가 나온다. 그렇기 때문에 이 key 값을 모른다면 암호화 방식이 어떤 것인지 안다 하여도 원하는 결과를 만들어낼 수 없는 것이다.

간단하게 보면 abcd 라는 문자열이 있는데 이를 1234 , 4321 이라는 키로 암호화를 진행한다고 하면 아래와 같다. encrypt 라는 임의의 암호화 함수가 있다고 가정하다. 이 함수는 본문과 key 값을 받아 암호화 하고 결과를 리턴한다.

encrypt("abcd","1234"); // return "10sd0fj392392"
encrypt("abcd","4321"); // return "20194jds930ab"

위에서 보듯이 abcd 라는 내용은 같지만 key 값에 따라 결과가 다름을 알 수 있다. 정리하면 서명은 머리와 몸통을 합친 후 정의한 암호화 방식으로 key 값을 이용해 암호화 한 결과이다. 자 이제 각 부분들에 대한 설명은 마쳤다. 그럼 실제로 코드를 보면서 만들어지는 과정을 보자.

header  = '{"alg":"HS256","typ":"JWT"}';
payload = '{"userID":"ant","iat"142939392}';

// 암호화에 사용될 key 값
key     = 'antsecretkey';

// 암호화된 서명을 만들어 내기 위한 부분, 앞서 말한대로 header 와 payload 를 합친다.
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload);
signature     = HMAC-SHA256(key, unsignedToken);

// 다 만들어졌으니 세 부분을 모두 합친다.
token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature);
(위 소스는 위키피디아에 소개된 부분을 차용하였다.)

자 이제 JWT 가 어떻게 만들어 지는지 알게 되었다. 괴상한 모습은 사실 base64 인코딩을 거쳤기 때문이었던 것이다.


JWT 사용 방법

이제는 그럼 이렇게 만들어진 토큰은 어떻게 사용할 수 있을까? 우리가 서버에 무언가 자원을 요청할때 쉽게 말해 API 를 호출할때에 이 토큰을 함께 건네주기만 하면 된다. 그럼 서버는 토큰을 받아서 해당 토큰이 유효한지 현재 요청에 대한 권한이 있는지를 검사한다.

그런데 여기서 2가지 궁금증이 생겨난다.

1. 서버로부터 받은 토큰은 어디에 보관을 해야 하나?
2. 서버에 토큰을 어떻게 전달 해야 하나?

1번 부터 보자. 최초의 인증 이후 서버로부터 토큰을 받게 되는데 이를 어딘가에 저장해 두지 않는다면 사용자는 매번 아이디와 비밀번호를 쳐서 토큰을 받아내야 할 것이다. 그래서 클라이언트는 최초 인증 후에 받게 되는 토큰을 저장한다. 일반적으로는 local storage 에 저장하거나 혹은 쿠키에 저장한다.

2번에 대한 답변. 일반적으로는 이러한 저장된 토큰을 Authorization header 에 실어서 보낸다.
실제 Request Headers 의 모습을 보면 아래와 같다.

  • Accept:*/*
  • Accept-Encoding:gzip, deflate, sdch
  • Accept-Language:ko-KR,ko;q=0.8,en-US;q=0.6,en;q=0.4
  • Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI
  • Connection:keep-alive


이상 JWT 에 대한 개념을 간략하게 둘러 보았다. 실제 서비스를 개발할때 위의 내용대로 토큰 인증을 구현해도 되지만 이미 많은 언어에서 해당 인증을 위한 라이브러리를 제공하고 있기 때문에 해당 라이브러리를 활용하는 것이 시간을 절약하는 방법이라 생각한다. 필자의 경우 최근 nodejs 로 개발하고 있는데 jsonwebtoken 이라는 패키지를 사용하고 있다.


참고문서
https://en.wikipedia.org/wiki/JSON_Web_Token

추가정보
https://jwt.io 
(이곳에 가면 사실 JWT 에 대한 스펙이나 기본 개념을 더 자세히 알 수 있다.)

2016년 8월 9일 화요일

Facebook SDK userID 가 여러개인 이유. App-scoped userID


페이스북 SDK 를 통해서 로그인을 연동하다가 발견한 사실.

SDK 에서 제공하는 API 를 통해서 로그인 하게 되면 Response 객체를 받게 되는 해당 객체의 구조는 아래와 같다.

{ status: "connected", authResponse: { session_key: true, accessToken: "kgkh3g42kh4g23kh4g2kh34g2kg4k2h4gkh3g4k2h4gk23h4gk2h34gk234gk2h34AndSoOn", expiresIn: 5183979, sig: "...", secret: "...", userID: "634565435" } }


여기서 유저를 식별하기 위한 userID 칼럼이 있는데 여기서 얻어지는 userID 값과 Graph API Explorer 에서 /me 에 대한 호출로 인해서 얻게 되는 userID 가 다르다는것을 알게 되었다. 그리고 위에서 얻은 userID 값은 Graph API EXplorer 에서도 작동하지 않았다. 그래서 왜 그런가 하고 구글링을 통해서 답을 얻었다.

API 2.0 부터는 App-scoped user ID 라는 개념이 생겼는데 말 그대로 특정 앱 안에서만 유효한 userID 라는 것이다. 즉 다른 앱이나 다른 곳에서 사용할 수 없다. 그렇기 때문에 userID 는 한 사용자당 여러개의 ID 를 가질 수 있는 것이다.