이번에 서비스를 운영하다가 OAuth2.0 관련 이슈가 있었다
Slack을 이용해서 소셜 로그인이 구현된 사이트였는데 갑자기 어느 순간부터 Slack 로그인 페이지가 에러 페이지로 redirect되었다
Slack OAuth2.0가 버전 업그레이드 되면서
로그인 URL의 쿼리 파라미터 scope 값의 구분자를(+ -> ,) 수정하는 간단한 방법으로 해결될 수 있었지만
https://api.slack.com/legacy/oauth <- 개발 초기에 참고했던 V1 25년 2월 기준 레거시가 되었다
Legacy: OAuth 2.0 for legacy Slack apps
OAuth 2.0 for legacy Slack apps.
api.slack.com
https://api.slack.com/authentication/oauth-v2 <- 25년 2월 기준 V2가 나왔다
Installing with OAuth
Using an Oauth 2.0 flow to create Slack apps with precise permissions.
api.slack.com
내가 OAuth에 대해서 명확하게 알고 있나라는 의문이 들어서 원리를 공부해보게 되었다
🤔 OAuth 등장배경
어플리케이션이 제 3자 서비스(Google, Slack 등)의 아이디/비번을 알고 있지 않아도, 제 3자 서비스의 리소스(데이터)에 안전하게 접근할 수 있도록 설계된 표준 프로토콜
OAuth가 없었다면, 어플리케이션이 제3자 서비스에 있는 유져의 정보를 얻기 위해서는
제3자 서비스의 유져 ID / PW 혹은 API Key 를 소유하고 있어야 하고
매 요청마다, ID / PW 혹은 API Key를 보내서 리소스를 얻게 되는데,
- 제3자 서비스의 ID와 PW를 상관이 없는 어플리케이션이 가지게 되고 (어플리케이션이 악의적일수도 있지 않는가)
- 매 리소스를 요청할 때마다, ID/PW 혹은 API Key를 보내는데 이는 탈취 위험이 있다
그래서 OAuth는
- 로그인은 제3자 서비스에서 진행 -> 어플리케이션이 ID/PW 혹은 API Key 알 필요 없음
- Access/Refresh 토큰 기반으로 제3자 서비스 API에 요청
- 권한(Scope)를 지정해서 특정 리소스만 허용 -> 인가(Authentication)
컨셉을 적용하여, 제 3자 서비스에 있는 리소스를 어플리케이션이 안전하게 가져올 수 있도록 프로토콜이 만들어졌다
🌊 인증 플로우
플로우는 4가지로 이론적으로는 구분된다! 플로우를 구성하는 객체들을 먼저 구분하면
유져 | 애플리케이션(Client) 사용하는 사용자 |
클라이언트 | 인증 요청하는 애플리케이션 |
인증 서버 | 인증을 처리, 토큰 발급하는 제3자 서비스의 인증 서버 |
리소스 서버 | 유져의 리소스/데이터를 저장하고 있는 제3자 서비스의 서버 |
👉 Authorization Code Grant (인가 코드 방식)
- 유져 로그인
- 유져가 소셜 로그인 버튼 클릭시, 클라이언트는 인증서버의 로그인 페이지로 Redirect
- 인증 서비스가 인가코드 발급
- 제3자 인증 서버는 CallBack URL을 통해 클라이언트에게 Authorization Code 발급
- 인가코드를 통해 Token 발급
- 클라이언트는 인가 코드 + Client ID, Client Secret 을 제3자 인증서버에 보내서 Access/Refresh Token 요청
- Two factor Authentication (이중인증)
- 클라이언트가 인가코드와 Client Secret 두개 모두 가지고 있음을 증명
- 클라이언트는 Token을 통해 리소스 획득
- 제3자 인증서버로부터 발급 받은 Token을 통해 제3자 리소스 서버에 데이터 요청
-> 이중인증으로 인해 보안성 높음
-> 보안성이 높아서 웹어플리케이션 / 모바일앱에서 많이 사용
👉 Client Credentails Grant (클라이언트 자격 증명 방식)
- Token 발급
- 클라이언트는 Client ID, Client Secret을 이용하여 인증서버에 Token 요청
- 토큰을 가지고 리소스 서버 접근
-> 로그인 등 인증을 진행할 유져가 없기 때문에 서버간(M2M) 통신에 사용
👉 Resource Owner Password(비밀번호 방식)
- 클라이언트는 유져의 ID/PW 받는다
- 클라이언트는 ID/PW를 가지고 인증서버에 로그인
- 로그인 성공시, 클라이언트는 바로 Token 발급 받음
-> 클라이언트가 유져의 ID/PW를 가지게 되므로 보안에 취약
👉 Implicit Grant (암시적 승인)
- 유져가 인증서버의 로그인창을 통해 로그인
- CallBack URL의 쿼리 파라미터에 Token 포함시켜 클라이언트에게 Redirect
-> 유져가 인증서버를 통해 로그인 하여, 클라이언트가 ID/PW를 알수없지만 Redirect URL에 Token이 포함되어 노출 / 보안 위협
🔎 인가코드 방식 자세히 살펴보기
인증 플로우 중에서 이중인증으로 보안성이 가장 뛰어나 보편적으로 많이 사용되는 방식을 자세히 살펴보자
우리가 위의 플로우에서 제3자 서비스 인증서버 입장에서 클라이언트는 아래 두가지로 나뉘어질 수 있다
- 브라우저 / 모바일앱 등 FrontEnd
- 애플리케이션 Server의 BackEnd
Client Secret과 인가코드를 관리하는 주체에 따라 두가지 케이스로 플로우가 나뉠거 같다
👉 Client Secret과 인가코드를 BackEnd에서 관리하는 경우
Backend For Frontend(BFF) 패턴이라고도 하며, 클라이언트가 직접 OAuth 서버와 소통하지 않고, Backend가 소통을 위임받음으로서, 보안이 강화된다는 장점이 있다 (Slack은 BFF 패턴으로 OAuth를 구현하라고 가이드되어있다)
- client secret 백엔드에서 관리
- 제3자 인증서버로부터 발급받는 token 안전하게 백엔드에서 관리
- XSS 방지 가능
- CORS를 허용하지 않는 인증서버인 경우 CORS 에러 해결 가능
단점은
- 매 OAuth 요청마다 백엔드를 거쳐야하므로 느려질수도
- 구현 복잡도가 올라감 + 서버 유지 비용
👉 Client Secret과 인가코드를 FrontEnd에서 관리하는 경우
state
CSRF (Cross-Site Request Forgery) 방지 할 수 있다
- 과정
- 클라이언트가 요청시에 난수를 생성하고, 쿼리파라미터 state에 난수를 넣어서 요청하고 해당 난수를 Session Storage에 보관
- 서버는 요청을 처리한 후에 응답시에 state값을 포함하여 응답
- 클라이언트는 응답을 받았을 때, 응답의 state값이 Session Storage에 있는 state값과 동일한지 check
CSRF는 피싱 사이트를 통해서 관리자 권한 탈취하여 특정 사이트에 관리자인 척 행위하는 일련의 해킹 수법인데
소셜 로그인 사례로 공격 플로우를 예시로 들자면,
- 해커가 유져를 소셜 로그인 페이지로 유도한다
- 과정에서 redirect_url은 해커가 만든 사이트, 그리고 해커가 생성한 난수를 state를 넣는다
- 유져는 로그인 완료 후 응답을 받는데, 그때 state 보안 정책에 따라서 유져 클라이언트 Session Storage에 저장된 state값과 응답에 포함된 state값을 비교한다
- Session Storage에 state값이 없거나, 응답에 포함된 state와 일치하지 않는다 -> CSRF를 잡을 수 있다
PKCE (Proof Key for Code Exchage)
Client Secret이 탈취될 수 있는 환경에서 Authorization code가 탈취되었을 때, 보안을 보장하는 프로토콜
클라이언트가 client secret과 Authorization code를 이용하여 token을 요청할 때,
SPA와 모바일 앱 같은 경우에는 브라우저가 소스코드를 가지고 있고, 앱 단의 실행 파일을 디컴파일하여 client secret을 탈취할 수 있다
- 만약 해커가 Authorization Code를 탈취했고,
- 클라이언트가 SPA 혹은 모바일 앱인 경우, 브라우저 혹은 디바이스에서 Client Secret 추출해서 Token을 얻어낼 수 있다
- 구성요소
- code_verifier : 클라이언트가 Authroization Code 요청마다 생성하는 난수
- code_challenge : code_verifier를 해싱 후에 인코딩한 값
- 과정
- 클라이언트가 Authorization Code 요청 시에 code_verifier를 생성 후에 클라이언트 스토리지에 보관
- 클라이언트는 code_verifier 기반으로 code_challenge 생성
- Authorization Code 요청 시에 code_challenge와 code_challenge_method(challenge를 해싱한 알고리즘명)를 같이 보냄
- 인증 서버는 해당 code_challenge를 보관하고 Authorization Code를 반환 응답
- 클라이언트는 Token 요청 시에 Authorization Code와 code_verifier를 같이 보냄
- 인증 서버는 code_verifier를 해싱 및 인코딩한 값이 일전에 클라이언트가 보낸 code_challenge와 동일한지 검증 -> Token 발급
OAuth 인증을 구현할 때, 원리와 플로우는 잘 몰랐었는데 결국 OAuth가 나온 이유와 하고자하는 방향성을 이해하고
표준에 맞춰서 slack 과 같이 커스텀 하여 인증서버를 만들어낼 수 있을 거 같다
다음에는 OIDC에 대해서 알아보고자 한다.
따로 사이드 프로젝트로 진행하고 있는 프로젝트에서는 OIDC 인증을 도입했는데
너무 이유없이 최신 표준으로 진행해본것 같아서
OIDC와 OAuth를 비교하면서 블로그를 작성해보고자 한다