seungjun.dev

OAuth 개념과 사용법 정리

OAuth가 뭐고, 어떻게 쓰는 걸까?

OAuth

특정 앱이 사용자의 비밀번호를 공유받지 않고도 다른 서비스에 있는 사용자 정보나 기능에 접근할 수 있도록 권한을 위임하는 절차나 규칙을 정의한 것

쉽게 말하면, 앱을 이용할 때 사용자가 해당 앱에 ID, PW 등의 정보를 제공하지 않고, 신뢰할 수 있는 외부 앱의 Open API에 ID, PW를 입력하여 해당 앱이 인증 과정을 처리해주는 방식이다.

OAuth는 왜 쓸까?

A 서비스가 B 클라우드 스토리지에 저장된 사용자의 사진에 접근해야 한다고 가정한다.

OAuth가 없다면 사용자가 A 서비스에게 B 스토리지의 ID와 PW를 모두 알려줘야 한다.

  • A 서비스가 B 스토리지의 계정 정보를 알게 되므로, 사진뿐만 아니라 다른 모든 정보에 접근할 수 있다.
  • A 서비스가 해킹당하면 B 스토리지 계정 정보가 그대로 유출된다.

OAuth는 이 문제를 해결하기 위해, 사용자의 비밀번호 대신 액세스 토큰이라는 임시 접근용 키를 발급하여 A 서비스에 전달하는 방식을 사용한다.

OAuth 2.0 인가 과정

OAuth 2.0은 1.0에서 알려진 보안 문제 등을 개선한 버전

OAuth는 인증(Authentication)이 아니라 **인가(Authorization)**을 위한 표준 프로토콜이다.

  • 인증 (Authentication): 사용자가 누구인지 확인하는 과정 (e.g. ID와 PW로 로그인)
  • 인가 (Authorization): 해당 사용자가 무엇을 할 수 있는지 권한을 부여하는 과정 (e.g. 캘린더 읽기, 클라우드 조회)

실무에서 OAuth가 소셜 로그인에 자주 사용되면서 "인증"으로 착각하기 쉽다.

  • OAuth 자체는 인가 프로토콜
  • 소셜 로그인은 OAuth + OpenID Conenct를 조합한 인증 구현

주요 구성 요소

  1. 리소스 소유자 (Resource Owner)

    • 사용자, 웹 서비스를 이용하려는 유저
    • 자신의 데이터(리소스)에 대한 접근 권한을 부여할 수 있는 주체
  2. 클라이언트 (Client)

    • 자사 또는 개인이 만든 애플리케이션 서버
    • 왜 클라이언트라고 부를까?
      • 클라이언트가 리소스 서버에게 필요한 자원을 요청하고 응답하는 관계이기 때문
  3. 권한 서버 (Authorization Server)

    • 권한을 부여해주는 서버
    • 리소스 소유자를 인증하고, 동의를 얻은 후, 클라이언트에게 액세스 토큰을 발급
  4. 리소스 서버 (Resource Server)

    • 실제 데이터(리소스)를 보관하는 API 서버 (e.g. 구글 캘린더 API)
    • 클라이언트가 액세스 토큰을 제시하면, 이를 검증하고 요청된 리소스를 제공
  5. 리프레시 토큰 (Refresh Token)

    • 클라이언트는 권한 서버로부터 액세스 토큰(비교적 짧은 만료기간)과 리프레시 토큰(비교적 긴 만료기간)을 함께 부여
    • 액세스 토큰은 보안상 만료 기간이 짧기 때문에 얼마 지나지 않아 만료되면 사용자는 로그인을 다시 시도해야 함
    • 그러나 리프레시 토큰이 있다면 액세스 토큰이 만료될 때 리프레시 토큰을 통해 액세스 토큰을 재발급 받아 재 로그인 할 필요가 없게 함

인가 코드 승인 방식 (Authorization Code Grant)

클라이언트가 백엔드 서버를 가지고 있는 경우(e.g. React + Node.js)에 사용하는 가장 안전하고 표준적인 흐름이다.

위 이미지는 PAYCO의 OAuth 2.0 인가 과정이다.

  • 사용자: 리소스 소유자, PAYCO 계정의 주인
  • 서비스: 클라이언트, PAYCO의 리소스에 접근하려는 앱
  • PAYCO 인증 서비스: 권한 서버, 로그인과 동의를 처리하고 토큰을 발급
  • PAYCO API 서비스: 리소스 서버, 실제 사용자 데이터(포인트, 프로필 등)를 보유하고 API를 제공

1단계: 인가 코드 요청 및 발급 (1 ~ 6)

