OAuth 2.0 시스템 프론트엔드 개발기 - 1. PKCE

김채은·2024년 2월 8일
0
post-thumbnail

들어가며

Text Me!가 단국대학교 총학생회와 제휴를 맺게 됐다. 우리 사이트를 통해서 총학생회 이벤트에 참여할 수 있도록 하려고 하는데, 회원가입을 하는 것보다 총학생회에 가입되어 있는 정보를 이용해야 유저가 편안하게 느끼고 이용률도 높을 것이라 생각했다.
따라서 총학생회 홈페이지에 OAuth 시스템을 구축해서 Text Me!에도 사용하고 앞으로 다른 곳에서도 활용할 수 있게 만들기로 했다. 특히 PKCE를 적용한 OAuth 구현은 내가 꼭 해보고 싶었던 개발이라서, 무페이 외주를 맡기로 했다 (^^)...

구현 목표

  • PKCE를 적용한 OAuth 2.0 시스템 구현
  • 총학생회 로그인을 편리하게 활용할 수 있도록 리액트 컴포넌트 라이브러리 배포

OAuth

OAuth(Open Authorization)은 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다.
쉽게 말해 별도의 회원가입 없이, 다른 웹사이트의 정보를 이용해서 접근 권한을 부여할 수 있는 인증 방식이다. 예를 들어 구글이나 카카오, 네이버 등을 통해 다른 웹 사이트에 로그인하는 것을 OAuth라고 한다.

Authorization Code Flow

사용자가 로그인 하기를 선택하면 OAuth 로그인 사이트로 이동하고, 로그인에 성공하면 서버는 일회용 Authorization Code(이하 Auth Code)와 함께 사용자를 애플리케이션으로 리다이렉션한다. URI 파라미터를 통해 전달된 Auth code를 서버로 전달하여 Access Token을 얻을 수 있다.

Auth Code는 일회용이므로, 동일한 코드로 새로운 Access Token을 요청하면 실패한다. 또한 Auth Code의 만료 시간은 짧으므로 코드를 받으면 곧바로 토큰 발급을 하는 게 좋다. OAuth 2.0은 Auth Code 만료 시간을 10분 이하로 제한하도록 권장하고 있다.

Implicit Grant Flow

Authorization Code Flow처럼 클라이언트에게 Auth Code를 발행하여 다시 Access Token으로 교환하는 과정 없이, 바로 Access Token을 발급 받도록 하는 플로우이다.

애플리케이션이 OAuth 서버에 요청하여, 브라우저를 통해 띄워진 인증 프롬프트로 로그인하여 Access Token을 받는다. 이 플로우의 최대 단점은 Access Token이 URI을 통해 직접 반환되는 것이다. URI은 로그나 캐시, 브라우저 히스토리 등에 저장되므로 보안에 취약할 수밖에 없다.

PKCE

PKCE(Proof Key for Code Exchange)는 Authorization Code Flow를 사용할 때 코드 교환 과정의 보안을 강화하기 위해 OAuth 2.1에서 필수적으로 사용하도록 지정되어 있는 기술이다.

PKCE는 다음과 같은 필드들을 추가적으로 가진다.

  • code_verifier: 앱으로부터 생성되는 임의의 랜덤 문자열이다.
  • code_challenge: code_verifier와 짝을 이루는 문자열이다. code_verifier를 그대로 사용하는 경우도 있지만 S256 해싱 알고리즘으로 암호화하는 것이 권장된다. OAuth 서버는 이 문자열을 복호화하여, code_verifier와 같은지 확인함으로써 클라이언트를 검증한다.
  • code_challenge_method: code_verifier의 변환에 어떤 함수가 사용되는지(S256 또는 plain) 지정하는 필드이다. 기본값은 plain으로, 이 경우 code_verifiercode_challenge는 같은 것으로 간주된다.

이 필드들을 통해 OAuth 서버는 인증 요청과 토큰 요청이 동일한 클라이언트로부터 온 것인지 확인할 수 있다.

