Closed binchoo closed 3 years ago
메이저 기업의 API를 사용해서 유저 데이터에 접근하는 권한을 얻으려면, 그들이 제공하는 전용 인증 플로우를 따라야 했다. 여러 서비스와 연계될 경우, 인증 플로우의 복잡함이 가중되었다. OAuth2.0
은 이러한 웹 기반의 인증 절차를 표준화한 것이다.
OAuth는 애플리케이션이 유저 데이터에 안전하게 접근하도록 한다. 유저는 3rd 파티 앱에서 로그인 아이디와 패스워드를 타이핑하지 않아도 된다. OAuth의 활용으로 구현할 수 있는 기능의 예는 아래와 같다.
이렇게 개인 정보 자료에 접근하는 API를 쓰려면, 앱은 유저로부터 접근 권한을 부여 받아야 한다. API 권한 부여 과정을 표준 프로토콜화하면 러닝 커브를 낮추어 개발 UX도 향상시키며, 표준의 적용을 통해 API의 보안에 큰 기여를 한다.
로그인 아디 + 패스워드 조합은 인증 수단으로서 적합하지 않다.
인증이란, 유저의 신분을 확인하는 것이다 자신이 A라고 주장하는 자가 진짜로 A인지 확인. 현실 세계의 인증은 주민등록증 사진과 내 얼굴의 유사성을 대조하는 과정과 같다.
웹에서의 인증은 어떨까? 전형적으로 유저에게 아이디+비밀번호를 요구하고 있다. 여기서 아이디는 ''자신이 A라고 주장''하는 것과 같으며, 비밀번호는 그 '주장대로 자기가 A임을 증명'하는 것이다.
보통 서비스들은 유저의 계정 정보를 보유한다만, 어떤 서비스들은 유저 검증을 위해 타 서비스에 의존하기도 한다.
인가란, 어떤 유저가 특정 행동을 해도 되는 사람이라고 인정해 준 것이다. 절차는 보통 이렇게 이뤄진다.
경찰이 과속 위반 차량을 세우고, 운전자에게 운전 면혀증을 요구했다.
경찰은 운전자가 면허증의 사람과 동일함을 확인했다. (인증 절차)
경찰은 운전 면허의 진위 여부, 만료일 등을 확인했다.
이 자는 운전을 할 수 있는 권한이 있는 사람임이 확인되었다. (인가 절차)
남이 본인을 대신하여 특정 작업을 할 수 있도록 인가한다.
발렛 파킹 직원에게 키를 맡기어 주차를 대신 하도록 했다.
그는 나를 대신하여 자동차를 운전해 주차를 할 수 있게 되었다.
그는 자동차를 운전하기 위해 키를 가지고 있구나! (위임 인가)
OAuth 프로토콜에서 식별하고 있는 주체들이다.
Access Token
을 내려준다. 클라이언트는 오너의 자동차 키를 얻게 된 셈이다. API 제공자의 규모가 작다면 굳이 인가 서버와 리소스 서버의 도메인을 달리 하진 않을 것이다.개발자 사용성을 위해 복잡한 암호화 서명을 스펙에서 제거했으나, 사용성과 보안성은 트레이드 오프 관계임을 명심하자.
(관심 없는 내용이므로 생략)
애플리케이션은 인가 서버에 미리 등록되어 API 요청마다 식별 가능해야 한다. 업체들은 아래처럼 구현했다.
구글: API 콘솔에서 클라이언트를 등록해야 함.
필요한 정보: 구글 계정, 제품 이름, 제품 로고(선택), 웹 앱의 경우 리다이렉션 URL
페북: 페북 개발자 사이트에서 클라이언트를 등록해야 함.
등록을 마치면 개발자는 클라이언트 크레덴셜을 지급받는다.
Authorization Code 방식에서 Access Token
을 내려받을 때나, Access Token
을 새로고침할 때 필요함.
개발자는 등록을 통해 클라이언트 크레덴셜을 지급 받는데, 이 값으로 당신은 인가 서버에게 '인증'을 받게 되는 것이다.
등록 시 제공한 클라이언트 정보는, 고객들에게 전시되어 UX를 향상시키는 용도로도 활용되고 있다. (소셜 로그인 시, 해당 앱 이름과 로고를 전시하는 것)
OAuth 1.0은 API 대상 명세였기 때문에, 모바일 앱, 데스크톱 앱, 자바스크립트 앱, 브라우저 확장 프로그램 등에 대응하지 못 했다. OAuth 2.0 부터는 Client Profiles
를 몇 가지 정의하였다.
Server-side web application
웹 서버에서 동작하는 OAuth 클라이언트. 리소스 오너(유저)가 웹 앱을 사용하면 서버 측은 적절한 API 콜을 서버 사이드 프로그래밍 언어로 호출한다. 리소스 오너는 클라이언트 크레덴셜에 대해 전혀 알고 있을 필요하 없다.
Client-side web application
웹 브라우저에서 동작하는 OAuth 클라이언트. 예를 들어 자바 스크립트 페이지, 브라우저 확장 프로그램 등이다. 클라이언트 크레덴셜이 리소스 오너(유저)에게 숨겨짐을 보장할 수 없으므로 몇몇 API 제공자들은 이 클라이언트 프로필을 지원해 주지 않는다.
Native application
Client-side web app과 비슷하게 클라이언트 크레덴셜이 유저에게 비밀로 유지된다는 보장이 없다. 다만 설치된 앱이기 때문에 브라우저 앱만큼 그 정보에 접근하기 쉽지는 않다.
Access Token
만 보유하면, 보호된 리소스에 접근할 수 있다. 암호화 키 등 필요하지 않음.
OAuth의 내용이 바로 이것이다: OAuth 액세스 토큰을 얻어 어플리케이션이 유저를 대신해 API 요청을 던질 수 있도록 만드는 것.
액세스 토큰을 얻고 나면, API 요청에 어떤 방식으로든 함께 실어 나르면 된다. 추천되는 방식은 HTTP Authorization
헤더에 토큰을 담는 방법이다.
GET /tasks/v1/lists/@default/tasks HTTP/1.1
Host: www.googleapis.com
Authorization: Bearer ya29.AHES6ZSzX
이것 말고도 액세스 토큰을 나르는 방법이 추가로 명세에 소개되어 있다. 하지만 API 제공자들이 이 내용을 모두 구현해야 하는 것은 아니라서..
쿼리 파라미터에 담기
https://www.googleapis.com/tasks/v1/lists/@default/tasks?
callback=outputTasks&access_token=ya29.AHES6ZTh00gsAn4
바디에 담기
클라이언트가 헤더를 수정하기 곤란할 때, 이 방식을 채용하게 될 것이다. HTTP application/x-www-form-urlencoded
바디에 access_token
값을 담아서 보내면 된다.
클라이언트는 프로필 별로 정의된 프로토콜을 통해, 자료에 접근할 수 있는 인가를 받게 된다. OAuth2.0은 주요 Grant Types
(인가 부여 방식) 4가지를 정의하였고, 추가적인 Grant Types
을 도입하기 위한 확장 매커니즘도 다루고 있다.
Authorization Code
서버 사이드 웹 앱에 적합한 방식이다. 리소스 오너가 자료 접근을 허용하고 리다이렉션을 통해 웹 앱으로 되돌아간다. 이 때 Authorization Code
가 리다이렉션 쿼리 파라미터에 담긴다. 이 인가 코드를 인가 서버에 전해주면 Access Token
을 얻을 수 있다. 웹 앱은 인가 서버에게 "인증" 받아야 하므로 cliend_id
와 client_secret
도 필요하겠져?
Implicit grant for brower-based client-side applications
Grant Types
중 가장 간략한 절차이다. 클라이언트 사이드 웹 앱에 적합하다. 리소스 오너가 앱에게 접근을 허용하면, 액세스 토큰이 URL#해시 조각으로써 발행이 된다. 앱은 JS 스크립트로 #해시 조각을 떼어내어 액세스 토큰으로 쓰면 된다. Authorization Code
가 필요하지 않은 방식이지만, 토큰 리프레시가 불가능하다.
Resource owner password-based grant
리소스 오너의 ID + 패스워드를 전달하고, Access Token
을 받는다. 신뢰 수준이 높은 클라이언트에게만 써야 하는 방식이다. 가령 API 제공자가 만든 모바일 앱에서라면 이 방식을 써도 될 것이다. 유저 패스워드가 클라이언트에 노출되지만, 클라이언트는 이걸 저장해선 안 된다. 이 상황이 보장된다면 첫 인가 이후, 유저는 비밀번호 변경 없이 인가를 취소할 수 있다. 액세스 토큰은 '일부' 자료에만 권한을 갖게 되므로, 전통적인 ID+패스워드 인가 방식보다는 보안성이 높다.
Client credentials
클라이언트 크레덴셜로, '클라이언트가 소유하는' 리소스에 대해서 액세스 토큰을 얻는다. 이 Grant Type
은 앱이 스토리지/데이터베이스 서비스 API를 사용하고 있을 때 적절하다. 특정 유저를 대신하여 어떤 작업을 하는 게 아니고, 자기 자신을 위한 것임!
유저(Resource Owner)
는 클라이언트 앱(Client Application)
에서 인가 서버(Authorization Server)
로 리다이렉션 당한다.code
가 추가된다. 이를 인가 코드(Authorization Code)
라 한다.code
가 전송되었다. 클라이언트 서버는 인가 서버에게 code
를 전달하고 Access Token
을 얻는다.Access Token
이 브라우저에 노출되지 않는다.https://developers.google.com/identity/protocols/oauth2/web-server?csw=1#python_1
API Provider에 당신의 앱을 등록
client_id
, client_secret
을 획득한다.
유저에게 당신 앱이 필요로 하는 권한을 설명하고, 인가 서버로 이동시킵니다.
https://accounts.google.com/o/oauth2/auth
몇 가지 쿼리 파라미터가 필요할 수 있으며, API Provider 공식 문서마다 요구하는 것이 다를 수 있다.
client_id
redirect_uri
: 유저가 권한 부여를 승낙하고서 다시 이동하게 될 페이지. 보통 클라이언트 등록 과정에서 함께 입력하는 경우가 많다.scope
: 클라이언트가 필요로 하는 유저 데이터. 보통 공백 문자로 구분되는 문자이며, 페이스북은 콤마로 구분한다. response_type=code
: 응답 값으로 Authorization Code
를 기대함을 알려줌. 즉, 해당 인증 절차가 Server-side Web Application Flow
임을 알려줌.state
: CSRF 공격을 방지를 위해 사용되는 유니크한 값. 인가 서버로 리다이렉션 할 때 유효하지 않은 파라미터들을 넘겼다면, 인가 서버는 오류 응답을 파라미터에 담아 클라이언트 리다이렉션 페이지로 돌려보낸다.
redirect_uri
+ error
[error_description
] [error_uri
] 형태로!
error=access_denied
유저가 권한 부여를 거부한 경우.
error=invalid_request
필요한 파라미터가 빠져있는 경우.
error=unauthorized_client
이 방법으로 클라이언트가 Authorization Code
를 얻을 권한이 없는 경우.
error=unsupported_response_type
이 방법으로 Authorization Code
를 내려줄 수 없는 경우.
error=invalid_scope
요청된 스코프가 뭔지 알 수 없는 경우.
error=server_error
인가 서버의 상태가 요청을 수행할 수 없는 경우.
error=temporarily_unavailable
인 가 서버의 상태가 일시적으로 요청을 수행할 수 없는 경우.
유저는 권한을 승낙하여 Authorization Token
과 함께 클라이언트 리다이렉션 페이지로 돌아간다.
이 URL은 redirect_uri
+ code
[state
] 형태이다
code
== Authorization Code
state
아까 요청에서 입력한 state
값과 동일한지 검증하자. CSRF 공격이 없었음을 검증.
클라이언트는 이제 Access Token
을 얻어야 한다. POST 메서드로 인가 서버에게 요청을 보내야 한다. 필요한 파라미터는 다음과 같다.
code
== Authorization Code
redirect_uri
리다이렉션 주소. Step1에서 사용한 친구와 동일하다.
grant_type
grant_type=authorization_code
이면 Authorization Code
를 내주고 Access Token
을 받겠어요, 라는 뜻이다.
한 편, 이 요청은 client_id
와 client_secret
을 사용하여 인증되어야 한다. 클라이언트 크레덴셜을 요청에 싣는 방식은 크게 2가지이다.
HTTP Authorization 헤더에 넣기
https://en.wikipedia.org/wiki/Basic_access_authentication 방식을 사용해야 한다.
Authorization: Basic
MDAwMDAwMDA0NzU1REU0MzpVRWhrTDRzTmVOOFlhbG50UHhnUjhaTWtpVU1nWWlJNg==
// username(client_id)와 password(client_secret)을 콜론으로 접합하고
// Base64로 인코딩한 값
그냥 HTTP POST 요청의 파라미터로 전달하기
많은 API 제공자가 흔하게 지원하고 있는 방식이다.
정리하자면, 이런 파라미터들을 요청에 추가적으로 삽입할 수 있다.
JSON 응답이 내려진다.
{
"access_token" : "ya29.AHES6ZSzX",
"token_type" : "Bearer",
"expires_in" : 3600,
"refresh_token" : "1/iQI98wWFfJNFWIzs5EDDrSiYewe3dFqt5vIV-9ibT9k"
}
access_token
: 액세스 토큰을 드디어 얻는다!token_type
: OAuth 인증의 Access Token
의 형식은 Bearer
이다.expires_in
: 남은 유효시간 (seconds)refresh_token
: 현재 액세스 토큰이 만료되었을 때, 새 액세스 토큰을 얻기 위해 제출하면 되는 토큰.액세스 토큰은 유저를 포함하여 다른 누구에게도 알려지면 안 된다.
액세스 토큰은 수명주기가 짧고, 리프레시 토큰은 수명주기가 길다. 굳이 토큰 2개를 쓰는 이유는 뭘까? 보안상의 이득.
액세스 토큰 타입은 Bearer
토큰이다. 즉 이 토큰으로 API 호출시에 서명이 필요하지 않다. 그러므로 악의적 사용자가 클라이언트에 내려진 액세스 토큰을 훔쳤을 때, 자유롭게 유저 데이터에 액세스하게 된다. 이 영향을 줄이기 위해 액세스 토큰에게 유효 시간을 걸어 둔다.
한 편 API 제공자가 액세스 토큰 검증을 요렇게 구현할 수도 있다: API들이 독립적으로 & 데이터 베이스 룩업 없이 & 암호학적 액세스 토큰을 검증함. 이런 상황에서 유저가 권한 부여를 취소하면, 그걸 반영해 줄 수 방도가 없다. (DB가 없잖아)
그러므로 액세스 토큰의 유효 시간을 짧게 걸어둬야 한다.
Authorization
헤더에 Bearer
타입으로 Access Token
을 전달하면 된다.
Authorization: Bearer
ya29.AHES6ZS_2G4-VuL041L0GpFJqH0wGfGSR
챕터: #12
[명세서] 75페이지 밖에 안 되니 다 읽어보셈.
[참고서] 잘 모르겠으면 이 친구도 참고 바람.
[마감 기한] 6월 13일