목표: 클라이언트가 액세스 토큰을 발급받기 위해 필요한 임시 인가 코드를 사용자를 통해 전달받는 것

  1. 서비스 접근 및 로그인 요청 (사용자 -> 클라이언트)

    • 사용자가 서비스에서 "PAYCO로 로그인하기" 버튼을 클릭
  2. 로그인 요청 (클라이언트 -> 권한 서버)

    • 클라이언트는 client_id, redirect_uri(백엔드 콜백 엔드포인트), scope(요청할 권한 범위) 등의 정보를 담아 PAYCO 인증 서비스(권한 서버)의 로그인 URL을 생성
    • 클라이언트는 사용자의 브라우저를 이 생성된 URL로 리디렉션
  3. 로그인 페이지 제공 (권한 서버 -> 사용자)

    • 사용자의 브라우저는 PAYCO의 로그인 페이지로 이동
    • 권한 서버는 사용자에게 ID/PW 입력창과 데이터 접근 동의 화면을 보여줌
  4. ID/PW 입력 (사용자 -> 권한 서버)

    • 사용자가 PAYCO ID/PW를 입력하여 인증을 완료하고, 권한 요청에 동의
  5. Authorization Code 발급 (권한 서버 -> 사용자)

    • 사용자의 동의를 확인하고, **일회용 임시 인가 코드(Authorization Code)**를 생성
    • 이 코드를 2단계에서 서비스가 지정했던 redirect_uri의 쿼리 파라미터에 담아 사용자의 브라우저를 다시 리디렉션시킴
  6. Redirect: Callback URL (사용자 -> 클라이언트)

    • 사용자의 브라우저는 5단계에서 받은 redirect_uri로 이동
    • 이때 브라우저가 클라이언트의 Callback API를 호출하면서 URL에 포함된 인가 코드를 전달

2단계: 액세스 토큰 발급 (7 ~ 9)

목표: 클라이언트가 인가 코드를 사용해 권한 서버로부터 액세스 토큰을 발급받는 것

  1. Access Token 요청 (클라이언트 -> 권한 서버)

    • 클라이언트가 6단계에서 받은 인가 코드와 함께 미리 발급받아 숨겨둔 client_id, client_secret을 PAYCO 인증 서비스의 토큰 발급 엔드포인트로 전송
    • Server-to-Server 통신으로 이루어지며, client_secret이 절대 외부에 노출되지 않음
  2. Access Token 발급 (권한 서버 -> 클라이언트)

    • 권한 서버는 인가 코드, client_id, client_secret이 모두 유효한지 검증
    • 검증이 성공하면, 클라이언트의 백엔드 서버로 실제 API 호출에 사용할 액세스 토큰을 발급 (리프레시 토큰도 함께 발급)
    • 클라이언트는 이 토큰을 DB 등에 저장하여 해당 사용자와 연결해 둠
  3. 인증 완료 및 로그인 성공 (클라이언트 -> 사용자)

    • 클라이언트가 토큰 발급이 성공적으로 완료되었음을 확인하고, 사용자에게 로그인 성공을 알림
    • 실제로는 세션 쿠키나 JWT를 발급해 프론트엔드에 전달하는 방식으로 로그인 상태 유지

3단계: 리소스 접근 (API 호출) (10 ~ 13)

목표: 클라이언트가 발급받은 액세스 토큰을 사용하여 리소스 서버의 자원에 접근하하는 것

  1. 서비스 요청 (사용자 -> 클라이언트)

    • 로그인된 사용자가 클라이언트 내에서 PAYCO 정보가 필요한 기능을 요청
  2. Access Token으로 API 호출 (클라이언트 -> 리소스 서버)

    • 클라이언트는 8단계에서 DB에 저장해 둔 액세스 토큰을 HTTP Authorization 헤더에 담아 리소스 서버에 데이터를 요청
  3. Access Token 검증 및 서비스 제공 (리소스 서버 -> 클라이언트)

    • 리소스 서버는 액세스 토큰이 유효한지, 그리고 요청한 API에 대한 scope(권한)가 있는지 검증
    • 검증이 성공하면, 요청받은 데이터를 클라이언트의 백엔드 서버로 응답
  4. 서비스 제공 (클라이언트 -> 사용자)

    • 클라이언트의 백엔드 서버는 12단계에서 받은 데이터를 가공하여, 사용자에게 최종적인 화면 제공

액세스 토큰은 왜 만료될까?

액세스 토큰은 API를 호출할 때마다 사용되는 만능 열쇠이다.

만약 이 토큰이 해커에게 탈취당하면, 해커는 토큰이 유효한 동안 진짜 사용자 행세를 할 수 있다.

이러한 보안 위협 때문에, 액세스 토큰의 유효 기간은 의도적으로 매우 짧게 설정된다.

