Skip to content

[BE] Oauth CORS

이승관 edited this page Dec 2, 2024 · 1 revision

기본 학습 정리

Origin 헤더

  • 목적:

    • 요청의 출처(origin)를 명시적으로 나타냅니다.
    • 주로 CORS(교차 출처 리소스 공유) 요청에서 사용됩니다.
  • 포함하는 정보:

    • 출처의 스키마(프로토콜), 호스트(도메인), 포트만 포함합니다.
    • 예: https://example.com:443
  • 어디서 사용?

    • POST, PUT, DELETE 같은 상태를 변경하는 요청이나 CORS와 관련된 모든 요청.
    • Referer보다 제한된 정보를 포함하므로 민감한 정보를 노출하지 않음.
  • 특징:

    • 요청의 정확한 출처만 표시되며, 경로(path) 정보는 포함되지 않음.
    • 브라우저에서 설정된 정책에 따라 항상 포함됩니다.
  • 예시:

    Origin: https://example.com
    

2. Referer 헤더

  • 목적:

    • 요청이 어디에서 왔는지를 명시합니다.
    • 사용자가 현재 요청으로 넘어오기 전에 방문한 직전 페이지의 URL을 제공합니다.
  • 포함하는 정보:

    • 프로토콜, 호스트, 포트뿐 아니라 **전체 URL(경로, 쿼리 문자열 포함)**을 포함할 수 있음.
    • 예: https://example.com/some/path?query=123
  • 어디서 사용?

    • 링크 클릭, 리소스 로드, 폼 제출 등 다양한 경우.
    • 주로 로그 분석, 트래픽 소스 추적, 보안 검사에 사용.
  • 특징:

    • 더 많은 정보를 담고 있지만, 개인정보나 민감한 데이터가 노출될 위험이 있음.
    • 브라우저 정책(예: strict-origin-when-cross-origin)에 따라 수정되거나 생략될 수 있음.
  • 예시:

    Referer: https://[example.com](http://example.com/)/some/page
    

CORS(Cross-Origin Resource Sharing)

  • 다른 출처(Origin)의 리소스를 공유하는 방식을 제어하는 보안 메커니즘
  • 웹 브라우저가 실행하는 보안 정책의 하나