동작 과정

  1. 유저가 로그인 링크를 클릭한다.
  2. 앱은 code_verifiercode_challenge를 생성한다.
  3. 인가 서버의 엔드포인트(/authorize)로 code_challenge와 함께 Auth Code 요청을 보낸다.
  4. 인가 서버는 유저를 로그인 & 인가 프롬프트로 리다이렉트한다.
  5. 유저가 앱이 요구하는 권한들을 허가하고 로그인한다.
  6. 인가 서버는 code_challenge를 저장하고 Auth code와 함께 유저를 앱으로 리다이렉트한다.
  7. 앱은 codecode_verifier를 인가 서버의 엔드포인트(/oauth/token)로 보낸다.
  8. 인가 서버는 code_challengecode_verifier를 검증한다.
  9. 인가 서버는 Access Token, 필요에 따라 Refresh Token을 응답한다.
  10. 앱이 사용자 정보에 접근하기 위해 Access Token을 사용하여 API를 호출한다.

이 플로우를 사용할 때 몇 가지 주의사항이 있다. 우선, code_verifiercode_challenge는 토큰별로 한번의 요청 사이클에만 사용될 수 있다. 매번 Authorization 요청이 일어날 때마다 새로운 code_challenge가 보내져야 한다. 유저가 인가 플로우를 마친 이후 공격자가 정보를 탈취해서 인가 플로우를 재실행하는 것을 막기 위해서이다.
또한 code_challengecode_verifier는 반드시 달라야 한다. code_challenge_method의 plain 속성은 S256의 사용이 불가능할 때만 사용한다. 그렇지 않을 경우, code_challenge가 탈취된다면 공격자는 손쉽게 Access Token을 얻을 수 있다.

OAuth Playground에서 PKCE Flow를 체험해볼 수 있다.

PKCE가 안전한 이유

Authorization Code Flow에서 보안적 위협은 Auth Code가 URL에 노출되는 것이다. 공격자가 Auth Code를 탈취하여 OAuth 서버에게 요청을 보내면 쉽게 Access Token을 발급받을 수 있다. 따라서, Auth Code를 요청한 클라이언트가 Access Token을 요청한 클라이언트가 맞는지 확인하는 과정이 필요한 것이다.

공격자가 Auth Code를 탈취한다고 해도, code_verifier가 없다면 Access Token을 발급받을 수 없다. code_challenge가 탈취된 경우에도 Auth Code 발급에 필요한 code_verifier는 클라이언트가 보유하고 있으며 토큰을 요청할 때 TLS 등의 보안 채널을 통해 전송되므로 공격자가 가로채기 어렵다.

구현

구현은 crypto-js 라이브러리를 활용한다.
32bytes의 랜덤 문자열을 생성하고 base64 url로 인코딩하여 codeVerifier를 생성하고, 이것을 sha256으로 해싱하면 codeChallenge가 된다.

  • pkce.ts
import CryptoJS from "crypto-js";

const getCodeVerifier = () => {
  return CryptoJS.lib.WordArray.random(32).toString(
    CryptoJS.enc.Base64url
  ) as string;
};
const getCodeChallenge = (codeVerifier: string) => {
  return CryptoJS.SHA256(codeVerifier).toString(
    CryptoJS.enc.Base64url
  ) as string;
};
export { getCodeVerifier, getCodeChallenge };

이렇게 만든 codeChallenge와 함께 서버로 요청을 보내면 서버가 이를 저장해뒀다가 이후 codeVerifier와 함께 요청이 들어왔을 때 사용자를 확인한다.

서버 요청부는 Mock 서버로 테스트를 완료했지만, 아직 API 명세가 마무리 되지 않아서 개발이 완료된 후 추가하려고 한다.

마무리

OAuth를 구현해본다는 게 흔치 않은 기회인데, 좋은 기회를 잡은 것 같아서 끝까지 열심히 해보고 싶다. 또, OAuth 구현뿐만 아니라 라이브러리를 만들어보고 배포해보는 과정도 처음 해보는 부분이라 기대가 되고, 여러 사람이 실제로 이용할 수 있는 결과물이 되었으면 좋겠다.

도움이 된 아티클

profile
배워서 남주는 개발자 김채은입니다 ( •̀ .̫ •́ )✧

0개의 댓글