그러나 이 짧은 유효 기간 때문에 사용자는 다시 로그인 해야 하는 상황이 많아지는 불편함을 겪는다.

리프레시 토큰 도입으로 해결

그래서 도입된 게 리프레시 토큰이다.

유효 기간을 30일 정도로 길게 하고, 보관은 무조건 안전한 백엔드 서버 DB에 한다. (절대 프론트엔드에 노출해서는 안된다)

리프레시 토큰은 오직 새로운 액세스 토큰 발급 용도로만 쓰인다.

리프레시 토큰 작동 과정

  1. 액세스 토큰 만료: 일정 시간이 지나 액세스 토큰이 만료됨
  2. API 호출 (실패): 클라이언트가 만료된 액세스 토큰으로 API를 호출하면, API 서버는 401 Unauthorized 오류를 응답
  3. 토큰 재발급 요청: 클라이언트의 백엔드 서버는 이 오류를 확인하고, 안전하게 DB에 보관해 둔 리프레시 토큰을 권한 서버로 전송
  4. 토큰 재발급 성공: 권한 서버는 리프레시 토큰이 유효한지 확인하고, 유효하다면 새로운 액세스 토큰을 다시 발급 (이때 보안을 위해 새 리프레시 토큰을 같이 발급해줄 수 있음)
  5. API 호출 (성공): 클라이언트는 새로 발급받은 액세스 토큰으로 4단계에서 실패했던 API 호출을 다시 시도 -> 성공

이 과정은 사용자가 모르게 백그라운드에서 빠르게 일어나므로, 사용자는 로그인이 끊기지 않고 계속 서비스를 이용할 수 있다.

오랜 기간 동안 서비스를 방문하지 않아 리프레시 토큰마저 만료되면, 그때는 정말 다시 로그인해야 한다.

실제 과제에 적용

GitHub OAuth 로그인을 구현 과정을 적어봤다.

작동 흐름

로그인

  1. [사용자] 깃허브 로그인 클릭
  2. [사용자] 백엔드로 로그인 요청 전송
  3. [백엔드] CSRF 방어를 위해 state값(암호학적으로 안전한 난수)을 생성
  4. [백엔드] state 값을 즉시 사용자의 세션에 저장
  5. [백엔드] client_id, scope, state 값을 포함한 깃허브 인증 URL 완성
  6. [백엔드] HTTP 302의 Location 헤더에 위 URL을 담아 브라우저로 보냄
  7. [브라우저] 302 응답을 받고 깃허브 인증 URL로 자동으로 리디렉션됨

인가 코드 요청 및 발급

  1. [사용자] 깃허브 인증 페이지로 이동하고, 깃허브 ID/PW로 로그인
  2. [사용자] 앱에서 요구하는 각종 권한 요청을 승인
  3. [깃허브] 사용자의 승인을 확인하고, 임시 인가 코드 생성
  4. [깃허브] 인가 코드와 state 값을 쿼리 파라미터로 붙여서 redirect_uri(백엔드 콜백 엔드포인트)로 리디렉션
  5. [브라우저] 깃허브에 의해 redirect_uri로 리디렉션됨, 접속 자체가 백엔드로 인가 코드와 state 값을 전달하는 요청이므로 fetch 등으로 별도의 요청을 보내지 않음

액세스 토큰 발급

  1. [백엔드] state값 검증 -> 세션에 저장해둔 값과 일치하는지 확인하고, 일치하지 않으면 CSRF 공격으로 판단
  2. [백엔드] 인가 코드를 활용해 깃허브의 토큰 발급 엔드포인트로 POST 요청
  3. [깃허브] client_id, client_secret, 인가 코드가 모두 유효한지 확인
  4. [깃허브] 확인이 완료되면, 깃허브는 인가 코드를 폐기하고(일회용이므로) 액세스 토큰을 생성
  5. [깃허브] 액세스 토큰을 JSON 응답으로 백엔드에 전송
  6. [백엔드] 전달받은 토큰을 DB에 유저와 매핑하여 암호화 저장

세션 생성 및 로그인 완료

  1. [백엔드] 토큰을 받자마자 깃허브 API에 사용자 정보를 요청
  2. [백엔드] 깃허브로부터 받은 사용자 정보를 확인하여, DB에서 해당 유저를 찾거나 새로 생성
  3. [백엔드] 자체 서비스의 JWT 생성
  4. [백엔드] 생성한 JWT를 쿠키에 담아 브라우저로 응답
  5. [백엔드] 로그인 처리가 완료되었으므로, 사용자를 메인 페이지로 리디렉션 (HTTP 302)
  6. [브라우저] JWT 쿠키를 받고, 리디렉션 되며 깃허브 계정을 통해 로그인한 상태가 됨