출처(Origin)의 구성 요소:

  • 프로토콜 (http://, https://)
  • 호스트 (example.com)
  • 포트 번호 (:443, :80)

왜 필요한가?

  1. 보안상의 이유:
    • 악의적인 사이트가 사용자의 데이터에 접근하는 것을 방지
    • XSS, CSRF 등의 공격 차단
  2. Same-Origin Policy:
    • 기본적으로 브라우저는 같은 출처의 리소스만 공유 허용
    • 다른 출처의 리소스 접근을 제한

CORS 발생하는 상황

// 1. 다른 도메인
fetch('https://api.example.com/data')  // 현재: www.mysite.com
                                      // ❌ 도메인이 다름

// 2. 다른 포트
fetch('https://mysite.com:3000/data')  // 현재: mysite.com:8080
                                      // ❌ 포트가 다름

// 3. 다른 프로토콜
fetch('http://mysite.com/data')       // 현재: https://mysite.com
                                      // ❌ 프로토콜이 다름

동작 방식

  1. 단순 요청 (Simple Request): 브라우저 → 서버 (직접 요청) 조건: GET, HEAD, POST 메서드만 사용 일반적인 헤더만 사용 Content-Type이 제한적
  2. 프리플라이트 요청 (Preflight Request): 브라우저 → 서버 (OPTIONS 요청으로 허용 여부 확인) 브라우저 ← 서버 (CORS 헤더로 응답) 브라우저 → 서버 (실제 요청 전송)

해결 방법

  1. 서버 측 CORS 설정
    • 적절한 CORS 헤더 설정
    • 허용할 출처 명시
  2. 프록시 사용
    • 개발 환경에서 프록시 서버 설정
    • Nginx 등의 리버스 프록시 활용
  3. 환경별 설정
    • 개발/운영 환경에 따른 CORS 설정 분리
    • 보안과 편의성의 균형

보안 고려사항

  1. 와일드카드(*) 사용 자제
    • 특정 도메인만 허용하는 것이 안전
  2. credentials 처리
    • 인증이 필요한 요청은 신중하게 설정
  3. 필요한 메서드/헤더만 허용
    • 최소 권한 원칙 적용

예시

마트 보안 시스템에 비유:

  • 마트(서버)에 들어오는 손님(요청)을 체크
  • 특정 신분증(Origin)을 가진 사람만 입장 허용
  • 사전 확인(Preflight)으로 출입 가능 여부 체크
  • 허가된 행동(Methods)만 수행 가능

궁금했던 점

  1. client에서 API요청으로 Oauth를 구현할 경우 CORS오류가 왜 발생할까??

    처음에 생각했던 문제점은 Nginx때문인줄 알았습니다.

    Nginx를 사용하기 때문에 client가 어떤 곳에서 요청을 보내는지 알 수 없기 때문에 항상 corinee.site로 시작하는 리다이렉트 응답을 보낼 수 밖에 없고 그것때문에 로컬에서는 CORS오류가 발생했습니다.

    또한

    1. 브라우저의 Same-Origin Policy

      // Same-Origin Policy란?
      - 브라우저의 기본 보안 정책
      - 다른 출처(origin)로의 요청을 제한
      
      // 예시
      // 현재 페이지: http://localhost:5173
      const response = await fetch('/api/auth/google');
      // 1. 서버가 https://accounts.google.com으로 리다이렉트 시도
      // 2. 다른 도메인으로의 리다이렉트는 CORS 정책 위반
      // 3. 브라우저가 요청 차단
    2. OAuth 리다이렉션 응답을 AJAX로 처리할 수 없음

      // AJAX 요청의 한계
      const handleGoogleLogin = async () => {
        try {
          const response = await axios.get('/api/auth/google');
          // 문제점:
          // 1. AJAX는 JavaScript 내에서 실행되는 비동기 요청
          // 2. 브라우저 주소창을 변경할 수 없음
          // 3. Google 로그인 페이지로 실제 이동이 불가능
        } catch (error) {
          console.error(error);
        }
      }
      
      // 리다이렉션이 필요한 이유
      - Google OAuth는 실제 Google 로그인 페이지로 이동 필요
      - 사용자가 직접 Google 페이지에서 로그인해야 
      - AJAX로는 이러한 페이지 전환을 구현할  없음
    3. 서버의 리다이렉트 응답이 AJAX 요청의 예상된 응답 형식과 맞지 않음

      // AJAX가 기대하는 응답 형식
      {
        data: { ... },
        status: 200,
        headers: { ... }
      }
      
      // OAuth 리다이렉션 응답
      Status Code: 302 Found
      Location: https://accounts.google.com/o/oauth2/v2/auth?...
      
      // 문제점:
      1. AJAX는 JSON이나 데이터 응답을 기대
      2. 리다이렉션은 302 상태코드와 Location 헤더 사용
      3. 브라우저가  불일치를 CORS 오류로 처리
    4. 실제 동작 비교

      // ❌ API 방식 (작동 안 함)
      const loginWithAPI = async () => {
        try {
          // 1. AJAX 요청 발생
          const response = await axios.get('/api/auth/google');
          // 2. 서버가 302 리다이렉트 응답
          // 3. CORS 오류 발생
          // 4. Google 페이지로 이동 불가
        } catch (error) {
          console.error('CORS 오류 발생');
        }
      }
      
      // ✅ 리다이렉션 방식 (정상 작동)
      const loginWithRedirect = () => {
        // 1. 브라우저 주소 변경
        window.location.href = '/api/auth/google';
        // 2. 서버가 302 리다이렉트 응답
        // 3. 브라우저가 Google 페이지로 자동 이동
        // 4. 정상적인 OAuth 흐름 시작
      }
  2. 현재 Oauth의 작동 순서가 어떻게 되는 것일까??

    스크린샷 2024-11-26 오후 12.15.23.png

    처음에는 이런 순서로 진행이 되는 줄 알았으나

    1. 클라이언트 -> 서버 (/api/auth/google)
    2. 서버 -> Google 인증 서버 (리다이렉트)
      • client_id, redirect_uri, scope 등 전달
    3. 사용자가 Google 로그인 수행
    4. Google -> 서버 (/api/auth/google/callback)
      • authorization code 전달
    5. 서버 -> Google
      • authorization code로 액세스 토큰 요청
    6. Google -> 서버
      • 액세스 토큰, 사용자 정보 전달
    7. 서버 -> 클라이언트
      • JWT 토큰 생성 후 리다이렉트

    이런 순서로 진행이 되고 있었습니다.

    주소 요청 방식을 사용하여 Google 로그인 페이지를 보여주고 성공한다면 /auth/google/callback 콜백을 통하여 토큰을 전달받는 방식이였습니다.

    • API방식을 사용할 수 없는 이유
      1. 기술적 제약:
      • AJAX 요청은 페이지 리다이렉션을 할 수 없음
      • Google OAuth는 실제 페이지 전환을 요구
      • 팝업이나 iframe 사용시 보안 정책 위반
      1. 보안 문제:
      • 인증 과정이 노출될 수 있음
      • CSRF 공격에 취약
      • 토큰 탈취 위험
      1. OAuth 2.0 스펙 위반:
      • 표준 인증 흐름을 따르지 않음
      • Google의 요구사항 미충족
      • 인증 제공자의 정책 위반
    • 리다이렉션 방식을 사용하는 이유
      1. 보안:
      • 신뢰할 수 있는 Google 페이지에서 직접 인증
      • 인증 정보의 안전한 전달
      • state 파라미터를 통한 CSRF 방지
      1. 사용자 경험:
      • 익숙한 Google 로그인 화면
      • 투명한 인증 과정
      • 2단계 인증 등 Google의 보안 기능 활용
      1. 유지보수:
      • 표준 스펙 준수로 예측 가능한 동작
      • 문제 발생시 디버깅 용이
      • OAuth 제공자의 업데이트 대응 쉬움
  3. client에서 로컬과 배포 환경 모두에서 동일하게 사용할 수 있는 방법을 적용할 수 없을까??

    처음 /auth/google에서 쿼리를 통해 받는 방식을 선택한다면 로컬에서도 사용할 수 있습니다.

💻 개발 일지

💻 공통

💻 FE

💻 BE

🙋‍♂️ 소개

📒 문서

☀️ 데일리 스크럼

🤝🏼 회의록

Clone this wiki locally